clearly-query 0.3.1.pre → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +73 -19
- data/SPEC.md +22 -21
- data/clearly-query.gemspec +1 -1
- data/lib/clearly/query/compose/conditions.rb +0 -17
- data/lib/clearly/query/composer.rb +70 -13
- data/lib/clearly/query/graph.rb +7 -3
- data/lib/clearly/query/helper.rb +33 -1
- data/lib/clearly/query/validate.rb +5 -27
- data/lib/clearly/query/version.rb +1 -1
- data/spec/lib/clearly/query/compose/custom_spec.rb +11 -1
- data/spec/lib/clearly/query/composer_query_spec.rb +55 -11
- data/spec/lib/clearly/query/composer_spec.rb +53 -26
- data/spec/lib/clearly/query/helper_spec.rb +19 -0
- data/spec/support/models/customer.rb +2 -6
- data/spec/support/models/order.rb +10 -11
- data/spec/support/models/part.rb +2 -6
- data/spec/support/models/product.rb +4 -8
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8b1fc977200d949ff4d1aa8db533b56d6d987a91
|
4
|
+
data.tar.gz: 0e5dc84af5e9b77647bfcb3ffe6dd77d1daf356b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 83e6b022830e7026f87eebfc3ef8f27a9053127493b8c4aaa327843a2eaeadebd9177f27f81cb91d77a1d1386b27af4f829645e77711f97bfc63c53bb61fba5f
|
7
|
+
data.tar.gz: abcaf272739a41933356e229ff6b4a2d3a5a04ebf90bebd2ecd3bd501c2ab65a9fee76f885fba8abc70ce714e695e0d51e850e34a66cfe96b1c1cffd005aeb5c
|
data/CHANGELOG.md
CHANGED
@@ -6,6 +6,20 @@ and [keeps a change log](http://keepachangelog.com/) (you're reading it!).
|
|
6
6
|
|
7
7
|
## Unreleased
|
8
8
|
|
9
|
+
## Release [v1.0.0](https://github.com/cofiem/clearly-query/releases/tag/v1.0.0) (2015-11-10)
|
10
|
+
|
11
|
+
### Added
|
12
|
+
- Operator to compare all text fields using OR.
|
13
|
+
- Improved tests and coverage.
|
14
|
+
|
15
|
+
### Changed
|
16
|
+
- Two methods for Composer: `#query` to compose an ActiveRecord query and `#conditions` to compose an array of Arel conditions.
|
17
|
+
- Hash cleaner applied within Composer methods.
|
18
|
+
- Graph traversal results are now cached.
|
19
|
+
|
20
|
+
### Fixed
|
21
|
+
- fixed a number of typos in SPEC and README.
|
22
|
+
|
9
23
|
## Release [v0.3.1-pre](https://github.com/cofiem/clearly-query/releases/tag/v0.3.1-pre) (2015-11-01)
|
10
24
|
|
11
25
|
### Added
|
@@ -22,6 +36,16 @@ and [keeps a change log](http://keepachangelog.com/) (you're reading it!).
|
|
22
36
|
- Transported hash filter modules and classes from [baw-server](https://github.com/QutBioacoustics/baw-server)
|
23
37
|
- Created change log
|
24
38
|
|
39
|
+
----
|
40
|
+
|
41
|
+
## Semver Summary
|
42
|
+
|
43
|
+
Given a version number MAJOR.MINOR.PATCH, increment the:
|
44
|
+
|
45
|
+
1. MAJOR version when you make incompatible API changes,
|
46
|
+
1. MINOR version when you add functionality in a backwards-compatible manner, and
|
47
|
+
1. PATCH version when you make backwards-compatible bug fixes.
|
48
|
+
|
25
49
|
## Change log categories
|
26
50
|
|
27
51
|
### Added
|
data/README.md
CHANGED
@@ -2,6 +2,9 @@
|
|
2
2
|
|
3
3
|
A library for constructing an sql query from a hash.
|
4
4
|
|
5
|
+
From a hash, validate, construct, and execute a query or create Arel conditions.
|
6
|
+
There are no assumptions or opinions on what is done with the results from the query.
|
7
|
+
|
5
8
|
Uses [Arel](https://github.com/rails/arel) and [ActiveRecord](https://github.com/rails/rails/tree/master/activerecord).
|
6
9
|
|
7
10
|
## Project Status
|
@@ -13,6 +16,7 @@ Uses [Arel](https://github.com/rails/arel) and [ActiveRecord](https://github.com
|
|
13
16
|
[![Documentation Status](https://inch-ci.org/github/cofiem/clearly-query.svg?branch=master)](https://inch-ci.org/github/cofiem/clearly-query)
|
14
17
|
[![Documentation](https://img.shields.io/badge/docs-rdoc.info-blue.svg)](http://www.rubydoc.info/github/cofiem/clearly-query)
|
15
18
|
[![Join the chat at https://gitter.im/cofiem/clearly-query](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/cofiem/clearly-query?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
19
|
+
[![Gem Version](https://badge.fury.io/rb/clearly-query.svg)](https://badge.fury.io/rb/clearly-query)
|
16
20
|
|
17
21
|
## Installation
|
18
22
|
|
@@ -31,7 +35,7 @@ Or install it yourself as:
|
|
31
35
|
## Usage
|
32
36
|
|
33
37
|
There are two main public classes in this gem.
|
34
|
-
The Definition class makes use of
|
38
|
+
The Definition class makes use of settings declared in a model.
|
35
39
|
The Composer converts a hash of options into an Arel query.
|
36
40
|
|
37
41
|
### [Clearly::Query::Definition](./lib/clearly/query/definition.rb)
|
@@ -50,12 +54,12 @@ and
|
|
50
54
|
fields: {
|
51
55
|
valid: [:name, :last_contact_at],
|
52
56
|
text: [:name],
|
53
|
-
mappings: [
|
57
|
+
mappings: [ # these mappings are built in the database, and are only used for comparison, not projection
|
54
58
|
{
|
55
59
|
name: :title,
|
56
60
|
value: Clearly::Query::Helper.string_concat(
|
57
61
|
Customer.arel_table[:name],
|
58
|
-
|
62
|
+
Clearly::Query::Helper.sql_quoted(' title'))
|
59
63
|
}
|
60
64
|
]
|
61
65
|
},
|
@@ -66,37 +70,87 @@ and
|
|
66
70
|
available: true,
|
67
71
|
associations: []
|
68
72
|
}
|
69
|
-
]
|
70
|
-
defaults: {
|
71
|
-
order_by: :created_at,
|
72
|
-
direction: :desc
|
73
|
-
}
|
73
|
+
]
|
74
74
|
}
|
75
75
|
end
|
76
76
|
|
77
|
+
The available specification keys are detailed below.
|
78
|
+
|
79
|
+
All field names that are available to include in a query hash:
|
80
|
+
|
81
|
+
{fields: { valid: [<Symbols>, ...] } }
|
82
|
+
|
83
|
+
All fields that contain text (e.g. `varchar`, `text`) that are available to include in a query hash.
|
84
|
+
This must be a subset (or equal) to the `valid` field array:
|
85
|
+
|
86
|
+
{fields: { text: [<Symbols>, ...] } }
|
87
|
+
|
88
|
+
Field mappings that specify a calculated value:
|
89
|
+
|
90
|
+
{fields: { mappings: [{ name: <Symbol>, value: <Arel::Nodes::Node, String, Arel::Attribute, others...> }, ... ] } }
|
91
|
+
|
92
|
+
Associations between tables, and whether the association is available in queries or not:
|
93
|
+
|
94
|
+
{
|
95
|
+
associations: [
|
96
|
+
{
|
97
|
+
join: <Model or Arel Table>,
|
98
|
+
on: <Arel fragment>,
|
99
|
+
available: <true or false>, # is this association available to be used in queries?
|
100
|
+
associations: [ <further associations for this table>, ... ]
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
77
104
|
### [Clearly::Query::Composer](./lib/clearly/query/composer.rb)
|
78
105
|
|
79
|
-
|
106
|
+
Use the Composer to Construct an Arel query from a hash of options.
|
80
107
|
See the [query hash specification](SPEC.md) for a comprehensive overview.
|
81
108
|
|
82
|
-
|
109
|
+
There are two ways to do this. Either compose an ActiveRecord query or compose the Arel conditions.
|
83
110
|
|
84
111
|
composer = Clearly::Query::Composer.from_active_record
|
85
112
|
query_hash = {and: {name: {contains: 'test'}}} # from e.g. HTTP request
|
86
|
-
cleaned_query_hash = Clearly::Query::Cleaner.new.do(query_hash)
|
87
113
|
model = Customer
|
88
|
-
|
89
|
-
|
114
|
+
arel_conditions = composer.conditions(model, query_hash)
|
115
|
+
# or
|
116
|
+
query = composer.query(model, query_hash)
|
90
117
|
|
91
|
-
|
118
|
+
### Building custom Arel queries
|
92
119
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
120
|
+
There is also a class to aid in building Arel queries yourself.
|
121
|
+
|
122
|
+
Have a look at the [Clearly::Query::Compose::Custom](./lib/clearly/query/compose/custom.rb) class and the
|
123
|
+
[tests](./spec/lib/clearly/query/compose/custom_spec.rb)
|
124
|
+
for more details.
|
125
|
+
|
126
|
+
## Helper methods and classes
|
127
|
+
|
128
|
+
There are a number of helper methods and classes available to make working with Arel, hashes, and ActiveRecord easier.
|
129
|
+
|
130
|
+
[Clearly::Query::Cleaner](./lib/clearly/query/cleaner.rb) validates a hash to make sure all hash keys are symbols (even in nested hashes and arrays):
|
131
|
+
|
132
|
+
cleaned_query_hash = Clearly::Query::Cleaner.new.do(hash)
|
133
|
+
|
134
|
+
This library uses the custom error `Clearly::Query::QueryArgumentError` (it inherits from `ArgumentError`).
|
135
|
+
|
136
|
+
There are a bunch of validation methods in the `Clearly::Query::Validate` module. Sometimes duck typing is not that great :/
|
137
|
+
|
138
|
+
There is also the `Clearly::Query::Graph` class, currently used only for constructing root to leaf routes for building joins.
|
139
|
+
|
140
|
+
Class methods in the `Clearly::Query::Helper` class provide abstractions over differences in database string concatenation,
|
141
|
+
help construct Arel infix operators, EXISTS clauses, literals, and SQL fragments.
|
142
|
+
These helper methods are mostly a result of obscure or odd functionality.
|
143
|
+
It's a collection of Arel experience that will probably be helpful.
|
98
144
|
|
99
145
|
## More Information about Arel
|
100
146
|
|
101
147
|
- [Using Arel to Compose SQL Queries](http://robots.thoughtbot.com/using-arel-to-compose-sql-queries)
|
102
148
|
- [The definitive guide to Arel, the SQL manager for Ruby](http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html)
|
149
|
+
|
150
|
+
## Contributing
|
151
|
+
|
152
|
+
1. [Fork this repo](https://github.com/cofiem/clearly-query/fork)
|
153
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
154
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
155
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
156
|
+
5. Create a new [pull request](https://github.com/cofiem/clearly-query/compare)
|
data/SPEC.md
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
# Query Hash Specification
|
2
2
|
|
3
|
-
Inspired by [
|
3
|
+
Inspired by [Elastic Search filters](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-filters.html).
|
4
4
|
|
5
5
|
## Available Filter Operators
|
6
6
|
|
7
|
-
### Combine Operators
|
7
|
+
### Combine / logical Operators
|
8
8
|
|
9
9
|
Operator | Query hash | SQL
|
10
10
|
----------|----------------|---------------------
|
@@ -12,13 +12,14 @@ Inspired by [elastic search filters](http://www.elasticsearch.org/guide/en/elast
|
|
12
12
|
or | {or: { ... }} | WHERE ... OR (...)
|
13
13
|
not | {not: { ... }} | WHERE ... NOT (...)
|
14
14
|
|
15
|
-
|
15
|
+
An implicit `and` operator is used when no logical operator is specified.
|
16
16
|
|
17
17
|
### Filter Operators
|
18
18
|
|
19
|
-
|
20
|
-
Be aware that it is possible operators may be
|
21
|
-
for unicode characters that are beyond the ASCII range'.
|
19
|
+
All filter operators have multiple forms to help with constructing queries that read more 'naturally'.
|
20
|
+
Be aware that it is possible operators may be
|
21
|
+
'case sensitive by default for unicode characters that are beyond the ASCII range'.
|
22
|
+
For example, in [sqlite](https://www.sqlite.org/lang_expr.html).
|
22
23
|
|
23
24
|
#### Comparison Operators
|
24
25
|
|
@@ -41,7 +42,6 @@ Comparison operators are self-explanatory.
|
|
41
42
|
|
42
43
|
There are special operators for `null` comparisons.
|
43
44
|
The only valid values for these operators is `true` or `false`.
|
44
|
-
Any other value is invalid.
|
45
45
|
|
46
46
|
Operator | Query hash | SQL
|
47
47
|
-----------------------|----------------------------|---------------------------------
|
@@ -51,30 +51,31 @@ Any other value is invalid.
|
|
51
51
|
|
52
52
|
##### Range
|
53
53
|
|
54
|
-
A simple range
|
54
|
+
A simple range can be specified from an inclusive lower bound and to an exclusive upper bound.
|
55
55
|
|
56
56
|
Operator | Query hash | SQL
|
57
57
|
------------------------|-----------------------------------------------------|------------------------------------------------------------------
|
58
58
|
range, in_range | {attr: {range: {from: 'value1', to: 'value2'}}} | "table"."attr" >= 'value1' AND "table"."attr" < 'value2'
|
59
|
-
not_range, not_in_range | {attr: {not_range: {from: 'value1', to: 'value2'}}} | ("table"."attr"
|
59
|
+
not_range, not_in_range | {attr: {not_range: {from: 'value1', to: 'value2'}}} | ("table"."attr" < 'value1' OR "table"."attr" >= 'value2')
|
60
60
|
|
61
|
-
A more complex range can be specified using a
|
61
|
+
A more complex range can be specified using a special format which allows for inclusive or exclusive bounds.
|
62
62
|
|
63
|
-
Operator |
|
64
|
-
|
65
|
-
interval | {attr: {interval: '(value1,value2]'}}
|
66
|
-
not_interval | {attr: {
|
63
|
+
Operator | Query hash | SQL
|
64
|
+
-------------|-------------------------------------------|----------------------------------------------------------
|
65
|
+
interval | {attr: {interval: '(value1,value2]'}} | "table"."attr" > 'value1' AND "table"."attr" <= 'value2'
|
66
|
+
not_interval | {attr: {not_interval: '(value1,value2]'}} | ("table"."attr" <= 'value1' OR "table"."attr" > 'value2')
|
67
67
|
|
68
|
-
The `interval` must match the regex `/(\[|\()(.*),(.*)(\)|\])
|
68
|
+
The `interval` must match the regex `/(\[|\()(.*),(.*)(\)|\])/`,
|
69
69
|
where `(` or `)` indicates exclusive and `[` or `]` indicates inclusive.
|
70
70
|
Specifying `[value1,value2]` is equivalent to `BETWEEN value1 AND value2`.
|
71
71
|
|
72
72
|
Any spaces between the brackets will be included in the value.
|
73
73
|
The result of including commas (`,`) in either value is undefined.
|
74
|
+
Use a single comma for separating the two values.
|
74
75
|
|
75
|
-
#####
|
76
|
+
##### Array
|
76
77
|
|
77
|
-
An array of values to match the attribute value
|
78
|
+
An array of values to match the attribute value. Compared using an exact match (which may be case sensitive, depending on the database).
|
78
79
|
|
79
80
|
Operator | Query hash | SQL
|
80
81
|
---------|-------------------------------------|-----------------------------------------------
|
@@ -83,11 +84,11 @@ An array of values to match the attribute value exactly.
|
|
83
84
|
|
84
85
|
##### Contents Match
|
85
86
|
|
86
|
-
|
87
|
-
These comparison operators are case insensitive where possible (
|
88
|
-
It is possible to match
|
87
|
+
A variety of ways to match the contents of model attribute content.
|
88
|
+
These comparison operators are case insensitive where possible (again, depends on the database).
|
89
|
+
It is possible to match the entire content, at the start of the content, at the end, or using a regular expression.
|
89
90
|
|
90
|
-
Regular expression match
|
91
|
+
Regular expression match may not be supported by all databases.
|
91
92
|
|
92
93
|
Operator | Query hash | SQL
|
93
94
|
------------------------------------------------------|-----------------------------------|-----------------------------------------------
|
data/clearly-query.gemspec
CHANGED
@@ -6,7 +6,7 @@ require 'clearly/query/version'
|
|
6
6
|
Gem::Specification.new do |spec|
|
7
7
|
spec.name = 'clearly-query'
|
8
8
|
spec.version = Clearly::Query::VERSION
|
9
|
-
spec.authors = ['
|
9
|
+
spec.authors = ['Mark Cottman-Fields']
|
10
10
|
spec.email = ['cofiem@gmail.com']
|
11
11
|
spec.summary = %q{A library for constructing an sql query from a hash.}
|
12
12
|
spec.description = %q{A library for constructing an sql query from a hash. Uses a strict, yet flexible specification.}
|
@@ -60,23 +60,6 @@ module Clearly
|
|
60
60
|
OPERATORS_REGEX +
|
61
61
|
OPERATORS_SPECIAL
|
62
62
|
|
63
|
-
# Add conditions to a query.
|
64
|
-
# @param [ActiveRecord::Relation] query
|
65
|
-
# @param [Array<Arel::Nodes::Node>, Arel::Nodes::Node] conditions
|
66
|
-
# @return [ActiveRecord::Relation] the modified query
|
67
|
-
def condition_apply(query, conditions)
|
68
|
-
conditions = [conditions].flatten
|
69
|
-
validate_not_blank(conditions)
|
70
|
-
validate_array(conditions)
|
71
|
-
|
72
|
-
conditions.each do |condition|
|
73
|
-
validate_condition(condition)
|
74
|
-
query = query.where(condition)
|
75
|
-
end
|
76
|
-
|
77
|
-
query
|
78
|
-
end
|
79
|
-
|
80
63
|
# Combine multiple conditions.
|
81
64
|
# @param [Symbol] combiner
|
82
65
|
# @param [Arel::Nodes::Node, Array<Arel::Nodes::Node>] conditions
|
@@ -6,6 +6,9 @@ module Clearly
|
|
6
6
|
include Clearly::Query::Compose::Conditions
|
7
7
|
include Clearly::Query::Validate
|
8
8
|
|
9
|
+
# All text fields operator.
|
10
|
+
OPERATOR_ALL_TEXT = :all_text_fields
|
11
|
+
|
9
12
|
# @return [Array<Clearly::Query::Definition>] available definitions
|
10
13
|
attr_reader :definitions
|
11
14
|
|
@@ -26,10 +29,10 @@ module Clearly
|
|
26
29
|
# @return [Clearly::Query::Composer]
|
27
30
|
def self.from_active_record
|
28
31
|
models = ActiveRecord::Base
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
32
|
+
.descendants
|
33
|
+
.reject { |d| d.name == 'ActiveRecord::SchemaMigration' }
|
34
|
+
.sort { |a, b| a.name <=> b.name }
|
35
|
+
.uniq { |d| d.arel_table.name }
|
33
36
|
|
34
37
|
definitions = models.map do |d|
|
35
38
|
if d.name.include?('HABTM_')
|
@@ -45,11 +48,31 @@ module Clearly
|
|
45
48
|
# Composes a query from a parsed filter hash.
|
46
49
|
# @param [ActiveRecord::Base] model
|
47
50
|
# @param [Hash] hash
|
48
|
-
# @return [
|
51
|
+
# @return [ActiveRecord::Relation]
|
49
52
|
def query(model, hash)
|
53
|
+
conditions = conditions(model, hash)
|
54
|
+
query = model.all
|
55
|
+
validate_query(query)
|
56
|
+
conditions.each do |condition|
|
57
|
+
validate_condition(condition)
|
58
|
+
query = query.where(condition)
|
59
|
+
end
|
60
|
+
query
|
61
|
+
end
|
62
|
+
|
63
|
+
# Composes Arel conditions from a parsed filter hash.
|
64
|
+
# @param [ActiveRecord::Base] model
|
65
|
+
# @param [Hash] hash
|
66
|
+
# @return [Array<Arel::Nodes::Node>]
|
67
|
+
def conditions(model, hash)
|
68
|
+
validate_model(model)
|
69
|
+
validate_hash(hash)
|
70
|
+
|
50
71
|
definition = select_definition_from_model(model)
|
72
|
+
cleaned_query_hash = Clearly::Query::Cleaner.new.do(hash)
|
73
|
+
|
51
74
|
# default combiner is :and
|
52
|
-
|
75
|
+
parse_conditions(definition, :and, cleaned_query_hash)
|
53
76
|
end
|
54
77
|
|
55
78
|
private
|
@@ -82,17 +105,15 @@ module Clearly
|
|
82
105
|
# @param [Symbol] query_key
|
83
106
|
# @param [Hash] query_value
|
84
107
|
# @return [Array<Arel::Nodes::Node>]
|
85
|
-
def
|
108
|
+
def parse_conditions(definition, query_key, query_value)
|
86
109
|
if query_value.blank? || query_value.size < 1
|
87
110
|
msg = "filter hash must have at least 1 entry, got '#{query_value.size}'"
|
88
111
|
fail Clearly::Query::QueryArgumentError.new(msg, {hash: query_value})
|
89
112
|
end
|
90
113
|
|
91
114
|
logical_operators = Clearly::Query::Compose::Conditions::OPERATORS_LOGICAL
|
92
|
-
|
93
115
|
mapped_fields = definition.field_mappings.keys
|
94
116
|
standard_fields = definition.all_fields - mapped_fields
|
95
|
-
|
96
117
|
conditions = []
|
97
118
|
|
98
119
|
if logical_operators.include?(query_key)
|
@@ -105,6 +126,11 @@ module Clearly
|
|
105
126
|
field_conditions = parse_standard_field(definition, query_key, query_value)
|
106
127
|
conditions.push(*field_conditions)
|
107
128
|
|
129
|
+
elsif OPERATOR_ALL_TEXT == query_key
|
130
|
+
# build conditions for all text fields combined with or
|
131
|
+
field_condition = parse_all_text_fields(definition, query_value)
|
132
|
+
conditions.push(field_condition)
|
133
|
+
|
108
134
|
elsif mapped_fields.include?(query_key)
|
109
135
|
# then deal with mapped fields
|
110
136
|
field_conditions = parse_mapped_field(definition, query_key, query_value)
|
@@ -114,12 +140,12 @@ module Clearly
|
|
114
140
|
# finally deal with fields from other tables
|
115
141
|
field_conditions = parse_custom(definition, query_key, query_value)
|
116
142
|
conditions.push(field_conditions)
|
143
|
+
|
117
144
|
else
|
118
145
|
fail Clearly::Query::QueryArgumentError.new("unrecognised operator or field '#{query_key}'")
|
119
146
|
end
|
120
147
|
|
121
148
|
conditions
|
122
|
-
|
123
149
|
end
|
124
150
|
|
125
151
|
# Parse a logical operator and it's value.
|
@@ -130,8 +156,9 @@ module Clearly
|
|
130
156
|
def parse_logical_operator(definition, logical_operator, value)
|
131
157
|
validate_definition_instance(definition)
|
132
158
|
validate_symbol(logical_operator)
|
159
|
+
validate_not_blank(value)
|
133
160
|
validate_hash(value)
|
134
|
-
conditions = value.map { |key, value|
|
161
|
+
conditions = value.map { |key, value| parse_conditions(definition, key, value) }
|
135
162
|
condition_combine(logical_operator, *conditions)
|
136
163
|
end
|
137
164
|
|
@@ -143,12 +170,40 @@ module Clearly
|
|
143
170
|
def parse_standard_field(definition, field, value)
|
144
171
|
validate_definition_instance(definition)
|
145
172
|
validate_symbol(field)
|
173
|
+
validate_not_blank(value)
|
146
174
|
validate_hash(value)
|
147
175
|
value.map do |operator, operation_value|
|
148
176
|
condition_components(operator, definition.table, field, definition.all_fields, operation_value)
|
149
177
|
end
|
150
178
|
end
|
151
179
|
|
180
|
+
# Parse the conditions for all text fields.
|
181
|
+
# @param [Clearly::Query::Definition] definition
|
182
|
+
# @param [Hash] value
|
183
|
+
# @return [Array<Arel::Nodes::Node>]
|
184
|
+
def parse_all_text_fields(definition, value)
|
185
|
+
validate_definition_instance(definition)
|
186
|
+
validate_not_blank(value)
|
187
|
+
validate_hash(value)
|
188
|
+
|
189
|
+
# build conditions for all text fields
|
190
|
+
conditions = definition.text_fields.map do |text_field|
|
191
|
+
value.map do |operator, operation_value|
|
192
|
+
# cater for standard fields and mapped fields
|
193
|
+
mapping = definition.get_field_mapping(text_field)
|
194
|
+
if mapping.nil?
|
195
|
+
condition_components(operator, definition.table, text_field, definition.text_fields, operation_value)
|
196
|
+
else
|
197
|
+
validate_node_or_attribute(mapping)
|
198
|
+
condition_node(operator, mapping, operation_value)
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# combine conditions using :or
|
204
|
+
condition_combine(:or, conditions)
|
205
|
+
end
|
206
|
+
|
152
207
|
# Parse a mapped field and it's conditions.
|
153
208
|
# @param [Clearly::Query::Definition] definition
|
154
209
|
# @param [Symbol] field
|
@@ -159,6 +214,7 @@ module Clearly
|
|
159
214
|
validate_symbol(field)
|
160
215
|
fail Clearly::Query::QueryArgumentError.new('field name must contain a dot (.)') unless field.to_s.include?('.')
|
161
216
|
|
217
|
+
validate_not_blank(value)
|
162
218
|
validate_hash(value)
|
163
219
|
|
164
220
|
# extract table and field
|
@@ -189,6 +245,7 @@ module Clearly
|
|
189
245
|
validate_definition_instance(definition)
|
190
246
|
mapping = definition.get_field_mapping(field)
|
191
247
|
validate_node_or_attribute(mapping)
|
248
|
+
validate_not_blank(value)
|
192
249
|
validate_hash(value)
|
193
250
|
value.map do |operator, operation_value|
|
194
251
|
condition_node(operator, mapping, operation_value)
|
@@ -206,12 +263,12 @@ module Clearly
|
|
206
263
|
[conditions].flatten.each { |c| validate_node_or_attribute(c) }
|
207
264
|
|
208
265
|
current_model = definition.model
|
209
|
-
current_table = definition.table
|
266
|
+
#current_table = definition.table
|
210
267
|
current_joins = definition.joins
|
211
268
|
|
212
269
|
other_table = other_definition.table
|
213
270
|
other_model = other_definition.model
|
214
|
-
other_joins = other_definition.joins
|
271
|
+
#other_joins = other_definition.joins
|
215
272
|
|
216
273
|
# build an exist subquery to apply conditions that
|
217
274
|
# refer to another table
|
data/lib/clearly/query/graph.rb
CHANGED
@@ -18,15 +18,19 @@ module Clearly
|
|
18
18
|
def initialize(root_node, child_key)
|
19
19
|
@root_node = root_node
|
20
20
|
@child_key = child_key
|
21
|
+
|
22
|
+
@discovered_nodes = []
|
23
|
+
@paths = []
|
24
|
+
|
21
25
|
self
|
22
26
|
end
|
23
27
|
|
24
28
|
# build an array that contains paths from the root to all leaves
|
25
29
|
# @return [Array] paths from root to leaf
|
26
30
|
def branches
|
27
|
-
@discovered_nodes
|
28
|
-
|
29
|
-
|
31
|
+
if @discovered_nodes.blank? && @paths.blank?
|
32
|
+
traverse_branches(@root_node, nil)
|
33
|
+
end
|
30
34
|
@paths
|
31
35
|
end
|
32
36
|
|
data/lib/clearly/query/helper.rb
CHANGED
@@ -13,7 +13,7 @@ module Clearly
|
|
13
13
|
|
14
14
|
case adapter
|
15
15
|
when 'mysql'
|
16
|
-
|
16
|
+
named_function('concat', args)
|
17
17
|
when 'sqlserver'
|
18
18
|
string_concat_infix('+', *args)
|
19
19
|
when 'postgres'
|
@@ -44,6 +44,38 @@ module Clearly
|
|
44
44
|
result
|
45
45
|
end
|
46
46
|
|
47
|
+
# Construct a SQL literal.
|
48
|
+
# This is useful for sql that is too complex for Arel.
|
49
|
+
# @param [String] value
|
50
|
+
# @return [Arel::Nodes::Node]
|
51
|
+
def sql_literal(value)
|
52
|
+
Arel::Nodes::SqlLiteral.new(value)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Construct a SQL quoted string.
|
56
|
+
# This is used for fragments of SQL.
|
57
|
+
# @param [String] value
|
58
|
+
# @return [Arel::Nodes::Node]
|
59
|
+
def sql_quoted(value)
|
60
|
+
Arel::Nodes.build_quoted(value)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Construct a SQL EXISTS clause.
|
64
|
+
# @param [Arel::Nodes::Node] node
|
65
|
+
# @return [Arel::Nodes::Node]
|
66
|
+
def exists(node)
|
67
|
+
Arel::Nodes::Exists.new(node)
|
68
|
+
end
|
69
|
+
|
70
|
+
# Construct an Arel representation of a SQL function.
|
71
|
+
# @param [String] name
|
72
|
+
# @param [String, Arel::Nodes::Node] expression
|
73
|
+
# @param [String] function_alias
|
74
|
+
# @return [Arel::Nodes::Node]
|
75
|
+
def named_function(name, expression, function_alias = nil)
|
76
|
+
Arel::Nodes::NamedFunction.new(name, expression, function_alias)
|
77
|
+
end
|
78
|
+
|
47
79
|
end
|
48
80
|
end
|
49
81
|
end
|
@@ -4,18 +4,6 @@ module Clearly
|
|
4
4
|
# Provides common validations for composing queries.
|
5
5
|
module Validate
|
6
6
|
|
7
|
-
# Validate query, table, and column values.
|
8
|
-
# @param [Arel::Query] query
|
9
|
-
# @param [Arel::Table] table
|
10
|
-
# @param [Symbol] column_name
|
11
|
-
# @param [Array<Symbol>] allowed
|
12
|
-
# @return [void]
|
13
|
-
def validate_query_table_column(query, table, column_name, allowed)
|
14
|
-
validate_query(query)
|
15
|
-
validate_table(table)
|
16
|
-
validate_name(column_name, allowed)
|
17
|
-
end
|
18
|
-
|
19
7
|
# Validate table and column values.
|
20
8
|
# @param [Arel::Table] table
|
21
9
|
# @param [Symbol] column_name
|
@@ -39,15 +27,6 @@ module Clearly
|
|
39
27
|
fail Clearly::Query::QueryArgumentError, "model must be in '#{models_allowed}', got '#{model}'" unless models_allowed.include?(model)
|
40
28
|
end
|
41
29
|
|
42
|
-
# Validate query and hash values.
|
43
|
-
# @param [ActiveRecord::Relation] query
|
44
|
-
# @param [Hash] hash
|
45
|
-
# @return [void]
|
46
|
-
def validate_query_hash(query, hash)
|
47
|
-
validate_query(query)
|
48
|
-
validate_hash(hash)
|
49
|
-
end
|
50
|
-
|
51
30
|
# Validate table value.
|
52
31
|
# @param [Arel::Table] table
|
53
32
|
# @raise [FilterArgumentError] if table is not an Arel::Table
|
@@ -154,7 +133,6 @@ module Clearly
|
|
154
133
|
# @raise [FilterArgumentError] if value is not a valid Hash.
|
155
134
|
# @return [void]
|
156
135
|
def validate_hash(value)
|
157
|
-
validate_not_blank(value)
|
158
136
|
fail Clearly::Query::QueryArgumentError, "value must be a Hash, got '#{value}'" unless value.is_a?(Hash)
|
159
137
|
end
|
160
138
|
|
@@ -181,7 +159,7 @@ module Clearly
|
|
181
159
|
fail Clearly::Query::QueryArgumentError, "value must be a boolean, got '#{value}'" if !value.is_a?(TrueClass) && !value.is_a?(FalseClass)
|
182
160
|
end
|
183
161
|
|
184
|
-
# Escape wildcards in
|
162
|
+
# Escape wildcards in LIKE value.
|
185
163
|
# @param [String] value
|
186
164
|
# @return [String] sanitized value
|
187
165
|
def sanitize_like_value(value)
|
@@ -247,16 +225,17 @@ module Clearly
|
|
247
225
|
# @param [Hash] value
|
248
226
|
# @return [void]
|
249
227
|
def validate_definition(value)
|
228
|
+
validate_not_blank(value)
|
250
229
|
validate_hash(value)
|
251
230
|
|
252
231
|
# fields
|
232
|
+
validate_not_blank(value[:fields])
|
253
233
|
validate_hash(value[:fields])
|
254
234
|
|
255
235
|
validate_not_blank(value[:fields][:valid])
|
256
236
|
validate_array(value[:fields][:valid])
|
257
237
|
validate_array_items(value[:fields][:valid])
|
258
238
|
|
259
|
-
validate_not_blank(value[:fields][:text])
|
260
239
|
validate_array(value[:fields][:text])
|
261
240
|
validate_array_items(value[:fields][:text])
|
262
241
|
|
@@ -264,6 +243,7 @@ module Clearly
|
|
264
243
|
validate_array(value[:fields][:mappings])
|
265
244
|
|
266
245
|
value[:fields][:mappings].each do |mapping|
|
246
|
+
validate_not_blank(mapping)
|
267
247
|
validate_hash(mapping)
|
268
248
|
validate_symbol(mapping[:name])
|
269
249
|
validate_not_blank(mapping[:value])
|
@@ -271,9 +251,6 @@ module Clearly
|
|
271
251
|
|
272
252
|
# associations
|
273
253
|
validate_spec_association(value[:associations])
|
274
|
-
|
275
|
-
# defaults
|
276
|
-
validate_hash(value[:defaults])
|
277
254
|
end
|
278
255
|
|
279
256
|
# Validate association specification
|
@@ -283,6 +260,7 @@ module Clearly
|
|
283
260
|
validate_array(value)
|
284
261
|
|
285
262
|
value.each do |association|
|
263
|
+
validate_not_blank(association)
|
286
264
|
validate_hash(association)
|
287
265
|
validate_not_blank(association[:join])
|
288
266
|
validate_not_blank(association[:on])
|
@@ -2,15 +2,25 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Clearly::Query::Compose::Custom do
|
4
4
|
include_context 'shared_setup'
|
5
|
+
|
6
|
+
# for access to compose_and, relation_none, and relation_all
|
5
7
|
include Clearly::Query::Compose::Core
|
6
8
|
|
7
9
|
it 'can be instantiated' do
|
8
10
|
Clearly::Query::Compose::Custom.new
|
9
11
|
end
|
10
12
|
|
11
|
-
it '
|
13
|
+
it 'constructs sql for select all' do
|
14
|
+
query = self.send(:relation_all, Order).where(customer_id: 10)
|
15
|
+
expect(query.to_sql).to eq('SELECT "orders".* FROM "orders" WHERE "orders"."customer_id" = 10')
|
16
|
+
end
|
12
17
|
|
18
|
+
it 'constructs sql for select none' do
|
19
|
+
query = self.send(:relation_none, Order).where(customer_id: 10)
|
20
|
+
expect(query.to_sql).to eq('')
|
21
|
+
end
|
13
22
|
|
23
|
+
it 'builds expected sql' do
|
14
24
|
custom = Clearly::Query::Compose::Custom.new
|
15
25
|
|
16
26
|
table = product_def.table
|
@@ -2,19 +2,34 @@ require 'spec_helper'
|
|
2
2
|
|
3
3
|
describe Clearly::Query::Composer do
|
4
4
|
include_context 'shared_setup'
|
5
|
-
let(:product_attributes) {
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
let(:product_attributes) {
|
6
|
+
{
|
7
|
+
name: 'plastic cup',
|
8
|
+
code: '000475PC',
|
9
|
+
brand: 'Generic',
|
10
|
+
introduced_at: '2015-01-01 00:00:00',
|
11
|
+
discontinued_at: nil
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
let(:customer_attributes) {
|
16
|
+
{
|
17
|
+
name: 'first last',
|
18
|
+
last_contact_at: '2015-11-09 10:00:00'
|
19
|
+
}
|
20
|
+
}
|
21
|
+
|
22
|
+
let(:order_attributes) {
|
23
|
+
{
|
24
|
+
|
25
|
+
}
|
26
|
+
}
|
10
27
|
|
11
28
|
it 'finds the only product' do
|
12
29
|
product = Product.create!(product_attributes)
|
13
30
|
query_hash = cleaner.do({name: {contains: 'cup'}})
|
14
|
-
|
15
|
-
expect(result.size).to eq(1)
|
31
|
+
query_ar = composer.query(Product, query_hash)
|
16
32
|
|
17
|
-
query_ar = Product.where(result[0])
|
18
33
|
expect(query_ar.count).to eq(1)
|
19
34
|
|
20
35
|
result_item = query_ar.to_a[0]
|
@@ -34,10 +49,8 @@ describe Clearly::Query::Composer do
|
|
34
49
|
end
|
35
50
|
|
36
51
|
query_hash = cleaner.do({name: {contains: '5'}})
|
37
|
-
|
38
|
-
expect(result.size).to eq(1)
|
52
|
+
query_ar = composer.query(Product, query_hash)
|
39
53
|
|
40
|
-
query_ar = Product.where(result[0])
|
41
54
|
expect(query_ar.count).to eq(1)
|
42
55
|
|
43
56
|
result_item = query_ar.to_a[0]
|
@@ -47,4 +60,35 @@ describe Clearly::Query::Composer do
|
|
47
60
|
expect(result_item.introduced_at).to eq(product_attributes[:introduced_at])
|
48
61
|
expect(result_item.discontinued_at).to eq(product_attributes[:discontinued_at])
|
49
62
|
end
|
63
|
+
|
64
|
+
it 'finds the matching order using mapped field' do
|
65
|
+
customer = Customer.create!(customer_attributes)
|
66
|
+
order_pending = Order.create!(customer: customer)
|
67
|
+
order_shipped = Order.create!(customer: customer, shipped_at: '2015-11-09 11:00:00')
|
68
|
+
|
69
|
+
query_hash = cleaner.do({title: {contains: 'not shipped'}})
|
70
|
+
query_ar = composer.query(Order, query_hash)
|
71
|
+
|
72
|
+
expect(query_ar.count).to eq(1)
|
73
|
+
|
74
|
+
result_item = query_ar.to_a[0]
|
75
|
+
expect(result_item.shipped_at).to eq(order_pending.shipped_at)
|
76
|
+
expect(result_item.customer_id).to eq(order_pending.customer_id)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'finds the correct order comparing dates' do
|
80
|
+
customer = Customer.create!(customer_attributes)
|
81
|
+
order1 = Order.create!(customer: customer, shipped_at: '2015-11-09 11:00:01')
|
82
|
+
order2 = Order.create!(customer: customer, shipped_at: '2015-11-09 11:00:00')
|
83
|
+
|
84
|
+
query_hash = cleaner.do({shipped_at: {gteq: '2015-11-09 11:00:01'}})
|
85
|
+
query_ar = composer.query(Order, query_hash)
|
86
|
+
|
87
|
+
expect(query_ar.count).to eq(1)
|
88
|
+
|
89
|
+
result_item = query_ar.to_a[0]
|
90
|
+
expect(result_item.shipped_at).to eq(order1.shipped_at)
|
91
|
+
expect(result_item.customer_id).to eq(order1.customer_id)
|
92
|
+
end
|
93
|
+
|
50
94
|
end
|
@@ -17,7 +17,7 @@ describe Clearly::Query::Composer do
|
|
17
17
|
invalid_composer = Clearly::Query::Composer.new([customer_def, customer_def])
|
18
18
|
query = cleaner.do({})
|
19
19
|
expect {
|
20
|
-
invalid_composer.
|
20
|
+
invalid_composer.conditions(Customer, query)
|
21
21
|
}.to raise_error(Clearly::Query::QueryArgumentError, "exactly one definition must match, found '2'")
|
22
22
|
end
|
23
23
|
|
@@ -36,13 +36,13 @@ describe Clearly::Query::Composer do
|
|
36
36
|
it 'is given an empty query' do
|
37
37
|
query = cleaner.do({})
|
38
38
|
expect {
|
39
|
-
composer.
|
39
|
+
composer.conditions(Customer, query)
|
40
40
|
}.to raise_error(Clearly::Query::QueryArgumentError, "filter hash must have at least 1 entry, got '0'")
|
41
41
|
end
|
42
42
|
|
43
43
|
it 'uses a regex operator using sqlite' do
|
44
44
|
expect {
|
45
|
-
conditions = composer.
|
45
|
+
conditions = composer.conditions(Product, {name: {regex: 'test'}})
|
46
46
|
query = Product.all
|
47
47
|
conditions.each { |c| query = query.where(c) }
|
48
48
|
expect(query.to_a).to eq([])
|
@@ -55,7 +55,7 @@ describe Clearly::Query::Composer do
|
|
55
55
|
|
56
56
|
it 'contains an unrecognised filter' do
|
57
57
|
expect {
|
58
|
-
composer.
|
58
|
+
composer.conditions(Customer, {
|
59
59
|
or: {
|
60
60
|
name: {
|
61
61
|
not_a_real_filter: 'Hello'
|
@@ -67,7 +67,7 @@ describe Clearly::Query::Composer do
|
|
67
67
|
|
68
68
|
it 'has no entry' do
|
69
69
|
expect {
|
70
|
-
composer.
|
70
|
+
composer.conditions(Customer, {
|
71
71
|
or: {
|
72
72
|
name: {
|
73
73
|
|
@@ -79,7 +79,7 @@ describe Clearly::Query::Composer do
|
|
79
79
|
|
80
80
|
it 'has not with no entries' do
|
81
81
|
expect {
|
82
|
-
composer.
|
82
|
+
composer.conditions(Customer, {
|
83
83
|
not: {
|
84
84
|
}
|
85
85
|
})
|
@@ -88,7 +88,7 @@ describe Clearly::Query::Composer do
|
|
88
88
|
|
89
89
|
it 'has or with no entries' do
|
90
90
|
expect {
|
91
|
-
composer.
|
91
|
+
composer.conditions(Customer, {
|
92
92
|
or: {
|
93
93
|
}
|
94
94
|
})
|
@@ -97,7 +97,7 @@ describe Clearly::Query::Composer do
|
|
97
97
|
|
98
98
|
it 'has not with more than one field' do
|
99
99
|
expect {
|
100
|
-
composer.
|
100
|
+
composer.conditions(Product, {
|
101
101
|
not: {
|
102
102
|
name: {
|
103
103
|
contains: 'Hello'
|
@@ -112,7 +112,7 @@ describe Clearly::Query::Composer do
|
|
112
112
|
|
113
113
|
it 'has not with more than one filter' do
|
114
114
|
expect {
|
115
|
-
composer.
|
115
|
+
composer.conditions(Product, {
|
116
116
|
not: {
|
117
117
|
name: {
|
118
118
|
contains: 'Hello',
|
@@ -125,7 +125,7 @@ describe Clearly::Query::Composer do
|
|
125
125
|
|
126
126
|
it 'has a combiner that is not recognised with valid filters' do
|
127
127
|
expect {
|
128
|
-
composer.
|
128
|
+
composer.conditions(Product, {
|
129
129
|
not_a_valid_combiner: {
|
130
130
|
name: {
|
131
131
|
contains: 'Hello'
|
@@ -140,7 +140,7 @@ describe Clearly::Query::Composer do
|
|
140
140
|
|
141
141
|
it "has a range missing 'from'" do
|
142
142
|
expect {
|
143
|
-
composer.
|
143
|
+
composer.conditions(Customer, {
|
144
144
|
and: {
|
145
145
|
name: {
|
146
146
|
range: {
|
@@ -154,7 +154,7 @@ describe Clearly::Query::Composer do
|
|
154
154
|
|
155
155
|
it "has a range missing 'to'" do
|
156
156
|
expect {
|
157
|
-
composer.
|
157
|
+
composer.conditions(Product, {
|
158
158
|
and: {
|
159
159
|
code: {
|
160
160
|
range: {
|
@@ -168,7 +168,7 @@ describe Clearly::Query::Composer do
|
|
168
168
|
|
169
169
|
it 'has a range with from/to and interval' do
|
170
170
|
expect {
|
171
|
-
composer.
|
171
|
+
composer.conditions(Customer, {
|
172
172
|
and: {
|
173
173
|
name: {
|
174
174
|
range: {
|
@@ -183,7 +183,7 @@ describe Clearly::Query::Composer do
|
|
183
183
|
|
184
184
|
it 'has a range with no recognised properties' do
|
185
185
|
expect {
|
186
|
-
composer.
|
186
|
+
composer.conditions(Customer, {
|
187
187
|
and: {
|
188
188
|
name: {
|
189
189
|
range: {
|
@@ -197,7 +197,7 @@ describe Clearly::Query::Composer do
|
|
197
197
|
|
198
198
|
it 'has a property that has no filters' do
|
199
199
|
expect {
|
200
|
-
composer.
|
200
|
+
composer.conditions(Customer, {
|
201
201
|
or: {
|
202
202
|
name: {
|
203
203
|
}
|
@@ -216,7 +216,7 @@ describe Clearly::Query::Composer do
|
|
216
216
|
|
217
217
|
expect {
|
218
218
|
query = cleaner.do(filter_params)
|
219
|
-
composer.
|
219
|
+
composer.conditions(Customer, query)
|
220
220
|
}.to raise_error(Clearly::Query::QueryArgumentError, 'array values cannot be hashes')
|
221
221
|
end
|
222
222
|
|
@@ -224,7 +224,7 @@ describe Clearly::Query::Composer do
|
|
224
224
|
filter_params = {"name" => {"inRange" => "(5,6)"}}
|
225
225
|
expect {
|
226
226
|
query = cleaner.do(filter_params)
|
227
|
-
composer.
|
227
|
+
composer.conditions(Customer, query)
|
228
228
|
}.to raise_error(Clearly::Query::QueryArgumentError, "range filter must be {'from': 'value', 'to': 'value'} or {'interval': '(|[.*,.*]|)'} got '(5,6)'")
|
229
229
|
end
|
230
230
|
|
@@ -233,7 +233,7 @@ describe Clearly::Query::Composer do
|
|
233
233
|
context 'succeeds when it' do
|
234
234
|
it 'is given a valid query without combiners' do
|
235
235
|
hash = cleaner.do({name: {contains: 'test'}})
|
236
|
-
conditions = composer.
|
236
|
+
conditions = composer.conditions(Customer, hash)
|
237
237
|
expect(conditions.size).to eq(1)
|
238
238
|
|
239
239
|
# sqlite only supports LIKE
|
@@ -246,7 +246,7 @@ describe Clearly::Query::Composer do
|
|
246
246
|
|
247
247
|
it 'is given a valid query with or combiner' do
|
248
248
|
hash = cleaner.do({or: {name: {contains: 'test'}, code: {eq: 4}}})
|
249
|
-
conditions = composer.
|
249
|
+
conditions = composer.conditions(Product, hash)
|
250
250
|
expect(conditions.size).to eq(1)
|
251
251
|
|
252
252
|
expect(conditions.first.to_sql).to eq("(\"products\".\"name\" LIKE '%test%' OR \"products\".\"code\" = '4')")
|
@@ -258,7 +258,7 @@ describe Clearly::Query::Composer do
|
|
258
258
|
|
259
259
|
it 'is given a valid query with camel cased keys' do
|
260
260
|
hash = cleaner.do({name: {does_not_start_with: 'test'}})
|
261
|
-
conditions = composer.
|
261
|
+
conditions = composer.conditions(Customer, hash)
|
262
262
|
expect(conditions.size).to eq(1)
|
263
263
|
|
264
264
|
expect(conditions.first.to_sql).to eq("\"customers\".\"name\" NOT LIKE 'test%'")
|
@@ -270,7 +270,7 @@ describe Clearly::Query::Composer do
|
|
270
270
|
|
271
271
|
it 'is given a valid range query that excludes the start and includes the end' do
|
272
272
|
hash = cleaner.do({name: {notInRange: {interval: '(2,5]'}}})
|
273
|
-
conditions = composer.
|
273
|
+
conditions = composer.conditions(Customer, hash)
|
274
274
|
expect(conditions.size).to eq(1)
|
275
275
|
|
276
276
|
expect(conditions.first.to_sql).to eq("(\"customers\".\"name\" <= '2' OR \"customers\".\"name\" > '5')")
|
@@ -282,7 +282,7 @@ describe Clearly::Query::Composer do
|
|
282
282
|
|
283
283
|
it 'is given a valid query that uses a table one step away' do
|
284
284
|
hash = cleaner.do({and: {name: {contains: 'test'}, 'orders.shipped_at' => {lt: '2015-10-24'}}})
|
285
|
-
conditions = composer.
|
285
|
+
conditions = composer.conditions(Customer, hash)
|
286
286
|
expect(conditions.size).to eq(1)
|
287
287
|
|
288
288
|
expected = "\"customers\".\"name\" LIKE '%test%' AND EXISTS (SELECT 1 FROM \"orders\" WHERE \"orders\".\"shipped_at\" < '2015-10-24' AND \"orders\".\"customer_id\" = \"customers\".\"id\")"
|
@@ -298,7 +298,7 @@ describe Clearly::Query::Composer do
|
|
298
298
|
# instead of the hash in Definition#parse_table_field
|
299
299
|
|
300
300
|
hash = cleaner.do({and: {name: {contains: 'test'}, 'customers.name' => {lt: '2015-10-24'}}})
|
301
|
-
conditions = composer.
|
301
|
+
conditions = composer.conditions(Part, hash)
|
302
302
|
expect(conditions.size).to eq(1)
|
303
303
|
|
304
304
|
expected = "\"parts\".\"name\" LIKE '%test%' AND EXISTS (SELECT 1 FROM \"customers\" INNER JOIN \"parts_products\" ON \"products\".\"id\" = \"parts_products\".\"product_id\" INNER JOIN \"products\" ON \"products\".\"id\" = \"orders_products\".\"product_id\" INNER JOIN \"orders_products\" ON \"orders\".\"id\" = \"orders_products\".\"order_id\" INNER JOIN \"orders\" ON \"orders\".\"customer_id\" = \"customers\".\"id\" WHERE \"customers\".\"name\" < '2015-10-24' AND \"customers\".\"id\" = \"orders\".\"customer_id\")"
|
@@ -311,10 +311,37 @@ describe Clearly::Query::Composer do
|
|
311
311
|
|
312
312
|
it 'is given a valid query that uses a custom field mapping' do
|
313
313
|
hash = cleaner.do({and: {shipped_at: {lt: '2015-10-24'}, title: {does_not_start_with: 'alice'}}})
|
314
|
-
conditions = composer.
|
314
|
+
conditions = composer.conditions(Order, hash)
|
315
315
|
expect(conditions.size).to eq(1)
|
316
316
|
|
317
|
-
expected = "\"orders\".\"shipped_at\" < '2015-10-24' AND (SELECT \"customers\".\"name\" FROM \"customers\" WHERE \"customers\".\"id\" = \"orders\".\"customer_id\") || ' (' || CASE WHEN \"orders\".\"shipped_at\" IS NULL THEN 'not shipped' ELSE \"orders\".\"shipped_at\" END || ')' NOT LIKE 'alice%'"
|
317
|
+
expected = "\"orders\".\"shipped_at\" < '2015-10-24' AND (SELECT \"customers\".\"name\" FROM \"customers\" WHERE \"customers\".\"id\" = \"orders\".\"customer_id\") || ' (' || (CASE WHEN \"orders\".\"shipped_at\" IS NULL THEN 'not shipped' ELSE \"orders\".\"shipped_at\" END) || ') ' NOT LIKE 'alice%'"
|
318
|
+
expect(conditions.first.to_sql).to eq(expected)
|
319
|
+
|
320
|
+
query = Order.all
|
321
|
+
conditions.each { |c| query = query.where(c) }
|
322
|
+
expect(query.to_a).to eq([])
|
323
|
+
end
|
324
|
+
|
325
|
+
it 'is given a valid query for all text fields' do
|
326
|
+
hash = cleaner.do({and: {all_text_fields: {starts_with: 'a'}}})
|
327
|
+
conditions = composer.conditions(Product, hash)
|
328
|
+
expect(conditions.size).to eq(1)
|
329
|
+
|
330
|
+
expected = "(((\"products\".\"brand\" || ' ' || \"products\".\"name\" || ' (' || \"products\".\"code\" || ')' LIKE 'a%' OR \"products\".\"name\" LIKE 'a%') OR \"products\".\"code\" LIKE 'a%') OR \"products\".\"brand\" LIKE 'a%')"
|
331
|
+
expect(conditions.first.to_sql).to eq(expected)
|
332
|
+
|
333
|
+
query = Product.all
|
334
|
+
conditions.each { |c| query = query.where(c) }
|
335
|
+
expect(query.to_a).to eq([])
|
336
|
+
end
|
337
|
+
|
338
|
+
it 'escapes characters in SQL like' do
|
339
|
+
hash = cleaner.do({and: {shipped_at: {lt: '2015-10-24'}, title: {does_not_start_with: 'a\l_i%c\'e|'}}})
|
340
|
+
conditions = composer.conditions(Order, hash)
|
341
|
+
expect(conditions.size).to eq(1)
|
342
|
+
|
343
|
+
escaped = 'a\\\l\\_i\\%c\'\'e\\|%'
|
344
|
+
expected = "\"orders\".\"shipped_at\" < '2015-10-24' AND (SELECT \"customers\".\"name\" FROM \"customers\" WHERE \"customers\".\"id\" = \"orders\".\"customer_id\") || ' (' || (CASE WHEN \"orders\".\"shipped_at\" IS NULL THEN 'not shipped' ELSE \"orders\".\"shipped_at\" END) || ') ' NOT LIKE '#{escaped}'"
|
318
345
|
expect(conditions.first.to_sql).to eq(expected)
|
319
346
|
|
320
347
|
query = Order.all
|
@@ -355,7 +382,7 @@ describe Clearly::Query::Composer do
|
|
355
382
|
end
|
356
383
|
|
357
384
|
hash = cleaner.do({and: {name: operator_hash}})
|
358
|
-
conditions = composer.
|
385
|
+
conditions = composer.conditions(Product, hash)
|
359
386
|
expect(conditions.size).to eq(1)
|
360
387
|
|
361
388
|
expected = {
|
@@ -14,4 +14,23 @@ describe Clearly::Query::Helper do
|
|
14
14
|
Clearly::Query::Helper.string_concat_infix('+')
|
15
15
|
}.to raise_error(ArgumentError,"string concatenation requires operator and two or more arguments, given '0'")
|
16
16
|
end
|
17
|
+
|
18
|
+
it 'builds a SQL function' do
|
19
|
+
format_date = 'YYYY-MM-DD'
|
20
|
+
format_date_quoted = Arel::Nodes.build_quoted(format_date)
|
21
|
+
table = Order.arel_table
|
22
|
+
column =:shipped_at
|
23
|
+
alias_name = 'as_alias'
|
24
|
+
|
25
|
+
query = Clearly::Query::Helper.named_function('to_char', [table[column], format_date_quoted], alias_name)
|
26
|
+
expect(query.to_sql).to eq("to_char(\"orders\".\"shipped_at\", 'YYYY-MM-DD') AS as_alias")
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'builds a SQL EXISTS condition' do
|
30
|
+
table = Order.arel_table
|
31
|
+
column =:shipped_at
|
32
|
+
|
33
|
+
query = Clearly::Query::Helper.exists(table[column])
|
34
|
+
expect(query.to_sql).to eq("EXISTS (\"orders\".\"shipped_at\")")
|
35
|
+
end
|
17
36
|
end
|
@@ -14,7 +14,7 @@ class Customer < ActiveRecord::Base
|
|
14
14
|
name: :title,
|
15
15
|
value: Clearly::Query::Helper.string_concat(
|
16
16
|
Customer.arel_table[:name],
|
17
|
-
|
17
|
+
Clearly::Query::Helper.sql_quoted(' title'))
|
18
18
|
}
|
19
19
|
]
|
20
20
|
},
|
@@ -53,11 +53,7 @@ class Customer < ActiveRecord::Base
|
|
53
53
|
}
|
54
54
|
]
|
55
55
|
}
|
56
|
-
]
|
57
|
-
defaults: {
|
58
|
-
order_by: :created_at,
|
59
|
-
direction: :desc
|
60
|
-
}
|
56
|
+
]
|
61
57
|
}
|
62
58
|
end
|
63
59
|
end
|
@@ -14,12 +14,15 @@ class Order < ActiveRecord::Base
|
|
14
14
|
{
|
15
15
|
name: :title,
|
16
16
|
value: Clearly::Query::Helper.string_concat(
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
17
|
+
Clearly::Query::Helper.sql_literal(
|
18
|
+
'(' +
|
19
|
+
Customer.arel_table
|
20
|
+
.where(Customer.arel_table[:id].eq(Order.arel_table[:customer_id]))
|
21
|
+
.project(Customer.arel_table[:name]).to_sql + ')'),
|
22
|
+
Clearly::Query::Helper.sql_quoted(' ('),
|
23
|
+
Clearly::Query::Helper.sql_literal('(CASE WHEN "orders"."shipped_at" IS NULL THEN \'not shipped\' ELSE "orders"."shipped_at" END)'),
|
24
|
+
Clearly::Query::Helper.sql_quoted(') ')
|
25
|
+
)
|
23
26
|
}
|
24
27
|
]
|
25
28
|
},
|
@@ -56,11 +59,7 @@ class Order < ActiveRecord::Base
|
|
56
59
|
}
|
57
60
|
]
|
58
61
|
}
|
59
|
-
]
|
60
|
-
defaults: {
|
61
|
-
order_by: :created_at,
|
62
|
-
direction: :desc
|
63
|
-
}
|
62
|
+
]
|
64
63
|
}
|
65
64
|
end
|
66
65
|
end
|
data/spec/support/models/part.rb
CHANGED
@@ -14,7 +14,7 @@ class Part < ActiveRecord::Base
|
|
14
14
|
name: :title,
|
15
15
|
value: Clearly::Query::Helper.string_concat(
|
16
16
|
Part.arel_table[:code],
|
17
|
-
|
17
|
+
Clearly::Query::Helper.sql_quoted(' '),
|
18
18
|
Part.arel_table[:manufacturer])
|
19
19
|
}
|
20
20
|
]
|
@@ -53,11 +53,7 @@ class Part < ActiveRecord::Base
|
|
53
53
|
}
|
54
54
|
]
|
55
55
|
}
|
56
|
-
]
|
57
|
-
defaults: {
|
58
|
-
order_by: :name,
|
59
|
-
direction: :asc
|
60
|
-
}
|
56
|
+
]
|
61
57
|
}
|
62
58
|
end
|
63
59
|
end
|
@@ -15,11 +15,11 @@ class Product < ActiveRecord::Base
|
|
15
15
|
name: :title,
|
16
16
|
value: Clearly::Query::Helper.string_concat(
|
17
17
|
Product.arel_table[:brand],
|
18
|
-
|
18
|
+
Clearly::Query::Helper.sql_quoted(' '),
|
19
19
|
Product.arel_table[:name],
|
20
|
-
|
20
|
+
Clearly::Query::Helper.sql_quoted(' ('),
|
21
21
|
Product.arel_table[:code],
|
22
|
-
|
22
|
+
Clearly::Query::Helper.sql_quoted(')'))
|
23
23
|
}
|
24
24
|
]
|
25
25
|
},
|
@@ -57,11 +57,7 @@ class Product < ActiveRecord::Base
|
|
57
57
|
}
|
58
58
|
]
|
59
59
|
}
|
60
|
-
]
|
61
|
-
defaults: {
|
62
|
-
order_by: :name,
|
63
|
-
direction: :asc
|
64
|
-
}
|
60
|
+
]
|
65
61
|
}
|
66
62
|
end
|
67
63
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: clearly-query
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
-
|
7
|
+
- Mark Cottman-Fields
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-11-
|
11
|
+
date: 2015-11-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: arel
|
@@ -235,9 +235,9 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
235
235
|
version: '0'
|
236
236
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
237
237
|
requirements:
|
238
|
-
- - "
|
238
|
+
- - ">="
|
239
239
|
- !ruby/object:Gem::Version
|
240
|
-
version:
|
240
|
+
version: '0'
|
241
241
|
requirements: []
|
242
242
|
rubyforge_project:
|
243
243
|
rubygems_version: 2.4.8
|