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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d045cfa5f732a083d9a8b102bd7c2cdd67b24722
4
- data.tar.gz: 8c486d7e7add811d6898ac9d695fa2981b108e21
3
+ metadata.gz: 8b1fc977200d949ff4d1aa8db533b56d6d987a91
4
+ data.tar.gz: 0e5dc84af5e9b77647bfcb3ffe6dd77d1daf356b
5
5
  SHA512:
6
- metadata.gz: 25e62009d801425f4f18a5955bc88656f6fab91ba071653b398caa0221abc8f2420c0ba48969dcf6289715e9a88580bc5a62cd08c5de2d1eada28bcfd63dc308
7
- data.tar.gz: b9604376d04f1f49ac70a4b01267af962a7607fa03e03f9e28b702484c26975636a59c9ee350e7660a93d5bc9a206cc791b2df32c706ea770003f71f4f2409ca
6
+ metadata.gz: 83e6b022830e7026f87eebfc3ef8f27a9053127493b8c4aaa327843a2eaeadebd9177f27f81cb91d77a1d1386b27af4f829645e77711f97bfc63c53bb61fba5f
7
+ data.tar.gz: abcaf272739a41933356e229ff6b4a2d3a5a04ebf90bebd2ecd3bd501c2ab65a9fee76f885fba8abc70ce714e695e0d51e850e34a66cfe96b1c1cffd005aeb5c
@@ -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 a settings declared in a model.
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
- Arel::Nodes.build_quoted(' title'))
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
- Constructs an Arel query from a hash of options.
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
- For example:
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
- conditions = composer.query(model, cleaned_query_hash)
89
- query = model.where(conditions)
114
+ arel_conditions = composer.conditions(model, query_hash)
115
+ # or
116
+ query = composer.query(model, query_hash)
90
117
 
91
- ## Contributing
118
+ ### Building custom Arel queries
92
119
 
93
- 1. [Fork this repo](https://github.com/cofiem/clearly-query/fork)
94
- 2. Create your feature branch (`git checkout -b my-new-feature`)
95
- 3. Commit your changes (`git commit -am 'Add some feature'`)
96
- 4. Push to the branch (`git push origin my-new-feature`)
97
- 5. Create a new [pull request](https://github.com/cofiem/clearly-query/compare)
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 [elastic search filters](http://www.elasticsearch.org/guide/en/elasticsearch/reference/current/query-dsl-filters.html).
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
- Implicit `and` is used when no combine operator is specified.
15
+ An implicit `and` operator is used when no logical operator is specified.
16
16
 
17
17
  ### Filter Operators
18
18
 
19
- Filter comparison operator has multiple forms to help with constructing queries that read more 'naturally'.
20
- Be aware that it is possible operators may be 'case sensitive by default
21
- for unicode characters that are beyond the ASCII range'. For example, in [sqlite](https://www.sqlite.org/lang_expr.html).
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 is inclusive lower bound and exclusive upper bound.
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" > 'value1' OR "table"."attr" >= 'value2')
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 regex which allows for inclusive or exclusive bounds.
61
+ A more complex range can be specified using a special format which allows for inclusive or exclusive bounds.
62
62
 
63
- Operator | Query hash | SQL
64
- -------------|---------------------------------------|----------------------------------------------------------
65
- interval | {attr: {interval: '(value1,value2]'}} | "table"."attr" > 'value1' AND "table"."attr" <= 'value2'
66
- not_interval | {attr: {interval: '(value1,value2]'}} | ("table"."attr" <= 'value1' OR "table"."attr" > 'value2')
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
- ##### Arrays
76
+ ##### Array
76
77
 
77
- An array of values to match the attribute value exactly.
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
- Match the contents of a model attributes.
87
- These comparison operators are case insensitive where possible (usually depends on database).
88
- It is possible to match at the entire content, at the start of the content, or at the end.
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 **may not be supported by all databases**.
91
+ Regular expression match may not be supported by all databases.
91
92
 
92
93
  Operator | Query hash | SQL
93
94
  ------------------------------------------------------|-----------------------------------|-----------------------------------------------
@@ -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 = ['@cofiem']
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
- .descendants
30
- .reject { |d| d.name == 'ActiveRecord::SchemaMigration' }
31
- .sort { |a, b| a.name <=> b.name }
32
- .uniq { |d| d.arel_table.name }
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 [Arel::Nodes::Node, Array<Arel::Nodes::Node>]
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
- parse_query(definition, :and, hash)
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 parse_query(definition, query_key, query_value)
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| parse_query(definition, 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
@@ -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
- @paths = []
29
- traverse_branches(@root_node, nil)
31
+ if @discovered_nodes.blank? && @paths.blank?
32
+ traverse_branches(@root_node, nil)
33
+ end
30
34
  @paths
31
35
  end
32
36
 
@@ -13,7 +13,7 @@ module Clearly
13
13
 
14
14
  case adapter
15
15
  when 'mysql'
16
- Arel::Nodes::NamedFunction.new('concat', *args)
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 like value..
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])
@@ -3,6 +3,6 @@ module Clearly
3
3
  # Clearly Query namespace
4
4
  module Query
5
5
  # Gem version
6
- VERSION = '0.3.1.pre'
6
+ VERSION = '1.0.0'
7
7
  end
8
8
  end
@@ -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 'build expected sql' do
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) { {name: 'plastic cup',
6
- code: '000475PC',
7
- brand: 'Generic',
8
- introduced_at: '2015-01-01 00:00:00',
9
- discontinued_at: nil}}
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
- result = composer.query(Product, query_hash)
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
- result = composer.query(Product, query_hash)
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.query(Customer, query)
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.query(Customer, query)
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.query(Product, {name: {regex: 'test'}})
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.query(Customer, {
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.query(Customer, {
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.query(Customer, {
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.query(Customer, {
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.query(Product, {
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.query(Product, {
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.query(Product, {
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.query(Customer, {
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.query(Product, {
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.query(Customer, {
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.query(Customer, {
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.query(Customer, {
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.query(Customer, query)
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.query(Customer, query)
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.query(Customer, hash)
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.query(Product, hash)
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.query(Customer, hash)
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.query(Customer, hash)
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.query(Customer, hash)
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.query(Part, hash)
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.query(Order, hash)
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.query(Product, hash)
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
- Arel::Nodes.build_quoted(' title'))
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
- Customer.arel_table
18
- .where(Customer.arel_table[:id].eq(Order.arel_table[:customer_id]))
19
- .project(Customer.arel_table[:name]),
20
- Arel::Nodes.build_quoted(' ('),
21
- Arel::Nodes::SqlLiteral.new('CASE WHEN "orders"."shipped_at" IS NULL THEN \'not shipped\' ELSE "orders"."shipped_at" END'),
22
- Arel::Nodes.build_quoted(')'))
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
@@ -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
- Arel::Nodes.build_quoted(' '),
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
- Arel::Nodes.build_quoted(' '),
18
+ Clearly::Query::Helper.sql_quoted(' '),
19
19
  Product.arel_table[:name],
20
- Arel::Nodes.build_quoted(' ('),
20
+ Clearly::Query::Helper.sql_quoted(' ('),
21
21
  Product.arel_table[:code],
22
- Arel::Nodes.build_quoted(')'))
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.3.1.pre
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
- - "@cofiem"
7
+ - Mark Cottman-Fields
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-01 00:00:00.000000000 Z
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: 1.3.1
240
+ version: '0'
241
241
  requirements: []
242
242
  rubyforge_project:
243
243
  rubygems_version: 2.4.8