clearly-query 0.3.1.pre → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml 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