From 6f059622fc70f604c7620631a977e40c4ac19666 Mon Sep 17 00:00:00 2001 From: Khosrow Afroozeh Date: Mon, 28 Nov 2022 22:32:25 +0100 Subject: [PATCH] [CLIENT-1362]Replace predicate expressions with new Aerospike Expression filters [CLIENT-1479] Support new expressions introduced in server version 5.6. Predicate Expressions are not yet removed due to semver. Expression filters are now supported on all commands, including `Client#get`, `Client#put`, `Client#delete`, `Client#operate`, `Client#scan`, `Client#query`, `Client#execute_udf`, etc. - Adds `Policy#filter_exp` and `Policy#fail_on_filtered_out` - Bit expressions: `Exp::Bit::` `#resize`, `#insert`, `#remove`, `#set`, `#or`, `#xor`, `#and`, `#not`, `#lshift`, `#rshift`, `#add`, `#subtract`, `#set_int`, `#get`, `#count`, `#lscan`, `#rscan`, `#get_int`, `#pack_math`, `#pack_get_int`, `#add_write`, `#add_read` - HLL Expressions: `Exp::HLL::` `#init`, `#add`, `#get_count`, `#get_union`, `#get_union_count`, `#get_intersect_count`, `#get_similarity`, `#describe`, `#may_contain`, `#add_write`, `#add_read` - Map Expressions: `Exp::Map::` `#put`, `#put_items`, `#increment`, `#clear`, `#remove_by_key`, `#remove_by_key_list`, `#remove_by_key_range`, `#remove_by_key_relative_index_range`, `#remove_by_value`, `#remove_by_value_list`, `#remove_by_value_range`, `#remove_by_value_relative_rank_range`, `#remove_by_value_relative_rank_range`, `#remove_by_index`, `#remove_by_index_range`, `#remove_by_rank`, `#remove_by_rank_range`, `#size`, `#get_by_key`, `#get_by_key_range`, `#get_by_key_list`, `#get_by_key_relative_index_range`, `#get_by_key_relative_index_range`, `#get_by_value`, `#get_by_value_range`, `#get_by_value_list`, `#get_by_value_relative_rank_range`, `#get_by_index`, `#get_by_index_range`, `#get_by_rank`, `#get_by_rank_range`, `#add_write`, `#add_read`, `#get_value_type` - List Expressions: `Exp::List::` `#append`, `#append_items`, `#insert`, `#insert_items`, `#increment`, `#set`, `#clear`, `#sort`, `#remove_by_value`, `#remove_by_value_list`, `#remove_by_value_range`, `#remove_by_value_relative_rank_range`, `#remove_by_index`, `#remove_by_index_range`, `#remove_by_rank`, `#remove_by_rank_range`, `#size`, `#get_by_value`, `#get_by_value_range`, `#get_by_value_list`, `#get_by_value_relative_rank_range`, `#get_by_index`, `#get_by_index_range`, `#get_by_index_range`, `#get_by_rank`, `#get_by_rank_range`, `#get_by_rank_range`, `#add_write`, `#add_read`, `#get_value_type`, `#pack_range_operation` - Read and Write operations: `Exp::Operation::` `#write`, `#read` --- Gemfile | 26 +- lib/aerospike.rb | 304 ++-- lib/aerospike/cdt/map_policy.rb | 18 +- lib/aerospike/cdt/map_return_type.rb | 10 +- lib/aerospike/client.rb | 58 +- lib/aerospike/command/command.rb | 202 +-- lib/aerospike/command/operate_args.rb | 99 ++ lib/aerospike/command/operate_command.rb | 17 +- lib/aerospike/exp/exp.rb | 1329 +++++++++++++++++ lib/aerospike/exp/exp_bit.rb | 388 +++++ lib/aerospike/exp/exp_hll.rb | 169 +++ lib/aerospike/exp/exp_list.rb | 403 +++++ lib/aerospike/exp/exp_map.rb | 493 ++++++ lib/aerospike/exp/operation.rb | 56 + lib/aerospike/operation.rb | 42 +- lib/aerospike/policy/policy.rb | 37 +- lib/aerospike/query/query_executor.rb | 16 +- .../query/query_partition_command.rb | 41 +- lib/aerospike/query/recordset.rb | 18 +- lib/aerospike/query/scan_executor.rb | 12 +- lib/aerospike/utils/buffer.rb | 84 +- lib/aerospike/utils/packer.rb | 13 +- lib/aerospike/value/value.rb | 72 +- spec/aerospike/cdt_hll_spec.rb | 273 ++-- spec/aerospike/cdt_list_spec.rb | 39 +- spec/aerospike/cdt_map_spec.rb | 119 +- spec/aerospike/exp/exp_bit_spec.rb | 406 +++++ spec/aerospike/exp/exp_hll_spec.rb | 170 +++ spec/aerospike/exp/exp_list_spec.rb | 514 +++++++ spec/aerospike/exp/exp_map_spec.rb | 564 +++++++ spec/aerospike/exp/expression_spec.rb | 629 ++++++++ spec/aerospike/exp/operation_spec.rb | 358 +++++ 32 files changed, 6295 insertions(+), 684 deletions(-) create mode 100644 lib/aerospike/command/operate_args.rb create mode 100644 lib/aerospike/exp/exp.rb create mode 100644 lib/aerospike/exp/exp_bit.rb create mode 100644 lib/aerospike/exp/exp_hll.rb create mode 100644 lib/aerospike/exp/exp_list.rb create mode 100644 lib/aerospike/exp/exp_map.rb create mode 100644 lib/aerospike/exp/operation.rb create mode 100644 spec/aerospike/exp/exp_bit_spec.rb create mode 100644 spec/aerospike/exp/exp_hll_spec.rb create mode 100644 spec/aerospike/exp/exp_list_spec.rb create mode 100644 spec/aerospike/exp/exp_map_spec.rb create mode 100644 spec/aerospike/exp/expression_spec.rb create mode 100644 spec/aerospike/exp/operation_spec.rb diff --git a/Gemfile b/Gemfile index 0b00b6b2..084d1765 100644 --- a/Gemfile +++ b/Gemfile @@ -1,28 +1,32 @@ source "https://rubygems.org" group :test do - gem 'rspec', '~> 3.4' - gem 'codecov', require: false + gem "rspec", "~> 3.4" + gem "codecov", require: false end group :development do + gem "pry" + gem "stackprof", "~> 0.2.22" + gem "rspec-stackprof" + gem "hexdump", "~> 1.0" gem "ruby-lsp", require: false - gem 'rubocop', require: false - gem 'rubocop-performance', require: false - gem 'rubocop-rake', require: false - gem 'rubocop-rspec', require: false + gem "rubocop", require: false + gem "rubocop-performance", require: false + gem "rubocop-rake", require: false + gem "rubocop-rspec", require: false end -gem 'bcrypt' -gem 'msgpack', '~> 1.2' -gem 'rake' +gem "bcrypt" +gem "msgpack", "~> 1.2" +gem "rake" platforms :mri, :rbx do - gem 'openssl' + gem "openssl" end platforms :jruby do - gem 'jruby-openssl', '~> 0.10', require: 'openssl' + gem "jruby-openssl", "~> 0.10", require: "openssl" end gemspec diff --git a/lib/aerospike.rb b/lib/aerospike.rb index 3f13025a..45ffe28f 100644 --- a/lib/aerospike.rb +++ b/lib/aerospike.rb @@ -19,163 +19,171 @@ require "stringio" require "monitor" require "timeout" -require 'resolv' -require 'msgpack' -require 'bcrypt' -require 'zlib' +require "resolv" +require "msgpack" +require "bcrypt" +require "zlib" -require 'aerospike/atomic/atomic' +require "aerospike/atomic/atomic" -require 'aerospike/client' -require 'aerospike/features' -require 'aerospike/utils/pool' -require 'aerospike/utils/connection_pool' -require 'aerospike/utils/packer' -require 'aerospike/utils/unpacker' -require 'aerospike/utils/buffer' -require 'aerospike/utils/string_parser' -require 'aerospike/host' -require 'aerospike/host/parse' -require 'aerospike/loggable' -require 'aerospike/record' -require 'aerospike/result_code' -require 'aerospike/version' -require 'aerospike/value/particle_type' -require 'aerospike/value/value' -require 'aerospike/command/single_command' -require 'aerospike/command/batch_direct_node' -require 'aerospike/command/batch_index_node' -require 'aerospike/command/field_type' -require 'aerospike/command/command' -require 'aerospike/command/execute_command' -require 'aerospike/command/write_command' -require 'aerospike/command/batch_item' -require 'aerospike/command/operate_command' -require 'aerospike/command/exists_command' -require 'aerospike/command/multi_command' -require 'aerospike/command/batch_direct_command' -require 'aerospike/command/batch_direct_exists_command' -require 'aerospike/command/batch_index_command' -require 'aerospike/command/batch_index_exists_command' -require 'aerospike/command/read_header_command' -require 'aerospike/command/touch_command' -require 'aerospike/command/read_command' -require 'aerospike/command/delete_command' -require 'aerospike/command/admin_command' -require 'aerospike/command/login_command' -require 'aerospike/command/unsupported_particle_type_validator' -require 'aerospike/key' -require 'aerospike/operation' -require 'aerospike/cdt/context' -require 'aerospike/cdt/list_operation' -require 'aerospike/cdt/list_order' -require 'aerospike/cdt/list_return_type' -require 'aerospike/cdt/list_sort_flags' -require 'aerospike/cdt/list_write_flags' -require 'aerospike/cdt/list_policy' -require 'aerospike/cdt/map_operation' -require 'aerospike/cdt/map_order' -require 'aerospike/cdt/map_return_type' -require 'aerospike/cdt/map_write_flags' -require 'aerospike/cdt/map_write_mode' -require 'aerospike/cdt/map_policy' -require 'aerospike/cdt/hll_operation' -require 'aerospike/cdt/hll_write_flags' -require 'aerospike/cdt/hll_policy' -require 'aerospike/cdt/bit_operation' -require 'aerospike/cdt/bit_overflow_action' -require 'aerospike/cdt/bit_resize_flags' -require 'aerospike/cdt/bit_write_flags' -require 'aerospike/cdt/bit_policy' -require 'aerospike/geo_json' -require 'aerospike/ttl' +require "aerospike/client" +require "aerospike/features" +require "aerospike/utils/pool" +require "aerospike/utils/connection_pool" +require "aerospike/utils/packer" +require "aerospike/utils/unpacker" +require "aerospike/utils/buffer" +require "aerospike/utils/string_parser" +require "aerospike/host" +require "aerospike/host/parse" +require "aerospike/loggable" +require "aerospike/record" +require "aerospike/result_code" +require "aerospike/version" +require "aerospike/value/particle_type" +require "aerospike/value/value" +require "aerospike/command/single_command" +require "aerospike/command/batch_direct_node" +require "aerospike/command/batch_index_node" +require "aerospike/command/field_type" +require "aerospike/command/command" +require "aerospike/command/execute_command" +require "aerospike/command/write_command" +require "aerospike/command/batch_item" +require "aerospike/command/operate_command" +require "aerospike/command/exists_command" +require "aerospike/command/multi_command" +require "aerospike/command/batch_direct_command" +require "aerospike/command/batch_direct_exists_command" +require "aerospike/command/batch_index_command" +require "aerospike/command/batch_index_exists_command" +require "aerospike/command/read_header_command" +require "aerospike/command/touch_command" +require "aerospike/command/read_command" +require "aerospike/command/delete_command" +require "aerospike/command/admin_command" +require "aerospike/command/login_command" +require "aerospike/command/unsupported_particle_type_validator" +require "aerospike/command/operate_args" +require "aerospike/key" +require "aerospike/operation" +require "aerospike/cdt/context" +require "aerospike/cdt/list_operation" +require "aerospike/cdt/list_order" +require "aerospike/cdt/list_return_type" +require "aerospike/cdt/list_sort_flags" +require "aerospike/cdt/list_write_flags" +require "aerospike/cdt/list_policy" +require "aerospike/cdt/map_operation" +require "aerospike/cdt/map_order" +require "aerospike/cdt/map_return_type" +require "aerospike/cdt/map_write_flags" +require "aerospike/cdt/map_write_mode" +require "aerospike/cdt/map_policy" +require "aerospike/cdt/hll_operation" +require "aerospike/cdt/hll_write_flags" +require "aerospike/cdt/hll_policy" +require "aerospike/cdt/bit_operation" +require "aerospike/cdt/bit_overflow_action" +require "aerospike/cdt/bit_resize_flags" +require "aerospike/cdt/bit_write_flags" +require "aerospike/cdt/bit_policy" +require "aerospike/geo_json" +require "aerospike/ttl" -require 'aerospike/policy/client_policy' -require 'aerospike/policy/priority' -require 'aerospike/policy/record_exists_action' -require 'aerospike/policy/generation_policy' -require 'aerospike/policy/policy' -require 'aerospike/policy/batch_policy' -require 'aerospike/policy/write_policy' -require 'aerospike/policy/scan_policy' -require 'aerospike/policy/query_policy' -require 'aerospike/policy/consistency_level' -require 'aerospike/policy/commit_level' -require 'aerospike/policy/admin_policy' -require 'aerospike/policy/auth_mode' +require "aerospike/policy/client_policy" +require "aerospike/policy/priority" +require "aerospike/policy/record_exists_action" +require "aerospike/policy/generation_policy" +require "aerospike/policy/policy" +require "aerospike/policy/batch_policy" +require "aerospike/policy/write_policy" +require "aerospike/policy/scan_policy" +require "aerospike/policy/query_policy" +require "aerospike/policy/consistency_level" +require "aerospike/policy/commit_level" +require "aerospike/policy/admin_policy" +require "aerospike/policy/auth_mode" -require 'aerospike/socket/base' -require 'aerospike/socket/ssl' -require 'aerospike/socket/tcp' +require "aerospike/socket/base" +require "aerospike/socket/ssl" +require "aerospike/socket/tcp" -require 'aerospike/connection/authenticate' -require 'aerospike/connection/create' +require "aerospike/connection/authenticate" +require "aerospike/connection/create" -require 'aerospike/cluster' -require 'aerospike/cluster/create_connection' -require 'aerospike/cluster/find_nodes_to_remove' -require 'aerospike/cluster/find_node' -require 'aerospike/cluster/partition' -require 'aerospike/cluster/partition_parser' -require 'aerospike/cluster/rack_parser' -require 'aerospike/node' -require 'aerospike/node/generation' -require 'aerospike/node/rebalance' -require 'aerospike/node/refresh/failed' -require 'aerospike/node/refresh/friends' -require 'aerospike/node/refresh/info' -require 'aerospike/node/refresh/partitions' -require 'aerospike/node/refresh/racks' -require 'aerospike/node/refresh/peers' -require 'aerospike/node/refresh/reset' -require 'aerospike/node/verify/cluster_name' -require 'aerospike/node/verify/name' -require 'aerospike/node/verify/partition_generation' -require 'aerospike/node/verify/rebalance_generation' -require 'aerospike/node/verify/peers_generation' -require 'aerospike/node_validator' -require 'aerospike/peer' -require 'aerospike/peers' -require 'aerospike/peers/fetch' -require 'aerospike/peers/parse' -require 'aerospike/info' -require 'aerospike/udf' -require 'aerospike/bin' -require 'aerospike/aerospike_exception' -require 'aerospike/user_role' -require 'aerospike/privilege' -require 'aerospike/role' +require "aerospike/cluster" +require "aerospike/cluster/create_connection" +require "aerospike/cluster/find_nodes_to_remove" +require "aerospike/cluster/find_node" +require "aerospike/cluster/partition" +require "aerospike/cluster/partition_parser" +require "aerospike/cluster/rack_parser" +require "aerospike/node" +require "aerospike/node/generation" +require "aerospike/node/rebalance" +require "aerospike/node/refresh/failed" +require "aerospike/node/refresh/friends" +require "aerospike/node/refresh/info" +require "aerospike/node/refresh/partitions" +require "aerospike/node/refresh/racks" +require "aerospike/node/refresh/peers" +require "aerospike/node/refresh/reset" +require "aerospike/node/verify/cluster_name" +require "aerospike/node/verify/name" +require "aerospike/node/verify/partition_generation" +require "aerospike/node/verify/rebalance_generation" +require "aerospike/node/verify/peers_generation" +require "aerospike/node_validator" +require "aerospike/peer" +require "aerospike/peers" +require "aerospike/peers/fetch" +require "aerospike/peers/parse" +require "aerospike/info" +require "aerospike/udf" +require "aerospike/bin" +require "aerospike/aerospike_exception" +require "aerospike/user_role" +require "aerospike/privilege" +require "aerospike/role" -require 'aerospike/task/index_task' -require 'aerospike/task/execute_task' -require 'aerospike/task/udf_remove_task' -require 'aerospike/task/udf_register_task' -require 'aerospike/task/task' -require 'aerospike/language' +require "aerospike/task/index_task" +require "aerospike/task/execute_task" +require "aerospike/task/udf_remove_task" +require "aerospike/task/udf_register_task" +require "aerospike/task/task" +require "aerospike/language" -require 'aerospike/query/recordset' -require 'aerospike/query/filter' -require 'aerospike/query/stream_command' -require 'aerospike/query/query_command' -require 'aerospike/query/scan_command' -require 'aerospike/query/statement' -require 'aerospike/query/pred_exp' -require 'aerospike/query/partition_tracker' -require 'aerospike/query/partition_status' -require 'aerospike/query/partition_filter' -require 'aerospike/query/node_partitions' -require 'aerospike/query/scan_executor' -require 'aerospike/query/scan_partition_command' -require 'aerospike/query/query_executor' -require 'aerospike/query/query_partition_command' +require "aerospike/query/recordset" +require "aerospike/query/filter" +require "aerospike/query/stream_command" +require "aerospike/query/query_command" +require "aerospike/query/scan_command" +require "aerospike/query/statement" +require "aerospike/query/pred_exp" +require "aerospike/query/partition_tracker" +require "aerospike/query/partition_status" +require "aerospike/query/partition_filter" +require "aerospike/query/node_partitions" +require "aerospike/query/scan_executor" +require "aerospike/query/scan_partition_command" +require "aerospike/query/query_executor" +require "aerospike/query/query_partition_command" -require 'aerospike/query/pred_exp/and_or' -require 'aerospike/query/pred_exp/geo_json_value' -require 'aerospike/query/pred_exp/integer_value' -require 'aerospike/query/pred_exp/op' -require 'aerospike/query/pred_exp/regex' -require 'aerospike/query/pred_exp/regex_flags' -require 'aerospike/query/pred_exp/string_value' +require "aerospike/exp/exp" +require "aerospike/exp/exp_map" +require "aerospike/exp/exp_list" +require "aerospike/exp/exp_bit" +require "aerospike/exp/exp_hll" +require "aerospike/exp/operation" + +require "aerospike/query/pred_exp/and_or" +require "aerospike/query/pred_exp/geo_json_value" +require "aerospike/query/pred_exp/integer_value" +require "aerospike/query/pred_exp/op" +require "aerospike/query/pred_exp/regex" +require "aerospike/query/pred_exp/regex_flags" +require "aerospike/query/pred_exp/string_value" module Aerospike extend Loggable diff --git a/lib/aerospike/cdt/map_policy.rb b/lib/aerospike/cdt/map_policy.rb index 7445aab7..fc6f1cd8 100644 --- a/lib/aerospike/cdt/map_policy.rb +++ b/lib/aerospike/cdt/map_policy.rb @@ -17,8 +17,8 @@ module Aerospike module CDT class MapPolicy - attr_accessor :order, :write_mode, :flags + attr_accessor :item_command, :items_command, :attributes def initialize(order: nil, write_mode: nil, flags: nil) if write_mode && flags @@ -28,10 +28,24 @@ def initialize(order: nil, write_mode: nil, flags: nil) @order = order || MapOrder::DEFAULT @write_mode = write_mode || MapWriteMode::DEFAULT @flags = flags || MapWriteFlags::DEFAULT + @attributes = order ? order[:attr] : 0 + + case @write_mode + when CDT::MapWriteMode::DEFAULT + @item_command = CDT::MapOperation::PUT + @items_command = CDT::MapOperation::PUT_ITEMS + when CDT::MapWriteMode::UPDATE_ONLY + @item_command = CDT::MapOperation::REPLACE + @items_command = CDT::MapOperation::REPLACE_ITEMS + when CDT::MapWriteMode::CREATE_ONLY + @item_command = CDT::MapOperation::ADD + @items_command = CDT::MapOperation::ADD_ITEMS + else + raise Exceptions.new(ResultCode::PARAMETER_ERROR, "invalid value for MapWriteMode #{write_mode}") + end end DEFAULT = MapPolicy.new - end end end diff --git a/lib/aerospike/cdt/map_return_type.rb b/lib/aerospike/cdt/map_return_type.rb index 2a829b22..b283a1fb 100644 --- a/lib/aerospike/cdt/map_return_type.rb +++ b/lib/aerospike/cdt/map_return_type.rb @@ -69,10 +69,18 @@ module MapReturnType # Return true if count > 0. EXISTS = 13 + ## + # :private + # + # TODO: Should be like ListOperation and Implement InvertibleMapOperation + # Inverts meaning of map command and return values. For example: + # map_remove_by_key_range(bin_name, key_begin, key_end, MapReturnType::KEY | MapReturnType::INVERTED) + # With the INVERTED flag enabled, the keys outside of the specified key range will be removed and returned. + INVERTED = 0x10000 + ## # Default return type: NONE DEFAULT_RETURN_TYPE = NONE - end end end diff --git a/lib/aerospike/client.rb b/lib/aerospike/client.rb index e9b8ef99..be1696ae 100644 --- a/lib/aerospike/client.rb +++ b/lib/aerospike/client.rb @@ -15,8 +15,8 @@ # License for the specific language governing permissions and limitations under # the License. -require 'digest' -require 'base64' +require "digest" +require "base64" module Aerospike @@ -36,7 +36,6 @@ module Aerospike # +:fail_if_not_connected+ set to true class Client - attr_accessor :default_admin_policy attr_accessor :default_batch_policy attr_accessor :default_info_policy @@ -48,8 +47,7 @@ class Client attr_accessor :cluster def initialize(hosts = nil, policy: ClientPolicy.new, connect: true) - - hosts = ::Aerospike::Host::Parse.(hosts || ENV['AEROSPIKE_HOSTS'] || 'localhost') + hosts = ::Aerospike::Host::Parse.(hosts || ENV["AEROSPIKE_HOSTS"] || "localhost") policy = create_policy(policy, ClientPolicy) set_default_policies(policy.policies) @cluster = Cluster.new(policy, hosts) @@ -249,7 +247,7 @@ def truncate(namespace, set_name = nil, before_last_update = nil, options = {}) end response = send_info_command(policy, str_cmd, node).upcase - return if response == 'OK' + return if response == "OK" raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::SERVER_ERROR, "Truncate failed: #{response}") end @@ -386,7 +384,8 @@ def batch_exists(keys, options = nil) def operate(key, operations, options = nil) policy = create_policy(options, OperatePolicy, default_operate_policy) - command = OperateCommand.new(@cluster, policy, key, operations) + args = OperateArgs.new(cluster, policy, default_write_policy, default_operate_policy, key, operations) + command = OperateCommand.new(@cluster, key, args) execute_command(command) command.record end @@ -415,7 +414,7 @@ def register_udf_from_file(client_path, server_path, language, options = nil) def register_udf(udf_body, server_path, language, options = nil) policy = create_policy(options, Policy, default_info_policy) - content = Base64.strict_encode64(udf_body).force_encoding('binary') + content = Base64.strict_encode64(udf_body).force_encoding("binary") str_cmd = "udf-put:filename=#{server_path};content=#{content};" str_cmd << "content-len=#{content.length};udf-type=#{language};" @@ -424,15 +423,15 @@ def register_udf(udf_body, server_path, language, options = nil) res = {} response_map.each do |k, response| - vals = response.to_s.split(';') + vals = response.to_s.split(";") vals.each do |pair| k, v = pair.split("=", 2) res[k] = v end end - if res['error'] - raise Aerospike::Exceptions::CommandRejected.new("Registration failed: #{res['error']}\nFile: #{res['file']}\nLine: #{res['line']}\nMessage: #{res['message']}") + if res["error"] + raise Aerospike::Exceptions::CommandRejected.new("Registration failed: #{res["error"]}\nFile: #{res["file"]}\nLine: #{res["line"]}\nMessage: #{res["message"]}") end UdfRegisterTask.new(@cluster, server_path) @@ -454,7 +453,7 @@ def remove_udf(udf_name, options = nil) response_map = @cluster.request_info(policy, str_cmd) _, response = response_map.first - if response == 'ok' + if response == "ok" UdfRemoveTask.new(@cluster, udf_name) else raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::SERVER_ERROR, response) @@ -466,27 +465,27 @@ def remove_udf(udf_name, options = nil) def list_udf(options = nil) policy = create_policy(options, Policy, default_info_policy) - str_cmd = 'udf-list' + str_cmd = "udf-list" # Send command to one node. That node will distribute it to other nodes. response_map = @cluster.request_info(policy, str_cmd) _, response = response_map.first - vals = response.split(';') + vals = response.split(";") vals.map do |udf_info| - next if udf_info.strip! == '' + next if udf_info.strip! == "" - udf_parts = udf_info.split(',') + udf_parts = udf_info.split(",") udf = UDF.new udf_parts.each do |values| - k, v = values.split('=', 2) + k, v = values.split("=", 2) case k - when 'filename' + when "filename" udf.filename = v - when 'hash' + when "hash" udf.hash = v - when 'type' + when "type" udf.language = v end end @@ -501,7 +500,7 @@ def list_udf(options = nil) # udf file = /.lua # # This method is only supported by Aerospike 3 servers. - def execute_udf(key, package_name, function_name, args=[], options = nil) + def execute_udf(key, package_name, function_name, args = [], options = nil) policy = create_policy(options, WritePolicy, default_write_policy) command = ExecuteCommand.new(@cluster, policy, key, package_name, function_name, args) @@ -514,10 +513,10 @@ def execute_udf(key, package_name, function_name, args=[], options = nil) result_map = record.bins # User defined functions don't have to return a value. - key, obj = result_map.detect{ |k, _| k.include?('SUCCESS') } + key, obj = result_map.detect { |k, _| k.include?("SUCCESS") } return obj if key - key, obj = result_map.detect{ |k, _| k.include?('FAILURE') } + key, obj = result_map.detect { |k, _| k.include?("FAILURE") } message = key ? obj.to_s : "Invalid UDF return value" raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::UDF_BAD_RESPONSE, message) end @@ -530,7 +529,7 @@ def execute_udf(key, package_name, function_name, args=[], options = nil) # # This method is only supported by Aerospike 3 servers. # If the policy is nil, the default relevant policy will be used. - def execute_udf_on_query(statement, package_name, function_name, function_args=[], options = nil) + def execute_udf_on_query(statement, package_name, function_name, function_args = [], options = nil) policy = create_policy(options, QueryPolicy, default_query_policy) nodes = @cluster.nodes @@ -559,7 +558,6 @@ def execute_udf_on_query(statement, package_name, function_name, function_args=[ ExecuteTask.new(@cluster, statement) end - # Create secondary index. # This asynchronous server call will return before command is complete. # The user can optionally wait for command completion by using the returned @@ -583,12 +581,12 @@ def create_index(namespace, set_name, index_name, bin_name, index_type, collecti # Send index command to one node. That node will distribute the command to other nodes. response = send_info_command(policy, str_cmd).upcase - if response == 'OK' + if response == "OK" # Return task that could optionally be polled for completion. return IndexTask.new(@cluster, namespace, index_name) end - if response.start_with?('FAIL:200') + if response.start_with?("FAIL:200") # Index has already been created. Do not need to poll for completion. return IndexTask.new(@cluster, namespace, index_name, true) end @@ -607,10 +605,10 @@ def drop_index(namespace, set_name, index_name, options = nil) # Send index command to one node. That node will distribute the command to other nodes. response = send_info_command(policy, str_cmd).upcase - return if response == 'OK' + return if response == "OK" # Index did not previously exist. Return without error. - return if response.start_with?('FAIL:201') + return if response.start_with?("FAIL:201") raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::INDEX_GENERIC, "Drop index failed: #{response}") end @@ -966,7 +964,5 @@ def execute_batch_direct_commands(policy, keys) threads.each(&:join) end - end # class - end # module diff --git a/lib/aerospike/command/command.rb b/lib/aerospike/command/command.rb index 071b9eca..a8088d9e 100644 --- a/lib/aerospike/command/command.rb +++ b/lib/aerospike/command/command.rb @@ -15,18 +15,17 @@ # License for the specific language governing permissions and limitations under # the License. -require 'time' -require 'zlib' +require "time" +require "zlib" -require 'msgpack' -require 'aerospike/result_code' -require 'aerospike/command/field_type' +require "msgpack" +require "aerospike/result_code" +require "aerospike/command/field_type" -require 'aerospike/policy/consistency_level' -require 'aerospike/policy/commit_level' +require "aerospike/policy/consistency_level" +require "aerospike/policy/commit_level" module Aerospike - private # Flags commented out are not supported by cmd client. @@ -34,9 +33,8 @@ module Aerospike INFO1_READ = Integer(1 << 0) # Get all bins. INFO1_GET_ALL = Integer(1 << 1) - # Short query - INFO1_SHORT_QUERY = Integer(1 << 2) - + # Short query + INFO1_SHORT_QUERY = Integer(1 << 2) INFO1_BATCH = Integer(1 << 3) # Do not read the bins @@ -78,19 +76,18 @@ module Aerospike # Completely replace existing record only. INFO3_REPLACE_ONLY = Integer(1 << 5) - MSG_TOTAL_HEADER_SIZE = 30 - FIELD_HEADER_SIZE = 5 - OPERATION_HEADER_SIZE = 8 - MSG_REMAINING_HEADER_SIZE = 22 - DIGEST_SIZE = 20 - COMPRESS_THRESHOLD = 128 - CL_MSG_VERSION = 2 - AS_MSG_TYPE = 3 - AS_MSG_TYPE_COMPRESSED = 4 + MSG_TOTAL_HEADER_SIZE = 30 + FIELD_HEADER_SIZE = 5 + OPERATION_HEADER_SIZE = 8 + MSG_REMAINING_HEADER_SIZE = 22 + DIGEST_SIZE = 20 + COMPRESS_THRESHOLD = 128 + CL_MSG_VERSION = 2 + AS_MSG_TYPE = 3 + AS_MSG_TYPE_COMPRESSED = 4 class Command #:nodoc: - - def initialize(node=nil) + def initialize(node = nil) @data_offset = 0 @data_buffer = nil @@ -118,6 +115,9 @@ def set_write(policy, operation, key, bins) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + bins.each do |bin| estimate_operation_size_for_bin(bin) end @@ -127,6 +127,7 @@ def set_write(policy, operation, key, bins) write_header_with_policy(policy, 0, INFO2_WRITE, field_count, bins.length) write_key(key, policy) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) bins.each do |bin| write_operation_for_bin(bin, operation) @@ -144,10 +145,14 @@ def set_delete(policy, key) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + size_buffer - write_header_with_policy(policy, 0, INFO2_WRITE|INFO2_DELETE, field_count, 0) + write_header_with_policy(policy, 0, INFO2_WRITE | INFO2_DELETE, field_count, 0) write_key(key) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) end_cmd end @@ -159,11 +164,15 @@ def set_touch(policy, key) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + estimate_operation_size size_buffer write_header_with_policy(policy, 0, INFO2_WRITE, field_count, 1) write_key(key) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) write_operation_for_operation_type(Aerospike::Operation::TOUCH) end_cmd end @@ -176,10 +185,14 @@ def set_exists(policy, key) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + size_buffer - write_header(policy, INFO1_READ|INFO1_NOBINDATA, 0, field_count, 0) + write_header(policy, INFO1_READ | INFO1_NOBINDATA, 0, field_count, 0) write_key(key) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) end_cmd end @@ -191,10 +204,14 @@ def set_read_for_key_only(policy, key) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + size_buffer - write_header(policy, INFO1_READ|INFO1_GET_ALL, 0, field_count, 0) + write_header(policy, INFO1_READ | INFO1_GET_ALL, 0, field_count, 0) write_key(key) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) end_cmd end @@ -207,6 +224,8 @@ def set_read(policy, key, bin_names) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 bin_names.each do |bin_name| estimate_operation_size_for_bin_name(bin_name) @@ -216,6 +235,7 @@ def set_read(policy, key, bin_names) write_header(policy, INFO1_READ, 0, field_count, bin_names.length) write_key(key) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) bin_names.each do |bin_name| write_operation_for_bin_name(bin_name, Aerospike::Operation::READ) @@ -235,7 +255,10 @@ def set_read_header(policy, key) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 - estimate_operation_size_for_bin_name('') + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + + estimate_operation_size_for_bin_name("") size_buffer # The server does not currently return record header data with _INFO1_NOBINDATA attribute set. @@ -246,70 +269,35 @@ def set_read_header(policy, key) write_key(key) write_predexp(policy.predexp, predexp_size) - write_operation_for_bin_name('', Aerospike::Operation::READ) + write_filter_exp(@policy.filter_exp, exp_size) + write_operation_for_bin_name("", Aerospike::Operation::READ) end_cmd end # Implements different command operations - def set_operate(policy, key, operations) + def set_operate(policy, key, args) begin_cmd field_count = estimate_key_size(key, policy) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 - read_attr = 0 - write_attr = 0 - read_header = false - record_bin_multiplicity = policy.record_bin_multiplicity == RecordBinMultiplicity::ARRAY - - operations.each do |operation| - case operation.op_type - when Aerospike::Operation::READ, Aerospike::Operation::CDT_READ, - Aerospike::Operation::HLL_READ, Aerospike::Operation::BIT_READ - read_attr |= INFO1_READ - - # Read all bins if no bin is specified. - read_attr |= INFO1_GET_ALL unless operation.bin_name - - when Aerospike::Operation::READ_HEADER - # The server does not currently return record header data with _INFO1_NOBINDATA attribute set. - # The workaround is to request a non-existent bin. - # TODO: Fix this on server. - # read_attr |= _INFO1_READ | _INFO1_NOBINDATA - read_attr |= INFO1_READ - read_header = true - - else - write_attr = INFO2_WRITE - end + exp_size = estimate_expression_size(policy.filter_exp) + field_count += 1 if exp_size > 0 - if [Aerospike::Operation::HLL_MODIFY, Aerospike::Operation::HLL_READ, - Aerospike::Operation::BIT_MODIFY, Aerospike::Operation::BIT_READ].include?(operation.op_type) - record_bin_multiplicity = true - end + @data_offset += args.size - estimate_operation_size_for_operation(operation) - end size_buffer - - write_attr |= INFO2_RESPOND_ALL_OPS if write_attr != 0 && record_bin_multiplicity - - if write_attr == 0 - write_header(policy, read_attr, write_attr, field_count, operations.length) - else - write_header_with_policy(policy, read_attr, write_attr, field_count, operations.length) - end + write_header_with_policy(policy, args.read_attr, args.write_attr, field_count, args.operations.length) write_key(key, policy) write_predexp(policy.predexp, predexp_size) + write_filter_exp(policy.filter_exp, exp_size) - operations.each do |operation| + args.operations.each do |operation| write_operation_for_operation(operation) end - write_operation_for_bin(nil, Aerospike::Operation::READ) if read_header - end_cmd mark_compressed(policy) end @@ -321,6 +309,9 @@ def set_udf(policy, key, package_name, function_name, args) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + arg_bytes = args.to_bytes field_count += estimate_udf_size(package_name, function_name, arg_bytes) @@ -329,6 +320,7 @@ def set_udf(policy, key, package_name, function_name, args) write_header(policy, 0, INFO2_WRITE, field_count, 0) write_key(key, policy) write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) write_field_string(package_name, Aerospike::FieldType::UDF_PACKAGE_NAME) write_field_string(function_name, Aerospike::FieldType::UDF_FUNCTION) write_field_bytes(arg_bytes, Aerospike::FieldType::UDF_ARGLIST) @@ -379,6 +371,9 @@ def set_scan(policy, namespace, set_name, bin_names, node_partitions) predexp_size = estimate_predexp(policy.predexp) field_count += 1 if predexp_size > 0 + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + # Estimate scan options size. # @data_offset += 2 + FIELD_HEADER_SIZE # field_count += 1 @@ -420,7 +415,7 @@ def set_scan(policy, namespace, set_name, bin_names, node_partitions) node_partitions.parts_full.each do |part| @data_buffer.write_uint16_little_endian(part.id, @data_offset) - @data_offset+=2 + @data_offset += 2 end end @@ -429,7 +424,7 @@ def set_scan(policy, namespace, set_name, bin_names, node_partitions) node_partitions.parts_partial.each do |part| @data_buffer.write_binary(part.digest, @data_offset) - @data_offset+=part.digest.length + @data_offset += part.digest.length end end @@ -442,6 +437,7 @@ def set_scan(policy, namespace, set_name, bin_names, node_partitions) end write_predexp(policy.predexp, predexp_size) + write_filter_exp(@policy.filter_exp, exp_size) # write_field_header(2, Aerospike::FieldType::SCAN_OPTIONS) @@ -479,7 +475,7 @@ def execute while true # too many retries iterations += 1 - break if (@policy.max_retries > 0) && (iterations > @policy.max_retries+1) + break if (@policy.max_retries > 0) && (iterations > @policy.max_retries + 1) # Sleep before trying again, after the first iteration sleep(@policy.sleep_between_retries) if iterations > 1 && @policy.sleep_between_retries > 0 @@ -567,7 +563,6 @@ def execute ensure Buffer.put(@data_buffer) end - end # while # execution timeout @@ -576,7 +571,6 @@ def execute protected - def estimate_key_size(key, policy = nil) field_count = 0 @@ -646,6 +640,15 @@ def estimate_predexp(predexp) return 0 end + def estimate_expression_size(exp) + unless exp.nil? + @data_offset += FIELD_HEADER_SIZE + @data_offset += exp.size + return exp.size + end + 0 + end + # Generic header write. def write_header(policy, read_attr, write_attr, field_count, operation_count) read_attr |= INFO1_CONSISTENCY_ALL if policy.consistency_level == Aerospike::ConsistencyLevel::CONSISTENCY_ALL @@ -717,7 +720,6 @@ def write_header_with_policy(policy, read_attr, write_attr, field_count, operati @data_buffer.write_byte(0, 24) @data_buffer.write_byte(0, 25) - @data_buffer.write_int16(field_count, 26) @data_buffer.write_int16(operation_count, 28) @@ -739,15 +741,14 @@ def write_key(key, policy = nil) if policy && policy.respond_to?(:send_key) && policy.send_key == true write_field_value(key.user_key_as_value, Aerospike::FieldType::KEY) end - end def write_operation_for_bin(bin, operation) - name_length = @data_buffer.write_binary(bin.name, @data_offset+OPERATION_HEADER_SIZE) - value_length = bin.value_object.write(@data_buffer, @data_offset+OPERATION_HEADER_SIZE+name_length) + name_length = @data_buffer.write_binary(bin.name, @data_offset + OPERATION_HEADER_SIZE) + value_length = bin.value_object.write(@data_buffer, @data_offset + OPERATION_HEADER_SIZE + name_length) # Buffer.Int32ToBytes(name_length+value_length+4, @data_buffer, @data_offset) - @data_buffer.write_int32(name_length+value_length+4, @data_offset) + @data_buffer.write_int32(name_length + value_length + 4, @data_offset) @data_offset += 4 @data_buffer.write_byte(operation, @data_offset) @@ -764,13 +765,13 @@ def write_operation_for_bin(bin, operation) def write_operation_for_operation(operation) name_length = 0 if operation.bin_name - name_length = @data_buffer.write_binary(operation.bin_name, @data_offset+OPERATION_HEADER_SIZE) + name_length = @data_buffer.write_binary(operation.bin_name, @data_offset + OPERATION_HEADER_SIZE) end - value_length = operation.bin_value.write(@data_buffer, @data_offset+OPERATION_HEADER_SIZE+name_length) + value_length = operation.bin_value.write(@data_buffer, @data_offset + OPERATION_HEADER_SIZE + name_length) # Buffer.Int32ToBytes(name_length+value_length+4, @data_buffer, @data_offset) - @data_buffer.write_int32(name_length+value_length+4, @data_offset) + @data_buffer.write_int32(name_length + value_length + 4, @data_offset) @data_offset += 4 @data_buffer.write_byte(operation.op_type, @data_offset) @@ -785,9 +786,9 @@ def write_operation_for_operation(operation) end def write_operation_for_bin_name(name, operation) - name_length = @data_buffer.write_binary(name, @data_offset+OPERATION_HEADER_SIZE) + name_length = @data_buffer.write_binary(name, @data_offset + OPERATION_HEADER_SIZE) # Buffer.Int32ToBytes(name_length+4, @data_buffer, @data_offset) - @data_buffer.write_int32(name_length+4, @data_offset) + @data_buffer.write_int32(name_length + 4, @data_offset) @data_offset += 4 @data_buffer.write_byte(operation, @data_offset) @@ -826,37 +827,37 @@ def write_field_value(value, ftype) end def write_field_string(str, ftype) - len = @data_buffer.write_binary(str, @data_offset+FIELD_HEADER_SIZE) + len = @data_buffer.write_binary(str, @data_offset + FIELD_HEADER_SIZE) write_field_header(len, ftype) @data_offset += len end def write_u16_little_endian(i, ftype) - @data_buffer.write_uint16_little_endian(i, @data_offset+FIELD_HEADER_SIZE) + @data_buffer.write_uint16_little_endian(i, @data_offset + FIELD_HEADER_SIZE) write_field_header(2, ftype) @data_offset += 2 end def write_field_int(i, ftype) - @data_buffer.write_int32(i, @data_offset+FIELD_HEADER_SIZE) + @data_buffer.write_int32(i, @data_offset + FIELD_HEADER_SIZE) write_field_header(4, ftype) @data_offset += 4 end def write_field_int64(i, ftype) - @data_buffer.write_int64(i, @data_offset+FIELD_HEADER_SIZE) + @data_buffer.write_int64(i, @data_offset + FIELD_HEADER_SIZE) write_field_header(8, ftype) @data_offset += 8 end def write_field_bytes(bytes, ftype) - @data_buffer.write_binary(bytes, @data_offset+FIELD_HEADER_SIZE) + @data_buffer.write_binary(bytes, @data_offset + FIELD_HEADER_SIZE) write_field_header(bytes.bytesize, ftype) @data_offset += bytes.bytesize end def write_field_header(size, ftype) - @data_buffer.write_int32(size+1, @data_offset) + @data_buffer.write_int32(size + 1, @data_offset) @data_offset += 4 @data_buffer.write_byte(ftype, @data_offset) @data_offset += 1 @@ -864,13 +865,20 @@ def write_field_header(size, ftype) def write_predexp(predexp, predexp_size) if predexp && predexp.size > 0 - write_field_header(predexp_size, Aerospike::FieldType::PREDEXP) + write_field_header(predexp_size, Aerospike::FieldType::FILTER_EXP) @data_offset = Aerospike::PredExp.write( predexp, @data_buffer, @data_offset ) end end + def write_filter_exp(exp, exp_size) + unless exp.nil? + write_field_header(exp_size, Aerospike::FieldType::FILTER_EXP) + @data_offset += exp.write(@data_buffer, @data_offset) + end + end + def begin_cmd @data_offset = MSG_TOTAL_HEADER_SIZE end @@ -884,7 +892,7 @@ def size_buffer_sz(size) end def end_cmd - size = (@data_offset-8) | Integer(CL_MSG_VERSION << 56) | Integer(AS_MSG_TYPE << 48) + size = (@data_offset - 8) | Integer(CL_MSG_VERSION << 56) | Integer(AS_MSG_TYPE << 48) @data_buffer.write_int64(size, 0) end @@ -898,13 +906,13 @@ def compress_buffer # write original size as header proto_s = "%08d" % 0 - proto_s[0, 8] = [@data_offset].pack('q>') + proto_s[0, 8] = [@data_offset].pack("q>") compressed.prepend(proto_s) # write proto - proto = (compressed.size+8) | Integer(CL_MSG_VERSION << 56) | Integer(AS_MSG_TYPE << 48) + proto = (compressed.size + 8) | Integer(CL_MSG_VERSION << 56) | Integer(AS_MSG_TYPE << 48) proto_s = "%08d" % 0 - proto_s[0, 8] = [proto].pack('q>') + proto_s[0, 8] = [proto].pack("q>") compressed.prepend(proto_s) @data_buffer = Buffer.new(-1, compressed) @@ -929,7 +937,5 @@ def compressed_size def mark_compressed(policy) @compress = policy.use_compression end - end # class - end # module diff --git a/lib/aerospike/command/operate_args.rb b/lib/aerospike/command/operate_args.rb new file mode 100644 index 00000000..394252bc --- /dev/null +++ b/lib/aerospike/command/operate_args.rb @@ -0,0 +1,99 @@ +# encoding: utf-8 +# Copyright 2016-2020 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License") you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/operation" + +module Aerospike + private + + class OperateArgs + attr_reader :write_policy, :operations, :partition + attr_reader :size, :read_attr, :write_attr, :has_write + + RESPOND_ALL_OPS_READ_CMDS = [Operation::BIT_READ, Operation::EXP_READ, Operation::HLL_READ, Operation::CDT_READ] + READ_CMDS = [Operation::BIT_READ, Operation::EXP_READ, Operation::HLL_READ, Operation::CDT_READ, Operation::CDT_READ, Operation::READ] + MODIFY_CMDS = [Operation::BIT_MODIFY, Operation::EXP_MODIFY, Operation::HLL_MODIFY, Operation::CDT_MODIFY] + + def initialize(cluster, policy, write_default, read_default, key, operations) + @operations = operations + + data_offset = 0 + rattr = 0 + wattr = 0 + write = false + read_bin = false + read_header = false + respond_all_ops = false + + @operations.each do |operation| + if READ_CMDS.include?(operation.op_type) + if RESPOND_ALL_OPS_READ_CMDS.include?(operation.op_type) + # Map @operations require respond_all_ops to be true. + respond_all_ops = true + end + + rattr |= Aerospike::INFO1_READ + + # Read all bins if no bin is specified. + rattr |= Aerospike::INFO1_GET_ALL if operation.bin_name.nil? + read_bin = true + elsif operation.op_type == Operation::READ_HEADER + rattr |= Aerospike::INFO1_READ + read_header = true + elsif MODIFY_CMDS.include?(operation.op_type) + # Map @operations require respond_all_ops to be true. + respond_all_ops = true + + wattr = Aerospike::INFO2_WRITE + write = true + else + wattr = Aerospike::INFO2_WRITE + write = true + end + data_offset += operation.bin_name.bytesize + Aerospike::OPERATION_HEADER_SIZE unless operation.bin_name.nil? + data_offset += operation.bin_value.estimate_size + end + + @size = data_offset + @has_write = write + + if read_header && !read_bin + rattr |= Aerospike::INFO1_NOBINDATA + end + @read_attr = rattr + + if policy.nil? + @write_policy = write ? write_default : read_default + else + @write_policy = policy + end + + # When GET_ALL is specified, RESPOND_ALL_OPS must be disabled. + if (respond_all_ops && policy.record_bin_multiplicity) && (rattr & Aerospike::INFO1_GET_ALL) == 0 + wattr |= Aerospike::INFO2_RESPOND_ALL_OPS + end + @write_attr = wattr + + if write + # @partition = Partition.write(cluster, @write_policy, key) + @partition = Partition.new_by_key(key) + else + # @partition = Partition.read(cluster, @write_policy, key) + @partition = Partition.new_by_key(key) + end + end + end +end diff --git a/lib/aerospike/command/operate_command.rb b/lib/aerospike/command/operate_command.rb index a675c755..32f34630 100644 --- a/lib/aerospike/command/operate_command.rb +++ b/lib/aerospike/command/operate_command.rb @@ -14,33 +14,28 @@ # License for the specific language governing permissions and limitations under # the License. -require 'aerospike/command/read_command' +require "aerospike/command/read_command" module Aerospike - private class OperateCommand < ReadCommand #:nodoc: + def initialize(cluster, key, args) + super(cluster, args.write_policy, key, nil) - def initialize(cluster, policy, key, operations) - super(cluster, policy, key, nil) - - @operations = operations + @args = args end def get_node @cluster.master_node(@partition) end - def write_bins - @operations.select{|op| op.op_type == Aerospike::Operation::WRITE}.map(&:bin).compact + @operations.select { |op| op.op_type == Aerospike::Operation::WRITE }.map(&:bin).compact end def write_buffer - set_operate(@policy, @key, @operations) + set_operate(@args.write_policy, @key, @args) end - end # class - end # module diff --git a/lib/aerospike/exp/exp.rb b/lib/aerospike/exp/exp.rb new file mode 100644 index 00000000..f994ab65 --- /dev/null +++ b/lib/aerospike/exp/exp.rb @@ -0,0 +1,1329 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may no +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + class Exp + + # Regex bit flags + module RegexFlags + # Regex defaults + NONE = 0 + + # Use POSIX Extended Regular Expression syntax when interpreting regex. + EXTENDED = 1 + + # Do not differentiate case. + ICASE = 2 + + # Do not report position of matches. + NOSUB = 4 + + # Match-any-character operators don't match a newline. + NEWLINE = 8 + end + + # Expression type. + module Type #:nodoc: + NIL = 0 + BOOL = 1 + INT = 2 + STRING = 3 + LIST = 4 + MAP = 5 + BLOB = 6 + FLOAT = 7 + GEO = 8 + HLL = 9 + end # module type + + # Expression write flags. + module WriteFlags + # Default. Allow create or update. + DEFAULT = 0 + + # If bin does not exist, a new bin will be created. + # If bin exists, the operation will be denied. + # If bin exists, fail with ResultCode#BIN_EXISTS_ERROR + # when #POLICY_NO_FAIL is not set. + CREATE_ONLY = 1 + + # If bin exists, the bin will be overwritten. + # If bin does not exist, the operation will be denied. + # If bin does not exist, fail with ResultCode#BIN_NOT_FOUND + # when #POLICY_NO_FAIL is not set. + UPDATE_ONLY = 2 + + # If expression results in nil value, then delete the bin. Otherwise, fail with + # ResultCode#OP_NOT_APPLICABLE when #POLICY_NO_FAIL is not set. + ALLOW_DELETE = 4 + + # Do not raise error if operation is denied. + POLICY_NO_FAIL = 8 + + # Ignore failures caused by the expression resolving to unknown or a non-bin type. + EVAL_NO_FAIL = 16 + end # module WriteFlags + + # Expression write flags. + module ReadFlags + # Default. + DEFAULT = 0 + + # Ignore failures caused by the expression resolving to unknown or a non-bin type. + EVAL_NO_FAIL = 16 + end # module ReadFlags + + # Create record key expression of specified type. + # + # ==== Examples + # # Integer record key >= 100000 + # Exp.ge(Exp.key(Type::INT), Exp.int_val(100000)) + def self.key(type) + CmdInt.new(KEY, type) + end + + # Create expression that returns if the primary key is stored in the record meta data + # as a boolean expression. This would occur when {@link Policy#send_key} + # is true on record write. This expression usually evaluates quickly because record + # meta data is cached in memory. + # + # ==== Examples + # # Key exists in record meta data + # Exp.key_exists + def self.key_exists + Cmd.new(KEY_EXISTS) + end + + #-------------------------------------------------- + # Record Bin + #-------------------------------------------------- + + # Create bin expression of specified type. + # + # ==== Examples + # # String bin "a" == "views" + # Exp.eq(Exp.bin("a", Type::STRING), Exp.str_val("views")) + def self.bin(name, type) + Bin.new(name, type) + end + + # Create 64 bit integer bin expression. + # + # ==== Examples + # # Integer bin "a" == 200 + # Exp.eq(Exp.int_bin("a"), Exp.val(200)) + def self.int_bin(name) + Bin.new(name, Type::INT) + end + + # Create 64 bit float bin expression. + # + # ==== Examples + # # Float bin "a" >= 1.5 + # Exp.ge(Exp.float_bin("a"), Exp.int_val(1.5)) + def self.float_bin(name) + Bin.new(name, Type::FLOAT) + end + + # Create string bin expression. + # + # ==== Examples + # # String bin "a" == "views" + # Exp.eq(Exp.str_bin("a"), Exp.str_val("views")) + def self.str_bin(name) + Bin.new(name, Type::STRING) + end + + # Create boolean bin expression. + # + # ==== Examples + # # Boolean bin "a" == true + # Exp.eq(Exp.bool_bin("a"), Exp.val(true)) + def self.bool_bin(name) + Bin.new(name, Type::BOOL) + end + + # Create bin expression. + # + # ==== Examples + # # Blob bin "a" == [1,2,3] + # Exp.eq(Exp.blob_bin("a"), Exp.val(new {1, 2, 3})) + def self.blob_bin(name) + Bin.new(name, Type::BLOB) + end + + # Create geospatial bin expression. + # + # ==== Examples + # # Geo bin "a" == region + # String region = "{ \"type\": \"AeroCircle\", \"coordinates\": [[-122.0, 37.5], 50000.0] }" + # Exp.geo_compare(Exp.geo_bin("loc"), Exp.geo(region)) + def self.geo_bin(name) + Bin.new(name, Type::GEO) + end + + # Create list bin expression. + # + # ==== Examples + # # Bin a[2] == 3 + # Exp.eq(ListExp.get_by_index(ListReturnType::VALUE, Type::INT, Exp.val(2), Exp.list_bin("a")), Exp.val(3)) + def self.list_bin(name) + Bin.new(name, Type::LIST) + end + + # Create map bin expression. + # + # ==== Examples + # # Bin a["key"] == "value" + # Exp.eq( + # MapExp.get_by_key(MapReturnType::VALUE, Type::STRING, Exp.str_val("key"), Exp.map_bin("a")), + # Exp.str_val("value")) + def self.map_bin(name) + Bin.new(name, Type::MAP) + end + + # Create hll bin expression. + # + # ==== Examples + # # HLL bin "a" count > 7 + # Exp.gt(HLLExp.get_count(Exp.hll_bin("a")), Exp.val(7)) + def self.hll_bin(name) + Bin.new(name, Type::HLL) + end + + # Create expression that returns if bin of specified name exists. + # + # ==== Examples + # # Bin "a" exists in record + # Exp.bin_exists("a") + def self.bin_exists(name) + return Exp.ne(Exp.bin_type(name), Exp.int_val(0)) + end + + # Create expression that returns bin's integer particle type:: + # See {@link ParticleType}. + # + # ==== Examples + # # Bin "a" particle type is a list + # Exp.eq(Exp.bin_type("a"), Exp.val(ParticleType::LIST)) + def self.bin_type(name) + CmdStr.new(BIN_TYPE, name) + end + + #-------------------------------------------------- + # Misc + #-------------------------------------------------- + + # Create expression that returns record set name string. This expression usually + # evaluates quickly because record meta data is cached in memory. + # + # ==== Examples + # # Record set name == "myset" + # Exp.eq(Exp.set_name, Exp.str_val("myset")) + def self.set_name + Cmd.new(SET_NAME) + end + + # Create expression that returns record size on disk. If server storage-engine is + # memory, then zero is returned. This expression usually evaluates quickly because + # record meta data is cached in memory. + # + # ==== Examples + # # Record device size >= 100 KB + # Exp.ge(Exp.device_size, Exp.int_val(100 * 1024)) + def self.device_size + Cmd.new(DEVICE_SIZE) + end + + # Create expression that returns record size in memory. If server storage-engine is + # not memory nor data-in-memory, then zero is returned. This expression usually evaluates + # quickly because record meta data is cached in memory. + # + # Requires server version 5.3.0+ + # + # ==== Examples + # # Record memory size >= 100 KB + # Exp.ge(Exp.memory_size, Exp.int_val(100 * 1024)) + def self.memory_size + Cmd.new(MEMORY_SIZE) + end + + # Create expression that returns record last update time expressed as 64 bit integer + # nanoseconds since 1970-01-01 epoch. This expression usually evaluates quickly because + # record meta data is cached in memory. + # + # ==== Examples + # # Record last update time >= 2020-01-15 + # Exp.ge(Exp.last_update, Exp.val(new GregorianCalendar(2020, 0, 15))) + def self.last_update + Cmd.new(LAST_UPDATE) + end + + # Create expression that returns milliseconds since the record was last updated. + # This expression usually evaluates quickly because record meta data is cached in memory. + # + # ==== Examples + # # Record last updated more than 2 hours ago + # Exp.gt(Exp.since_update, Exp.val(2 * 60 * 60 * 1000)) + def self.since_update + Cmd.new(SINCE_UPDATE) + end + + # Create expression that returns record expiration time expressed as 64 bit integer + # nanoseconds since 1970-01-01 epoch. This expression usually evaluates quickly because + # record meta data is cached in memory. + # + # ==== Examples + # # Record expires on 2021-01-01 + # Exp.and( + # Exp.ge(Exp.void_time, Exp.val(new GregorianCalendar(2021, 0, 1))), + # Exp.lt(Exp.void_time, Exp.val(new GregorianCalendar(2021, 0, 2)))) + def self.void_time + Cmd.new(VOID_TIME) + end + + # Create expression that returns record expiration time (time to live) in integer seconds. + # This expression usually evaluates quickly because record meta data is cached in memory. + # + # ==== Examples + # # Record expires in less than 1 hour + # Exp.lt(Exp.ttl, Exp.val(60 * 60)) + def self.ttl + Cmd.new(TTL) + end + + # Create expression that returns if record has been deleted and is still in tombstone state. + # This expression usually evaluates quickly because record meta data is cached in memory. + # + # ==== Examples + # # Deleted records that are in tombstone state. + # Exp.is_tombstone + def self.is_tombstone + Cmd.new(IS_TOMBSTONE) + end + + # Create expression that returns record digest modulo as integer. This expression usually + # evaluates quickly because record meta data is cached in memory. + # + # ==== Examples + # # Records that have digest(key) % 3 == 1 + # Exp.eq(Exp.digest_modulo(3), Exp.int_val(1)) + def self.digest_modulo(mod) + CmdInt.new(DIGEST_MODULO, mod) + end + + # Create expression that performs a regex match on a string bin or string value expression. + # + # ==== Examples + # # Select string bin "a" that starts with "prefix" and ends with "suffix". + # # Ignore case and do not match newline. + # Exp.regex_compare("prefix.*suffix", RegexFlags.ICASE | RegexFlags.NEWLINE, Exp.str_bin("a")) + # + # @param regex regular expression string + # @param flags regular expression bit flags. See {@link Exp::RegexFlags} + # @param bin string bin or string value expression + def self.regex_compare(regex, flags, bin) + Regex.new(bin, regex, flags) + end + + #-------------------------------------------------- + # GEO Spatial + #-------------------------------------------------- + + # Create compare geospatial operation. + # + # ==== Examples + # # Query region within coordinates. + # region = + # "{ " + + # " \"type\": \"Polygon\", " + + # " \"coordinates\": [ " + + # " [[-122.500000, 37.000000],[-121.000000, 37.000000], " + + # " [-121.000000, 38.080000],[-122.500000, 38.080000], " + + # " [-122.500000, 37.000000]] " + + # " ] " + + # "}" + # Exp.geo_compare(Exp.geo_bin("a"), Exp.geo(region)) + def self.geo_compare(left, right) + CmdExp.new(GEO, left, right) + end + + # Create geospatial json string value. + def self.geo(val) + Geo.new(val) + end + + #-------------------------------------------------- + # Value + #-------------------------------------------------- + + # Create boolean value. + def self.bool_val(val) + Bool.new(val) + end + + # Create 64 bit integer value. + def self.int_val(val) + Int.new(val) + end + + # Create 64 bit floating point value. + def self.float_val(val) + Float.new(val) + end + + # Create string value. + def self.str_val(val) + Str.new(val) + end + + # Create blob byte value. + def self.blob_val(val) + Blob.new(val) + end + + # Create list value. + def self.list_val(*list) + ListVal.new(list) + end + + # Create map value. + def self.map_val(map) + MapVal.new(map) + end + + # Create nil value. + def self.nil_val + Nil.new + end + + #-------------------------------------------------- + # Boolean Operator + #-------------------------------------------------- + + # Create "not" operator expression. + # + # ==== Examples + # # ! (a == 0 || a == 10) + # Exp.not( + # Exp.or( + # Exp.eq(Exp.int_bin("a"), Exp.val(0)), + # Exp.eq(Exp.int_bin("a"), Exp.int_val(10)))) + def self.not(exp) + CmdExp.new(NOT, exp) + end + + # Create "and" (&&) operator that applies to a variable number of expressions. + # + # ==== Examples + # # (a > 5 || a == 0) && b < 3 + # Exp.and( + # Exp.or( + # Exp.gt(Exp.int_bin("a"), Exp.val(5)), + # Exp.eq(Exp.int_bin("a"), Exp.val(0))), + # Exp.lt(Exp.int_bin("b"), Exp.val(3))) + def self.and(*exps) + CmdExp.new(AND, *exps) + end + + # Create "or" (||) operator that applies to a variable number of expressions. + # + # ==== Examples + # # a == 0 || b == 0 + # Exp.or( + # Exp.eq(Exp.int_bin("a"), Exp.val(0)), + # Exp.eq(Exp.int_bin("b"), Exp.val(0))) + def self.or(*exps) + CmdExp.new(OR, *exps) + end + + # Create expression that returns true if only one of the expressions are true. + # Requires server version 5.6.0+. + # + # ==== Examples + # # exclusive(a == 0, b == 0) + # Exp.exclusive( + # Exp.eq(Exp.int_bin("a"), Exp.val(0)), + # Exp.eq(Exp.int_bin("b"), Exp.val(0))) + def self.exclusive(*exps) + CmdExp.new(EXCLUSIVE, *exps) + end + + # Create equal (==) expression. + # + # ==== Examples + # # a == 11 + # Exp.eq(Exp.int_bin("a"), Exp.int_val(11)) + def self.eq(left, right) + CmdExp.new(EQ, left, right) + end + + # Create not equal (!=) expression + # + # ==== Examples + # # a != 13 + # Exp.ne(Exp.int_bin("a"), Exp.int_val(13)) + def self.ne(left, right) + CmdExp.new(NE, left, right) + end + + # Create greater than (>) operation. + # + # ==== Examples + # # a > 8 + # Exp.gt(Exp.int_bin("a"), Exp.val(8)) + def self.gt(left, right) + CmdExp.new(GT, left, right) + end + + # Create greater than or equal (>=) operation. + # + # ==== Examples + # # a >= 88 + # Exp.ge(Exp.int_bin("a"), Exp.val(88)) + def self.ge(left, right) + CmdExp.new(GE, left, right) + end + + # Create less than (<) operation. + # + # ==== Examples + # # a < 1000 + # Exp.lt(Exp.int_bin("a"), Exp.int_val(1000)) + def self.lt(left, right) + CmdExp.new(LT, left, right) + end + + # Create less than or equals (<=) operation. + # + # ==== Examples + # # a <= 1 + # Exp.le(Exp.int_bin("a"), Exp.int_val(1)) + def self.le(left, right) + CmdExp.new(LE, left, right) + end + + #-------------------------------------------------- + # Number Operator + #-------------------------------------------------- + + # Create "add" (+) operator that applies to a variable number of expressions. + # Return sum of all arguments. All arguments must resolve to the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # a + b + c == 10 + # Exp.eq( + # Exp.add(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.int_val(10)) + def self.add(*exps) + CmdExp.new(ADD, *exps) + end + + # Create "subtract" (-) operator that applies to a variable number of expressions. + # If only one argument is provided, return the negation of that argument. + # Otherwise, return the sum of the 2nd to Nth argument subtracted from the 1st + # argument. All arguments must resolve to the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # a - b - c > 10 + # Exp.gt( + # Exp.sub(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.int_val(10)) + def self.sub(*exps) + CmdExp.new(SUB, *exps) + end + + # Create "multiply" (*) operator that applies to a variable number of expressions. + # Return the product of all arguments. If only one argument is supplied, return + # that argument. All arguments must resolve to the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # a * b * c < 100 + # Exp.lt( + # Exp.mul(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.int_val(100)) + def self.mul(*exps) + CmdExp.new(MUL, *exps) + end + + # Create "divide" (/) operator that applies to a variable number of expressions. + # If there is only one argument, returns the reciprocal for that argument. + # Otherwise, return the first argument divided by the product of the rest. + # All arguments must resolve to the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # a / b / c > 1 + # Exp.gt( + # Exp.div(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.int_val(1)) + def self.div(*exps) + CmdExp.new(DIV, *exps) + end + + # Create "power" operator that raises a "base" to the "exponent" power. + # All arguments must resolve to floats. + # Requires server version 5.6.0+. + # + # ==== Examples + # # pow(a, 2.0) == 4.0 + # Exp.eq( + # Exp.pow(Exp.float_bin("a"), Exp.val(2.0)), + # Exp.val(4.0)) + def self.pow(base, exponent) + CmdExp.new(POW, base, exponent) + end + + # Create "log" operator for logarithm of "num" with base "base". + # All arguments must resolve to floats. + # Requires server version 5.6.0+. + # + # ==== Examples + # # log(a, 2.0) == 4.0 + # Exp.eq( + # Exp.log(Exp.float_bin("a"), Exp.val(2.0)), + # Exp.val(4.0)) + def self.log(num, base) + CmdExp.new(LOG, num, base) + end + + # Create "modulo" (%) operator that determines the remainder of "numerator" + # divided by "denominator". All arguments must resolve to integers. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a % 10 == 0 + # Exp.eq( + # Exp.mod(Exp.int_bin("a"), Exp.int_val(10)), + # Exp.val(0)) + def self.mod(numerator, denominator) + CmdExp.new(MOD, numerator, denominator) + end + + # Create operator that returns absolute value of a number. + # All arguments must resolve to integer or float. + # Requires server version 5.6.0+. + # + # ==== Examples + # # abs(a) == 1 + # Exp.eq( + # Exp.abs(Exp.int_bin("a")), + # Exp.int_val(1)) + def self.abs(value) + CmdExp.new(ABS, value) + end + + # Create expression that rounds a floating point number down to the closest integer value. + # The return type is float. Requires server version 5.6.0+. + # + # ==== Examples + # # floor(2.95) == 2.0 + # Exp.eq( + # Exp.floor(Exp.val(2.95)), + # Exp.val(2.0)) + def self.floor(num) + CmdExp.new(FLOOR, num) + end + + # Create expression that rounds a floating point number up to the closest integer value. + # The return type is float. Requires server version 5.6.0+. + # + # ==== Examples + # # ceil(2.15) >= 3.0 + # Exp.ge( + # Exp.ceil(Exp.val(2.15)), + # Exp.val(3.0)) + def self.ceil(num) + CmdExp.new(CEIL, num) + end + + # Create expression that converts a float to an integer. + # Requires server version 5.6.0+. + # + # ==== Examples + # # int(2.5) == 2 + # Exp.eq( + # Exp.to_int(Exp.val(2.5)), + # Exp.val(2)) + def self.to_int(num) + CmdExp.new(TO_INT, num) + end + + # Create expression that converts an integer to a float. + # Requires server version 5.6.0+. + # + # ==== Examples + # # float(2) == 2.0 + # Exp.eq( + # Exp.to_float(Exp.val(2))), + # Exp.val(2.0)) + def self.to_float(num) + CmdExp.new(TO_FLOAT, num) + end + + # Create integer "and" (&) operator that is applied to two or more integers. + # All arguments must resolve to integers. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a & 0xff == 0x11 + # Exp.eq( + # Exp.int_and(Exp.int_bin("a"), Exp.val(0xff)), + # Exp.val(0x11)) + def self.int_and(*exps) + CmdExp.new(INT_AND, *exps) + end + + # Create integer "or" (|) operator that is applied to two or more integers. + # All arguments must resolve to integers. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a | 0x10 != 0 + # Exp.ne( + # Exp.int_or(Exp.int_bin("a"), Exp.val(0x10)), + # Exp.val(0)) + def self.int_or(*exps) + CmdExp.new(INT_OR, *exps) + end + + # Create integer "xor" (^) operator that is applied to two or more integers. + # All arguments must resolve to integers. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a ^ b == 16 + # Exp.eq( + # Exp.int_xor(Exp.int_bin("a"), Exp.int_bin("b")), + # Exp.int_val(16)) + def self.int_xor(*exps) + CmdExp.new(INT_XOR, *exps) + end + + # Create integer "not" (~) operator. + # Requires server version 5.6.0+. + # + # ==== Examples + # # ~a == 7 + # Exp.eq( + # Exp.int_not(Exp.int_bin("a")), + # Exp.val(7)) + def self.int_not(exp) + CmdExp.new(INT_NOT, exp) + end + + # Create integer "left shift" (<<) operator. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a << 8 > 0xff + # Exp.gt( + # Exp.lshift(Exp.int_bin("a"), Exp.val(8)), + # Exp.val(0xff)) + def self.lshift(value, shift) + CmdExp.new(INT_LSHIFT, value, shift) + end + + # Create integer "logical right shift" (>>>) operator. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a >>> 8 > 0xff + # Exp.gt( + # Exp.rshift(Exp.int_bin("a"), Exp.val(8)), + # Exp.val(0xff)) + def self.rshift(value, shift) + CmdExp.new(INT_RSHIFT, value, shift) + end + + # Create integer "arithmetic right shift" (>>) operator. + # Requires server version 5.6.0+. + # + # ==== Examples + # # a >> 8 > 0xff + # Exp.gt( + # Exp.arshift(Exp.int_bin("a"), Exp.val(8)), + # Exp.val(0xff)) + def self.arshift(value, shift) + CmdExp.new(INT_ARSHIFT, value, shift) + end + + # Create expression that returns count of integer bits that are set to 1. + # Requires server version 5.6.0+. + # + # ==== Examples + # # count(a) == 4 + # Exp.eq( + # Exp.count(Exp.int_bin("a")), + # Exp.val(4)) + def self.count(exp) + CmdExp.new(INT_COUNT, exp) + end + + # Create expression that scans integer bits from left (most significant bit) to + # right (least significant bit), looking for a search bit value. When the + # search value is found, the index of that bit (where the most significant bit is + # index 0) is returned. If "search" is true, the scan will search for the bit + # value 1. If "search" is false it will search for bit value 0. + # Requires server version 5.6.0+. + # + # ==== Examples + # # lscan(a, true) == 4 + # Exp.eq( + # Exp.lscan(Exp.int_bin("a"), Exp.val(true)), + # Exp.val(4)) + def self.lscan(value, search) + CmdExp.new(INT_LSCAN, value, search) + end + + # Create expression that scans integer bits from right (least significant bit) to + # left (most significant bit), looking for a search bit value. When the + # search value is found, the index of that bit (where the most significant bit is + # index 0) is returned. If "search" is true, the scan will search for the bit + # value 1. If "search" is false it will search for bit value 0. + # Requires server version 5.6.0+. + # + # ==== Examples + # # rscan(a, true) == 4 + # Exp.eq( + # Exp.rscan(Exp.int_bin("a"), Exp.val(true)), + # Exp.val(4)) + def self.rscan(value, search) + CmdExp.new(INT_RSCAN, value, search) + end + + # Create expression that returns the minimum value in a variable number of expressions. + # All arguments must be the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # min(a, b, c) > 0 + # Exp.gt( + # Exp.min(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.val(0)) + def self.min(*exps) + CmdExp.new(MIN, *exps) + end + + # Create expression that returns the maximum value in a variable number of expressions. + # All arguments must be the same type (or float). + # Requires server version 5.6.0+. + # + # ==== Examples + # # max(a, b, c) > 100 + # Exp.gt( + # Exp.max(Exp.int_bin("a"), Exp.int_bin("b"), Exp.int_bin("c")), + # Exp.int_val(100)) + def self.max(*exps) + CmdExp.new(MAX, *exps) + end + + #-------------------------------------------------- + # Variables + #-------------------------------------------------- + + # Conditionally select an expression from a variable number of expression pairs + # followed by default expression action. Requires server version 5.6.0+. + # + # ==== Examples + # Args Format: bool exp1, action exp1, bool exp2, action exp2, ..., action-default + # + # # Apply operator based on type:: + # Exp.cond( + # Exp.eq(Exp.int_bin("type"), Exp.val(0)), Exp.add(Exp.int_bin("val1"), Exp.int_bin("val2")), + # Exp.eq(Exp.int_bin("type"), Exp.int_val(1)), Exp.sub(Exp.int_bin("val1"), Exp.int_bin("val2")), + # Exp.eq(Exp.int_bin("type"), Exp.val(2)), Exp.mul(Exp.int_bin("val1"), Exp.int_bin("val2")), + # Exp.val(-1)) + def self.cond(*exps) + CmdExp.new(COND, *exps) + end + + # Define variables and expressions in scope. + # Requires server version 5.6.0+. + # + # ==== Examples + # Args Format: , , ..., + # def: {@link Exp#def(String, Exp)} + # exp: Scoped expression + # + # ==== Examples + # # 5 < a < 10 + # Exp.let( + # Exp.def("x", Exp.int_bin("a")), + # Exp.and( + # Exp.lt(Exp.val(5), Exp.var("x")), + # Exp.lt(Exp.var("x"), Exp.int_val(10)))) + def self.let(*exps) + Let.new(exps) + end + + # Assign variable to a {@link Exp#let(Exp...)} expression that can be accessed later. + # Requires server version 5.6.0+. + # + # ==== Examples + # # 5 < a < 10 + # Exp.let( + # Exp.def("x", Exp.int_bin("a")), + # Exp.and( + # Exp.lt(Exp.val(5), Exp.var("x")), + # Exp.lt(Exp.var("x"), Exp.int_val(10)))) + def self.def(name, value) + Def.new(name, value) + end + + # Retrieve expression value from a variable. + # Requires server version 5.6.0+. + # + # ==== Examples + # # 5 < a < 10 + # Exp.let( + # Exp.def("x", Exp.int_bin("a")), + # Exp.and( + # Exp.lt(Exp.val(5), Exp.var("x")), + # Exp.lt(Exp.var("x"), Exp.int_val(10)))) + def self.var(name) + CmdStr.new(VAR, name) + end + + #-------------------------------------------------- + # Miscellaneous + #-------------------------------------------------- + + # Create unknown value. Used to intentionally fail an expression. + # The failure can be ignored with {@link Exp::WriteFlags#EVAL_NO_FAIL} + # or {@link Exp::ReadFlags#EVAL_NO_FAIL}. + # Requires server version 5.6.0+. + # + # ==== Examples + # # double v = balance - 100.0 + # # return (v > 0.0)? v : unknown + # Exp.let( + # Exp.def("v", Exp.sub(Exp.float_bin("balance"), Exp.int_val(100.0))), + # Exp.cond( + # Exp.ge(Exp.var("v"), Exp.val(0.0)), Exp.var("v"), + # Exp.unknown)) + def self.unknown + Cmd.new(UNKNOWN) + end + + # # Merge precompiled expression into a new expression tree. + # # Useful for storing common precompiled expressions and then reusing + # # these expressions as part of a greater expression. + # # + # # ==== Examples + # # # Merge precompiled expression into new expression. + # # Expression e = Exp.build(Exp.eq(Exp.int_bin("a"), Exp.val(200))) + # # Expression merged = Exp.build(Exp.and(Exp.expr(e), Exp.eq(Exp.int_bin("b"), Exp.int_val(100)))) + # def self.expr(Expression e) + # new ExpBytes.new(e) + # end + + #-------------------------------------------------- + # Internal + #-------------------------------------------------- + MODIFY = 0x40 + + def bytes + if @bytes.nil? + Packer.use do |packer| + pack(packer) + @bytes = packer.bytes + end + end + @bytes + end + + # Estimate expression size in wire protocol. + # For internal use only. + def size + bytes.length + end + + # Write expression in wire protocol. + # For internal use only. + def write(buf, offset) + buf.write_binary(bytes, offset) + end + + private + + UNKNOWN = 0 + EQ = 1 + NE = 2 + GT = 3 + GE = 4 + LT = 5 + LE = 6 + REGEX = 7 + GEO = 8 + AND = 16 + OR = 17 + NOT = 18 + EXCLUSIVE = 19 + ADD = 20 + SUB = 21 + MUL = 22 + DIV = 23 + POW = 24 + LOG = 25 + MOD = 26 + ABS = 27 + FLOOR = 28 + CEIL = 29 + TO_INT = 30 + TO_FLOAT = 31 + INT_AND = 32 + INT_OR = 33 + INT_XOR = 34 + INT_NOT = 35 + INT_LSHIFT = 36 + INT_RSHIFT = 37 + INT_ARSHIFT = 38 + INT_COUNT = 39 + INT_LSCAN = 40 + INT_RSCAN = 41 + MIN = 50 + MAX = 51 + DIGEST_MODULO = 64 + DEVICE_SIZE = 65 + LAST_UPDATE = 66 + SINCE_UPDATE = 67 + VOID_TIME = 68 + TTL = 69 + SET_NAME = 70 + KEY_EXISTS = 71 + IS_TOMBSTONE = 72 + MEMORY_SIZE = 73 + KEY = 80 + BIN = 81 + BIN_TYPE = 82 + COND = 123 + VAR = 124 + LET = 125 + QUOTED = 126 + CALL = 127 + NANOS_PER_MILLIS = 1000000 + + def self.pack(ctx, command, *vals) + Packer.use do |packer| + # ctx is not support for bit commands + packer.write_array_header(vals.to_a.length + 1) + packer.write(command) + vals.each do |v| + if v.is_a?(Exp) + v.pack(packer) + else + Value.of(v).pack(packer) + end + end + return packer.bytes + end + end + + def self.pack_ctx(packer, ctx) + unless ctx.to_a.empty? + packer.write_array_header(3) + packer.write(0xff) + packer.write_array_header(ctx.length * 2) + + ctx.each do |c| + packer.write(c.id) + c.value.pack(packer) + end + end + end + + # For internal use only. + class Module < Exp + attr_reader :bin + attr_reader :bytes + attr_reader :ret_type + attr_reader :module + + def initialize(bin, bytes, ret_type, modul) + @bin = bin + @bytes = bytes + @ret_type = ret_type + @module = modul + end + + def pack(packer) + packer.write_array_header(5) + packer.write(Exp::CALL) + packer.write(@ret_type) + packer.write(@module) + # packer.pack_byte_array(@bytes, 0, @bytes.length) + packer.write_raw(@bytes) + @bin.pack(packer) + end + end + + class Bin < Exp + attr_reader :name + attr_reader :type + + def initialize(name, type) + @name = name + @type = type + end + + def pack(packer) + packer.write_array_header(3) + packer.write(BIN) + packer.write(@type) + packer.write(@name) + end + end + + class Regex < Exp + attr_reader :bin + attr_reader :regex + attr_reader :flags + + def initialize(bin, regex, flags) + @bin = bin + @regex = regex + @flags = flags + end + + def pack(packer) + packer.write_array_header(4) + packer.write(REGEX) + packer.write(@flags) + packer.write(@regex) + @bin.pack(packer) + end + end + + class Let < Exp + attr_reader :exps + + def initialize(exps) + @exps = exps + end + + def pack(packer) + # Let wire format: LET , , , , ..., + count = (@exps.length - 1) * 2 + 2 + packer.write_array_header(count) + packer.write(LET) + + @exps.each do |exp| + exp.pack(packer) + end + end + end + + class Def < Exp + attr_reader :name + attr_reader :exp + + def initialize(name, exp) + @name = name + @exp = exp + end + + def pack(packer) + packer.write(@name) + @exp.pack(packer) + end + end + + class CmdExp < Exp + attr_reader :exps + attr_reader :cmd + + def initialize(cmd, *exps) + @exps = exps + @cmd = cmd + end + + def pack(packer) + packer.write_array_header(@exps.length + 1) + packer.write(@cmd) + @exps.each do |exp| + exp.pack(packer) + end + end + end + + class CmdInt < Exp + attr_reader :cmd + attr_reader :val + + def initialize(cmd, val) + @cmd = cmd + @val = val + end + + def pack(packer) + packer.write_array_header(2) + Value.of(@cmd).pack(packer) + Value.of(@val).pack(packer) + end + end + + class CmdStr < Exp + attr_reader :str + attr_reader :cmd + + def initialize(cmd, str) + @str = str + @cmd = cmd + end + + def pack(packer) + packer.write_array_header(2) + Value.of(@cmd).pack(packer) + packer.write(@str) + end + end + + class Cmd < Exp + attr_reader :cmd + + def initialize(cmd) + @cmd = cmd + end + + def pack(packer) + packer.write_array_header(1) + packer.write(@cmd) + end + end + + class Bool < Exp + attr_reader :val + + def initialize(val) + @val = val + end + + def pack(packer) + BoolValue.new(@val).pack(packer) + end + end + + class Int < Exp + attr_reader :val + + def initialize(val) + @val = val.to_i + end + + def pack(packer) + IntegerValue.new(@val).pack(packer) + end + end + + class Float < Exp + attr_reader :val + + def initialize(val) + @val = val.to_f + end + + def pack(packer) + FloatValue.new(@val).pack(packer) + end + end + + class Str < Exp + attr_reader :val + + def initialize(val) + @val = val + end + + def pack(packer) + StringValue.new(@val).pack(packer) + end + end + + class Geo < Exp + attr_reader :val + + def initialize(val) + @val = val + end + + def pack(packer) + Value.of(@val).pack(packer) + end + end + + class Blob < Exp + attr_reader :val + + def initialize(val) + @val = val + end + + def pack(packer) + BytesValue.new(@val).pack(packer) + end + end + + class ListVal < Exp + attr_reader :list + + def initialize(list) + @list = list + end + + def pack(packer) + # List values need an extra array and QUOTED in order to distinguish + # between a multiple argument array call and a local list. + packer.write_array_header(2) + packer.write(QUOTED) + Value.of(@list).pack(packer) + end + end + + class MapVal < Exp + attr_reader :map + + def initialize(map) + @map = map + end + + def pack(packer) + Value.of(@map).pack(packer) + end + end + + class Nil < Exp + def pack(packer) + Value.of(nil).pack(packer) + end + end + + class ExpBytes < Exp + attr_reader :bytes + + def initialize(e) + @bytes = e.bytes + end + + def pack(packer) + Value.of(@bytes).pack(packer) + end + end + end # class Exp +end # module diff --git a/lib/aerospike/exp/exp_bit.rb b/lib/aerospike/exp/exp_bit.rb new file mode 100644 index 00000000..2824d313 --- /dev/null +++ b/lib/aerospike/exp/exp_bit.rb @@ -0,0 +1,388 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may no +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + # Bit expression generator. See {@link Exp}. + # + # The bin expression argument in these methods can be a reference to a bin or the + # result of another expression. Expressions that modify bin values are only used + # for temporary expression evaluation and are not permanently applied to the bin. + # Bit modify expressions the blob bin's value. + # + # Offset orientation is left-to-right. Negative offsets are supported. + # If the offset is negative, the offset starts backwards from end of the bitmap. + # If an offset is out of bounds, a parameter error will be returned. + class Exp::Bit + # Create expression that resizes _byte[]_ to _byte_size_ according to _resize_flags_ (See {CDT::BitResizeFlags}) + # and returns byte[]. + # + # bin = [0b00000001, 0b01000010] + # byte_size = 4 + # resize_flags = 0 + # returns [0b00000001, 0b01000010, 0b00000000, 0b00000000] + # + # ==== Examples + # # Resize bin "a" and compare bit count + # Exp.eq( + # BitExp.count(Exp.val(0), Exp.val(3), + # BitExp.resize(BitPolicy.Default, Exp.val(4), 0, Exp.blobBin("a"))), + # Exp.val(2)) + def self.resize(byte_size, resize_flags, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, RESIZE, byte_size, policy.flags, resize_flags) + self.add_write(bin, bytes) + end + + # Create expression that inserts value bytes into byte[] bin at byte_offset and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # byte_offset = 1 + # value = [0b11111111, 0b11000111] + # bin result = [0b00000001, 0b11111111, 0b11000111, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # + # ==== Examples + # # Insert bytes into bin "a" and compare bit count + # Exp.eq( + # BitExp.count(Exp.val(0), Exp.val(3), + # BitExp.insert(BitPolicy.Default, Exp.val(1), Exp.val(bytes), Exp.blobBin("a"))), + # Exp.val(2)) + def self.insert(byte_offset, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, INSERT, byte_offset, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that removes bytes from byte[] bin at byte_offset for byte_size and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # byte_offset = 2 + # byte_size = 3 + # bin result = [0b00000001, 0b01000010] + # + # ==== Examples + # # Remove bytes from bin "a" and compare bit count + # Exp.eq( + # BitExp.count(Exp.val(0), Exp.val(3), + # BitExp.remove(BitPolicy.Default, Exp.val(2), Exp.val(3), Exp.blobBin("a"))), + # Exp.val(2)) + def self.remove(byte_offset, byte_size, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, REMOVE, byte_offset, byte_size, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that sets value on byte[] bin at bit_offset for bit_size and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 13 + # bit_size = 3 + # value = [0b11100000] + # bin result = [0b00000001, 0b01000111, 0b00000011, 0b00000100, 0b00000101] + # + # ==== Examples + # # Set bytes in bin "a" and compare bit count + # Exp.eq( + # BitExp.count(Exp.val(0), Exp.val(3), + # BitExp.set(BitPolicy.Default, Exp.val(13), Exp.val(3), Exp.val(bytes), Exp.blobBin("a"))), + # Exp.val(2)) + def self.set(bit_offset, bit_size, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, SET, bit_offset, bit_size, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that performs bitwise "or" on value and byte[] bin at bit_offset for bit_size + # and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 17 + # bit_size = 6 + # value = [0b10101000] + # bin result = [0b00000001, 0b01000010, 0b01010111, 0b00000100, 0b00000101] + # + def self.or(bit_offset, bit_size, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, OR, bit_offset, bit_size, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that performs bitwise "xor" on value and byte[] bin at bit_offset for bit_size + # and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 17 + # bit_size = 6 + # value = [0b10101100] + # bin result = [0b00000001, 0b01000010, 0b01010101, 0b00000100, 0b00000101] + # + def self.xor(bit_offset, bit_size, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, XOR, bit_offset, bit_size, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that performs bitwise "and" on value and byte[] bin at bit_offset for bit_size + # and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 23 + # bit_size = 9 + # value = [0b00111100, 0b10000000] + # bin result = [0b00000001, 0b01000010, 0b00000010, 0b00000000, 0b00000101] + # + def self.and(bit_offset, bit_size, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, AND, bit_offset, bit_size, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that negates byte[] bin starting at bit_offset for bit_size and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 25 + # bit_size = 6 + # bin result = [0b00000001, 0b01000010, 0b00000011, 0b01111010, 0b00000101] + # + def self.not(bit_offset, bit_size, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, NOT, bit_offset, bit_size, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that shifts left byte[] bin starting at bit_offset for bit_size and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 32 + # bit_size = 8 + # shift = 3 + # bin result = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00101000] + # + def self.lshift(bit_offset, bit_size, shift, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, LSHIFT, bit_offset, bit_size, shift, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that shifts right byte[] bin starting at bit_offset for bit_size and returns byte[]. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 0 + # bit_size = 9 + # shift = 1 + # bin result = [0b00000000, 0b11000010, 0b00000011, 0b00000100, 0b00000101] + # + def self.rshift(bit_offset, bit_size, shift, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, RSHIFT, bit_offset, bit_size, shift, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that adds value to byte[] bin starting at bit_offset for bit_size and returns byte[]. + # BitSize must be <= 64. Signed indicates if bits should be treated as a signed number. + # If add overflows/underflows, {@link BitOverflowAction} is used. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 24 + # bit_size = 16 + # value = 128 + # signed = false + # bin result = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b10000101] + # + def self.add(bit_offset, bit_size, value, signed, bit_overflow_action, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = self.pack_math(ADD, policy, bit_offset, bit_size, value, signed, bit_overflow_action) + self.add_write(bin, bytes) + end + + # Create expression that subtracts value from byte[] bin starting at bit_offset for bit_size and returns byte[]. + # BitSize must be <= 64. Signed indicates if bits should be treated as a signed number. + # If add overflows/underflows, {@link BitOverflowAction} is used. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 24 + # bit_size = 16 + # value = 128 + # signed = false + # bin result = [0b00000001, 0b01000010, 0b00000011, 0b0000011, 0b10000101] + # + def self.subtract(bit_offset, bit_size, value, signed, bit_overflow_action, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = self.pack_math(SUBTRACT, policy, bit_offset, bit_size, value, signed, bit_overflow_action) + self.add_write(bin, bytes) + end + + # Create expression that sets value to byte[] bin starting at bit_offset for bit_size and returns byte[]. + # BitSize must be <= 64. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 1 + # bit_size = 8 + # value = 127 + # bin result = [0b00111111, 0b11000010, 0b00000011, 0b0000100, 0b00000101] + # + def self.set_int(bit_offset, bit_size, value, bin, policy: CDT::BitPolicy::DEFAULT) + bytes = Exp.pack(nil, SET_INT, bit_offset, bit_size, value, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that returns bits from byte[] bin starting at bit_offset for bit_size. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 9 + # bit_size = 5 + # returns [0b10000000] + # + # ==== Examples + # # Bin "a" bits = [0b10000000] + # Exp.eq( + # BitExp.get(Exp.val(9), Exp.val(5), Exp.blobBin("a")), + # Exp.val(new byte[] {(byte)0b10000000})) + def self.get(bit_offset, bit_size, bin) + bytes = Exp.pack(nil, GET, bit_offset, bit_size) + self.add_read(bin, bytes, Exp::Type::BLOB) + end + + # Create expression that returns integer count of set bits from byte[] bin starting at + # bit_offset for bit_size. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 20 + # bit_size = 4 + # returns 2 + # + # ==== Examples + # # Bin "a" bit count <= 2 + # Exp.le(BitExp.count(Exp.val(0), Exp.val(5), Exp.blobBin("a")), Exp.val(2)) + def self.count(bit_offset, bit_size, bin) + bytes = Exp.pack(nil, COUNT, bit_offset, bit_size) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns integer bit offset of the first specified value bit in byte[] bin + # starting at bit_offset for bit_size. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 24 + # bit_size = 8 + # value = true + # returns 5 + # + # ==== Examples + # # lscan(a) == 5 + # Exp.eq(BitExp.lscan(Exp.val(24), Exp.val(8), Exp.val(true), Exp.blobBin("a")), Exp.val(5)) + # + # @param bit_offset offset int expression + # @param bit_size size int expression + # @param value boolean expression + # @param bin bin or blob value expression + def self.lscan(bit_offset, bit_size, value, bin) + bytes = Exp.pack(nil, LSCAN, bit_offset, bit_size, value) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns integer bit offset of the last specified value bit in byte[] bin + # starting at bit_offset for bit_size. + # Example: + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 32 + # bit_size = 8 + # value = true + # returns 7 + # + # ==== Examples + # # rscan(a) == 7 + # Exp.eq(BitExp.rscan(Exp.val(32), Exp.val(8), Exp.val(true), Exp.blobBin("a")), Exp.val(7)) + # + # @param bit_offset offset int expression + # @param bit_size size int expression + # @param value boolean expression + # @param bin bin or blob value expression + def self.rscan(bit_offset, bit_size, value, bin) + bytes = Exp.pack(nil, RSCAN, bit_offset, bit_size, value) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns integer from byte[] bin starting at bit_offset for bit_size. + # Signed indicates if bits should be treated as a signed number. + # + # bin = [0b00000001, 0b01000010, 0b00000011, 0b00000100, 0b00000101] + # bit_offset = 8 + # bit_size = 16 + # signed = false + # returns 16899 + # + # ==== Examples + # # getInt(a) == 16899 + # Exp.eq(BitExp.getInt(Exp.val(8), Exp.val(16), false, Exp.blobBin("a")), Exp.val(16899)) + def self.get_int(bit_offset, bit_size, signed, bin) + bytes = self.pack_get_int(bit_offset, bit_size, signed) + self.add_read(bin, bytes, Exp::Type::INT) + end + + private + + MODULE = 1 + RESIZE = 0 + INSERT = 1 + REMOVE = 2 + SET = 3 + OR = 4 + XOR = 5 + AND = 6 + NOT = 7 + LSHIFT = 8 + RSHIFT = 9 + ADD = 10 + SUBTRACT = 11 + SET_INT = 12 + GET = 50 + COUNT = 51 + LSCAN = 52 + RSCAN = 53 + GET_INT = 54 + + INT_FLAGS_SIGNED = 1 + + def self.pack_math(command, policy, bit_offset, bit_size, value, signed, bit_overflow_action) + Packer.use do |packer| + # Pack.init only required when CTX is used and server does not support CTX for bit operations. + # Pack.init(packer, ctx) + packer.write_array_header(6) + packer.write(command) + bit_offset.pack(packer) + bit_size.pack(packer) + value.pack(packer) + packer.write(policy.flags) + + flags = bit_overflow_action + flags |= INT_FLAGS_SIGNED if signed + + packer.write(flags) + return packer.bytes + end + end + + def self.pack_get_int(bit_offset, bit_size, signed) + Packer.use do |packer| + # Pack.init only required when CTX is used and server does not support CTX for bit operations. + # Pack.init(packer, ctx) + packer.write_array_header(signed ? 4 : 3) + packer.write(GET_INT) + bit_offset.pack(packer) + bit_size.pack(packer) + packer.write(INT_FLAGS_SIGNED) if signed + return packer.bytes + end + end + + def self.add_write(bin, bytes) + Exp::Module.new(bin, bytes, Exp::Type::BLOB, MODULE | Exp::MODIFY) + end + + def self.add_read(bin, bytes, ret_type) + Exp::Module.new(bin, bytes, ret_type, MODULE) + end + end # class +end # module diff --git a/lib/aerospike/exp/exp_hll.rb b/lib/aerospike/exp/exp_hll.rb new file mode 100644 index 00000000..16b4c53e --- /dev/null +++ b/lib/aerospike/exp/exp_hll.rb @@ -0,0 +1,169 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License") you may no +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + # HyperLogLog (HLL) expression generator. See {@link Exp}. + # + # The bin expression argument in these methods can be a reference to a bin or the + # result of another expression. Expressions that modify bin values are only used + # for temporary expression evaluation and are not permanently applied to the bin. + # HLL modify expressions return the HLL bin's value. + class Exp::HLL + + # Create expression that creates a new HLL or resets an existing HLL with minhash bits. + # + # @param policy write policy, use {@link HLLPolicy#Default} for default + # @param index_bit_count number of index bits. Must be between 4 and 16 inclusive. + # @param min_hash_bit_count number of min hash bits. Must be between 4 and 51 inclusive. + # Also, index_bit_count + min_hash_bit_count must be <= 64. Optional. + # @param bin HLL bin or value expression + def self.init(index_bit_count, bin, min_hash_bit_count: Exp.int_val(-1), policy: CDT::HLLPolicy::DEFAULT) + bytes = Exp.pack(nil, INIT, index_bit_count, min_hash_bit_count, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that adds values to a HLL set and returns HLL set. If HLL bin does not + # exist, use index_bit_count and min_hash_bit_count to create HLL set. + # + # ==== Examples + # # Add values to HLL bin "a" and check count > 7 + # Exp.gt( + # HLLExp.getCount( + # HLLExp.add(HLLPolicy.Default, Exp.val(list), Exp.val(10), Exp.val(20), Exp.hllBin("a"))), + # Exp.val(7)) + # + # @param policy write policy, use {@link HLLPolicy#Default} for default + # @param list list bin or value expression of values to be added + # @param index_bit_count number of index bits expression. Must be between 4 and 16 inclusive. + # @param min_hash_bit_count number of min hash bits expression. Must be between 4 and 51 inclusive. + # Also, index_bit_count + min_hash_bit_count must be <= 64. + # @param bin HLL bin or value expression + def self.add(list, bin, policy: CDT::HLLPolicy::DEFAULT, index_bit_count: Exp.val(-1), min_hash_bit_count: Exp.val(-1)) + bytes = Exp.pack(nil, ADD, list, index_bit_count, min_hash_bit_count, policy.flags) + self.add_write(bin, bytes) + end + + # Create expression that returns estimated number of elements in the HLL bin. + # + # ==== Examples + # # HLL bin "a" count > 7 + # Exp.gt(HLLExp.getCount(Exp.hllBin("a")), Exp.val(7)) + def self.get_count(bin) + bytes = Exp.pack(nil, COUNT) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns a HLL object that is the union of all specified HLL objects + # in the list with the HLL bin. + # + # ==== Examples + # # Union of HLL bins "a" and "b" + # HLLExp.getUnion(Exp.hllBin("a"), Exp.hllBin("b")) + # + # # Union of local HLL list with bin "b" + # HLLExp.getUnion(Exp.val(list), Exp.hllBin("b")) + def self.get_union(list, bin) + bytes = Exp.pack(nil, UNION, list) + self.add_read(bin, bytes, Exp::Type::HLL) + end + + # Create expression that returns estimated number of elements that would be contained by + # the union of these HLL objects. + # + # ==== Examples + # # Union count of HLL bins "a" and "b" + # HLLExp.getUnionCount(Exp.hllBin("a"), Exp.hllBin("b")) + # + # # Union count of local HLL list with bin "b" + # HLLExp.getUnionCount(Exp.val(list), Exp.hllBin("b")) + def self.get_union_count(list, bin) + bytes = Exp.pack(nil, UNION_COUNT, list) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns estimated number of elements that would be contained by + # the intersection of these HLL objects. + # + # ==== Examples + # # Intersect count of HLL bins "a" and "b" + # HLLExp.getIntersectCount(Exp.hllBin("a"), Exp.hllBin("b")) + # + # # Intersect count of local HLL list with bin "b" + # HLLExp.getIntersectCount(Exp.val(list), Exp.hllBin("b")) + def self.get_intersect_count(list, bin) + bytes = Exp.pack(nil, INTERSECT_COUNT, list) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that returns estimated similarity of these HLL objects as a + # 64 bit float. + # + # ==== Examples + # # Similarity of HLL bins "a" and "b" >= 0.75 + # Exp.ge(HLLExp.getSimilarity(Exp.hllBin("a"), Exp.hllBin("b")), Exp.val(0.75)) + def self.get_similarity(list, bin) + bytes = Exp.pack(nil, SIMILARITY, list) + self.add_read(bin, bytes, Exp::Type::FLOAT) + end + + # Create expression that returns index_bit_count and min_hash_bit_count used to create HLL bin + # in a list of longs. list[0] is index_bit_count and list[1] is min_hash_bit_count. + # + # ==== Examples + # # Bin "a" index_bit_count < 10 + # Exp.lt( + # ListExp.getByIndex(ListReturnType.VALUE, Exp::Type::INT, Exp.val(0), + # HLLExp.describe(Exp.hllBin("a"))), + # Exp.val(10)) + def self.describe(bin) + bytes = Exp.pack(nil, DESCRIBE) + self.add_read(bin, bytes, Exp::Type::LIST) + end + + # Create expression that returns one if HLL bin may contain all items in the list. + # + # ==== Examples + # # Bin "a" may contain value "x" + # ArrayList list = new ArrayList() + # list.add(Value.get("x")) + # Exp.eq(HLLExp.mayContain(Exp.val(list), Exp.hllBin("a")), Exp.val(1)) + def self.may_contain(list, bin) + bytes = Exp.pack(nil, MAY_CONTAIN, list) + self.add_read(bin, bytes, Exp::Type::INT) + end + + private + + MODULE = 2 + INIT = 0 + ADD = 1 + COUNT = 50 + UNION = 51 + UNION_COUNT = 52 + INTERSECT_COUNT = 53 + SIMILARITY = 54 + DESCRIBE = 55 + MAY_CONTAIN = 56 + + def self.add_write(bin, bytes) + Exp::Module.new(bin, bytes, Exp::Type::HLL, MODULE | Exp::MODIFY) + end + + def self.add_read(bin, bytes, ret_type) + Exp::Module.new(bin, bytes, ret_type, MODULE) + end + end +end diff --git a/lib/aerospike/exp/exp_list.rb b/lib/aerospike/exp/exp_list.rb new file mode 100644 index 00000000..70a03007 --- /dev/null +++ b/lib/aerospike/exp/exp_list.rb @@ -0,0 +1,403 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License") you may n +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + + # List expression generator. See {@link Exp}. + # + # The bin expression argument in these methods can be a reference to a bin or the + # result of another expression. Expressions that modify bin values are only used + # for temporary expression evaluation and are not permanently applied to the bin. + # + # List modify expressions the bin's value. This value will be a list except + # when the list is nested within a map. In that case, a map is returned for the + # list modify expression. + # + # List expressions support negative indexing. If the index is negative, the + # resolved index starts backwards from end of list. If an index is out of bounds, + # a parameter error will be returned. If a range is partially out of bounds, the + # valid part of the range will be returned. Index/Range examples: + # + # Index 0: First item in list. + # Index 4: Fifth item in list. + # Index -1: Last item in list. + # Index -3: Third to last item in list. + # Index 1 Count 2: Second and third items in list. + # Index -3 Count 3: Last three items in list. + # Index -5 Count 4: Range between fifth to last item to second to last item inclusive. + # + # Nested expressions are supported by optional CTX context arguments. Example: + # + # bin = [[7,9,5],[1,2,3],[6,5,4,1]] + # Get size of last list. + # ListExp.size(Exp.listBin("bin"), CTX.listIndex(-1)) + # result = 4 + class Exp::List + + # Create expression that appends value to end of list. + def self.append(value, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, APPEND, value, policy.order, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that appends list items to end of list. + def self.append_items(list, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, APPEND_ITEMS, list, policy.order, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that inserts value to specified index of list. + def self.insert(index, value, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, INSERT, index, value, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that inserts each input list item starting at specified index of list. + def self.insert_items(index, list, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, INSERT_ITEMS, index, list, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that increments list[index] by value. + # Value expression should resolve to a number. + def self.increment(index, value, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, INCREMENT, index, value, policy.order, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that sets item value at specified index in list. + def self.set(index, value, bin, ctx: nil, policy: CDT::ListPolicy::DEFAULT) + bytes = Exp.pack(ctx, SET, index, value, policy.flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes all items in list. + def self.clear(bin, ctx: nil) + bytes = Exp.pack(ctx, CLEAR) + self.add_write(bin, bytes, ctx) + end + + # Create expression that sorts list according to sort_flags. + # + # @param sort_flags sort flags. See {@link ListSortFlagsend. + # @param bin bin or list value expression + # @param ctx optional context path for nested CDT + def self.sort(sort_flags, bin, ctx: nil) + bytes = Exp.pack(ctx, SORT, sort_flags) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list items identified by value. + def self.remove_by_value(value, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE, CDT::ListReturnType::NONE, value) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list items identified by values. + def self.remove_by_value_list(values, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_LIST, CDT::ListReturnType::NONE, values) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list items identified by value range (value_begin inclusive, value_end exclusive). + # If value_begin is nil, the range is less than value_end. If value_end is nil, the range is + # greater than equal to value_begin. + def self.remove_by_value_range(value_begin, value_end, bin, ctx: nil) + bytes = self.pack_range_operation(REMOVE_BY_VALUE_INTERVAL, CDT::ListReturnType::NONE, value_begin, value_end, ctx) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list items nearest to value and greater by relative rank with a count limit if provided. + # + # Examples for ordered list [0,4,5,9,11,15]: + # + # (value,rank,count) = [removed items] + # (5,0,2) = [5,9] + # (5,1,1) = [9] + # (5,-1,2) = [4,5] + # (3,0,1) = [4] + # (3,3,7) = [11,15] + # (3,-3,2) = [] + def self.remove_by_value_relative_rank_range(value, rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_REL_RANK_RANGE, CDT::ListReturnType::NONE, value, rank, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_REL_RANK_RANGE, CDT::ListReturnType::NONE, value, rank) + end + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list item identified by index. + def self.remove_by_index(index, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_INDEX, CDT::ListReturnType::NONE, index) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes "count" list items starting at specified index. + def self.remove_by_index_range(index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_INDEX_RANGE, CDT::ListReturnType::NONE, index, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_INDEX_RANGE, CDT::ListReturnType::NONE, index) + end + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes list item identified by rank. + def self.remove_by_rank(rank, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_RANK, CDT::ListReturnType::NONE, rank) + self.add_write(bin, bytes, ctx) + end + + # Create expression that removes "count" list items starting at specified rank. + def self.remove_by_rank_range(rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_RANK_RANGE, CDT::ListReturnType::NONE, rank, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_RANK_RANGE, CDT::ListReturnType::NONE, rank) + end + self.add_write(bin, bytes, ctx) + end + + # Create expression that returns list size. + # + # ==== Examples + # # List bin "a" size > 7 + # Exp.gt(ListExp.size(Exp.listBin("a")), Exp.val(7)) + # end + def self.size(bin, ctx: nil) + bytes = Exp.pack(ctx, SIZE) + self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that selects list items identified by value and returns selected + # data specified by return_type. + # + # ==== Examples + # # List bin "a" contains at least one item == "abc" + # Exp.gt( + # ListExp.getByValue(CDT::ListReturnType::COUNT, Exp.val("abc"), Exp.listBin("a")), + # Exp.val(0)) + # end + # + # @param return_type metadata attributes to return. See {@link CDT::ListReturnTypeend + # @param value search expression + # @param bin list bin or list value expression + # @param ctx optional context path for nested CDT + def self.get_by_value(return_type, value, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_VALUE, return_type, value) + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects list items identified by value range and returns selected data + # specified by return_type. + # + # ==== Examples + # # List bin "a" items >= 10 && items < 20 + # ListExp.getByValueRange(CDT::ListReturnType::VALUE, Exp.val(10), Exp.val(20), Exp.listBin("a")) + # end + # + # @param return_type metadata attributes to return. See {@link CDT::ListReturnTypeend + # @param value_begin begin expression inclusive. If nil, range is less than value_end. + # @param value_end end expression exclusive. If nil, range is greater than equal to value_begin. + # @param bin bin or list value expression + # @param ctx optional context path for nested CDT + def self.get_by_value_range(return_type, value_begin, value_end, bin, ctx: nil) + bytes = self.pack_range_operation(GET_BY_VALUE_INTERVAL, return_type, value_begin, value_end, ctx) + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects list items identified by values and returns selected data + # specified by return_type. + def self.get_by_value_list(return_type, values, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_VALUE_LIST, return_type, values) + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects list items nearest to value and greater by relative rank with a count limit + # and returns selected data specified by return_type (See {@link CDT::ListReturnTypeend). + # + # Examples for ordered list [0,4,5,9,11,15]: + # + # (value,rank,count) = [selected items] + # (5,0,2) = [5,9] + # (5,1,1) = [9] + # (5,-1,2) = [4,5] + # (3,0,1) = [4] + # (3,3,7) = [11,15] + # (3,-3,2) = [] + def self.get_by_value_relative_rank_range(return_type, value, rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_VALUE_REL_RANK_RANGE, return_type, value, rank, count) + else + bytes = Exp.pack(ctx, GET_BY_VALUE_REL_RANK_RANGE, return_type, value, rank) + end + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects list item identified by index and returns + # selected data specified by return_type. + # + # ==== Examples + # # a[3] == 5 + # Exp.eq( + # ListExp.getByIndex(CDT::ListReturnType::VALUE, Exp::Type::INT, Exp.val(3), Exp.listBin("a")), + # Exp.val(5)) + # end + # + # @param return_type metadata attributes to return. See {@link CDT::ListReturnTypeend + # @param value_type expected type of value + # @param index list index expression + # @param bin list bin or list value expression + # @param ctx optional context path for nested CDT + def self.get_by_index(return_type, value_type, index, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_INDEX, return_type, index) + self.add_read(bin, bytes, value_type) + end + + # Create expression that selects list items starting at specified index to the end of list + # and returns selected data specified by return_type (See {@link CDT::ListReturnTypeend). + def self.get_by_index_range(return_type, index, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_INDEX_RANGE, return_type, index) + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects "count" list items starting at specified index + # and returns selected data specified by return_type (See {@link CDT::ListReturnTypeend). + def self.get_by_index_range(return_type, index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_INDEX_RANGE, return_type, index, count) + else + bytes = Exp.pack(ctx, GET_BY_INDEX_RANGE, return_type, index) + end + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects list item identified by rank and returns selected + # data specified by return_type. + # + # ==== Examples + # # Player with lowest score. + # ListExp.getByRank(CDT::ListReturnType::VALUE, Type.STRING, Exp.val(0), Exp.listBin("a")) + # end + # + # @param return_type metadata attributes to return. See {@link CDT::ListReturnTypeend + # @param value_type expected type of value + # @param rank rank expression + # @param bin list bin or list value expression + # @param ctx optional context path for nested CDT + def self.get_by_rank(return_type, value_type, rank, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_RANK, return_type, rank) + self.add_read(bin, bytes, value_type) + end + + # Create expression that selects list items starting at specified rank to the last ranked item + # and returns selected data specified by return_type (See {@link CDT::ListReturnTypeend). + def self.get_by_rank_range(return_type, rank, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_RANK_RANGE, return_type, rank) + self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects "count" list items starting at specified rank and returns + # selected data specified by return_type (See {@link CDT::ListReturnTypeend). + def self.get_by_rank_range(return_type, rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_RANK_RANGE, return_type, rank, count) + else + bytes = Exp.pack(ctx, GET_BY_RANK_RANGE, return_type, rank) + end + self.add_read(bin, bytes, get_value_type(return_type)) + end + + private + + MODULE = 0 + APPEND = 1 + APPEND_ITEMS = 2 + INSERT = 3 + INSERT_ITEMS = 4 + SET = 9 + CLEAR = 11 + INCREMENT = 12 + SORT = 13 + SIZE = 16 + GET_BY_INDEX = 19 + GET_BY_RANK = 21 + GET_BY_VALUE = 22 # GET_ALL_BY_VALUE on server + GET_BY_VALUE_LIST = 23 + GET_BY_INDEX_RANGE = 24 + GET_BY_VALUE_INTERVAL = 25 + GET_BY_RANK_RANGE = 26 + GET_BY_VALUE_REL_RANK_RANGE = 27 + REMOVE_BY_INDEX = 32 + REMOVE_BY_RANK = 34 + REMOVE_BY_VALUE = 35 + REMOVE_BY_VALUE_LIST = 36 + REMOVE_BY_INDEX_RANGE = 37 + REMOVE_BY_VALUE_INTERVAL = 38 + REMOVE_BY_RANK_RANGE = 39 + REMOVE_BY_VALUE_REL_RANK_RANGE = 40 + + def self.add_write(bin, bytes, ctx) + if ctx.to_a.empty? + ret_type = Exp::Type::LIST + else + ret_type = ((ctx[0].id & 0x10) == 0) ? Exp::Type::MAP : Exp::Type::LIST + end + Exp::Module.new(bin, bytes, ret_type, MODULE | Exp::MODIFY) + end + + def self.add_read(bin, bytes, ret_type) + Exp::Module.new(bin, bytes, ret_type, MODULE) + end + + def self.get_value_type(return_type) + if (return_type & ~CDT::ListReturnType::INVERTED) == CDT::ListReturnType::VALUE + Exp::Type::LIST + else + Exp::Type::INT + end + end + + def self.pack_range_operation(command, return_type, value_begin, value_end, ctx) + Packer.use do |packer| + Exp.pack_ctx(packer, ctx) + packer.write_array_header(value_end.nil? ? 3 : 4) + packer.write(command) + packer.write(return_type) + + unless value_begin.nil? + if value_begin.is_a?(Exp) + value_begin.pack(packer) + else + Value.of(value_begin).pack(packer) + end + else + packer.write(nil) + end + + unless value_end.nil? + if value_end.is_a?(Exp) + value_end.pack(packer) + else + Value.of(value_end).pack(packer) + end + end + packer.bytes + end + end + end # class Exp::List +end # module diff --git a/lib/aerospike/exp/exp_map.rb b/lib/aerospike/exp/exp_map.rb new file mode 100644 index 00000000..0cae9958 --- /dev/null +++ b/lib/aerospike/exp/exp_map.rb @@ -0,0 +1,493 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may no +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + # Map expression generator. See {@link Exp}. + # + # The bin expression argument in these methods can be a reference to a bin or the + # result of another expression. Expressions that modify bin values are only used + # for temporary expression evaluation and are not permanently applied to the bin. + # + # Map modify expressions return the bin's value. This value will be a map except + # when the map is nested within a list. In that case, a list is returned for the + # map modify expression. + # + # All maps maintain an index and a rank. The index is the item offset from the start of the map, + # for both unordered and ordered maps. The rank is the sorted index of the value component. + # Map supports negative indexing for index and rank. + # + # Index examples: + # + # Index 0: First item in map. + # Index 4: Fifth item in map. + # Index -1: Last item in map. + # Index -3: Third to last item in map. + # Index 1 Count 2: Second and third items in map. + # Index -3 Count 3: Last three items in map. + # Index -5 Count 4: Range between fifth to last item to second to last item inclusive. + # + # Rank examples: + # + # Rank 0: Item with lowest value rank in map. + # Rank 4: Fifth lowest ranked item in map. + # Rank -1: Item with highest ranked value in map. + # Rank -3: Item with third highest ranked value in map. + # Rank 1 Count 2: Second and third lowest ranked items in map. + # Rank -3 Count 3: Top three ranked items in map. + # + # Nested expressions are supported by optional CTX context arguments. Example: + # + # bin = {key1={key11=9,key12=4}, key2={key21=3,key22=5}} + # Set map value to 11 for map key "key21" inside of map key "key2". + # Get size of map key2. + # MapExp.size(mapBin("bin"), CTX.mapKey(Value.get("key2")) + # result = 2 + class Exp::Map + # Create expression that writes key/value item to a map bin. The 'bin' expression should either + # reference an existing map bin or be a expression that returns a map. + # + # ==== Examples + # # Add entry{11,22} to existing map bin. + # e = Exp.build(MapExp.put(MapPolicy.Default, Exp.val(11), Exp.val(22), Exp.mapBin(binName))) + # client.operate(key, ExpOperation.write(binName, e, Exp::WriteFlags::DEFAULT)) + #ctx, + # # Combine entry{11,22} with source map's first index entry and write resulting map to target map bin. + # e = Exp.build( + # MapExp.put(MapPolicy.Default, Exp.val(11), Exp.val(22), + # MapExp.getByIndexRange(CDT::MapReturnType::KEY_VALUE, Exp.val(0), Exp.val(1), Exp.mapBin(sourceBinName))) + # ) + # client.operate(key, ExpOperation.write(target_bin_name, e, Exp::WriteFlags::DEFAULT)) + def self.put(key, value, bin, ctx: nil, policy: CDT::MapPolicy::DEFAULT) + Packer.use do |packer| + if policy.flags != 0 + Exp.pack_ctx(packer, ctx) + packer.write_array_header(5) + packer.write(PUT) + key.pack(packer) + value.pack(packer) + packer.write(policy.attributes) + packer.write(policy.flags) + else + if policy.item_command == REPLACE + # Replace doesn't allow map attributes because it does not create on non-existing key. + Exp.pack_ctx(packer, ctx) + packer.write_array_header(3) + packer.write(policy.item_command) + key.pack(packer) + value.pack(packer) + else + Exp.pack_ctx(packer, ctx) + packer.write_array_header(4) + packer.write(policy.item_command) + key.pack(packer) + value.pack(packer) + packer.write(policy.attributes) + end + end + self.add_write(bin, packer.bytes, ctx) + end + end + + # Create expression that writes each map item to a map bin. + def self.put_items(map, bin, ctx: nil, policy: CDT::MapPolicy::DEFAULT) + Packer.use do |packer| + if policy.flags != 0 + Exp.pack_ctx(packer, ctx) + packer.write_array_header(4) + packer.write(PUT_ITEMS) + map.pack(packer) + packer.write(policy.attributes) + packer.write(policy.flags) + else + if policy.items_command == REPLACE_ITEMS + # Replace doesn't allow map attributes because it does not create on non-existing key. + Exp.pack_ctx(packer, ctx) + packer.write_array_header(2) + packer.write(policy.items_command) + map.pack(packer) + else + Exp.pack_ctx(packer, ctx) + packer.write_array_header(3) + packer.write(policy.items_command) + map.pack(packer) + packer.write(policy.attributes) + end + end + self.add_write(bin, packer.bytes, ctx) + end + end + + # Create expression that increments values by incr for all items identified by key. + # Valid only for numbers. + def self.increment(key, incr, bin, ctx: nil, policy: CDT::MapPolicy::DEFAULT) + bytes = Exp.pack(ctx, INCREMENT, key, incr, policy.attributes) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes all items in map. + def self.clear(bin, ctx: nil) + bytes = Exp.pack(ctx, CLEAR) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map item identified by key. + def self.remove_by_key(key, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_KEY, CDT::MapReturnType::NONE, key) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items identified by keys. + def self.remove_by_key_list(keys, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_KEY_LIST, CDT::MapReturnType::NONE, keys) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items identified by key range (key_begin inclusive, key_end exclusive). + # If key_begin is nil, the range is less than key_end. + # If key_end is nil, the range is greater than equal to key_begin. + def self.remove_by_key_range(key_begin, key_end, bin, ctx: nil) + bytes = Exp::List.pack_range_operation(REMOVE_BY_KEY_INTERVAL, CDT::MapReturnType::NONE, key_begin, key_end, ctx) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items nearest to key and greater by index with a count limit if provided. + # + # Examples for map [{0=17},{4=2},{5=15},{9=10}]: + # + # (value,index,count) = [removed items] + # (5,0,1) = [{5=15}] + # (5,1,2) = [{9=10}] + # (5,-1,1) = [{4=2}] + # (3,2,1) = [{9=10}] + # (3,-2,2) = [{0=17}] + def self.remove_by_key_relative_index_range(key, index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_KEY_REL_INDEX_RANGE, CDT::MapReturnType::NONE, key, index, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_KEY_REL_INDEX_RANGE, CDT::MapReturnType::NONE, key, index) + end + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items identified by value. + def self.remove_by_value(value, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE, CDT::MapReturnType::NONE, value) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items identified by values. + def self.remove_by_value_list(values, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_LIST, CDT::MapReturnType::NONE, values) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items identified by value range (valueBegin inclusive, valueEnd exclusive). + # If valueBegin is nil, the range is less than valueEnd. + # If valueEnd is nil, the range is greater than equal to valueBegin. + def self.remove_by_value_range(valueBegin, valueEnd, bin, ctx: nil) + bytes = Exp::List.pack_range_operation(REMOVE_BY_VALUE_INTERVAL, CDT::MapReturnType::NONE, valueBegin, valueEnd, ctx) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items nearest to value and greater by relative rank. + # + # Examples for map [{4=2},{9=10},{5=15},{0=17}]: + # + # (value,rank) = [removed items] + # (11,1) = [{0=17}] + # (11,-1) = [{9=10},{5=15},{0=17}] + def self.remove_by_value_relative_rank_range(value, rank, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_REL_RANK_RANGE, CDT::MapReturnType::NONE, value, rank) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map items nearest to value and greater by relative rank with a count limit. + # + # Examples for map [{4=2},{9=10},{5=15},{0=17}]: + # + # (value,rank,count) = [removed items] + # (11,1,1) = [{0=17}] + # (11,-1,1) = [{9=10}] + def self.remove_by_value_relative_rank_range(value, rank, count, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_VALUE_REL_RANK_RANGE, CDT::MapReturnType::NONE, value, rank, count) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map item identified by index. + def self.remove_by_index(index, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_INDEX, CDT::MapReturnType::NONE, index) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes "count" map items starting at specified index limited by count if provided. + def self.remove_by_index_range(index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_INDEX_RANGE, CDT::MapReturnType::NONE, index, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_INDEX_RANGE, CDT::MapReturnType::NONE, index) + end + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes map item identified by rank. + def self.remove_by_rank(rank, bin, ctx: nil) + bytes = Exp.pack(ctx, REMOVE_BY_RANK, CDT::MapReturnType::NONE, rank) + return self.add_write(bin, bytes, ctx) + end + + # Create expression that removes "count" map items starting at specified rank. If count is not provided, + # all items until the last ranked item will be removed + def self.remove_by_rank_range(rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, REMOVE_BY_RANK_RANGE, CDT::MapReturnType::NONE, rank, count) + else + bytes = Exp.pack(ctx, REMOVE_BY_RANK_RANGE, CDT::MapReturnType::NONE, rank) + end + return self.add_write(bin, bytes, ctx) + end + + # Create expression that returns list size. + # + # ==== Examples + # # Map bin "a" size > 7 + # Exp.gt(MapExp.size(mapBin("a")), Exp.val(7)) + def self.size(bin, ctx: nil) + bytes = Exp.pack(ctx, SIZE) + return self.add_read(bin, bytes, Exp::Type::INT) + end + + # Create expression that selects map item identified by key and returns selected data + # specified by return_type. + # + # ==== Examples + # # Map bin "a" contains key "B" + # Exp.gt( + # MapExp.getByKey(CDT::MapReturnType::COUNT, Exp::Type::INT, Exp.val("B"), Exp.mapBin("a")), + # Exp.val(0)) + # + # @param return_type metadata attributes to return. See {@link MapReturnType} + # @param value_type expected type of return value + # @param key map key expression + # @param bin bin or map value expression + # @param ctx optional context path for nested CDT + def self.get_by_key(return_type, value_type, key, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_KEY, return_type, key) + return self.add_read(bin, bytes, value_type) + end + + # Create expression that selects map items identified by key range (key_begin inclusive, key_end exclusive). + # If key_begin is nil, the range is less than key_end. + # If key_end is nil, the range is greater than equal to key_begin. + # + # Expression returns selected data specified by return_type (See {@link MapReturnType}). + def self.get_by_key_range(return_type, key_begin, key_end, bin, ctx: nil) + bytes = Exp::List.pack_range_operation(GET_BY_KEY_INTERVAL, return_type, key_begin, key_end, ctx) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items identified by keys and returns selected data specified by + # return_type (See {@link MapReturnType}). + def self.get_by_key_list(return_type, keys, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_KEY_LIST, return_type, keys) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items nearest to key and greater by index with a coun. + # Expression returns selected data specified by return_type (See {@link MapReturnType}). + # + # Examples for ordered map [{0=17},{4=2},{5=15},{9=10}]: + # + # (value,index) = [selected items] + # (5,0) = [{5=15},{9=10}] + # (5,1) = [{9=10}] + # (5,-1) = [{4=2},{5=15},{9=10}] + # (3,2) = [{9=10}] + # (3,-2) = [{0=17},{4=2},{5=15},{9=10}] + def self.get_by_key_relative_index_range(return_type, key, index, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_KEY_REL_INDEX_RANGE, return_type, key, index) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items nearest to key and greater by index with a count limit if provided. + # Expression returns selected data specified by return_type (See {@link MapReturnType}). + # + # Examples for ordered map [{0=17},{4=2},{5=15},{9=10}]: + # + # (value,index,count) = [selected items] + # (5,0,1) = [{5=15}] + # (5,1,2) = [{9=10}] + # (5,-1,1) = [{4=2}] + # (3,2,1) = [{9=10}] + # (3,-2,2) = [{0=17}] + def self.get_by_key_relative_index_range(return_type, key, index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_KEY_REL_INDEX_RANGE, return_type, key, index, count) + else + bytes = Exp.pack(ctx, GET_BY_KEY_REL_INDEX_RANGE, return_type, key, index) + end + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items identified by value and returns selected data + # specified by return_type. + # + # ==== Examples + # # Map bin "a" contains value "BBB" + # Exp.gt( + # MapExp.getByValue(CDT::MapReturnType::COUNT, Exp.val("BBB"), Exp.mapBin("a")), + # Exp.val(0)) + # + # @param return_type metadata attributes to return. See {@link MapReturnType} + # @param value value expression + # @param bin bin or map value expression + # @param ctx optional context path for nested CDT + def self.get_by_value(return_type, value, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_VALUE, return_type, value) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items identified by value range (valueBegin inclusive, valueEnd exclusive) + # If valueBegin is nil, the range is less than valueEnd. + # If valueEnd is nil, the range is greater than equal to valueBegin. + # + # Expression returns selected data specified by return_type (See {@link MapReturnType}). + def self.get_by_value_range(return_type, valueBegin, valueEnd, bin, ctx: nil) + bytes = Exp::List.pack_range_operation(GET_BY_VALUE_INTERVAL, return_type, valueBegin, valueEnd, ctx) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items identified by values and returns selected data specified by + # return_type (See {@link MapReturnType}). + def self.get_by_value_list(return_type, values, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_VALUE_LIST, return_type, values) + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map items nearest to value and greater by relative rank (with a count limit if passed). + # Expression returns selected data specified by return_type (See {@link MapReturnType}). + # + # Examples for map [{4=2},{9=10},{5=15},{0=17}]: + # + # (value,rank) = [selected items] + # (11,1) = [{0=17}] + # (11,-1) = [{9=10},{5=15},{0=17}] + def self.get_by_value_relative_rank_range(return_type, value, rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_VALUE_REL_RANK_RANGE, return_type, value, rank, count) + else + bytes = Exp.pack(ctx, GET_BY_VALUE_REL_RANK_RANGE, return_type, value, rank) + end + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map item identified by index and returns selected data specified by + # return_type (See {@link MapReturnType}). + def self.get_by_index(return_type, value_type, index, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_INDEX, return_type, index) + return self.add_read(bin, bytes, value_type) + end + + # Create expression that selects map items starting at specified index to the end of map and returns selected + # data specified by return_type (See {@link MapReturnType}) limited by count if provided. + def self.get_by_index_range(return_type, index, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_INDEX_RANGE, return_type, index, count) + else + bytes = Exp.pack(ctx, GET_BY_INDEX_RANGE, return_type, index) + end + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + # Create expression that selects map item identified by rank and returns selected data specified by + # return_type (See {@link MapReturnType}). + def self.get_by_rank(return_type, value_type, rank, bin, ctx: nil) + bytes = Exp.pack(ctx, GET_BY_RANK, return_type, rank) + return self.add_read(bin, bytes, value_type) + end + + # Create expression that selects map items starting at specified rank to the last ranked item and + # returns selected data specified by return_type (See {@link MapReturnType}). + def self.get_by_rank_range(return_type, rank, bin, ctx: nil, count: nil) + unless count.nil? + bytes = Exp.pack(ctx, GET_BY_RANK_RANGE, return_type, rank, count) + else + bytes = Exp.pack(ctx, GET_BY_RANK_RANGE, return_type, rank) + end + return self.add_read(bin, bytes, get_value_type(return_type)) + end + + private + + MODULE = 0 + PUT = 67 + PUT_ITEMS = 68 + REPLACE = 69 + REPLACE_ITEMS = 70 + INCREMENT = 73 + CLEAR = 75 + REMOVE_BY_KEY = 76 + REMOVE_BY_INDEX = 77 + REMOVE_BY_RANK = 79 + REMOVE_BY_KEY_LIST = 81 + REMOVE_BY_VALUE = 82 + REMOVE_BY_VALUE_LIST = 83 + REMOVE_BY_KEY_INTERVAL = 84 + REMOVE_BY_INDEX_RANGE = 85 + REMOVE_BY_VALUE_INTERVAL = 86 + REMOVE_BY_RANK_RANGE = 87 + REMOVE_BY_KEY_REL_INDEX_RANGE = 88 + REMOVE_BY_VALUE_REL_RANK_RANGE = 89 + SIZE = 96 + GET_BY_KEY = 97 + GET_BY_INDEX = 98 + GET_BY_RANK = 100 + GET_BY_VALUE = 102; # GET_ALL_BY_VALUE on server + GET_BY_KEY_INTERVAL = 103 + GET_BY_INDEX_RANGE = 104 + GET_BY_VALUE_INTERVAL = 105 + GET_BY_RANK_RANGE = 106 + GET_BY_KEY_LIST = 107 + GET_BY_VALUE_LIST = 108 + GET_BY_KEY_REL_INDEX_RANGE = 109 + GET_BY_VALUE_REL_RANK_RANGE = 110 + + def self.add_write(bin, bytes, ctx) + if ctx.to_a.empty? + ret_type = Exp::Type::MAP + else + ret_type = ((ctx[0].id & 0x10) == 0) ? Exp::Type::MAP : Exp::Type::LIST + end + Exp::Module.new(bin, bytes, ret_type, MODULE | Exp::MODIFY) + end + + def self.add_read(bin, bytes, ret_type) + Exp::Module.new(bin, bytes, ret_type, MODULE) + end + + def self.get_value_type(return_type) + t = return_type & ~CDT::MapReturnType::INVERTED + + if t <= CDT::MapReturnType::COUNT + return Exp::Type::INT + end + + if t == CDT::MapReturnType::KEY_VALUE + return Exp::Type::MAP + end + return Exp::Type::LIST + end + end # class MapExp +end # module Aerospike diff --git a/lib/aerospike/exp/operation.rb b/lib/aerospike/exp/operation.rb new file mode 100644 index 00000000..7cfc6104 --- /dev/null +++ b/lib/aerospike/exp/operation.rb @@ -0,0 +1,56 @@ +# encoding: utf-8 +# Copyright 2014-2022 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may no +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +module Aerospike + ## + # Expression operations. + class Exp::Operation + ## + # Create operation that performs an expression that writes to a record bin. + # Requires server version 5.6.0+. + # + # @param bin_name name of bin to store expression result + # @param exp expression to evaluate + # @param flags expression write flags. See {@link Exp::WriteFlags} + def self.write(bin_name, exp, flags = Aerospike::Exp::WriteFlags::DEFAULT) + create_operation(Aerospike::Operation::EXP_MODIFY, bin_name, exp, flags) + end + + ## + # Create operation that performs a read expression. + # Requires server version 5.6.0+. + # + # @param name variable name of read expression result. This name can be used as the + # bin name when retrieving bin results from the record. + # @param exp expression to evaluate + # @param flags expression read flags. See {@link Exp::ExpReadFlags} + def self.read(name, exp, flags = Aerospike::Exp::ReadFlags::DEFAULT) + create_operation(Aerospike::Operation::EXP_READ, name, exp, flags) + end + + private + + def self.create_operation(type, name, exp, flags) + Packer.use do |packer| + packer.write_array_header(2) + exp.pack(packer) + packer.write(flags) + + return Operation.new(type, name, BytesValue.new(packer.bytes)) + end + end + end +end diff --git a/lib/aerospike/operation.rb b/lib/aerospike/operation.rb index 5e691c6d..974110c6 100644 --- a/lib/aerospike/operation.rb +++ b/lib/aerospike/operation.rb @@ -14,30 +14,30 @@ # License for the specific language governing permissions and limitations under # the License. -require 'aerospike/value/value' +require "aerospike/value/value" module Aerospike - class Operation - attr_reader :op_type, :bin_name, :bin_value, :ctx - READ = 1 + READ = 1 READ_HEADER = 1 - WRITE = 2 - CDT_READ = 3 - CDT_MODIFY = 4 - ADD = 5 - APPEND = 9 - PREPEND = 10 - TOUCH = 11 - BIT_READ = 12 - BIT_MODIFY = 13 - DELETE = 14 - HLL_READ = 15 - HLL_MODIFY = 16 - - def initialize(op_type, bin_name=nil, bin_value=NullValue.new, ctx = nil) + WRITE = 2 + CDT_READ = 3 + CDT_MODIFY = 4 + ADD = 5 + EXP_READ = 7 + EXP_MODIFY = 8 + APPEND = 9 + PREPEND = 10 + TOUCH = 11 + BIT_READ = 12 + BIT_MODIFY = 13 + DELETE = 14 + HLL_READ = 15 + HLL_MODIFY = 16 + + def initialize(op_type, bin_name = nil, bin_value = NullValue.new, ctx = nil) @op_type = op_type @bin_name = bin_name @bin_value = Value.of(bin_value) @@ -49,11 +49,11 @@ def bin Aerospike::Bin.new(bin_name, bin_value) if bin_name && bin_value end - def self.get(bin_name=nil) + def self.get(bin_name = nil) Operation.new(READ, bin_name) end - def self.get_header(bin_name=nil) + def self.get_header(bin_name = nil) Operation.new(READ_HEADER, bin_name) end @@ -80,7 +80,5 @@ def self.touch def self.delete Operation.new(DELETE) end - end - end # module diff --git a/lib/aerospike/policy/policy.rb b/lib/aerospike/policy/policy.rb index b0d5bf7b..4d98b1bd 100644 --- a/lib/aerospike/policy/policy.rb +++ b/lib/aerospike/policy/policy.rb @@ -13,26 +13,44 @@ # See the License for the specific language governing permissions and # limitations under the License. -require 'aerospike/policy/priority' -require 'aerospike/policy/consistency_level' -require 'aerospike/policy/replica' - +require "aerospike/policy/priority" +require "aerospike/policy/consistency_level" +require "aerospike/policy/replica" module Aerospike # Container object for client policy command. class Policy - - attr_accessor :priority, :timeout, :max_retries, :sleep_between_retries, :consistency_level, + attr_accessor :filter_exp, :priority, :timeout, :max_retries, :sleep_between_retries, :consistency_level, :predexp, :fail_on_filtered_out, :replica, :use_compression alias total_timeout timeout alias total_timeout= timeout= - def initialize(opt={}) + def initialize(opt = {}) # Container object for transaction policy attributes used in all database # operation calls. + # Optional expression filter. If filterExp exists and evaluates to false, the + # transaction is ignored. + # + # Default: nil + # + # ==== Examples: + # + # p = Policy.new + # p.filter_exp = Exp.build(Exp.eq(Exp.int_bin("a"), Exp.int_val(11))); + @filter_exp = opt[:filter_exp] + + # Throw exception if {#filter_exp} is defined and that filter evaluates + # to false (transaction ignored). The {AerospikeException} + # will contain result code {ResultCode::FILTERED_OUT}. + # + # This field is not applicable to batch, scan or query commands. + # + # Default: false + @fail_on_filtered_out = opt[:fail_on_filtered_out] || false + # Priority of request relative to other transactions. # Currently, only used for scans. @priority = opt[:priority] || Priority::DEFAULT @@ -74,7 +92,6 @@ def initialize(opt={}) # ] @predexp = opt[:predexp] || nil - # Throw exception if @predexp is defined and that filter evaluates # to false (transaction ignored). The Aerospike::Exceptions::Aerospike # will contain result code Aerospike::ResultCode::FILTERED_OUT. @@ -86,7 +103,6 @@ def initialize(opt={}) # read operation. @consistency_level = opt[:consistency_level] || Aerospike::ConsistencyLevel::CONSISTENCY_ONE - # Send read commands to the node containing the key's partition replica type. # Write commands are not affected by this setting, because all writes are directed # to the node containing the key's master partition. @@ -118,8 +134,5 @@ def initialize(opt={}) # timeout was not exceeded. Enter zero to skip sleep. @sleep_between_retries = opt[:sleep_between_retries] || 0.5 end - - end # class - end # module diff --git a/lib/aerospike/query/query_executor.rb b/lib/aerospike/query/query_executor.rb index 68bb3b9d..c55cfa0a 100644 --- a/lib/aerospike/query/query_executor.rb +++ b/lib/aerospike/query/query_executor.rb @@ -17,28 +17,29 @@ module Aerospike class QueryExecutor # :nodoc: - def self.query_partitions(cluster, policy, tracker, statement, recordset) interval = policy.sleep_between_retries should_retry = false loop do + # reset last_expn + @last_expn = nil + list = tracker.assign_partitions_to_nodes(cluster, statement.namespace) if policy.concurrent_nodes threads = [] # Use a thread per node list.each do |node_partition| - threads << Thread.new do Thread.current.abort_on_exception = true command = QueryPartitionCommand.new(node_partition.node, tracker, policy, statement, recordset, node_partition) begin command.execute rescue => e + @last_expn = e unless e == QUERY_TERMINATED_EXCEPTION should_retry ||= command.should_retry(e) - # puts "should retry: #{should_retry}" Aerospike.logger.error(e.backtrace.join("\n")) unless e == QUERY_TERMINATED_EXCEPTION end end @@ -51,23 +52,20 @@ def self.query_partitions(cluster, policy, tracker, statement, recordset) begin command.execute rescue => e + @last_expn = e unless e == QUERY_TERMINATED_EXCEPTION should_retry ||= command.should_retry(e) Aerospike.logger.error(e.backtrace.join("\n")) unless e == QUERY_TERMINATED_EXCEPTION end end end - complete = tracker.complete?(@cluster, policy) - - if complete || !should_retry - recordset.thread_finished + if tracker.complete?(@cluster, policy) || !should_retry + recordset.thread_finished(@last_expn) return end sleep(interval) if policy.sleep_between_retries > 0 statement.reset_task_id end end - end - end diff --git a/lib/aerospike/query/query_partition_command.rb b/lib/aerospike/query/query_partition_command.rb index 704660d0..024ac40c 100644 --- a/lib/aerospike/query/query_partition_command.rb +++ b/lib/aerospike/query/query_partition_command.rb @@ -14,15 +14,13 @@ # License for the specific language governing permissions and limitations under # the License. -require 'aerospike/query/stream_command' -require 'aerospike/query/recordset' +require "aerospike/query/stream_command" +require "aerospike/query/recordset" module Aerospike - private class QueryPartitionCommand < QueryCommand #:nodoc: - def initialize(node, tracker, policy, statement, recordset, node_partitions) super(node, policy, statement, recordset, @node_partitions) @node_partitions = node_partitions @@ -39,29 +37,29 @@ def write_buffer if @statement.namespace @data_offset += @statement.namespace.bytesize + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end if @statement.set_name @data_offset += @statement.set_name.bytesize + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end # Estimate recordsPerSecond field size. This field is used in new servers and not used # (but harmless to add) in old servers. if @policy.records_per_second > 0 @data_offset += 4 + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end # Estimate socket timeout field size. This field is used in new servers and not used # (but harmless to add) in old servers. @data_offset += 4 + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 # Estimate task_id field. @data_offset += 8 + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 filter = @statement.filters[0] bin_names = @statement.bin_names @@ -73,16 +71,16 @@ def write_buffer # Estimate INDEX_TYPE field. if col_type > 0 @data_offset += FIELD_HEADER_SIZE + 1 - field_count+=1 + field_count += 1 end # Estimate INDEX_RANGE field. @data_offset += FIELD_HEADER_SIZE - filter_size+=1 # num filters + filter_size += 1 # num filters filter_size += filter.estimate_size @data_offset += filter_size - field_count+=1 + field_count += 1 # TODO: Implement # packed_ctx = filter.packed_ctx @@ -102,13 +100,18 @@ def write_buffer field_count += 1 end + unless @policy.filter_exp.nil? + exp_size = estimate_expression_size(@policy.filter_exp) + field_count += 1 if exp_size > 0 + end + # Estimate aggregation/background function size. if @statement.function_name @data_offset += FIELD_HEADER_SIZE + 1 # udf type @data_offset += @statement.package_name.bytesize + FIELD_HEADER_SIZE @data_offset += @statement.function_name.bytesize + FIELD_HEADER_SIZE - function_arg_buffer='' + function_arg_buffer = "" if @statement.function_args && @statement.function_args.length > 0 function_arg_buffer = Value.of(@statement.function_args).to_bytes end @@ -133,24 +136,24 @@ def write_buffer if parts_full_size > 0 @data_offset += parts_full_size + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end if parts_partial_digest_size > 0 @data_offset += parts_partial_digest_size + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end if parts_partial_bval_size > 0 @data_offset += parts_partial_bval_size + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end # Estimate max records field size. This field is used in new servers and not used # (but harmless to add) in old servers. if max_records > 0 @data_offset += 8 + FIELD_HEADER_SIZE - field_count+=1 + field_count += 1 end operation_count = 0 @@ -180,6 +183,8 @@ def write_buffer # Write records per second. write_field_int(@policy.records_per_second, FieldType::RECORDS_PER_SECOND) if @policy.records_per_second > 0 + write_filter_exp(@policy.filter_exp, exp_size) + # Write socket idle timeout. write_field_int(@policy.socket_timeout, FieldType::SOCKET_TIMEOUT) @@ -260,7 +265,5 @@ def should_retry(e) # !! converts nil to false !!@tracker&.should_retry(@node_partitions, e) end - end # class - end # module diff --git a/lib/aerospike/query/recordset.rb b/lib/aerospike/query/recordset.rb index a3423f40..6cfa0f75 100644 --- a/lib/aerospike/query/recordset.rb +++ b/lib/aerospike/query/recordset.rb @@ -22,7 +22,6 @@ module Aerospike # so the production and the consumptoin are decoupled # there can be an unlimited count of producer threads and consumer threads class Recordset - attr_reader :records def initialize(queue_size = 5000, thread_count = 1, type) @@ -66,18 +65,21 @@ def active? # this is called by working threads to signal their job is finished # it decreases the count of active threads and puts an EOF on queue when all threads are finished - def thread_finished + # e is an exception that has happened in the exceutor, and outside of the threads themselves + def thread_finished(expn = nil) @active_threads.update do |v| v -= 1 @records.enq(nil) if v == 0 v end + + raise expn unless expn.nil? end # this is called by a thread who faced an exception to singnal to terminate the whole operation # it also may be called by the user to terminate the command in the middle of fetching records from server nodes # it clears the queue so that if any threads are waiting for the queue get unblocked and find out about the cancellation - def cancel(expn=nil) + def cancel(expn = nil) set_exception(expn) @cancelled.set(true) @records.clear @@ -104,18 +106,16 @@ def is_scan? @filters.nil? || @filters.empty? end - private + private - def set_exception(expn=nil) + def set_exception(expn = nil) expn ||= (@type == :scan ? SCAN_TERMINATED_EXCEPTION : QUERY_TERMINATED_EXCEPTION) @thread_exception.set(expn) end - end private - SCAN_TERMINATED_EXCEPTION = Aerospike::Exceptions::ScanTerminated.new() - QUERY_TERMINATED_EXCEPTION = Aerospike::Exceptions::QueryTerminated.new() - + SCAN_TERMINATED_EXCEPTION = Aerospike::Exceptions::ScanTerminated.new() + QUERY_TERMINATED_EXCEPTION = Aerospike::Exceptions::QueryTerminated.new() end diff --git a/lib/aerospike/query/scan_executor.rb b/lib/aerospike/query/scan_executor.rb index ae0b17e0..a28d67c3 100644 --- a/lib/aerospike/query/scan_executor.rb +++ b/lib/aerospike/query/scan_executor.rb @@ -17,26 +17,28 @@ module Aerospike class ScanExecutor # :nodoc: - def self.scan_partitions(policy, cluster, tracker, namespace, set_name, recordset, bin_names = nil) interval = policy.sleep_between_retries should_retry = false loop do + # reset last_expn + @last_expn = nil + list = tracker.assign_partitions_to_nodes(cluster, namespace) if policy.concurrent_nodes threads = [] # Use a thread per node list.each do |node_partition| - threads << Thread.new do Thread.current.abort_on_exception = true command = ScanPartitionCommand.new(policy, tracker, node_partition, namespace, set_name, bin_names, recordset) begin command.execute rescue => e + @last_expn = e unless e == SCAN_TERMINATED_EXCEPTION should_retry ||= command.should_retry(e) Aerospike.logger.error(e.backtrace.join("\n")) unless e == SCAN_TERMINATED_EXCEPTION end @@ -50,6 +52,7 @@ def self.scan_partitions(policy, cluster, tracker, namespace, set_name, recordse begin command.execute rescue => e + @last_expn = e unless e == SCAN_TERMINATED_EXCEPTION should_retry ||= command.should_retry(e) Aerospike.logger.error(e.backtrace.join("\n")) unless e == SCAN_TERMINATED_EXCEPTION end @@ -57,13 +60,12 @@ def self.scan_partitions(policy, cluster, tracker, namespace, set_name, recordse end if tracker.complete?(@cluster, policy) || !should_retry - recordset.thread_finished + recordset.thread_finished(@last_expn) return end sleep(interval) if policy.sleep_between_retries > 0 + statement.reset_task_id end end - end - end diff --git a/lib/aerospike/utils/buffer.rb b/lib/aerospike/utils/buffer.rb index 4b633d68..d14c757b 100644 --- a/lib/aerospike/utils/buffer.rb +++ b/lib/aerospike/utils/buffer.rb @@ -17,36 +17,34 @@ # License for the specific language governing permissions and limitations under # the License. -require 'aerospike/utils/pool' +require "aerospike/utils/pool" module Aerospike - private # Buffer class to ease the work around class Buffer #:nodoc: - @@buf_pool = Pool.new @@buf_pool.create_proc = Proc.new { Buffer.new } attr_accessor :buf - INT16 = 's>' - UINT16 = 'n' - UINT16LE = 'v' - INT32 = 'l>' - UINT32 = 'N' - INT64 = 'q>' - UINT64 = 'Q>' - UINT64LE = 'Q' - DOUBLE = 'G' + INT16 = "s>" + UINT16 = "n" + UINT16LE = "v" + INT32 = "l>" + UINT32 = "N" + INT64 = "q>" + UINT64 = "Q>" + UINT64LE = "Q" + DOUBLE = "G" DEFAULT_BUFFER_SIZE = 16 * 1024 MAX_BUFFER_SIZE = 10 * 1024 * 1024 - def initialize(size=DEFAULT_BUFFER_SIZE, buf = nil) + def initialize(size = DEFAULT_BUFFER_SIZE, buf = nil) @buf = (buf ? buf : ("%0#{size}d" % 0)) - @buf.force_encoding('binary') + @buf.force_encoding("binary") @slice_end = @buf.bytesize end @@ -61,6 +59,7 @@ def self.put(buffer) def size @buf.bytesize end + alias_method :length, :size def eat!(n) @@ -135,7 +134,7 @@ def write_double(f, offset) 8 end - def read(offset, len=nil) + def read(offset, len = nil) if len @buf[offset, len] else @@ -144,37 +143,37 @@ def read(offset, len=nil) end def read_int16(offset) - vals = @buf[offset..offset+1] + vals = @buf[offset..offset + 1] vals.unpack(INT16)[0] end def read_uint16(offset) - vals = @buf[offset..offset+1] + vals = @buf[offset..offset + 1] vals.unpack(UINT16)[0] end def read_int32(offset) - vals = @buf[offset..offset+3] + vals = @buf[offset..offset + 3] vals.unpack(INT32)[0] end def read_uint32(offset) - vals = @buf[offset..offset+3] + vals = @buf[offset..offset + 3] vals.unpack(UINT32)[0] end def read_int64(offset) - vals = @buf[offset..offset+7] + vals = @buf[offset..offset + 7] vals.unpack(INT64)[0] end def read_uint64_little_endian(offset) - vals = @buf[offset..offset+7] + vals = @buf[offset..offset + 7] vals.unpack(UINT64LE)[0] end def read_uint64(offset) - vals = @buf[offset..offset+7] + vals = @buf[offset..offset + 7] vals.unpack(UINT64)[0] end @@ -183,14 +182,14 @@ def read_var_int64(offset, len) i = 0 while i < len val <<= 8 - val |= @buf[offset+i].ord & 0xFF + val |= @buf[offset + i].ord & 0xFF i = i.succ end val end def read_double(offset) - vals = @buf[offset..offset+7] + vals = @buf[offset..offset + 7] vals.unpack(DOUBLE)[0] end @@ -199,39 +198,48 @@ def read_bool(offset, length) end def to_s - @buf[0..@slice_end-1] + @buf[0..@slice_end - 1] end def reset - for i in 0..@buf.size-1 - @buf[i] = ' ' + for i in 0..@buf.size - 1 + @buf[i] = " " end end - def dump(start=0, finish=nil) + def dump(start = 0, finish = nil) + buf ||= @buf.bytes finish ||= @slice_end - 1 width = 16 - ascii = '|' + ascii = "|" counter = 0 - print '%06x ' % start + print "%08x " % start @buf.bytes[start...finish].each do |c| if counter >= start - print '%02x ' % c + print "%02x " % c ascii << (c.between?(32, 126) ? c : ?.) - if ascii.length >= width - ascii << '|' + print " " if ascii.length == (width / 2 + 1) + if ascii.length > width + ascii << "|" puts ascii - ascii = '|' - print '%06x ' % (counter + 1) + ascii = "|" + print "%08x " % (counter + 1) end end counter += 1 end - puts - end + # print the remainder in buffer + if ascii.length.positive? + fill_size = ((width - ascii.length + 1) * 3) + fill_size += 1 if ascii.length <= (width / 2) + filler = " " * fill_size + print filler + ascii << "|" + puts ascii + end + end end # buffer - end # module diff --git a/lib/aerospike/utils/packer.rb b/lib/aerospike/utils/packer.rb index b3b3bf87..d3dbefd2 100644 --- a/lib/aerospike/utils/packer.rb +++ b/lib/aerospike/utils/packer.rb @@ -14,13 +14,11 @@ # License for the specific language governing permissions and limitations under # the License. -require 'msgpack' -require 'aerospike/utils/pool' +require "msgpack" +require "aerospike/utils/pool" module Aerospike - class Packer < MessagePack::Packer #:nodoc: - AS_EXT_TYPE = -1 @@pool = Pool.new @@ -44,9 +42,12 @@ def write_raw_short(val) buffer << [val].pack("S>") end + def write_raw(buf) + buffer.write(buf) + end + def bytes - self.to_s.force_encoding('binary') + self.to_s.force_encoding("binary") end end - end diff --git a/lib/aerospike/value/value.rb b/lib/aerospike/value/value.rb index a473db54..fb77bb64 100644 --- a/lib/aerospike/value/value.rb +++ b/lib/aerospike/value/value.rb @@ -17,12 +17,11 @@ # License for the specific language governing permissions and limitations under # the License. -require 'aerospike/aerospike_exception' +require "aerospike/aerospike_exception" module Aerospike # Polymorphic value classes used to efficiently serialize objects into the wire protocol. class Value #:nodoc: - def self.of(value, allow_64bits = false) case value when Integer @@ -83,12 +82,10 @@ def self.validate_hash_key(value) raise Aerospike::Exceptions::Aerospike.new(Aerospike::ResultCode::TYPE_NOT_SUPPORTED, "Value type #{value.class} not supported as hash key.") end end - end # Value # Empty value. class NullValue < Value #:nodoc: - def initialize self end @@ -102,7 +99,7 @@ def get end def to_s - '' + "" end def estimate_size @@ -118,13 +115,12 @@ def pack(packer) end def to_bytes - '' + "" end end NULL = NullValue.new.freeze - # Infinity value. class InfinityValue < Value #:nodoc: def initialize @@ -156,7 +152,7 @@ def pack(packer) end def to_bytes - '' + "" end def to_msgpack_ext @@ -197,7 +193,7 @@ def pack(packer) end def to_bytes - '' + "" end def to_msgpack_ext @@ -209,10 +205,9 @@ def to_msgpack_ext # Byte array value. class BytesValue < Value #:nodoc: - def initialize(value) @bytes = value - @bytes.force_encoding('binary') + @bytes.force_encoding("binary") self end @@ -244,16 +239,14 @@ def write(buffer, offset) def pack(packer) packer.write(Aerospike::ParticleType::BLOB.chr + @bytes) end - end # BytesValue ####################################### # value string. class StringValue < Value #:nodoc: - def initialize(val) - @value = val || '' + @value = val || "" self end @@ -289,14 +282,12 @@ def to_s def to_sym @value.to_sym end - end # StringValue ####################################### # Integer value. class IntegerValue < Value #:nodoc: - def initialize(val) @value = val || 0 self @@ -326,20 +317,18 @@ def get def to_bytes # Convert integer to big endian unsigned 64 bits. # @see http://ruby-doc.org/core-2.3.0/Array.html#method-i-pack - [@value].pack('Q>') + [@value].pack("Q>") end def to_s @value.to_s end - end # IntegerValue ####################################### # Float value. class FloatValue < Value #:nodoc: - def initialize(val) @value = val || 0.0 self @@ -367,13 +356,12 @@ def get end def to_bytes - [@value].pack('G') + [@value].pack("G") end def to_s @value.to_s end - end # FloatValue ####################################### @@ -381,7 +369,6 @@ def to_s # List value. # Supported by Aerospike 3 servers only. class ListValue < Value #:nodoc: - def initialize(list) @list = list || [] end @@ -415,7 +402,7 @@ def to_bytes end def to_s - @list.map{|v| v.to_s}.to_s + @list.map { |v| v.to_s }.to_s end private @@ -430,15 +417,13 @@ def bytes @bytes end - end # #######################################/ # Map value. - # Supported by Aerospike 3 servers only. + # Supported by Aerospike 3+ servers only. class MapValue < Value #:nodoc: - def initialize(vmap) @vmap = vmap || {} end @@ -475,7 +460,7 @@ def to_bytes end def to_s - @vmap.map{|k, v| "#{k.to_s} => #{v.to_s}" }.to_s + @vmap.map { |k, v| "#{k.to_s} => #{v.to_s}" }.to_s end private @@ -490,7 +475,6 @@ def bytes @bytes end - end # #######################################/ @@ -498,7 +482,6 @@ def bytes # GeoJSON value. # Supported by Aerospike server version 3.7 and later. class GeoJSONValue < Value #:nodoc: - def initialize(json) @json = json @bytes = json.to_json @@ -535,7 +518,6 @@ def to_bytes def to_s @json end - end # #######################################/ @@ -543,14 +525,19 @@ def to_s # HLLValue value. Encapsulates a HyperLogLog value. # Supported by Aerospike server version 4.9 and later. class HLLValue < Value #:nodoc: + attr_reader :bytes def initialize(value) @bytes = value - @bytes.force_encoding('binary') + @bytes.force_encoding("binary") self end + def ==(other) + @bytes.to_s == other.to_s + end + def type Aerospike::ParticleType::HLL end @@ -578,7 +565,6 @@ def write(buffer, offset) def pack(packer) packer.write(Aerospike::ParticleType::BLOB.chr + @bytes) end - end ####################################### @@ -594,63 +580,49 @@ def self.encoding=(encoding) protected def self.bytes_to_particle(type, buf, offset, length) # :nodoc: - case type when Aerospike::ParticleType::STRING bytes = buf.read(offset, length) bytes.force_encoding(Aerospike.encoding) - when Aerospike::ParticleType::INTEGER buf.read_int64(offset) - when Aerospike::ParticleType::DOUBLE buf.read_double(offset) - when Aerospike::ParticleType::BOOL buf.read_bool(offset, length) - when Aerospike::ParticleType::BLOB - buf.read(offset,length) - + buf.read(offset, length) when Aerospike::ParticleType::LIST Unpacker.use do |unpacker| data = buf.read(offset, length) unpacker.unpack(data) end - when Aerospike::ParticleType::MAP Unpacker.use do |unpacker| data = buf.read(offset, length) unpacker.unpack(data) end - when Aerospike::ParticleType::GEOJSON # ignore the flags for now ncells = buf.read_int16(offset + 1) hdrsz = 1 + 2 + (ncells * 8) Aerospike::GeoJSON.new(buf.read(offset + hdrsz, length - hdrsz)) - when Aerospike::ParticleType::HLL - bytes = buf.read(offset,length) + bytes = buf.read(offset, length) Aerospike::HLLValue.new(bytes) - else nil end end def self.bytes_to_key_value(type, buf, offset, len) # :nodoc: - case type when Aerospike::ParticleType::STRING StringValue.new(buf.read(offset, len)) - when Aerospike::ParticleType::INTEGER IntegerValue.new(buf.read_var_int64(offset, len)) - when Aerospike::ParticleType::BLOB - BytesValue.new(buf.read(offset,len)) - + BytesValue.new(buf.read(offset, len)) else nil end @@ -663,7 +635,6 @@ def self.bytes_to_key_value(type, buf, offset, len) # :nodoc: # Boolean value. # Supported by Aerospike server 5.6+ only. class BoolValue < Value #:nodoc: - def initialize(val) @value = val || false self @@ -698,6 +669,5 @@ def to_bytes def to_s @value.to_s end - end # BoolValue end # module diff --git a/spec/aerospike/cdt_hll_spec.rb b/spec/aerospike/cdt_hll_spec.rb index 8b526441..5f8252b8 100644 --- a/spec/aerospike/cdt_hll_spec.rb +++ b/spec/aerospike/cdt_hll_spec.rb @@ -18,7 +18,6 @@ include Aerospike::ResultCode describe "client.operate() - HLL Operations", skip: !Support.min_version?("4.9") do - let(:client) { Support.client } let(:key) { Support.gen_random_key(0, key_val: "ophkey") } let(:key0) { Support.gen_random_key(0, key_val: "ophkey0") } @@ -43,7 +42,6 @@ illegal_descriptions = [] before(:each) do - legal_zero = [] legal_min = [] legal_mid = [] @@ -94,7 +92,7 @@ illegal_max << index_bits illegal_zero << 0 - illegal_min << (min_minhash_bits-1) + illegal_min << (min_minhash_bits - 1) illegal_max << max_minhash_bits illegal_descriptions << illegal_zero @@ -104,20 +102,20 @@ illegal_min << index_bits illegal_max << index_bits - illegal_min << (min_minhash_bits-1) - illegal_max << (max_minhash_bits+1) + illegal_min << (min_minhash_bits - 1) + illegal_max << (max_minhash_bits + 1) illegal_descriptions << illegal_min illegal_descriptions << illegal_max - if index_bits+max_minhash_bits > 64 + if index_bits + max_minhash_bits > 64 illegal_max1 << index_bits - illegal_max1 << (1+max_minhash_bits-(64-(index_bits+max_minhash_bits))) + illegal_max1 << (1 + max_minhash_bits - (64 - (index_bits + max_minhash_bits))) illegal_descriptions << illegal_max1 end end index_bits += 4 - end while index_bits <= max_index_bits+5 + end while index_bits <= max_index_bits + 5 end it "Init should work" do @@ -137,15 +135,15 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1)]) # create_only c = HLLPolicy.new(write_flags: HLLWriteFlags::CREATE_ONLY) expect_success(key, [HLLOperation::init(hll_bin, index_bits, -1, c)]) expect_errors(key, Aerospike::ResultCode::BIN_EXISTS_ERROR, - [HLLOperation::init(hll_bin, index_bits, -1, c)]) + [HLLOperation::init(hll_bin, index_bits, -1, c)]) # update_only u = HLLPolicy.new(write_flags: HLLWriteFlags::UPDATE_ONLY) @@ -153,7 +151,7 @@ expect_success(key, [HLLOperation::init(hll_bin, index_bits, -1, u)]) expect_success(key, [Aerospike::Operation::put(Aerospike::Bin.new(hll_bin, nil))]) expect_errors(key, Aerospike::ResultCode::BIN_NOT_FOUND, - [HLLOperation::init(hll_bin, index_bits, -1, u)]) + [HLLOperation::init(hll_bin, index_bits, -1, u)]) # create_only no_fail cn = HLLPolicy.new(write_flags: HLLWriteFlags::CREATE_ONLY | HLLWriteFlags::NO_FAIL) @@ -174,13 +172,13 @@ f = HLLPolicy.new(write_flags: HLLWriteFlags::ALLOW_FOLD) expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, - [HLLOperation::init(hll_bin, index_bits, -1, f)]) + [HLLOperation::init(hll_bin, index_bits, -1, f)]) end it "Bad Init should NOT work" do expect_success(key, [Aerospike::Operation::delete, HLLOperation::init(hll_bin, max_index_bits, 0)]) expect_errors(key, Aerospike::ResultCode::OP_NOT_APPLICABLE, - [HLLOperation::init(hll_bin, -1, max_minhash_bits)]) + [HLLOperation::init(hll_bin, -1, max_minhash_bits)]) end it "Add Init should work" do @@ -194,21 +192,21 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1)]) # create_only c = HLLPolicy.new(write_flags: HLLWriteFlags::CREATE_ONLY) expect_success(key, [HLLOperation::add(hll_bin, *entries, index_bit_count: index_bits, policy: c)]) expect_errors(key, Aerospike::ResultCode::BIN_EXISTS_ERROR, - [HLLOperation::add( hll_bin, entries, index_bits, -1, c)]) + [HLLOperation::add(hll_bin, entries, index_bits, -1, c)]) # update_only u = HLLPolicy.new(write_flags: HLLWriteFlags::UPDATE_ONLY) expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, - [HLLOperation::add(hll_bin, entries, index_bit_count: index_bits, policy: u)]) + [HLLOperation::add(hll_bin, entries, index_bit_count: index_bits, policy: u)]) # create_only no_fail cn = HLLPolicy.new(write_flags: HLLWriteFlags::CREATE_ONLY | HLLWriteFlags::NO_FAIL) @@ -222,14 +220,14 @@ f = HLLPolicy.new(write_flags: HLLWriteFlags::ALLOW_FOLD) expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, - [HLLOperation::add(hll_bin, entries, index_bit_count: index_bits, policy: f)]) + [HLLOperation::add(hll_bin, entries, index_bit_count: index_bits, policy: f)]) end it "Fold should work" do vals0 = [] vals1 = [] - (0...(n_entries/2)).each do |i| + (0...(n_entries / 2)).each do |i| vals0 << "key #{i}" end @@ -249,29 +247,29 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1), - HLLOperation::init(hll_bin, index_bits, -1)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1), + HLLOperation::init(hll_bin, index_bits, -1)]) # Exists. expect_success(key, [HLLOperation::fold(hll_bin, fold_down)]) expect_errors(key, Aerospike::ResultCode::OP_NOT_APPLICABLE, - [HLLOperation::fold(hll_bin, fold_up)]) + [HLLOperation::fold(hll_bin, fold_up)]) # Does not exist. expect_success(key, [Aerospike::Operation::put(Aerospike::Bin.new(hll_bin, nil))]) expect_errors(key, Aerospike::ResultCode::BIN_NOT_FOUND, - [HLLOperation::fold(hll_bin, fold_down)]) + [HLLOperation::fold(hll_bin, fold_down)]) end it "Set Union should work" do vals = [] - (0...keys.length).each do |i| + (0...keys.length).each do |i| sub_vals = [] - (0...n_entries/3).each do |j| + (0...n_entries / 3).each do |j| sub_vals << "key#{i} #{j}" end @@ -295,9 +293,9 @@ # Keep record around win hll_bin is removed. hlls = [] record = expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::add(other_name, *entries, index_bit_count: index_bits), - Aerospike::Operation::get(other_name)]) + [Aerospike::Operation::delete, + HLLOperation::add(other_name, *entries, index_bit_count: index_bits), + Aerospike::Operation::get(other_name)]) result_list = record.bins[other_name] hll = result_list[1] @@ -308,7 +306,7 @@ expect_success(key, [HLLOperation::set_union(hll_bin, *hlls, policy: c)]) expect_errors(key, Aerospike::ResultCode::BIN_EXISTS_ERROR, - [HLLOperation::set_union(hll_bin, *hlls, c)]) + [HLLOperation::set_union(hll_bin, *hlls, c)]) # update_only u = HLLPolicy.new(write_flags: HLLWriteFlags::UPDATE_ONLY) @@ -316,7 +314,7 @@ expect_success(key, [HLLOperation::set_union(hll_bin, *hlls, policy: u)]) expect_success(key, [Aerospike::Operation::put(Aerospike::Bin.new(hll_bin, nil))]) expect_errors(key, Aerospike::ResultCode::BIN_NOT_FOUND, - [HLLOperation::set_union(hll_bin, *hlls, u)]) + [HLLOperation::set_union(hll_bin, *hlls, u)]) # create_only no_fail cn = HLLPolicy.new(write_flags: HLLWriteFlags::CREATE_ONLY | HLLWriteFlags::NO_FAIL) @@ -348,21 +346,21 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1), - HLLOperation::init(hll_bin, index_bits, -1)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1), + HLLOperation::init(hll_bin, index_bits, -1)]) # Exists. expect_success(key, [HLLOperation::refresh_count(hll_bin), - HLLOperation::refresh_count(hll_bin)]) + HLLOperation::refresh_count(hll_bin)]) expect_success(key, [HLLOperation::add(hll_bin, *entries)]) expect_success(key, [HLLOperation::refresh_count(hll_bin), - HLLOperation::refresh_count(hll_bin)]) + HLLOperation::refresh_count(hll_bin)]) # Does not exist. expect_success(key, [Aerospike::Operation::put(Aerospike::Bin.new(hll_bin, nil))]) expect_errors(key, Aerospike::ResultCode::BIN_NOT_FOUND, - [HLLOperation::refresh_count(hll_bin)]) + [HLLOperation::refresh_count(hll_bin)]) end it "Get Count should work" do @@ -370,9 +368,9 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1), - HLLOperation::add(hll_bin, *entries, index_bit_count: index_bits)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1), + HLLOperation::add(hll_bin, *entries, index_bit_count: index_bits)]) # Exists. record = expect_success(key, [HLLOperation::get_count(hll_bin)]) @@ -382,7 +380,8 @@ # Does not exist. expect_success(key, [Aerospike::Operation::put(Aerospike::Bin.new(hll_bin, nil))]) record = expect_success(key, [HLLOperation::get_count(hll_bin)]) - expect(record.bins).to be nil + expected = { "ophbin" => nil } + expect(record.bins).to eq expected end it "Get Union should work" do @@ -394,14 +393,14 @@ (0...keys.length).each do |i| sub_vals = [] - (0...(n_entries/3)).each do |j| + (0...(n_entries / 3)).each do |j| sub_vals << "key#{i} #{j}" end record = expect_success(keys[i], - [Aerospike::Operation::delete, - HLLOperation::add(hll_bin, *sub_vals, index_bit_count: index_bits), - Aerospike::Operation::get(hll_bin)]) + [Aerospike::Operation::delete, + HLLOperation::add(hll_bin, *sub_vals, index_bit_count: index_bits), + Aerospike::Operation::get(hll_bin)]) result_list = record.bins[hll_bin] hlls << result_list[1] @@ -411,13 +410,13 @@ # Keep record around win hll_bin is removed. expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, -1), - HLLOperation::add(hll_bin, *vals[0], index_bit_count: index_bits)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, -1), + HLLOperation::add(hll_bin, *vals[0], index_bit_count: index_bits)]) record = expect_success(key, - [HLLOperation::get_union(hll_bin, *hlls), - HLLOperation::get_union_count(hll_bin, *hlls)]) + [HLLOperation::get_union(hll_bin, *hlls), + HLLOperation::get_union_count(hll_bin, *hlls)]) result_list = record.bins[hll_bin] union_count = result_list[1] @@ -428,8 +427,8 @@ bin = Aerospike::Bin.new(hll_bin, union_hll) record = expect_success(key, - [Aerospike::Operation::put(bin), - HLLOperation::get_count(hll_bin)]) + [Aerospike::Operation::put(bin), + HLLOperation::get_count(hll_bin)]) result_list = record.bins[hll_bin] union_count_2 = result_list[1] @@ -442,8 +441,8 @@ minhash_bits = desc[1] expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init(hll_bin, index_bits, minhash_bits)]) + [Aerospike::Operation::delete, + HLLOperation::init(hll_bin, index_bits, minhash_bits)]) record = client.get(key) hll = record.bins[hll_bin] @@ -452,8 +451,8 @@ client.put(key, Aerospike::Bin.new(hll_bin, hll)) record = expect_success(key, - [HLLOperation::get_count(hll_bin), - HLLOperation::describe(hll_bin)]) + [HLLOperation::get_count(hll_bin), + HLLOperation::describe(hll_bin)]) result_list = record.bins[hll_bin] count = result_list[0] @@ -468,7 +467,7 @@ overlaps = [0.0001, 0.001, 0.01, 0.1, 0.5] overlaps.each do |overlap| - expected_intersect_count =(n_entries * overlap).floor + expected_intersect_count = (n_entries * overlap).floor common = [] (0...expected_intersect_count).each do |i| @@ -505,9 +504,9 @@ n_minhash_bits = desc[1] record = expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init(hll_bin, n_index_bits, n_minhash_bits), - Aerospike::Operation::get(hll_bin)]) + [Aerospike::Operation::delete, + HLLOperation::init(hll_bin, n_index_bits, n_minhash_bits), + Aerospike::Operation::get(hll_bin)]) result_list = record.bins[hll_bin] hlls = [] @@ -515,8 +514,8 @@ hlls << result_list[1] record = expect_success(key, - [HLLOperation::get_similarity(hll_bin, *hlls), - HLLOperation::get_intersect_count(hll_bin, *hlls)]) + [HLLOperation::get_similarity(hll_bin, *hlls), + HLLOperation::get_intersect_count(hll_bin, *hlls)]) result_list = record.bins[hll_bin] @@ -538,11 +537,11 @@ break if minhash_bits != 0 record = expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::add(hll_bin, *entries, index_bit_count: index_bits, minhash_bit_count: minhash_bits), - Aerospike::Operation::get(hll_bin), - HLLOperation::add(other_bin_name, *entries, index_bit_count: index_bits, minhash_bit_count: 4), - Aerospike::Operation::get(other_bin_name)]) + [Aerospike::Operation::delete, + HLLOperation::add(hll_bin, *entries, index_bit_count: index_bits, minhash_bit_count: minhash_bits), + Aerospike::Operation::get(hll_bin), + HLLOperation::add(other_bin_name, *entries, index_bit_count: index_bits, minhash_bit_count: 4), + Aerospike::Operation::get(other_bin_name)]) hlls = [] hmhs = [] @@ -556,35 +555,35 @@ hmhs << hmhs[0] record = expect_success(key, - [HLLOperation::get_intersect_count(hll_bin, *hlls), - HLLOperation::get_similarity(hll_bin, *hlls)]) + [HLLOperation::get_intersect_count(hll_bin, *hlls), + HLLOperation::get_similarity(hll_bin, *hlls)]) result_list = record.bins[hll_bin] intersect_count = result_list[0] - expect(intersect_count < 1.8*entries.length).to be true + expect(intersect_count < 1.8 * entries.length).to be true hlls << hlls[0] expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, - [HLLOperation::get_intersect_count(hll_bin, *hlls)]) + [HLLOperation::get_intersect_count(hll_bin, *hlls)]) expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, - [HLLOperation::get_similarity(hll_bin, *hlls)]) + [HLLOperation::get_similarity(hll_bin, *hlls)]) record = expect_success(key, - [HLLOperation::get_intersect_count(hll_bin, *hmhs), - HLLOperation::get_similarity(hll_bin, *hmhs)]) + [HLLOperation::get_intersect_count(hll_bin, *hmhs), + HLLOperation::get_similarity(hll_bin, *hmhs)]) result_list = record.bins[hll_bin] intersect_count = result_list[0] - expect(intersect_count < 1.8*entries.length).to be true + expect(intersect_count < 1.8 * entries.length).to be true hmhs << hmhs[0] expect_errors(key, Aerospike::ResultCode::OP_NOT_APPLICABLE, - [HLLOperation::get_intersect_count(hll_bin, *hmhs)]) + [HLLOperation::get_intersect_count(hll_bin, *hmhs)]) expect_errors(key, Aerospike::ResultCode::OP_NOT_APPLICABLE, - [HLLOperation::get_similarity(hll_bin, *hmhs)]) + [HLLOperation::get_similarity(hll_bin, *hmhs)]) end end @@ -601,41 +600,40 @@ def hll_post_op def expect_errors(key, err_code, ops) expect { client.operate(key, ops, operate_policy) - }.to raise_error (Aerospike::Exceptions::Aerospike){ |error| + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| error.result_code == err_code } end def expect_success(key, ops) - client.operate(key, ops, operate_policy) + client.operate(key, ops, operate_policy) end def expect_init(index_bits, minhash_bits, should_pass) - hll_policy = HLLPolicy::DEFAULT - ops = [ - HLLOperation::init(hll_bin, index_bits, minhash_bits, hll_policy), - HLLOperation::get_count(hll_bin), - HLLOperation::refresh_count(hll_bin), - HLLOperation::describe(hll_bin), - ] - - if !should_pass - expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, ops) - return - end + hll_policy = HLLPolicy::DEFAULT + ops = [ + HLLOperation::init(hll_bin, index_bits, minhash_bits, hll_policy), + HLLOperation::get_count(hll_bin), + HLLOperation::refresh_count(hll_bin), + HLLOperation::describe(hll_bin), + ] - record = expect_success(key, ops) - result_list = record.bins[hll_bin] + if !should_pass + expect_errors(key, Aerospike::ResultCode::PARAMETER_ERROR, ops) + return + end - count = result_list[1] - count1 = result_list[2] - description = result_list[3] + record = expect_success(key, ops) + result_list = record.bins[hll_bin] - # expect_description(description, index_bits, minhash_bits) - expect(count).to eq 0 - expect(count1).to eq 0 - end + count = result_list[1] + count1 = result_list[2] + description = result_list[3] + # expect_description(description, index_bits, minhash_bits) + expect(count).to eq 0 + expect(count1).to eq 0 + end def expect_description(description, index_bits, minhash_bits) expect(index_bits).to eq description[0] @@ -644,8 +642,8 @@ def expect_description(description, index_bits, minhash_bits) def check_bits(index_bits, minhash_bits) return !(index_bits < min_index_bits || index_bits > max_index_bits || - (minhash_bits != 0 && minhash_bits < min_minhash_bits) || - minhash_bits > max_minhash_bits || index_bits+minhash_bits > 64) + (minhash_bits != 0 && minhash_bits < min_minhash_bits) || + minhash_bits > max_minhash_bits || index_bits + minhash_bits > 64) end def relative_count_error(n_index_bits) @@ -657,9 +655,8 @@ def expect_description(description, index_bits, minhash_bits) expect(minhash_bits).to eq description[1] end - def is_within_relative_error(expected, estimate, relative_error) - return expected*(1-relative_error) <= estimate || estimate <= expected*(1+relative_error) + return expected * (1 - relative_error) <= estimate || estimate <= expected * (1 + relative_error) end def expect_hll_count(index_bits, hll_count, expected) @@ -698,7 +695,6 @@ def expect_add_init(index_bits, minhash_bits) end def expect_fold(vals0, vals1, index_bits) - (min_index_bits..index_bits).each do |ix| if !check_bits(index_bits, 0) || !check_bits(ix, 0) # Expected valid inputs @@ -706,11 +702,11 @@ def expect_fold(vals0, vals1, index_bits) end recorda = expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::add(hll_bin, *vals0, index_bit_count: index_bits), - HLLOperation::get_count(hll_bin), - HLLOperation::refresh_count(hll_bin), - HLLOperation::describe(hll_bin)]) + [Aerospike::Operation::delete, + HLLOperation::add(hll_bin, *vals0, index_bit_count: index_bits), + HLLOperation::get_count(hll_bin), + HLLOperation::refresh_count(hll_bin), + HLLOperation::describe(hll_bin)]) resulta_list = recorda.bins[hll_bin] counta = resulta_list[1] @@ -722,12 +718,12 @@ def expect_fold(vals0, vals1, index_bits) expect(counta).to eq counta1 recordb = expect_success(key, - [HLLOperation::fold(hll_bin, ix), - HLLOperation::get_count(hll_bin), - HLLOperation::add(hll_bin, *vals0), - HLLOperation::add(hll_bin, *vals1), - HLLOperation::get_count(hll_bin), - HLLOperation::describe(hll_bin)]) + [HLLOperation::fold(hll_bin, ix), + HLLOperation::get_count(hll_bin), + HLLOperation::add(hll_bin, *vals0), + HLLOperation::add(hll_bin, *vals1), + HLLOperation::get_count(hll_bin), + HLLOperation::describe(hll_bin)]) resultb_list = recordb.bins[hll_bin] countb = resultb_list[1] @@ -738,7 +734,7 @@ def expect_fold(vals0, vals1, index_bits) expect(0).to eq n_added0 expect_description(descriptionb, ix, 0) expect_hll_count(ix, countb, vals0.length) - expect_hll_count(ix, countb1, vals0.length+vals1.length) + expect_hll_count(ix, countb1, vals0.length + vals1.length) end end @@ -772,10 +768,10 @@ def expect_set_union(vals, index_bits, folding, allow_folding) union_expected += sub_vals.length - record = expect_success(keys[i], - [Aerospike::Operation::delete, + record = expect_success(keys[i], [ + Aerospike::Operation::delete, HLLOperation::add(hll_bin, *sub_vals, index_bit_count: ix), - HLLOperation::get_count(hll_bin) + HLLOperation::get_count(hll_bin), ]) result_list = record.bins[hll_bin] count = result_list[1] @@ -820,8 +816,8 @@ def expect_set_union(vals, index_bits, folding, allow_folding) (0...keys.length).each do |i| sub_vals = vals[i] record = expect_success(key, - [HLLOperation::add(hll_bin, *sub_vals, index_bit_count: index_bits), - HLLOperation::get_count(hll_bin)]) + [HLLOperation::add(hll_bin, *sub_vals, index_bit_count: index_bits), + HLLOperation::get_count(hll_bin)]) result_list = record.bins[hll_bin] n_added = result_list[0] count = result_list[1] @@ -833,14 +829,14 @@ def expect_set_union(vals, index_bits, folding, allow_folding) end def absolute_similarity_error(index_bits, minhash_bits, expected_similarity) - min_err_index = 1 / Math.sqrt(1< (expected_similarity-similarity).abs).to be true + expect(sim_err_6sigma > (expected_similarity - similarity).abs).to be true expect(is_within_relative_error(expected_intersect_count, intersect_count, sim_err_6sigma)).to be true end @@ -860,10 +856,10 @@ def expect_similarity(overlap, common, vals, index_bits, minhash_bits) (0...keys.length).each do |i| record = expect_success(keys[i], - [Aerospike::Operation::delete, - HLLOperation::add(hll_bin, *vals[i], index_bit_count: index_bits, minhash_bit_count: minhash_bits), - HLLOperation::add(hll_bin, *common, index_bit_count: index_bits, minhash_bit_count: minhash_bits), - Aerospike::Operation::get(hll_bin)]) + [Aerospike::Operation::delete, + HLLOperation::add(hll_bin, *vals[i], index_bit_count: index_bits, minhash_bit_count: minhash_bits), + HLLOperation::add(hll_bin, *common, index_bit_count: index_bits, minhash_bit_count: minhash_bits), + Aerospike::Operation::get(hll_bin)]) result_list = record.bins[hll_bin] hlls << result_list[2] @@ -871,18 +867,18 @@ def expect_similarity(overlap, common, vals, index_bits, minhash_bits) # Keep record around win hll_bin is removed. record = expect_success(key, - [Aerospike::Operation::delete, - HLLOperation::init("#{hll_bin}other", index_bits, minhash_bits), - HLLOperation::set_union(hll_bin, *hlls), - HLLOperation::describe(hll_bin)]) + [Aerospike::Operation::delete, + HLLOperation::init("#{hll_bin}other", index_bits, minhash_bits), + HLLOperation::set_union(hll_bin, *hlls), + HLLOperation::describe(hll_bin)]) result_list = record.bins[hll_bin] description = result_list[1] expect_description(description, index_bits, minhash_bits) record = expect_success(key, - [HLLOperation::get_similarity(hll_bin, *hlls), - HLLOperation::get_intersect_count(hll_bin, *hlls)]) + [HLLOperation::get_similarity(hll_bin, *hlls), + HLLOperation::get_intersect_count(hll_bin, *hlls)]) result_list = record.bins[hll_bin] sim = result_list[0] intersect_count = result_list[1] @@ -890,7 +886,6 @@ def expect_similarity(overlap, common, vals, index_bits, minhash_bits) expected_intersect_count = common.length expect_hmh_similarity(index_bits, minhash_bits, sim, expected_similarity, intersect_count, - expected_intersect_count) + expected_intersect_count) end - end diff --git a/spec/aerospike/cdt_list_spec.rb b/spec/aerospike/cdt_list_spec.rb index 04fd8f87..ac491edb 100644 --- a/spec/aerospike/cdt_list_spec.rb +++ b/spec/aerospike/cdt_list_spec.rb @@ -18,7 +18,6 @@ include Aerospike::ResultCode describe "client.operate() - CDT List Operations", skip: !Support.feature?(Aerospike::Features::CDT_LIST) do - let(:client) { Support.client } let(:key) { Support.gen_random_key } let(:list_bin) { "list" } @@ -198,7 +197,8 @@ def list_post_op operation = ListOperation.set(list_bin, 2, 99) result = client.operate(key, [operation]) - expect(result.bins).to be nil + expected = { list_bin => nil } + expect(result.bins).to eq expected expect(list_post_op).to eql([1, 2, 99, 4, 5]) end end @@ -210,7 +210,8 @@ def list_post_op operation = ListOperation.clear(list_bin) result = client.operate(key, [operation]) - expect(result.bins).to be nil + expected = { list_bin => nil } + expect(result.bins).to eq expected expect(list_post_op).to eql([]) end end @@ -268,7 +269,8 @@ def list_post_op operation = ListOperation.get_by_index(list_bin, 2) result = client.operate(key, [operation]) - expect(result.bins).to be nil + expected = { list_bin => nil } + expect(result.bins).to eq expected end it "returns the value at the specified index" do @@ -562,7 +564,8 @@ def list_post_op .and_return(ListReturnType::DEFAULT) result = client.operate(key, [operation]) - expect(result.bins).to be nil + expected = { list_bin => nil } + expect(result.bins).to eq expected end it "returns the list index" do @@ -629,15 +632,14 @@ def list_post_op end 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], - ] + [7, 9, 5], + [1, 2, 3], + [6, 5, 4, 1], + ] client.put(key, Aerospike::Bin.new(list_bin, list)) @@ -649,17 +651,17 @@ def list_post_op 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] ] + 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) list = [ - [7, 9, 5], - [1, 2, 3], - [6, 5, 4, 1], - ] + [7, 9, 5], + [1, 2, 3], + [6, 5, 4, 1], + ] client.put(key, Aerospike::Bin.new(list_bin, list)) @@ -706,13 +708,12 @@ def list_post_op [2, 4, 11], [6, 1, 9], ], - }) + } + ) end end - describe "ListPolicy", skip: !Support.min_version?("3.16") do - context "ordered list" do let(:list_value) { [5, 4, 3, 2, 1] } let(:order) { ListOrder::ORDERED } @@ -797,7 +798,7 @@ def list_post_op end context "Wildcard value", skip: !Support.min_version?("4.3.1") do - let(:list_value) { [ ["John", 55], ["Jim", 95], ["Joe", 80], ["Jim", 46] ] } + let(:list_value) { [["John", 55], ["Jim", 95], ["Joe", 80], ["Jim", 46]] } it "returns all list elements that match a wildcard" do operation = ListOperation.get_by_value(list_bin, ["Jim", Aerospike::Value::WILDCARD]) diff --git a/spec/aerospike/cdt_map_spec.rb b/spec/aerospike/cdt_map_spec.rb index f17fb785..154b1dbf 100644 --- a/spec/aerospike/cdt_map_spec.rb +++ b/spec/aerospike/cdt_map_spec.rb @@ -18,7 +18,6 @@ include Aerospike::CDT describe "client.operate() - CDT Map Operations", skip: !Support.feature?(Aerospike::Features::CDT_MAP) do - let(:client) { Support.client } let(:key) { Support.gen_random_key } let(:map_bin) { "map_bin" } @@ -36,7 +35,6 @@ def map_post_op end describe "MapOperation Context", skip: !Support.min_version?("4.6") do - it "should support Create Map ops" do client.delete(key) @@ -49,13 +47,12 @@ def map_post_op 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), - ] - ) + [ + 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], @@ -81,12 +78,12 @@ def map_post_op 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, - }, + "key1" => { + "key11" => 9, "key12" => 4, + }, + "key2" => { + "key21" => 11, "key22" => 5, + }, }) end @@ -108,12 +105,12 @@ def map_post_op 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, - }, + "key1" => { + "key11" => 9, "key12" => 4, + }, + "key2" => { + "key21" => 11, "key22" => 5, + }, }) end @@ -122,10 +119,10 @@ def map_post_op m = { "key1" => { - "key11" => {"key111" => 1}, "key12" => {"key121" => 5}, + "key11" => { "key111" => 1 }, "key12" => { "key121" => 5 }, }, "key2" => { - "key21" => {"key211" => 7}, + "key21" => { "key211" => 7 }, }, } @@ -138,10 +135,10 @@ def map_post_op expect(record.bins[map_bin]).to eq({ "key1" => { - "key11" => {"key111" => 1}, "key12" => {"key121" => 11}, + "key11" => { "key111" => 1 }, "key12" => { "key121" => 11 }, }, "key2" => { - "key21" => {"key211" => 7}, + "key21" => { "key211" => 7 }, }, }) end @@ -213,8 +210,9 @@ def map_post_op operation = MapOperation.clear(map_bin) result = client.operate(key, [operation]) - expect(result.bins).to be_nil - expect(map_post_op).to eql({ }) + expected = { map_bin => nil } + expect(result.bins).to eq expected + expect(map_post_op).to eql({}) end end @@ -251,7 +249,8 @@ def map_post_op operation = MapOperation.remove_by_key_range(map_bin, "b", "c") result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1, "c" => 3 }) end @@ -259,7 +258,8 @@ def map_post_op operation = MapOperation.remove_by_key_range(map_bin, "b") result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end @@ -267,7 +267,8 @@ def map_post_op operation = MapOperation.remove_by_key_range(map_bin, nil, "b") result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "b" => 2, "c" => 3 }) end end @@ -299,7 +300,7 @@ def map_post_op it "removes the items identified by a single value" do operation = MapOperation.remove_by_value(map_bin, 2) - .and_return(MapReturnType::KEY) + .and_return(MapReturnType::KEY) result = client.operate(key, [operation]) expect(result.bins[map_bin]).to eql(["b", "d"]) @@ -327,7 +328,8 @@ def map_post_op operation = MapOperation.remove_by_value_range(map_bin, 2, 3) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1, "c" => 3 }) end @@ -335,7 +337,8 @@ def map_post_op operation = MapOperation.remove_by_value_range(map_bin, 2) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end @@ -343,7 +346,8 @@ def map_post_op operation = MapOperation.remove_by_value_range(map_bin, nil, 3) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "c" => 3 }) end end @@ -357,7 +361,7 @@ def map_post_op result = client.operate(key, [operation]) expect(result.bins[map_bin]).to eql({ 9 => 10 }) - expect(map_post_op).to eql({ 4 => 2, 5 => 15, 0 => 17 }) + expect(map_post_op).to eql({ 4 => 2, 5 => 15, 0 => 17 }) end it "removes elements from specified key until the end" do @@ -377,7 +381,8 @@ def map_post_op operation = MapOperation.remove_by_index(map_bin, 1) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1, "c" => 3 }) end end @@ -389,7 +394,8 @@ def map_post_op operation = MapOperation.remove_by_index_range(map_bin, 1, 2) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end @@ -397,7 +403,8 @@ def map_post_op operation = MapOperation.remove_by_index_range(map_bin, 1) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end end @@ -409,7 +416,8 @@ def map_post_op operation = MapOperation.remove_by_rank(map_bin, 1) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1, "c" => 3 }) end end @@ -421,7 +429,8 @@ def map_post_op operation = MapOperation.remove_by_rank_range(map_bin, 1, 2) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end @@ -429,7 +438,8 @@ def map_post_op operation = MapOperation.remove_by_rank_range(map_bin, 1) result = client.operate(key, [operation]) - expect(result.bins).to be_nil + expected = { map_bin => nil } + expect(result.bins).to eq expected expect(map_post_op).to eql({ "a" => 1 }) end end @@ -542,7 +552,7 @@ def map_post_op end describe "MapOperation.get_by_value_range" do - let(:map_value) { { "a" => 1, "b" => 2, "c" => 3, "d" => 2} } + let(:map_value) { { "a" => 1, "b" => 2, "c" => 3, "d" => 2 } } it "gets the specified key range from the map" do operation = MapOperation.get_by_value_range(map_bin, 2, 3) @@ -609,7 +619,7 @@ def map_post_op .and_return(MapReturnType::KEY) result = client.operate(key, [operation]) - expect(result.bins[map_bin]).to eql([ "b", "c" ]) + expect(result.bins[map_bin]).to eql(["b", "c"]) end it "gets all items starting at the specified index to the end of the map" do @@ -617,7 +627,7 @@ def map_post_op .and_return(MapReturnType::KEY) result = client.operate(key, [operation]) - expect(result.bins[map_bin]).to eql([ "b", "c" ]) + expect(result.bins[map_bin]).to eql(["b", "c"]) end end @@ -641,7 +651,7 @@ def map_post_op .and_return(MapReturnType::KEY) result = client.operate(key, [operation]) - expect(result.bins[map_bin]).to eql([ "b", "a" ]) + expect(result.bins[map_bin]).to eql(["b", "a"]) end it "gets all items starting at the specified rank to the end of the map" do @@ -649,7 +659,7 @@ def map_post_op .and_return(MapReturnType::KEY) result = client.operate(key, [operation]) - expect(result.bins[map_bin]).to eql([ "b", "a" ]) + expect(result.bins[map_bin]).to eql(["b", "a"]) end end @@ -708,7 +718,8 @@ def map_post_op .and_return(MapReturnType::NONE) result = client.operate(key, [operation]) - expect(result.bins).to eql(nil) + expected = { "map_bin" => nil } + expect(result.bins).to eq expected end end @@ -1063,12 +1074,14 @@ def map_post_op end context "Wildcard value", skip: !Support.min_version?("4.3.1") do - let(:map_value) { { - 4 => ["John", 55], - 5 => ["Jim", 95], - 9 => ["Joe", 80], - 12 => ["Jim", 46], - } } + let(:map_value) { + { + 4 => ["John", 55], + 5 => ["Jim", 95], + 9 => ["Joe", 80], + 12 => ["Jim", 46], + } + } it "returns all values that match a wildcard" do operation = MapOperation.get_by_value(map_bin, ["Jim", Aerospike::Value::WILDCARD]) diff --git a/spec/aerospike/exp/exp_bit_spec.rb b/spec/aerospike/exp/exp_bit_spec.rb new file mode 100644 index 00000000..7dc72374 --- /dev/null +++ b/spec/aerospike/exp/exp_bit_spec.rb @@ -0,0 +1,406 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp::Bit do + let(:client) { Support.client } + + describe "Expression filters", skip: false do + before :all do + @key_count = 100 + @namespace = "test" + @set = "query1000" + Support.client.truncate(@namespace, @set) + + opts = { expiration: 24 * 60 * 60 } + @key_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + bytes = bytes_to_str([0b00000001, 0b01000010]) + bin = { "bin" => Aerospike::BytesValue.new(bytes) } + Support.client.delete(key) + Support.client.put(key, bin) + + data = ["asd", ii] + data2 = ["asd", ii, ii + 1] + + ops = [ + Aerospike::CDT::HLLOperation.add("hllbin", *data, index_bit_count: 8, minhash_bit_count: 0), + Aerospike::CDT::HLLOperation.add("hllbin2", *data2, index_bit_count: 8, minhash_bit_count: 0), + ] + + Support.client.operate(key, ops) + end + end + + def bytes_to_str(bytes) + bytes.pack("C*").force_encoding("binary") + end + + def run_query(filter) + opts = { filter_exp: filter } + stmt = Aerospike::Statement.new(@namespace, @set) + client.query(stmt, opts) + end + + def count_results(rs) + count = 0 + rs.each do + count += 1 + end + count + end + + it "count should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(16), + Exp.blob_bin("bin"), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "resize should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(16), + Exp::Bit.resize( + Exp.int_val(4), + Aerospike::CDT::BitResizeFlags::DEFAULT, + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "insert should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(16), + Exp::Bit.insert( + Exp.int_val(0), + Exp.blob_val(bytes_to_str([0b11111111])), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(9), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.remove( + Exp.int_val(0), + Exp.int_val(1), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "set should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.set( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_val(bytes_to_str([0b10101010])), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(4), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "or should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.or( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_val(bytes_to_str([0b10101010])), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(5), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "xor should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.xor( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_val(bytes_to_str([0b10101011])), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(4), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "and should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.and( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_val(bytes_to_str([0b10101011])), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "not should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.not( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(7), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "lshift should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.lshift( + Exp.int_val(0), + Exp.int_val(16), + Exp.int_val(9), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "rshift should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.rshift( + Exp.int_val(0), + Exp.int_val(8), + Exp.int_val(3), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "add should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.add( + Exp.int_val(0), + Exp.int_val(8), + Exp.int_val(128), + false, + Aerospike::CDT::BitOverflowAction::WRAP, + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "subtract should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.subtract( + Exp.int_val(0), + Exp.int_val(8), + Exp.int_val(1), + false, + Aerospike::CDT::BitOverflowAction::WRAP, + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it " set_int should work" do + rs = run_query( + Exp.eq( + Exp::Bit.count( + Exp.int_val(0), + Exp.int_val(8), + Exp::Bit.set_int( + Exp.int_val(0), + Exp.int_val(8), + Exp.int_val(255), + Exp.blob_bin("bin"), + ), + ), + Exp.int_val(8), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get should work" do + rs = run_query( + Exp.eq( + Exp::Bit.get( + Exp.int_val(0), + Exp.int_val(8), + Exp.blob_bin("bin"), + ), + Exp.blob_val(bytes_to_str([0b00000001])), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "lscan should work" do + rs = run_query( + Exp.eq( + Exp::Bit.lscan( + Exp.int_val(8), + Exp.int_val(8), + Exp.bool_val(true), + Exp.blob_bin("bin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "rscan should work" do + rs = run_query( + Exp.eq( + Exp::Bit.rscan( + Exp.int_val(8), + Exp.int_val(8), + Exp.bool_val(true), + Exp.blob_bin("bin"), + ), + Exp.int_val(6), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_int should work" do + rs = run_query( + Exp.eq( + Exp::Bit.get_int( + Exp.int_val(0), + Exp.int_val(8), + false, + Exp.blob_bin("bin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + end +end diff --git a/spec/aerospike/exp/exp_hll_spec.rb b/spec/aerospike/exp/exp_hll_spec.rb new file mode 100644 index 00000000..2f14cc63 --- /dev/null +++ b/spec/aerospike/exp/exp_hll_spec.rb @@ -0,0 +1,170 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp::HLL do + let(:client) { Support.client } + + describe "Expression filters" do + before :all do + @key_count = 100 + @namespace = "test" + @set = "query1000" + + Support.client.truncate(@namespace, @set) + + opts = { expiration: 24 * 60 * 60 } + @key_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + bin = { "bin" => ii, "lbin" => [ii, "a"] } + Support.client.delete(key) + Support.client.put(key, bin) + + data = ["asd", ii] + data2 = ["asd", ii, ii + 1] + + ops = [ + Aerospike::CDT::HLLOperation.add("hllbin", *data, index_bit_count: 8, minhash_bit_count: 0), + Aerospike::CDT::HLLOperation.add("hllbin2", *data2, index_bit_count: 8, minhash_bit_count: 0), + ] + + Support.client.operate(key, ops) + end + end + + def run_query(filter) + opts = { filter_exp: filter } + stmt = Aerospike::Statement.new(@namespace, @set) + client.query(stmt, opts) + end + + def count_results(rs) + count = 0 + rs.each do + count += 1 + end + count + end + + it "get_count should work" do + rs = run_query( + Exp.eq( + Exp::HLL.get_count( + Exp::HLL.add( + Exp.list_val(Aerospike::Value.of(48715414)), + Exp.hll_bin("hllbin"), + index_bit_count: Exp.int_val(8), + min_hash_bit_count: Exp.int_val(0), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + + it "may_contain should work" do + rs = run_query( + Exp.eq( + Exp::HLL.may_contain( + Exp.list_val(Aerospike::Value.of(55)), + Exp.hll_bin("hllbin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "list_get_by_index should work" do + rs = run_query( + Exp.lt( + Exp::List.get_by_index( + Aerospike::CDT::ListReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(0), + Exp::HLL.describe(Exp.hll_bin("hllbin")), + ), + Exp.int_val(10), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_union should work" do + rs = run_query( + Exp.eq( + Exp::HLL.get_count( + Exp::HLL.get_union( + Exp.hll_bin("hllbin"), + Exp.hll_bin("hllbin2"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "get_union_count should work" do + rs = run_query( + Exp.eq( + Exp::HLL.get_union_count( + Exp.hll_bin("hllbin"), + Exp.hll_bin("hllbin2"), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "get_intersect_count should work" do + rs = run_query( + Exp.eq( + Exp::HLL.get_intersect_count( + Exp.hll_bin("hllbin"), + Exp.hll_bin("hllbin2"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + + it "get_similarity should work" do + rs = run_query( + Exp.gt( + Exp::HLL.get_similarity( + Exp.hll_bin("hllbin"), + Exp.hll_bin("hllbin2"), + ), + Exp.float_val(0.5), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + end +end diff --git a/spec/aerospike/exp/exp_list_spec.rb b/spec/aerospike/exp/exp_list_spec.rb new file mode 100644 index 00000000..cf8eadd6 --- /dev/null +++ b/spec/aerospike/exp/exp_list_spec.rb @@ -0,0 +1,514 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp::List do + let(:client) { Support.client } + + describe "Expression filters" do + before :all do + @key_count = 100 + @namespace = "test" + @set = "query1000" + + opts = { expiration: 24 * 60 * 60 } + @key_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + ibin = { "bin" => [1, 2, 3, ii] } + Support.client.delete(key, opts) + Support.client.put(key, ibin, opts) + end + end + + def run_query(filter) + opts = { filter_exp: filter } + stmt = Aerospike::Statement.new(@namespace, @set) + client.query(stmt, opts) + end + + def count_results(rs) + count = 0 + rs.each do + count += 1 + end + count + end + + it "append should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.append( + Exp.int_val(999), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(5), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "append_items should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.append_items( + Exp::list_val(Aerospike::Value.of(555), Aerospike::Value.of("asd")), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(6), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "clear should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.clear(Exp::list_bin("bin")), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "ListReturnType::Count should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_value( + Aerospike::CDT::ListReturnType::COUNT, + Exp.int_val(234), + Exp::List.insert( + Exp.int_val(1), + Exp.int_val(234), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "ListReturnType::Count should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_value_list( + Aerospike::CDT::ListReturnType::COUNT, + Exp::list_val(Aerospike::Value.of(51), Aerospike::Value.of(52)), + Exp::list_bin("bin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 2 + end + + it "insert_items should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.insert_items( + Exp.int_val(4), + Exp::list_val(Aerospike::Value.of(222), Aerospike::Value.of(223)), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(6), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_index( + Aerospike::CDT::ListReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(3), + Exp::List.increment( + Exp.int_val(3), + Exp.int_val(100), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(102), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_index( + Aerospike::CDT::ListReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(3), + Exp::List.set( + Exp.int_val(3), + Exp.int_val(100), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(100), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_index_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(2), + Exp::list_bin("bin"), + count: 2, + ), + Exp::list_val(Aerospike::Value.of(3), Aerospike::Value.of(15)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_index_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(2), + Exp::list_bin("bin"), + ), + Exp::list_val(Aerospike::Value.of(3), Aerospike::Value.of(15)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_rank( + Aerospike::CDT::ListReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(3), + Exp::list_bin("bin"), + ), + Exp.int_val(25), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_rank_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(2), + Exp::list_bin("bin"), + ), + Exp::list_val(Aerospike::Value.of(3), Aerospike::Value.of(25)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_rank_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(2), + Exp::list_bin("bin"), + count: 2, + ), + Exp::list_val(Aerospike::Value.of(3), Aerospike::Value.of(3)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_value_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(1), + Exp.int_val(3), + Exp::list_bin("bin"), + ), + Exp::list_val(Aerospike::Value.of(1), Aerospike::Value.of(2)), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "ListReturnType::Count should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_value_relative_rank_range( + Aerospike::CDT::ListReturnType::COUNT, + Exp.int_val(2), + Exp.int_val(0), + Exp::list_bin("bin"), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "ListReturnType::Value should work" do + rs = run_query( + Exp.eq( + Exp::List.get_by_value_relative_rank_range( + Aerospike::CDT::ListReturnType::VALUE, + Exp.int_val(2), + Exp.int_val(1), + Exp::list_bin("bin"), + count: 1, + ), + Exp::list_val(Aerospike::Value.of(3)), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + + it "remove_by_value should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_value( + Exp.int_val(3), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + + it "remove_by_value_list should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_value_list( + Exp::list_val(Aerospike::Value.of(1), Aerospike::Value.of(2)), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "remove_by_value_range should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_value_range( + Exp.int_val(1), + Exp.int_val(3), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 98 + end + + it "remove_by_value_relative_rank_range should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_value_relative_rank_range( + Exp.int_val(3), + Exp.int_val(1), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 97 + end + + it "remove_by_value_relative_rank_range for count should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_value_relative_rank_range( + Exp.int_val(2), + Exp.int_val(1), + Exp::list_bin("bin"), + count: 1, + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_index( + Exp.int_val(0), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index_range should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_index_range( + Exp.int_val(2), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index_range for count should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_index_range( + Exp.int_val(2), + Exp::list_bin("bin"), + count: 1, + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index_range for count should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_index_range( + Exp.int_val(2), + Exp::list_bin("bin"), + count: 1, + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_rank( + Exp.int_val(2), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank_range should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_rank_range( + Exp.int_val(2), + Exp::list_bin("bin"), + ), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank_range for count should work" do + rs = run_query( + Exp.eq( + Exp::List.size( + Exp::List.remove_by_rank_range( + Exp.int_val(2), + Exp::list_bin("bin"), + count: 1, + ), + ), + Exp.int_val(3), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + end +end diff --git a/spec/aerospike/exp/exp_map_spec.rb b/spec/aerospike/exp/exp_map_spec.rb new file mode 100644 index 00000000..3e0a4eee --- /dev/null +++ b/spec/aerospike/exp/exp_map_spec.rb @@ -0,0 +1,564 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp::Map do + let(:client) { Support.client } + + describe "Expression filters" do + before :all do + @key_count = 100 + @namespace = "test" + @set = "query1000" + + opts = { expiration: 24 * 60 * 60 } + @key_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + ibin = { "bin" => { "test" => ii, "test2" => "a" } } + Support.client.delete(key, opts) + Support.client.put(key, ibin, opts) + end + end + + def run_query(filter) + opts = { filter_exp: filter } + stmt = Aerospike::Statement.new(@namespace, @set) + client.query(stmt, opts) + end + + def count_results(rs) + count = 0 + rs.each do + count += 1 + end + count + end + + it "get_by_key should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_key( + Aerospike::CDT::MapReturnType::VALUE, + Exp::Type::INT, + Exp.str_val("test3"), + Exp::Map.put( + Exp.str_val("test3"), + Exp.int_val(999), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(999), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_key_list should work" do + amap = { + "test4" => 333, + "test5" => 444, + } + rs = run_query( + Exp.eq( + Exp::Map.get_by_key_list( + Aerospike::CDT::MapReturnType::VALUE, + Exp.list_val(Aerospike::Value.of("test4"), Aerospike::Value.of("test5")), + Exp::Map.put_items( + Exp::map_val(amap), + Exp::map_bin("bin"), + ), + ), + Exp.list_val(Aerospike::Value.of(333), Aerospike::Value.of(444)), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_value should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_value( + Aerospike::CDT::MapReturnType::COUNT, + Exp.int_val(5), + Exp::Map.increment( + Exp.str_val("test"), + Exp.int_val(1), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "Clear should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.clear(Exp::map_bin("bin")), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_value_list should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_value_list( + Aerospike::CDT::MapReturnType::COUNT, + Exp.list_val(Aerospike::Value.of(1), Aerospike::Value.of("a")), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "get_by_value_relative_rank_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_value_relative_rank_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.int_val(1), + Exp.int_val(0), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 99 + end + + it "get_by_value_relative_rank_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_value_relative_rank_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.int_val(1), + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_index should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_index( + Aerospike::CDT::MapReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(0), + Exp::map_bin("bin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "get_by_index_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_index_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.int_val(0), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_index_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_index_range( + Aerospike::CDT::MapReturnType::VALUE, + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + Exp.list_val(Aerospike::Value.of(2)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "get_by_rank should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_rank( + Aerospike::CDT::MapReturnType::VALUE, + Exp::Type::INT, + Exp.int_val(0), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "get_by_rank_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_rank_range( + Aerospike::CDT::MapReturnType::VALUE, + Exp.int_val(1), + Exp::map_bin("bin"), + ), + Exp.list_val(Aerospike::Value.of("a")), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_rank_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_rank_range( + Aerospike::CDT::MapReturnType::VALUE, + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + Exp.list_val(Aerospike::Value.of(15)), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "get_by_value_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_value_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.int_val(0), + Exp.int_val(18), + Exp::map_bin("bin"), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 18 + end + + it "get_by_key_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_key_range( + Aerospike::CDT::MapReturnType::COUNT, + nil, + Exp.str_val("test25"), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_key_relative_index_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_key_relative_index_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.str_val("test"), + Exp.int_val(0), + Exp::map_bin("bin"), + ), + Exp.int_val(2), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "get_by_key_relative_index_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.get_by_key_relative_index_range( + Aerospike::CDT::MapReturnType::COUNT, + Exp.str_val("test"), + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_key should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_key( + Exp.str_val("test"), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_key_list should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_key_list( + Exp.list_val(Aerospike::Value.of("test"), Aerospike::Value.of("test2")), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_key_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_key_range( + Exp.str_val("test"), + nil, + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_key_relative_index_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_key_relative_index_range( + Exp.str_val("test"), + Exp.int_val(0), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_key_relative_index_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_key_relative_index_range( + Exp.str_val("test"), + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_value should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_value( + Exp.int_val(5), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "remove_by_value_list should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_value_list( + Exp.list_val(Aerospike::Value.of("a"), Aerospike::Value.of(15)), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 1 + end + + it "remove_by_value_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_value_range( + Exp.int_val(5), + Exp.int_val(15), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 10 + end + + it "remove_by_index should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_index( + Exp.int_val(0), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_index_range( + Exp.int_val(0), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_index_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_index_range( + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_rank( + Exp.int_val(0), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank_range should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_rank_range( + Exp.int_val(0), + Exp::map_bin("bin"), + ), + ), + Exp.int_val(0), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + + it "remove_by_rank_range with count should work" do + rs = run_query( + Exp.eq( + Exp::Map.size( + Exp::Map.remove_by_rank_range( + Exp.int_val(0), + Exp::map_bin("bin"), + count: 1, + ), + ), + Exp.int_val(1), + ), + ) + count = count_results(rs) + expect(count).to eq 100 + end + end +end diff --git a/spec/aerospike/exp/expression_spec.rb b/spec/aerospike/exp/expression_spec.rb new file mode 100644 index 00000000..d8208d2e --- /dev/null +++ b/spec/aerospike/exp/expression_spec.rb @@ -0,0 +1,629 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp do + let(:client) { Support.client } + + describe "Expression filters" do + before :all do + @namespace = "test" + @set = "query1000" + @record_count = 1000 + @record_count.times do |i| + key = Aerospike::Key.new(@namespace, @set, i) + bin_map = { + "bin1" => "value#{i}", + "bin2" => i, + "bin3" => [i, i + 1_000, i + 1_000_000], + "bin4" => { "key#{i}" => i }, + } + Support.client.put(key, bin_map) + end + + Support.client.drop_index(@namespace, @set, "index_intval") + Support.client.drop_index(@namespace, @set, "index_strval") + + wpolicy = { generation: 0, expiration: 24 * 60 * 60 } + + starbucks = [ + [-122.1708441, 37.4241193], + [-122.1492040, 37.4273569], + [-122.1441078, 37.4268202], + [-122.1251714, 37.4130590], + [-122.0964289, 37.4218102], + [-122.0776641, 37.4158199], + [-122.0943475, 37.4114654], + [-122.1122861, 37.4028493], + [-122.0947230, 37.3909250], + [-122.0831037, 37.3876090], + [-122.0707119, 37.3787855], + [-122.0303178, 37.3882739], + [-122.0464861, 37.3786236], + [-122.0582128, 37.3726980], + [-122.0365083, 37.3676930], + ] + + @record_count.times do |ii| + + # On iteration 333 we pause for a few mSec and note the + # time. Later we can check last_update time for either + # side of this gap ... + # + # Also, we update the WritePolicy to never expire so + # records w/ 0 TTL can be counted later. + + key = Aerospike::Key.new(@namespace, @set, ii) + + lng = -122.0 + (0.01 * ii) + lat = 37.5 + (0.01 * ii) + point = Aerospike::GeoJSON.point(lat, lng) + + if ii < starbucks.length + region = Aerospike::GeoJSON.circle(starbucks[ii][0], starbucks[ii][1], 3000.0) + else + # Somewhere off Africa ... + region = Aerospike::GeoJSON.circle(0.0, 0.0, 3000.0) + end + + # Accumulate prime factors of the index into a list and map. + listval = [] + mapval = {} + [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31].each do |ff| + if ii >= ff && ii % ff == 0 + listval << ff + mapval[ff] = sprintf("0x%04x", ff) + end + end + + ballast = ("0" * (ii * 16)).force_encoding("binary") + + bins = { + "intval" => ii, + "strval" => sprintf("0x%04x", ii), + "modval" => ii % 10, + # "locval" => point, + # "rgnval" => region, + "lstval" => listval, + "mapval" => mapval, + "ballast" => ballast, + } + + Support.client.put(key, bins, wpolicy) + end + + tasks = [] + tasks << Support.client.create_index(@namespace, @set, "index_strval", "strval", :string) + tasks << Support.client.create_index(@namespace, @set, "index_intval", "intval", :numeric) + tasks.each(&:wait_till_completed) + expect(tasks.all?(&:completed?)).to be true + end + + context "Invalid cases" do + it "should return an error when expression is not boolean" do + stmt = Aerospike::Statement.new(@namespace, @set) + stmt.filters << Aerospike::Filter.Range("intval", 0, 400) + opts = { filter_exp: (Aerospike::Exp.int_val(100)) } + expect { + rs = client.query(stmt, opts) + rs.each do end + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::PARAMETER_ERROR + } + end + end + + context "Valid expression" do + it "should additionally filter indexed query results" do + stmt = Aerospike::Statement.new(@namespace, @set) + stmt.filters << Aerospike::Filter.Range("intval", 0, 400) + opts = { filter_exp: (Aerospike::Exp.ge(Aerospike::Exp.int_bin("modval"), Aerospike::Exp.int_val(8))) } + + # The query clause selects [0, 1, ... 400, 401] The predexp + # only takes mod 8 and 9, should be 2 pre decade or 80 total. + + rs = client.query(stmt, opts) + count = 0 + rs.each do |rec| + count += 1 + end + expect(count).to eq 80 + end + + it "should work for implied scans" do + stmt = Aerospike::Statement.new(@namespace, @set) + opts = { filter_exp: (Aerospike::Exp.eq(Aerospike::Exp.str_bin("strval"), Aerospike::Exp.str_val("0x0001"))) } + + rs = client.query(stmt, opts) + count = 0 + rs.each do |rec| + count += 1 + end + expect(count).to eq 1 + end + + it "expression and or and not must all work" do + stmt = Aerospike::Statement.new(@namespace, @set) + opts = { filter_exp: (Aerospike::Exp.or( + Aerospike::Exp.and( + Aerospike::Exp.not(Aerospike::Exp.eq(Aerospike::Exp.str_bin("strval"), Aerospike::Exp.str_val("0x0001"))), + Aerospike::Exp.ge(Aerospike::Exp.int_bin("modval"), Aerospike::Exp.int_val(8)), + ), + Aerospike::Exp.eq(Aerospike::Exp.str_bin("strval"), Aerospike::Exp.str_val("0x0104")), + Aerospike::Exp.eq(Aerospike::Exp.str_bin("strval"), Aerospike::Exp.str_val("0x0105")), + Aerospike::Exp.eq(Aerospike::Exp.str_bin("strval"), Aerospike::Exp.str_val("0x0106")), + )) } + + rs = client.query(stmt, opts) + count = 0 + rs.each do |rec| + count += 1 + end + expect(count).to eq 203 + end + end + + context "for" do + @namespace = "test" + @set = "query1000" + + def query_method(exp, ops = {}) + stmt = Aerospike::Statement.new(@namespace, @set) + ops[:filter_exp] = (exp) + rs = client.query(stmt, ops) + count = 0 + rs.each do |rec| + count += 1 + end + count + end + + before :all do + Support.client.truncate(@namespace, @set) + + @record_count = 100 + @record_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + bins = { + "bin" => ii, + "bin2" => "#{ii}", + "bin3" => ii.to_f / 3, + "bin4" => BytesValue.new("blob#{ii}"), + "bin5" => ["a", "b", ii], + "bin6" => { "a": "test", "b": ii }, + } + Support.client.put(key, bins) + end + end + + # [title, result, exp] + matrix = [ + # data types + ["int_bin must work", 1, Exp.eq(Exp.int_bin("bin"), Exp.int_val(1))], + ["str_bin must work", 1, Exp.eq(Exp.str_bin("bin2"), Exp.str_val("1"))], + ["float_bin must work", 1, Exp.eq(Exp.float_bin("bin3"), Exp.float_val(2))], + ["blob_bin must work", 1, Exp.eq(Exp.blob_bin("bin4"), Exp.blob_val("blob5"))], + ["bin_type must work", 100, Exp.ne(Exp.bin_type("bin"), Exp.int_val(0))], + # logical ops + ["and must work", 1, Exp.and(Exp.eq(Exp.int_bin("bin"), Exp.int_val(1)), Exp.eq(Exp.str_bin("bin2"), Exp.str_val("1")))], + ["or must work", 2, Exp.or(Exp.eq(Exp.int_bin("bin"), Exp.int_val(1)), Exp.eq(Exp.int_bin("bin"), Exp.int_val(3)))], + ["not must work", 99, Exp.not(Exp.eq(Exp.int_bin("bin"), Exp.int_val(1)))], + # comparisons + ["eq must work", 1, Exp.eq(Exp.int_bin("bin"), Exp.int_val(1))], + ["ne must work", 99, Exp.ne(Exp.int_bin("bin"), Exp.int_val(1))], + ["lt must work", 99, Exp.lt(Exp.int_bin("bin"), Exp.int_val(99))], + ["le must work", 100, Exp.le(Exp.int_bin("bin"), Exp.int_val(99))], + ["gt must work", 98, Exp.gt(Exp.int_bin("bin"), Exp.int_val(1))], + ["ge must work", 99, Exp.ge(Exp.int_bin("bin"), Exp.int_val(1))], + # record ops + ["memory_size must work", 100, Exp.ge(Exp.memory_size, Exp.int_val(0))], + ["last_update must work", 100, Exp.gt(Exp.last_update, Exp.int_val(15000))], + ["since_update must work", 100, Exp.gt(Exp.since_update, Exp.int_val(150))], + ["is_tombstone must work", 100, Exp.not(Exp.is_tombstone)], + ["set_name must work", 100, Exp.eq(Exp.set_name, Exp.str_val("query1000"))], + ["bin_exists must work", 100, Exp.bin_exists("bin4")], + ["digest_modulo must work", 34, Exp.eq(Exp.digest_modulo(3), Exp.int_val(1))], + ["key must work", 0, Exp.eq(Exp.key(Exp::Type::INT), Exp.int_val(50))], + ["key_exists must work", 0, Exp.key_exists], + ["nil must work", 100, Exp.eq(Exp.nil_val, Exp.nil_val)], + ["regex_compare must work", 75, Exp.regex_compare("[1-5]", Exp::RegexFlags::ICASE, Exp.str_bin("bin2"))], + ] + + matrix.each do |title, result, exp| + it title do + expect(query_method(exp)).to eq result + end + end + end + + context "command" do + before :all do + @record_count.times do |ii| + key = Aerospike::Key.new(@namespace, @set, ii) + bins = { "bin" => ii } + Support.client.delete(key) + Support.client.put(key, bins) + end + end + + it "should Delete" do + key = Aerospike::Key.new(@namespace, @set, 15) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(16), + )), + } + expect { + client.delete(key, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + client.delete(key, opts) + end + + it "should Put" do + key = Aerospike::Key.new(@namespace, @set, 25) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + expect { + client.put(key, { "bin" => 26 }, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(25), + )), + } + client.put(key, { "bin" => 26 }, opts) + end + + it "should Get" do + key = Aerospike::Key.new(@namespace, @set, 35) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + + expect { + client.get(key, nil, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(35), + )), + } + client.get(key, ["bin"], opts) + end + + it "should Exists" do + key = Aerospike::Key.new(@namespace, @set, 45) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + expect { + client.exists(key, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(45), + )), + } + client.exists(key, opts) + end + + it "should Add" do + key = Aerospike::Key.new(@namespace, @set, 55) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + expect { + client.add(key, { "test55" => "test" }, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(55), + )), + } + client.add(key, { "test55" => "test" }, opts) + end + + it "should Prepend" do + key = Aerospike::Key.new(@namespace, @set, 55) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + expect { + client.prepend(key, { "test55" => "test" }, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(55), + )), + } + client.prepend(key, { "test55" => "test" }, opts) + end + + it "should Touch" do + key = Aerospike::Key.new(@namespace, @set, 65) + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(15), + )), + } + expect { + client.touch(key, opts) + }.to raise_aerospike_error(Aerospike::ResultCode::FILTERED_OUT) + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(65), + )), + } + client.touch(key, opts) + end + + it "should Scan" do + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.eq( + Exp.int_bin("bin"), + Exp.int_val(75), + )), + } + + rs = client.scan_all(@namespace, @set, nil, opts) + count = 0 + rs.each do |res| + count += 1 + end + expect(count).to eq 1 + end + end + + context "for ops" do + @namespace = "test" + @set = "query1000" + + bin_a = "A" + bin_b = "B" + bin_c = "C" + bin_d = "D" + bin_e = "E" + + key_a = Aerospike::Key.new(@namespace, @set, "A") + key_b = Aerospike::Key.new(@namespace, @set, Aerospike::BytesValue.new("B")) + key_c = Aerospike::Key.new(@namespace, @set, "C") + + before :all do + Support.client.truncate(@namespace, @set) + + Support.client.put(key_a, { bin_a => 1, bin_b => 1.1, bin_c => "abcde", bin_d => 1, bin_e => -1 }) + Support.client.put(key_b, { bin_a => 2, bin_b => 2.2, bin_c => "abcdeabcde", bin_d => 1, bin_e => -2 }) + Support.client.put(key_c, { bin_a => 0, bin_b => -1, bin_c => 1 }) + end + + # [title, exp, key, exp_key, bin, expected, reverse_exp] + matrix = [ + ["exclusive", Exp.exclusive(Exp.eq(Exp.int_bin(bin_a), Exp.int_val(1)), Exp.eq(Exp.int_bin(bin_d), Exp.int_val(1))), key_a, key_b, bin_a, 2, false], + ["add_int", Exp.eq(Exp.add(Exp.int_bin(bin_a), Exp.int_bin(bin_d), Exp.int_val(1)), Exp.int_val(4)), key_a, key_b, bin_a, 2, false], + ["sub_int", Exp.eq(Exp.sub(Exp.int_val(1), Exp.int_bin(bin_a), Exp.int_bin(bin_d)), Exp.int_val(-2)), key_a, key_b, bin_a, 2, false], + ["mul_int", Exp.eq(Exp.mul(Exp.int_val(2), Exp.int_bin(bin_a), Exp.int_bin(bin_d)), Exp.int_val(4)), key_a, key_b, bin_a, 2, false], + ["div_int", Exp.eq(Exp.div(Exp.int_val(8), Exp.int_bin(bin_a), Exp.int_bin(bin_d)), Exp.int_val(4)), key_a, key_b, bin_a, 2, false], + ["mod_int", Exp.eq(Exp.mod(Exp.int_bin(bin_a), Exp.int_val(2)), Exp.int_val(0)), key_a, key_b, bin_a, 2, false], + ["abs_int", Exp.eq(Exp.abs(Exp.int_bin(bin_e)), Exp.int_val(2)), key_a, key_b, bin_a, 2, false], + ["floor", Exp.eq(Exp.floor(Exp.float_bin(bin_b)), Exp.float_val(2)), key_a, key_b, bin_a, 2, false], + ["ceil", Exp.eq(Exp.ceil(Exp.float_bin(bin_b)), Exp.float_val(3)), key_a, key_b, bin_a, 2, false], + ["to_int", Exp.eq(Exp.to_int(Exp.float_bin(bin_b)), Exp.int_val(2)), key_a, key_b, bin_a, 2, false], + ["to_float", Exp.eq(Exp.to_float(Exp.int_bin(bin_a)), Exp.float_val(2)), key_a, key_b, bin_a, 2, false], + ["int_and", Exp.not( + Exp.and( + Exp.eq( + Exp.int_and(Exp.int_bin(bin_a), Exp.int_val(0)), + Exp.int_val(0) + ), + Exp.eq( + Exp.int_and(Exp.int_bin(bin_a), Exp.int_val(0xFFFF)), + Exp.int_val(1), + ) + ) + ), key_a, key_a, bin_a, 1, true], + ["int_or", Exp.not( + Exp.and( + Exp.eq( + Exp.int_or(Exp.int_bin(bin_a), Exp.int_val(0)), + Exp.int_val(1) + ), + Exp.eq( + Exp.int_or(Exp.int_bin(bin_a), Exp.int_val(0xFF)), + Exp.int_val(0xFF), + ) + ) + ), key_a, key_a, bin_a, 1, true], + ["int_xor", Exp.not( + Exp.and( + Exp.eq( + Exp.int_xor(Exp.int_bin(bin_a), Exp.int_val(0)), + Exp.int_val(1) + ), + Exp.eq( + Exp.int_xor(Exp.int_bin(bin_a), Exp.int_val(0xFF)), + Exp.int_val(0xFE), + ) + ) + ), key_a, key_a, bin_a, 1, true], + ["int_not", Exp.not( + Exp.eq( + Exp.int_not(Exp.int_bin(bin_a)), + Exp.int_val(-2) + ) + ), key_a, key_a, bin_a, 1, true], + ["lshift", Exp.not( + Exp.eq( + Exp.lshift(Exp.int_bin(bin_a), Exp.int_val(2)), + Exp.int_val(4) + ) + ), key_a, key_a, bin_a, 1, true], + ["rshift", Exp.not( + Exp.eq( + Exp.rshift(Exp.int_bin(bin_e), Exp.int_val(62)), + Exp.int_val(3) + ) + ), key_b, key_b, bin_e, -2, true], + ["arshift", Exp.not( + Exp.eq( + Exp.arshift(Exp.int_bin(bin_e), Exp.int_val(62)), + Exp.int_val(-1) + ) + ), key_b, key_b, bin_e, -2, true], + ["bit_count", Exp.not( + Exp.eq( + Exp.count(Exp.int_bin(bin_a)), + Exp.int_val(1) + ) + ), key_a, key_a, bin_a, 1, true], + ["lscan", Exp.not( + Exp.eq( + Exp.lscan(Exp.int_bin(bin_a), Exp.bool_val(true)), + Exp.int_val(63) + ) + ), key_a, key_a, bin_a, 1, true], + ["rscan", Exp.not( + Exp.eq( + Exp.rscan(Exp.int_bin(bin_a), Exp.bool_val(true)), + Exp.int_val(63) + ) + ), key_a, key_a, bin_a, 1, true], + ["min", Exp.not( + Exp.eq( + Exp.min(Exp.int_bin(bin_a), Exp.int_bin(bin_d), Exp.int_bin(bin_e)), + Exp.int_val(-1) + ) + ), key_a, key_a, bin_a, 1, true], + ["max", Exp.not( + Exp.eq( + Exp.max(Exp.int_bin(bin_a), Exp.int_bin(bin_d), Exp.int_bin(bin_e)), + Exp.int_val(1) + ) + ), key_a, key_a, bin_a, 1, true], + ["cond", Exp.not( + Exp.eq( + Exp.cond( + Exp.eq(Exp.int_bin(bin_a), Exp.int_val(0)), Exp.add(Exp.int_bin(bin_d), Exp.int_bin(bin_e)), + Exp.eq(Exp.int_bin(bin_a), Exp.int_val(1)), Exp.sub(Exp.int_bin(bin_d), Exp.int_bin(bin_e)), + Exp.eq(Exp.int_bin(bin_a), Exp.int_val(2)), Exp.mul(Exp.int_bin(bin_d), Exp.int_bin(bin_e)), + Exp.int_val(-1) + ), + Exp.int_val(2) + ) + ), key_a, key_a, bin_a, 1, true], + + ["add_float", Exp.let( + Exp.def("val", Exp.add(Exp.float_bin(bin_b), Exp.float_val(1.1))), + Exp.and( + Exp.ge(Exp.var("val"), Exp.float_val(3.2999)), + Exp.le(Exp.var("val"), Exp.float_val(3.3001)), + ) + ), + key_a, key_b, bin_a, 2, false], + ["log_float", Exp.let( + Exp.def("val", Exp.log(Exp.float_bin(bin_b), Exp.float_val(2.0))), + Exp.and( + Exp.ge(Exp.var("val"), Exp.float_val(1.1374)), + Exp.le(Exp.var("val"), Exp.float_val(1.1376)) + ) + ), key_a, key_b, bin_a, 2, false], + ["pow_float", Exp.let( + Exp.def("val", Exp.pow(Exp.float_bin(bin_b), Exp.float_val(2.0))), + Exp.and( + Exp.ge(Exp.var("val"), Exp.float_val(4.8399)), + Exp.le(Exp.var("val"), Exp.float_val(4.8401)) + ) + ), key_a, key_b, bin_a, 2, false], + ] + + matrix.each do |title, exp, key, exp_key, bin, expected, reverse_exp| + it "#{title} should work" do + opts = { + fail_on_filtered_out: true, + filter_exp: (exp), + } + + expect { + client.get(key, nil, opts) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::FILTERED_OUT + } + + opts = { + fail_on_filtered_out: true, + filter_exp: (Exp.not(exp)), + } if reverse_exp + r = client.get(exp_key, nil, opts) + client.get(key) + expect(r.bins[bin]).to eq expected + end + end + end + end # describe +end # describe diff --git a/spec/aerospike/exp/operation_spec.rb b/spec/aerospike/exp/operation_spec.rb new file mode 100644 index 00000000..c58266f2 --- /dev/null +++ b/spec/aerospike/exp/operation_spec.rb @@ -0,0 +1,358 @@ +# encoding: utf-8 +# Copyright 2014 Aerospike, Inc. +# +# Portions may be licensed to Aerospike, Inc. under one or more contributor +# license agreements. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may not +# use this file except in compliance with the License. You may obtain a copy of +# the License at http:#www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations under +# the License. + +require "aerospike/query/statement" +include Aerospike + +describe Aerospike::Exp::Operation do + let(:client) { Support.client } + + describe "Expression Operations" do + before do + client.default_operate_policy.record_bin_multiplicity = RecordBinMultiplicity::ARRAY + end + + after do + client.default_operate_policy.record_bin_multiplicity = RecordBinMultiplicity::SINGLE + end + + before :each do + @namespace = "test" + @set = "test" + + @bin_a = "A" + @bin_b = "B" + @bin_c = "C" + @bin_d = "D" + @bin_h = "H" + @exp_var = "EV" + + @key_a = Aerospike::Key.new(@namespace, @set, "A") + @key_b = Aerospike::Key.new(@namespace, @set, Aerospike::BytesValue.new("B")) + + client.delete(@key_a) + client.delete(@key_b) + + client.put(@key_a, { @bin_a => 1, @bin_d => 2 }) + client.put(@key_b, { @bin_b => 2, @bin_d => 2 }) + end + + def bytes_to_str(bytes) + bytes.pack("C*").force_encoding("binary") + end + + # it "Expression ops on lists should work" do + # list = Aerospike::ListValue.new([Aerospike::StringValue.new("a"), Aerospike::StringValue.new("b"), Aerospike::StringValue.new("c"), Aerospike::StringValue.new("d")]) + # exp = Aerospike::Exp::list_val(list) + # rec = client.operate(@key_a, [Aerospike::Exp::Operation.write(@bin_c, exp), + # Aerospike::Operation::get(@bin_c), + # Aerospike::Exp::Operation.read("var", exp)]) + + # expected = { "C" => [nil, ["a", "b", "c", "d"]], "var" => ["a", "b", "c", "d"] } + # expect(rec.bins).to eq expected + # end + + it "Read Eval error should work" do + exp = Aerospike::Exp::add(Aerospike::Exp::int_bin(@bin_a), Aerospike::Exp::int_val(4)) + + r = client.operate(@key_a, [Aerospike::Exp::Operation.read(@exp_var, exp)]) + expect(r.bins&.length).to be > 0 + + expect { + client.operate(@key_b, [Aerospike::Exp::Operation.read(@exp_var, exp)]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::OP_NOT_APPLICABLE + } + + r = client.operate(@key_b, [Aerospike::Exp::Operation.read(@exp_var, exp, Aerospike::Exp::ReadFlags::EVAL_NO_FAIL)]) + expect(r.bins&.length).to be > 0 + end + + it "Read On Write Eval error should work" do + rexp = Aerospike::Exp::int_bin(@bin_d) + wexp = Aerospike::Exp::int_bin(@bin_a) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_d, wexp), + Aerospike::Exp::Operation.read(@exp_var, rexp), + ]) + expect(r.bins&.length).to be > 0 + + expect { + client.operate(@key_b, [ + Aerospike::Exp::Operation.write(@bin_d, wexp), + Aerospike::Exp::Operation.read(@exp_var, rexp), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::OP_NOT_APPLICABLE + } + + r = client.operate(@key_b, [ + Aerospike::Exp::Operation.write(@bin_d, wexp, Aerospike::Exp::WriteFlags::EVAL_NO_FAIL), + Aerospike::Exp::Operation.read(@exp_var, rexp, Aerospike::Exp::ReadFlags::EVAL_NO_FAIL), + ]) + expect(r.bins&.length).to be > 0 + end + + it "Write Eval error should work" do + wexp = Aerospike::Exp::add(Aerospike::Exp::int_bin(@bin_a), Aerospike::Exp::int_val(4)) + rexp = Aerospike::Exp::int_bin(@bin_c) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp), + Aerospike::Exp::Operation.read(@exp_var, rexp), + ]) + expect(r.bins&.length).to be > 0 + + expect { + client.operate(@key_b, [ + Aerospike::Exp::Operation.write(@bin_c, wexp), + Aerospike::Exp::Operation.read(@exp_var, rexp), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::OP_NOT_APPLICABLE + } + + r = client.operate(@key_b, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::EVAL_NO_FAIL), + Aerospike::Exp::Operation.read(@exp_var, rexp, Aerospike::Exp::ReadFlags::EVAL_NO_FAIL), + ]) + expect(r.bins&.length).to be > 0 + end + + it "Write Policy error should work" do + wexp = Aerospike::Exp::add(Aerospike::Exp::int_bin(@bin_a), Aerospike::Exp::int_val(4)) + + expect { + client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::UPDATE_ONLY), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::BIN_NOT_FOUND + } + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::UPDATE_ONLY | Aerospike::Exp::WriteFlags::POLICY_NO_FAIL), + ]) + expect(r.bins&.length).to be > 0 + + client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::CREATE_ONLY), + ]) + expect(r.bins&.length).to be > 0 + + expect { + client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::CREATE_ONLY), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::BIN_EXISTS_ERROR + } + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::UPDATE_ONLY | Aerospike::Exp::WriteFlags::POLICY_NO_FAIL), + ]) + expect(r.bins&.length).to be > 0 + + dexp = Aerospike::Exp::nil_val + + expect { + client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, dexp), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::OP_NOT_APPLICABLE + } + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, dexp, Aerospike::Exp::WriteFlags::POLICY_NO_FAIL), + ]) + expect(r.bins&.length).to be > 0 + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, dexp, Aerospike::Exp::WriteFlags::ALLOW_DELETE), + ]) + expect(r.bins&.length).to be > 0 + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, wexp, Aerospike::Exp::WriteFlags::CREATE_ONLY), + ]) + expect(r.bins&.length).to be > 0 + end + + it "Return Unknown should work" do + exp = Aerospike::Exp::cond( + Aerospike::Exp::eq(Aerospike::Exp::int_bin(@bin_c), Aerospike::Exp::int_val(5)), Aerospike::Exp::unknown, + Aerospike::Exp::bin_exists(@bin_a), Aerospike::Exp::int_val(5), + Aerospike::Exp::unknown, + ) + + expect { + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation::get(@bin_c), + ]) + }.to raise_error (Aerospike::Exceptions::Aerospike) { |error| + error.result_code == Aerospike::ResultCode::OP_NOT_APPLICABLE + } + + r = client.operate(@key_b, [ + Aerospike::Exp::Operation.write(@bin_c, exp, Aerospike::Exp::WriteFlags::EVAL_NO_FAIL), + Aerospike::Operation::get(@bin_c), + ]) + expect(r.bins&.length).to be > 0 + + expected = { @bin_c => [nil, nil] } + expect(r.bins).to eq expected + end + + it "Return Nil should work" do + exp = Aerospike::Exp::nil_val + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.read(@exp_var, exp), + Aerospike::Operation.get(@bin_c), + ]) + + expected = { @exp_var => nil, @bin_c => nil } + expect(r.bins).to eq expected + end + + it "Return Int should work" do + exp = Aerospike::Exp::add(Aerospike::Exp::int_bin(@bin_a), Aerospike::Exp::int_val(4)) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => 5, @bin_c => [nil, 5] } + expect(r.bins).to eq expected + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => 5 } + expect(r.bins).to eq expected + end + + it "Return Float should work" do + exp = Aerospike::Exp::add(Aerospike::Exp::to_float(Aerospike::Exp::int_bin(@bin_a)), Aerospike::Exp::float_val(4.1)) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => 5.1, @bin_c => [nil, 5.1] } + expect(r.bins).to eq expected + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => 5.1 } + expect(r.bins).to eq expected + end + + it "Return String should work" do + str = "xxx" + exp = Aerospike::Exp::str_val(str) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => str, @bin_c => [nil, str] } + expect(r.bins).to eq expected + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => str } + expect(r.bins).to eq expected + end + + it "Return BLOB should work" do + blob = bytes_to_str([0x78, 0x78, 0x78]) + exp = Aerospike::Exp::blob_val(blob) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => blob, @bin_c => [nil, blob] } + expect(r.bins).to eq expected + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => blob } + expect(r.bins).to eq expected + end + + it "Return Boolean should work" do + exp = Aerospike::Exp::eq(Aerospike::Exp::int_bin(@bin_a), Aerospike::Exp::int_val(1)) + + r = client.operate(@key_a, [ + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { @exp_var => true, @bin_c => [nil, true] } + expect(r.bins).to eq expected + end + + it "Return HLL should work" do + exp = Aerospike::Exp::HLL.init(Aerospike::Exp::int_val(4), Aerospike::Exp::nil_val) + + r = client.operate(@key_a, [ + Aerospike::CDT::HLLOperation.init(@bin_h, 4, -1), + Aerospike::Exp::Operation.write(@bin_c, exp), + Aerospike::Operation.get(@bin_h), + Aerospike::Operation.get(@bin_c), + Aerospike::Exp::Operation.read(@exp_var, exp), + ]) + + expected = { + @bin_h => [ + nil, + Aerospike::HLLValue.new(bytes_to_str([0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), + ], + @bin_c => [ + nil, + Aerospike::HLLValue.new(bytes_to_str([0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), + ], + @exp_var => Aerospike::HLLValue.new(bytes_to_str([0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])), + } + expect(r.bins).to eq expected + + r = client.operate(@key_a, [Aerospike::Exp::Operation.read(@exp_var, exp)]) + expected = { @exp_var => bytes_to_str([0, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) } + expect(r.bins).to eq expected + end + end +end