From 9ddfe77f49afce72282c74a921876bec83dda225 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Thu, 15 Oct 2020 08:17:02 +0200 Subject: [PATCH] [CLIENT-1246] Adds missing API for Context#list_index_create and Context#map_key_create --- CHANGELOG.md | 8 ++++ lib/aerospike/cdt/context.rb | 12 ++++++ lib/aerospike/cdt/list_operation.rb | 43 +++++++++++++++++---- lib/aerospike/cdt/list_order.rb | 7 ++++ lib/aerospike/cdt/map_operation.rb | 58 +++++++++++++++++++++-------- lib/aerospike/cdt/map_order.rb | 6 +-- spec/aerospike/cdt_list_spec.rb | 22 +++++++++++ spec/aerospike/cdt_map_spec.rb | 55 ++++++++++++++++++++++++++- 8 files changed, 184 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b20c25f5..4f8395e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,14 @@ All notable changes to this project will be documented in this file. +## [2.17.0] - 2020-10-15 + +* **New Features** + * [CLIENT-1246] Adds missing API for Context#list_index_create and Context#map_key_create + +* **Bug Fixes** + * Fixed an issue were MsgPack extensions were not recursively cleared from the CDTs during unpacking. + ## [2.16.0] - 2020-10-12 * **New Features** diff --git a/lib/aerospike/cdt/context.rb b/lib/aerospike/cdt/context.rb index b3c1e2bb..e5c6987c 100644 --- a/lib/aerospike/cdt/context.rb +++ b/lib/aerospike/cdt/context.rb @@ -34,6 +34,12 @@ def initialize(id, value) @value = value end + ## + # Create list with given type at index offset, given an order and pad. + def self.list_index_create(index, order, pad) + Context.new(0x10 | ListOrder.flag(order, pad), index) + end + ## # Lookup list by index offset. # If the index is negative, the resolved index starts backwards from end of list. @@ -90,6 +96,12 @@ def self.map_key(key) Context.new(0x22, key) end + ## + # Create map with given type at map key. + def self.map_key_create(key, order) + Context.new(0x22 | order[:flag], key) + end + ## # Lookup map by value. def self.map_value(key) diff --git a/lib/aerospike/cdt/list_operation.rb b/lib/aerospike/cdt/list_operation.rb index ae7dbeb8..a746ba4d 100644 --- a/lib/aerospike/cdt/list_operation.rb +++ b/lib/aerospike/cdt/list_operation.rb @@ -84,18 +84,33 @@ class ListOperation < Operation REMOVE_BY_RANK_RANGE = 39 REMOVE_BY_VALUE_REL_RANK_RANGE = 40 - attr_reader :list_op, :arguments, :policy, :return_type, :ctx + attr_reader :list_op, :arguments, :policy, :return_type, :ctx, :flag - def initialize(op_type, list_op, bin_name, *arguments, return_type: nil, ctx: nil) + def initialize(op_type, list_op, bin_name, *arguments, return_type: nil, ctx: nil, flag: nil) @op_type = op_type @bin_name = bin_name @bin_value = nil @list_op = list_op @ctx = ctx + @flag = flag @arguments = arguments @return_type = return_type end + ## + # creates list create operation. + # Server creates list at given context level. The context is allowed to be beyond list + # boundaries only if pad is set to true. In that case, nil list entries will be inserted to + # satisfy the context position. + def self.create(bin_name, order, pad, ctx: nil) + # If context not defined, the set order for top-level bin list. + if !ctx || ctx.length == 0 + self.set_order(bin_name, order) + else + ListOperation.new(Operation::CDT_MODIFY, SET_TYPE, bin_name, order, ctx: ctx, flag: ListOrder.flag(order, pad)) + end + end + ## # Create a set list order operation. # Server sets list order. @@ -504,11 +519,7 @@ def pack_bin_value packer.write_array_header(3) Value.of(0xff).pack(packer) - packer.write_array_header(@ctx.length*2) - @ctx.each do |ctx| - Value.of(ctx.id).pack(packer) - Value.of(ctx.value).pack(packer) - end + pack_context(packer) packer.write_array_header(args.length+1) Value.of(@list_op).pack(packer) @@ -529,6 +540,24 @@ def pack_bin_value end BytesValue.new(bytes) end + + def pack_context(packer) + packer.write_array_header(@ctx.length*2) + if @flag + (1...@ctx.length).each do |i| + Value.of(@ctx[i].id).pack(packer) + Value.of(@ctx[i].value).pack(packer) + end + + Value.of(@ctx[-1].id | @flag).pack(packer) + Value.of(@ctx[-1].value).pack(packer) + else + @ctx.each do |ctx| + Value.of(ctx.id).pack(packer) + Value.of(ctx.value).pack(packer) + end + end + end end class InvertibleListOp < ListOperation diff --git a/lib/aerospike/cdt/list_order.rb b/lib/aerospike/cdt/list_order.rb index b69477c5..cd99d67e 100644 --- a/lib/aerospike/cdt/list_order.rb +++ b/lib/aerospike/cdt/list_order.rb @@ -32,6 +32,13 @@ module ListOrder ## # Default order DEFAULT = UNORDERED + + private + + def self.flag(attributes, pad) + (attributes == 1) ? 0xc0 : (pad ? 0x80 : 0x40) + end + end end end diff --git a/lib/aerospike/cdt/map_operation.rb b/lib/aerospike/cdt/map_operation.rb index 9f9b9fcf..0bd70906 100644 --- a/lib/aerospike/cdt/map_operation.rb +++ b/lib/aerospike/cdt/map_operation.rb @@ -96,26 +96,39 @@ class MapOperation < Operation GET_BY_KEY_REL_INDEX_RANGE = 109 GET_BY_VALUE_REL_RANK_RANGE = 110 - attr_reader :map_op, :arguments, :return_type, :ctx + attr_reader :map_op, :arguments, :return_type, :ctx, :flag - def initialize(op_type, map_op, bin_name, *arguments, ctx: nil, return_type: nil) + def initialize(op_type, map_op, bin_name, *arguments, ctx: nil, return_type: nil, flag: nil) @op_type = op_type @bin_name = bin_name @bin_value = nil @map_op = map_op @ctx = ctx + @flag = flag @arguments = arguments @return_type = return_type self end + ## + # Creates a map create operation. + # Server creates map at given context level. + def self.create(bin_name, order, ctx: nil) + if !ctx || ctx.length == 0 + # If context not defined, the set order for top-level bin map. + self.set_policy(MapPolicy.new(order: order, flag: 0), bin_name) + else + MapOperation.new(Operation::CDT_MODIFY, SET_TYPE, bin_name, order[:attr], ctx: ctx, flag: order[:flag]) + end + end + ## # Create set map policy operation. # Server sets map policy attributes. Server returns null. # # The required map policy attributes can be changed after the map is created. def self.set_policy(bin_name, policy, ctx: nil) - MapOperation.new(Operation::CDT_MODIFY, SET_TYPE, bin_name, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, SET_TYPE, bin_name, policy.order[:attr], ctx: ctx) end ## @@ -126,16 +139,16 @@ def self.set_policy(bin_name, policy, ctx: nil) # The map policy also specifies the flags used when writing items to the map. def self.put(bin_name, key, value, ctx: nil, policy: MapPolicy::DEFAULT) if policy.flags != MapWriteFlags::DEFAULT - MapOperation.new(Operation::CDT_MODIFY, PUT, bin_name, key, value, policy.order, policy.flags, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, PUT, bin_name, key, value, policy.order[:attr], policy.flags, ctx: ctx) else case policy.write_mode when MapWriteMode::UPDATE_ONLY # Replace doesn't allow map order because it does not create on non-existing key. MapOperation.new(Operation::CDT_MODIFY, REPLACE, bin_name, key, value, ctx: ctx) when MapWriteMode::CREATE_ONLY - MapOperation.new(Operation::CDT_MODIFY, ADD, bin_name, key, value, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, ADD, bin_name, key, value, policy.order[:attr], ctx: ctx) else - MapOperation.new(Operation::CDT_MODIFY, PUT, bin_name, key, value, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, PUT, bin_name, key, value, policy.order[:attr], ctx: ctx) end end end @@ -148,16 +161,16 @@ def self.put(bin_name, key, value, ctx: nil, policy: MapPolicy::DEFAULT) # The map policy also specifies the flags used when writing items to the map. def self.put_items(bin_name, values, ctx: nil, policy: MapPolicy::DEFAULT) if policy.flags != MapWriteFlags::DEFAULT - MapOperation.new(Operation::CDT_MODIFY, PUT_ITEMS, bin_name, values, policy.order, policy.flags, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, PUT_ITEMS, bin_name, values, policy.order[:attr], policy.flags, ctx: ctx) else case policy.write_mode when MapWriteMode::UPDATE_ONLY # Replace doesn't allow map order because it does not create on non-existing key. MapOperation.new(Operation::CDT_MODIFY, REPLACE_ITEMS, bin_name, values, ctx: ctx) when MapWriteMode::CREATE_ONLY - MapOperation.new(Operation::CDT_MODIFY, ADD_ITEMS, bin_name, values, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, ADD_ITEMS, bin_name, values, policy.order[:attr], ctx: ctx) else - MapOperation.new(Operation::CDT_MODIFY, PUT_ITEMS, bin_name, values, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, PUT_ITEMS, bin_name, values, policy.order[:attr], ctx: ctx) end end end @@ -170,7 +183,7 @@ def self.put_items(bin_name, values, ctx: nil, policy: MapPolicy::DEFAULT) # The map policy dictates the type of map to create when it does not exist. # The map policy also specifies the mode used when writing items to the map. def self.increment(bin_name, key, incr, ctx: nil, policy: MapPolicy::DEFAULT) - MapOperation.new(Operation::CDT_MODIFY, INCREMENT, bin_name, key, incr, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, INCREMENT, bin_name, key, incr, policy.order[:attr], ctx: ctx) end ## @@ -181,7 +194,7 @@ def self.increment(bin_name, key, incr, ctx: nil, policy: MapPolicy::DEFAULT) # The map policy dictates the type of map to create when it does not exist. # The map policy also specifies the mode used when writing items to the map. def self.decrement(bin_name, key, decr, ctx: nil, policy: MapPolicy::DEFAULT) - MapOperation.new(Operation::CDT_MODIFY, DECREMENT, bin_name, key, decr, policy.order, ctx: ctx) + MapOperation.new(Operation::CDT_MODIFY, DECREMENT, bin_name, key, decr, policy.order[:attr], ctx: ctx) end ## @@ -626,11 +639,7 @@ def pack_bin_value packer.write_array_header(3) Value.of(0xff).pack(packer) - packer.write_array_header(@ctx.length*2) - @ctx.each do |ctx| - Value.of(ctx.id).pack(packer) - Value.of(ctx.value).pack(packer) - end + pack_context(packer) packer.write_array_header(args.length+1) Value.of(@map_op).pack(packer) @@ -651,6 +660,23 @@ def pack_bin_value BytesValue.new(bytes) end + def pack_context(packer) + packer.write_array_header(@ctx.length*2) + if @flag + (1...@ctx.length).each do |i| + Value.of(@ctx[i].id).pack(packer) + Value.of(@ctx[i].value).pack(packer) + end + + Value.of(@ctx[-1].id | @flag).pack(packer) + Value.of(@ctx[-1].value).pack(packer) + else + @ctx.each do |ctx| + Value.of(ctx.id).pack(packer) + Value.of(ctx.value).pack(packer) + end + end + end end end diff --git a/lib/aerospike/cdt/map_order.rb b/lib/aerospike/cdt/map_order.rb index eb66a6d5..1fffc22e 100644 --- a/lib/aerospike/cdt/map_order.rb +++ b/lib/aerospike/cdt/map_order.rb @@ -20,15 +20,15 @@ module MapOrder ## # Map is not ordered. This is the default. - UNORDERED = 0 + UNORDERED = {attr: 0, flag: 0x40} ## # Order map by key. - KEY_ORDERED = 1 + KEY_ORDERED = {attr: 1, flag: 0x80} ## # Order map by key, then value. - KEY_VALUE_ORDERED = 3 + KEY_VALUE_ORDERED = {attr: 3, flag: 0xc0} ## # Default order diff --git a/spec/aerospike/cdt_list_spec.rb b/spec/aerospike/cdt_list_spec.rb index ec0aa824..04fd8f87 100644 --- a/spec/aerospike/cdt_list_spec.rb +++ b/spec/aerospike/cdt_list_spec.rb @@ -630,6 +630,28 @@ def list_post_op describe "Context", skip: !Support.min_version?("4.6") do + it "appends a single item to the list and returns the list size" do + client.delete(key) + + list = [ + [7, 9, 5], + [1, 2, 3], + [6, 5, 4, 1], + ] + + client.put(key, Aerospike::Bin.new(list_bin, list)) + + # Append value to new list created after the original 3 lists. + operation = [ + ListOperation.append(list_bin, 2, ctx: [Context.list_index_create(3, ListOrder::ORDERED, false)], policy: ListPolicy.new(order: ListOrder::ORDERED)), + Aerospike::Operation.get(list_bin), + ] + record = client.operate(key, operation) + + results = record.bins[list_bin] + expect(record.bins[list_bin]).to eq [ [7, 9, 5], [1, 2, 3], [6, 5, 4,1], [2] ] + end + it "is used to change nested list" do client.delete(key) diff --git a/spec/aerospike/cdt_map_spec.rb b/spec/aerospike/cdt_map_spec.rb index 2ea075bb..f17fb785 100644 --- a/spec/aerospike/cdt_map_spec.rb +++ b/spec/aerospike/cdt_map_spec.rb @@ -21,7 +21,7 @@ let(:client) { Support.client } let(:key) { Support.gen_random_key } - let(:map_bin) { "map" } + let(:map_bin) { "map_bin" } let(:map_value) { nil } before(:each) do @@ -37,6 +37,59 @@ def map_post_op describe "MapOperation Context", skip: !Support.min_version?("4.6") do + it "should support Create Map ops" do + client.delete(key) + + m = { + "key1" => [7, 9, 5], + } + + client.put(key, Aerospike::Bin.new(map_bin, m)) + expect(map_post_op).to eq(m) + + ctx = [Context.map_key("key2")] + record = client.operate(key, + [ + ListOperation.create(map_bin, ListOrder::ORDERED, false, ctx: ctx), + ListOperation.append(map_bin, 2, ctx: ctx), + ListOperation.append(map_bin, 1, ctx: ctx), + Operation.get(map_bin), + ] + ) + + expect(record.bins[map_bin]).to eq({ + "key1" => [7, 9, 5], + "key2" => [1, 2], + }) + end + + it "should support Nested Map ops with Lists" do + client.delete(key) + + m = { + "key1" => { + "key11" => 9, "key12" => 4, + }, + "key2" => { + "key21" => 3, "key22" => 5, + }, + } + + client.put(key, Aerospike::Bin.new(map_bin, m)) + record = client.operate(key, [Aerospike::Operation.get(map_bin)]) + expect(record.bins[map_bin]).to eq(m) + + record = client.operate(key, [MapOperation.put(map_bin, "key21", 11, ctx: [Context::map_key("key2")]), Aerospike::Operation.get(map_bin)]) + expect(record.bins[map_bin]).to eq({ + "key1" => { + "key11" => 9, "key12" => 4, + }, + "key2" => { + "key21" => 11, "key22" => 5, + }, + }) + end + it "should support Nested Map ops" do client.delete(key)