arel_hash 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +15 -0
- data/Rakefile +6 -0
- data/arel_hash.gemspec +25 -0
- data/lib/arel/any_node.rb +6 -0
- data/lib/arel/contains_node.rb +6 -0
- data/lib/arel_hash.rb +31 -0
- data/lib/arel_hash/arel/nodes/casted.rb +12 -0
- data/lib/arel_hash/arel_hash_factory.rb +54 -0
- data/lib/arel_hash/node_factory.rb +75 -0
- data/lib/arel_hash/optimizer.rb +72 -0
- data/lib/arel_hash/sanitizer.rb +27 -0
- data/lib/arel_hash/version.rb +3 -0
- data/spec/arel_hash/optimizer_spec.rb +46 -0
- data/spec/arel_hash/sanitizer_spec.rb +20 -0
- data/spec/arel_hash_spec.rb +91 -0
- data/spec/spec_helper.rb +2 -0
- metadata +125 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 7aaa91bdfe961029995869190679c3423e5b7532
|
4
|
+
data.tar.gz: cc879c66d4d164e4dd5e265bbfafdb91f582c9ab
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 708f2575e65dc2648ac7c053aa494a1c26d5a65bc0b45ad722164be13498f32b64a2aedaced20ac25e1f96e536d4490be5c5263cf7994a0ebd3d36b9b2bf6a29
|
7
|
+
data.tar.gz: ebb46a957a56352a97a22a7b4f0d594e4427ba12bf365f2ec3f691792c6bbbc460ab83cb2de8168118efd51c200396291ac7e91f1faf6ad33db0d3416ed76fd2
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2016 Bert Bruynooghe
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# ArelHash
|
2
|
+
|
3
|
+
ArelHash is a library that offers some utilities for serializing/deserializing Arel expressions into/from hashes.
|
4
|
+
At the heart of the where part of such an ArelHash expression, we have core expressions in the form of:
|
5
|
+
|
6
|
+
``` { <predication>: { <left_operand> => <right_operand> } } ```
|
7
|
+
|
8
|
+
with ```operand``` being a 'column name' if it is a symbol, or a constant otherwise.
|
9
|
+
|
10
|
+
These expressions can be combined using *OR* and/or *AND*:
|
11
|
+
|
12
|
+
{ or: [ <subexpression>, <subexpression>, ... ] }
|
13
|
+
{ and: [ <subexpression>, <subexpression>, ... ] }
|
14
|
+
|
15
|
+
|
data/Rakefile
ADDED
data/arel_hash.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'arel_hash/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "arel_hash"
|
8
|
+
spec.version = ArelHash::VERSION
|
9
|
+
spec.authors = ['Bert Bruynooghe']
|
10
|
+
spec.email = ['bert.bruynooghe@up-nxt.com']
|
11
|
+
spec.summary = %q{A serialization specification of ARel.}
|
12
|
+
spec.description = %q{A serialization specification of ARel, with some utilities.}
|
13
|
+
spec.homepage = ''
|
14
|
+
spec.license = 'MIT'
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_development_dependency 'bundler', "~> 1.5"
|
22
|
+
spec.add_development_dependency 'rake'
|
23
|
+
spec.add_development_dependency 'rspec'
|
24
|
+
spec.add_dependency 'arel', '>= 6.0.0'
|
25
|
+
end
|
data/lib/arel_hash.rb
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'arel_hash/version'
|
2
|
+
require 'arel'
|
3
|
+
require 'arel_hash/arel/nodes/casted'
|
4
|
+
require 'arel/any_node'
|
5
|
+
require 'arel/contains_node'
|
6
|
+
require 'arel_hash/node_factory'
|
7
|
+
require 'arel_hash/sanitizer'
|
8
|
+
require 'arel_hash/optimizer'
|
9
|
+
require 'arel_hash/arel_hash_factory'
|
10
|
+
|
11
|
+
module ArelHash
|
12
|
+
Nodes = Arel::Nodes
|
13
|
+
Equality = Nodes::Equality
|
14
|
+
|
15
|
+
ZERO_RESULTS_NODE = Equality.new(Nodes::build_quoted('f'), Nodes::build_quoted('t'))
|
16
|
+
ZERO_RESULTS_HASH = { or: [] }
|
17
|
+
NO_FILTER_HASH = { and: [] }
|
18
|
+
|
19
|
+
def self.create_node(table, hash)
|
20
|
+
NodeFactory.new(table).create_node(hash)
|
21
|
+
end
|
22
|
+
|
23
|
+
def self.singleton_tuple!(hash)
|
24
|
+
raise "#{hash}: only hashes with maximum one key are supported" unless hash.length == 1
|
25
|
+
return hash.keys.first, hash.values.first
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.create_from_active_record(hash, properties)
|
29
|
+
ArelHashFactory.new(properties).create_from_active_record(hash)
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# temporary fix for arel issue: see https://github.com/rails/arel/pull/414
|
2
|
+
module Arel
|
3
|
+
module Nodes
|
4
|
+
class Casted
|
5
|
+
unless self.instance_methods(false).include? :hash
|
6
|
+
def hash
|
7
|
+
[self.class, self.val, self.attribute].hash
|
8
|
+
end
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module ArelHash
|
2
|
+
class ArelHashFactory
|
3
|
+
def initialize(properties)
|
4
|
+
@properties = properties
|
5
|
+
end
|
6
|
+
|
7
|
+
def create_from_active_record(hash)
|
8
|
+
{ :and => hash.map { |k, v| name_value_to_arel_hash(k.to_sym, v) } }
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
# converts a tuple from a where hash (as in ActiveRecord where) to a valid arel_hash expression
|
14
|
+
def name_value_to_arel_hash(attr_name, value)
|
15
|
+
case value
|
16
|
+
when Array
|
17
|
+
arel_hash_for_array_value(attr_name, value)
|
18
|
+
when Range
|
19
|
+
arel_hash_for_range_value(attr_name, value)
|
20
|
+
else
|
21
|
+
arel_hash_for_singleton_value(attr_name, value)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def arel_hash_for_array_value(attr_name, values)
|
26
|
+
{ or: self.class.make_canonical(values).map do |option|
|
27
|
+
{ and: option.map do |requirement|
|
28
|
+
arel_hash_for_singleton_value(attr_name, requirement)
|
29
|
+
end }
|
30
|
+
end }
|
31
|
+
end
|
32
|
+
|
33
|
+
def arel_hash_for_range_value(attr_name, range)
|
34
|
+
{ and: [create_arel_hash_tuple(:gteq, attr_name, range.first),
|
35
|
+
create_arel_hash_tuple(range.exclude_end? ? :lt : :lt_eq, attr_name, range.end)] }
|
36
|
+
end
|
37
|
+
|
38
|
+
def arel_hash_for_singleton_value(attr_name, value)
|
39
|
+
create_arel_hash_tuple(:eq, attr_name, value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def create_arel_hash_tuple(predication, attr_name, value)
|
43
|
+
if @properties.fetch(attr_name.to_s, {})['multivalue']
|
44
|
+
{ "#{predication}_any".to_sym => { value => attr_name } }
|
45
|
+
else
|
46
|
+
{ predication => { attr_name => value } }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.make_canonical(values)
|
51
|
+
values.map { |v| v.is_a?(Array) ? v : [v] }
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module ArelHash
|
2
|
+
class NodeFactory
|
3
|
+
# @param [Arel::Table] table the arel table to work on
|
4
|
+
def initialize(table)
|
5
|
+
@table = table
|
6
|
+
end
|
7
|
+
|
8
|
+
# @param [Hash<Symbol, Hash<Symbol, Object>>] hash a hash, which is a serialization of an Arel::Node
|
9
|
+
def create_node(hash)
|
10
|
+
operator, value = ArelHash.singleton_tuple!(hash)
|
11
|
+
do_create_node(operator, value)
|
12
|
+
end
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def do_create_node(operator, value)
|
17
|
+
if %i(or and).include?(operator)
|
18
|
+
create_collection_node(operator, value)
|
19
|
+
else
|
20
|
+
create_predication_node(operator, value)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def create_collection_node(operator, value)
|
25
|
+
if operator == :or
|
26
|
+
create_or_node(value)
|
27
|
+
elsif operator == :and
|
28
|
+
create_and_node(value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def create_predication_node(predicate, attribute_value_hash)
|
33
|
+
first, last = ArelHash.singleton_tuple!(attribute_value_hash).map { |v| wrap_operand(v) }
|
34
|
+
first.send(*expand_meta_predicates(predicate, last))
|
35
|
+
end
|
36
|
+
|
37
|
+
def expand_meta_predicates(predicate, value)
|
38
|
+
# TODO: support ALL
|
39
|
+
if (predicate=predicate.to_s).end_with?('_any') && value.is_a?(Arel::Attributes::Attribute)
|
40
|
+
predicate = predicate[0...-4]
|
41
|
+
value = Nodes::Any.new(value)
|
42
|
+
end
|
43
|
+
return predicate.to_sym, value
|
44
|
+
end
|
45
|
+
|
46
|
+
def wrap_operand(operand)
|
47
|
+
case operand
|
48
|
+
when Symbol then
|
49
|
+
@table[operand]
|
50
|
+
when Array then
|
51
|
+
operand.map { |o| wrap_operand(o) }
|
52
|
+
else
|
53
|
+
Nodes::build_quoted(operand).extend Arel::Predications
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def create_and_node(values)
|
58
|
+
join_nodes(create_node_collection(values), :and)
|
59
|
+
end
|
60
|
+
|
61
|
+
def create_or_node(values)
|
62
|
+
join_nodes(create_node_collection(values), :or) || ZERO_RESULTS_NODE
|
63
|
+
end
|
64
|
+
|
65
|
+
def join_nodes(node_collection, operator)
|
66
|
+
node_collection.reduce(nil) do |m, node|
|
67
|
+
(m && node) ? m.send(operator, node) : node
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def create_node_collection(values)
|
72
|
+
values.map { |v| create_node(v) }.uniq
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module ArelHash
|
2
|
+
class Optimizer
|
3
|
+
def initialize(predicates = [])
|
4
|
+
@predicates = predicates
|
5
|
+
end
|
6
|
+
|
7
|
+
def optimize(arel_hash)
|
8
|
+
operator, value = ArelHash.singleton_tuple!(arel_hash)
|
9
|
+
(%i(and or).include?(operator)) ? optimize_collection_node(operator, value) : arel_hash
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def optimize_collection_node(operator, children)
|
15
|
+
children = children.map { |h| optimize(h) }.uniq
|
16
|
+
arel_hash = shallow_optimize_collection_node(operator, children)
|
17
|
+
children = ArelHash.singleton_tuple!(arel_hash).last
|
18
|
+
(children.length == 1) ? children.first : arel_hash
|
19
|
+
end
|
20
|
+
|
21
|
+
def shallow_optimize_collection_node(operator, children)
|
22
|
+
if operator == :or
|
23
|
+
Hash[operator, optimize_or_nodes(children)]
|
24
|
+
elsif operator == :and
|
25
|
+
Hash[operator, optimize_and_nodes(children)]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def optimize_or_nodes(nodes)
|
30
|
+
nodes.delete_if { |n| n == ArelHash::ZERO_RESULTS_HASH && nodes.length > 1 }
|
31
|
+
nodes = optimize_eqs_and_ins(nodes) if @predicates.include?('in') && @predicates.include?('eq')
|
32
|
+
nodes.include?(ArelHash::NO_FILTER_HASH) ? [ArelHash::NO_FILTER_HASH] : nodes
|
33
|
+
end
|
34
|
+
|
35
|
+
def optimize_and_nodes(nodes)
|
36
|
+
nodes.delete_if { |n| n == ArelHash::NO_FILTER_HASH && nodes.length > 1 }
|
37
|
+
nodes = optimize_duplicate_eqs(nodes)
|
38
|
+
nodes.include?(ArelHash::ZERO_RESULTS_HASH) ? [ArelHash::ZERO_RESULTS_HASH] : nodes
|
39
|
+
end
|
40
|
+
|
41
|
+
def optimize_duplicate_eqs(nodes)
|
42
|
+
eqs, other = partition_by_keys(nodes, 'eq')
|
43
|
+
values_per_attribute(eqs).map do |attr_name, values|
|
44
|
+
(values.length > 1) ? ArelHash::ZERO_RESULTS_HASH : { eq: Hash[attr_name, values.first] }
|
45
|
+
end.concat(other)
|
46
|
+
end
|
47
|
+
|
48
|
+
# @return [Array<Hash>] an array of predicate arelHashes
|
49
|
+
def optimize_eqs_and_ins(nodes)
|
50
|
+
ins_or_eqs, other = partition_by_keys(nodes, *%w(eq in))
|
51
|
+
values_per_attribute(ins_or_eqs).map do |attr_name, value|
|
52
|
+
(value.length > 1) ? { in: Hash[attr_name, value] } : { eq: Hash[attr_name, value.first] }
|
53
|
+
end.concat(other)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @param [Array<Hash<Symbol, Hash<Symbol, Object>>>] nodes
|
57
|
+
# @return [Hash<Symbol, Array<String>>] attribute_name/value pair, with value being an Array
|
58
|
+
def values_per_attribute(nodes)
|
59
|
+
nodes.each_with_object({}) do |arel_hash, m|
|
60
|
+
name_value_pair = ArelHash.singleton_tuple!(arel_hash).last
|
61
|
+
attr_name, value = ArelHash.singleton_tuple!(name_value_pair)
|
62
|
+
m[attr_name] = (m[attr_name]||[]).concat(Array(value)).uniq
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def partition_by_keys(hash_collection, *keys)
|
67
|
+
hash_collection.partition do |h|
|
68
|
+
keys.include?(ArelHash.singleton_tuple!(h).first.to_s)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
module ArelHash
|
2
|
+
class Sanitizer
|
3
|
+
def initialize(predicates = [], attribute_names = [])
|
4
|
+
@predicates = (predicates || [])
|
5
|
+
@attribute_names = (attribute_names || [])
|
6
|
+
end
|
7
|
+
|
8
|
+
def sanitize(arel_hash)
|
9
|
+
operator, operand = ArelHash.singleton_tuple!(arel_hash)
|
10
|
+
if operator == :and || operator == :or
|
11
|
+
Hash[operator, operand.map { |o| sanitize(o) }]
|
12
|
+
else
|
13
|
+
sanitize_predication_hash(operator, operand)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def sanitize_predication_hash(operator, attr_name_value)
|
20
|
+
valid = @predicates.empty? || @predicates.include?(operator.to_s)
|
21
|
+
valid &&= attr_name_value.flatten.all? do |v|
|
22
|
+
(!v.is_a?(Symbol)) || @attribute_names.empty? || @attribute_names.include?(v.to_s)
|
23
|
+
end
|
24
|
+
valid ? Hash[operator, attr_name_value] : ArelHash::ZERO_RESULTS_HASH
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ArelHash
|
4
|
+
describe Optimizer do
|
5
|
+
let(:optimizer) { Optimizer.new(%w(eq in)) }
|
6
|
+
it 'removes AND nodes with single operands' do
|
7
|
+
expect(optimizer.optimize(and: [{ eq: { x: 1 } }])).to eq(eq: { x: 1 })
|
8
|
+
end
|
9
|
+
it 'removes OR nodes with single operands' do
|
10
|
+
expect(optimizer.optimize(or: [{ eq: { x: 1 } }])).to eq(eq: { x: 1 })
|
11
|
+
end
|
12
|
+
it 'maintains AND nodes without operands' do
|
13
|
+
expect(optimizer.optimize(and: [])).to eq(and: [])
|
14
|
+
end
|
15
|
+
it 'maintains OR nodes without operands' do
|
16
|
+
expect(optimizer.optimize(or: [])).to eq(or: [])
|
17
|
+
end
|
18
|
+
it 'removes duplicate nodes' do
|
19
|
+
expect(optimizer.optimize(and: [{ eq: { x: 1 } }, { eq: { x: 1 } }])).to eq(eq: { x: 1 })
|
20
|
+
end
|
21
|
+
describe 'ZERO_RESULTS_HASH' do
|
22
|
+
it 'eats other AND operands' do
|
23
|
+
expect(optimizer.optimize(and: [ZERO_RESULTS_HASH, { eq: { x: 1 } }])).to eq ZERO_RESULTS_HASH
|
24
|
+
end
|
25
|
+
it 'gets eaten by other OR operands' do
|
26
|
+
expect(optimizer.optimize(or: [ZERO_RESULTS_HASH, { eq: { x: 1 } }])).to eq(eq: { x: 1 })
|
27
|
+
end
|
28
|
+
end
|
29
|
+
describe 'NO_FILTER_HASH' do
|
30
|
+
it 'eats other OR operands' do
|
31
|
+
expect(optimizer.optimize(or: [NO_FILTER_HASH, { eq: { x: 1 } }])).to eq NO_FILTER_HASH
|
32
|
+
end
|
33
|
+
it 'gets eaten by other AND operands' do
|
34
|
+
expect(optimizer.optimize(and: [NO_FILTER_HASH, { eq: { x: 1 } }])).to eq(eq: { x: 1 })
|
35
|
+
end
|
36
|
+
end
|
37
|
+
it 'combines EQ and IN for OR' do
|
38
|
+
expected = { in: { amount: [5, 6, 7, 8] } }
|
39
|
+
expect(optimizer.optimize(or: [{ eq: { amount: 5 } }, { eq: { amount: 6 } }, { in: { amount: [7, 8] } }])).to eq expected
|
40
|
+
end
|
41
|
+
it 'returns zero results for AND of EQ with same attribute and different values' do
|
42
|
+
expect(optimizer.optimize(and: [{ eq: { amount: 5 } }, { eq: { amount: 6 } }])).to eq ZERO_RESULTS_HASH
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
module ArelHash
|
3
|
+
describe Sanitizer do
|
4
|
+
it 'does not sanitize if predicates and attribute_names are set to nil' do
|
5
|
+
expect(Sanitizer.new.sanitize(eq: { x: 1 })).to eq(eq: { x: 1 })
|
6
|
+
end
|
7
|
+
it 'sanitizes base on given predicates' do
|
8
|
+
expect(Sanitizer.new([ 'not_eq'], [1]).sanitize(eq: { x: 1 })).to eq(ZERO_RESULTS_HASH)
|
9
|
+
end
|
10
|
+
it 'sanitizes based on given attribute_names' do
|
11
|
+
expect(Sanitizer.new(['eq'], [ 'y' ]).sanitize(eq: { x: 1 })).to eq(ZERO_RESULTS_HASH)
|
12
|
+
end
|
13
|
+
it 'sanitizes deeply' do
|
14
|
+
expect(Sanitizer.new(['eq'], ['y']).sanitize(or: [or: [eq: { x: 1 }]])).to eq(or: [or: [ZERO_RESULTS_HASH]])
|
15
|
+
end
|
16
|
+
it 'sanitizes based on operand type' do
|
17
|
+
expect(Sanitizer.new([], %w(x)).sanitize(eq: {1 => :x})).to eq(eq: {1 => :x})
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module ArelHash
|
4
|
+
describe '.create_node' do
|
5
|
+
let(:table) { Arel::Table.new(:test) }
|
6
|
+
let(:result) { double }
|
7
|
+
describe 'predication node' do
|
8
|
+
it 'is supported' do
|
9
|
+
expected = Equality.new(table[:amount], Nodes::build_quoted(5))
|
10
|
+
expect(ArelHash.create_node(table, eq: { amount: 5 })).to eq expected
|
11
|
+
end
|
12
|
+
it 'does support inverted operand orders' do
|
13
|
+
expected = Equality.new(Nodes::build_quoted(5), table[:amount])
|
14
|
+
expect(ArelHash.create_node(table, eq: { 5 => :amount })).to eq expected
|
15
|
+
end
|
16
|
+
it 'does not support multi column hashes' do
|
17
|
+
multi_column_hash = { age: 4, amount: 50 }
|
18
|
+
expect do
|
19
|
+
ArelHash.create_node(table, eq: multi_column_hash)
|
20
|
+
end.to raise_exception "#{multi_column_hash}: only hashes with maximum one key are supported"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe 'AND' do
|
25
|
+
it 'returns nil if no operands' do
|
26
|
+
expect(ArelHash.create_node(table, and: [])).to eq nil
|
27
|
+
end
|
28
|
+
it 'ignores the AND if only one operand' do
|
29
|
+
expected = Equality.new(table[:age], Nodes::build_quoted(4))
|
30
|
+
expect(ArelHash.create_node(table, and: [eq: { age: 4 }])).to eq expected
|
31
|
+
end
|
32
|
+
it 'properly builds a node for 2 operands' do
|
33
|
+
expected = Nodes::LessThanOrEqual.new(table[:age], Nodes::build_quoted(6))
|
34
|
+
.and(Nodes::GreaterThanOrEqual.new(table[:age], Nodes::build_quoted(4)))
|
35
|
+
expect(ArelHash.create_node(table, and: [{ lteq: { age: 6 } }, { gteq: { age: 4 } }])).to eq expected
|
36
|
+
end
|
37
|
+
it 'does not support multi predicate hashes' do
|
38
|
+
multi_predicate_hash = { lteq: { age: 6 }, gteq: { age: 4 } }
|
39
|
+
expect do
|
40
|
+
ArelHash.create_node(table, and: [multi_predicate_hash])
|
41
|
+
end.to raise_exception "#{multi_predicate_hash}: only hashes with maximum one key are supported"
|
42
|
+
end
|
43
|
+
it 'handles empty operands' do
|
44
|
+
expect(ArelHash.create_node(table, and: [])).to eq(nil)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe 'OR' do
|
49
|
+
it 'returns zero result if no operands' do
|
50
|
+
expect(ArelHash.create_node(table, or: [])).to eq ArelHash::ZERO_RESULTS_NODE
|
51
|
+
end
|
52
|
+
it 'ignores the OR if only one operand' do
|
53
|
+
expected = Equality.new(table[:amount], Nodes::build_quoted(5))
|
54
|
+
expect(ArelHash.create_node(table, or: [eq: { amount: 5 }])).to eq expected
|
55
|
+
end
|
56
|
+
it 'properly builds a node for 2 operands' do
|
57
|
+
expected = Equality.new(table[:amount], Nodes::build_quoted(5))
|
58
|
+
.or(Equality.new(table[:age], Nodes::build_quoted(4)))
|
59
|
+
expect(ArelHash.create_node(table, or: [{ eq: { amount: 5 } }, { eq: { age: 4 } }])).to eq expected
|
60
|
+
end
|
61
|
+
it 'properly builds a node for 3 operands' do
|
62
|
+
expected = Equality.new(table[:amount], Nodes::build_quoted(5))
|
63
|
+
.or(Equality.new(table[:age], Nodes::build_quoted(4)))
|
64
|
+
.or(Equality.new(table[:shoe_size], Nodes::build_quoted(3)))
|
65
|
+
expect(ArelHash.create_node(table, or: [{ eq: { amount: 5 } }, { eq: { age: 4 } }, { eq: { shoe_size: 3 } }])).to eq expected
|
66
|
+
end
|
67
|
+
it 'does not support multi predicate hashes' do
|
68
|
+
multi_predicate_hash = { lteq: { age: 4 }, gteq: { age: 6 } }
|
69
|
+
expect do
|
70
|
+
ArelHash.create_node(table, or: [multi_predicate_hash])
|
71
|
+
end.to raise_exception "#{multi_predicate_hash}: only hashes with maximum one key are supported"
|
72
|
+
end
|
73
|
+
it 'squashes ORs with identical operands' do
|
74
|
+
expected = Equality.new(table[:amount], Nodes::build_quoted(5))
|
75
|
+
expect(ArelHash.create_node(table, or: [{ eq: { amount: 5 } }, { eq: { amount: 5 } }])).to eq expected
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe 'any' do
|
80
|
+
it 'builds regular any nodes' do
|
81
|
+
expected = table[:amount].eq_any([Nodes::build_quoted(1), Nodes::build_quoted(2)])
|
82
|
+
expect(ArelHash.create_node(table, eq_any: { amount: [1, 2] })).to eq expected
|
83
|
+
end
|
84
|
+
it 'creates ANY nodes if second operand is a symbol' do
|
85
|
+
# TODO: not sure if we have to break down [[],[]] into ORs and ANDs of eq_any
|
86
|
+
expected = Equality.new(Nodes::build_quoted(1), Nodes::Any.new(table[:amount]))
|
87
|
+
expect(ArelHash.create_node(table, eq_any: { 1 => :amount })).to eq expected
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: arel_hash
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Bert Bruynooghe
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-03-17 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.5'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.5'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: arel
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 6.0.0
|
62
|
+
type: :runtime
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: 6.0.0
|
69
|
+
description: A serialization specification of ARel, with some utilities.
|
70
|
+
email:
|
71
|
+
- bert.bruynooghe@up-nxt.com
|
72
|
+
executables: []
|
73
|
+
extensions: []
|
74
|
+
extra_rdoc_files: []
|
75
|
+
files:
|
76
|
+
- ".gitignore"
|
77
|
+
- ".rspec"
|
78
|
+
- ".travis.yml"
|
79
|
+
- Gemfile
|
80
|
+
- LICENSE.txt
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- arel_hash.gemspec
|
84
|
+
- lib/arel/any_node.rb
|
85
|
+
- lib/arel/contains_node.rb
|
86
|
+
- lib/arel_hash.rb
|
87
|
+
- lib/arel_hash/arel/nodes/casted.rb
|
88
|
+
- lib/arel_hash/arel_hash_factory.rb
|
89
|
+
- lib/arel_hash/node_factory.rb
|
90
|
+
- lib/arel_hash/optimizer.rb
|
91
|
+
- lib/arel_hash/sanitizer.rb
|
92
|
+
- lib/arel_hash/version.rb
|
93
|
+
- spec/arel_hash/optimizer_spec.rb
|
94
|
+
- spec/arel_hash/sanitizer_spec.rb
|
95
|
+
- spec/arel_hash_spec.rb
|
96
|
+
- spec/spec_helper.rb
|
97
|
+
homepage: ''
|
98
|
+
licenses:
|
99
|
+
- MIT
|
100
|
+
metadata: {}
|
101
|
+
post_install_message:
|
102
|
+
rdoc_options: []
|
103
|
+
require_paths:
|
104
|
+
- lib
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - ">="
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '0'
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project:
|
117
|
+
rubygems_version: 2.2.3
|
118
|
+
signing_key:
|
119
|
+
specification_version: 4
|
120
|
+
summary: A serialization specification of ARel.
|
121
|
+
test_files:
|
122
|
+
- spec/arel_hash/optimizer_spec.rb
|
123
|
+
- spec/arel_hash/sanitizer_spec.rb
|
124
|
+
- spec/arel_hash_spec.rb
|
125
|
+
- spec/spec_helper.rb
|