activerecord-refined 0.10.2 → 0.12.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +19 -22
  4. data/activerecord-refined.gemspec +4 -3
  5. data/docs/conditions.md +3 -1
  6. data/docs/ctes.md +2 -2
  7. data/docs/expressions.md +37 -20
  8. data/docs/functions.md +14 -10
  9. data/docs/index.md +2 -17
  10. data/docs/windows.md +2 -2
  11. data/examples/expressions.rb +21 -15
  12. data/lib/active_record/refined/ast/arithmetics.rb +137 -0
  13. data/lib/active_record/refined/ast/conditions.rb +451 -0
  14. data/lib/active_record/refined/ast/expressions.rb +304 -0
  15. data/lib/active_record/refined/ast/functions.rb +254 -0
  16. data/lib/active_record/refined/ast/grouping.rb +63 -0
  17. data/lib/active_record/refined/ast/json.rb +530 -0
  18. data/lib/active_record/refined/ast/node.rb +298 -0
  19. data/lib/active_record/refined/ast/ordering.rb +104 -0
  20. data/lib/active_record/refined/ast/predications.rb +384 -0
  21. data/lib/active_record/refined/ast/windows.rb +126 -0
  22. data/lib/active_record/refined/ast.rb +39 -2364
  23. data/lib/active_record/refined/block_context.rb +671 -0
  24. data/lib/active_record/refined/block_syntax.rb +128 -0
  25. data/lib/active_record/refined/dialect/mysql_compat.rb +16 -0
  26. data/lib/active_record/refined/dialect/mysqlish_json_functions.rb +45 -0
  27. data/lib/active_record/refined/dialect/oracle.rb +1 -7
  28. data/lib/active_record/refined/dialect/postgresql.rb +8 -0
  29. data/lib/active_record/refined/dialect/sql_server.rb +29 -5
  30. data/lib/active_record/refined/dialect/sqlite.rb +23 -0
  31. data/lib/active_record/refined/dialect.rb +132 -66
  32. data/lib/active_record/refined/query_methods.rb +444 -0
  33. data/lib/{activerecord-refined → active_record/refined}/version.rb +3 -2
  34. data/lib/active_record/refined/writes.rb +72 -0
  35. data/lib/active_record/refined.rb +7 -1270
  36. data/lib/activerecord-refined.rb +0 -4
  37. metadata +36 -5
@@ -8,11 +8,15 @@ module ActiveRecord
8
8
  # and asked, rather than branched on, for whatever a query builds
9
9
  # differently from one database to the next. The base is the standard
10
10
  # spelling an unclassified adapter keeps; each subclass overrides only
11
- # where its family departs from it.
11
+ # where its family departs from it. Where no spelling is anyone's
12
+ # standard -- most of JSON -- the base refuses instead, and every family
13
+ # carries its own.
12
14
  #
13
15
  # The families are loaded as they are met: an application on PostgreSQL
14
16
  # never loads the Oracle class, whose adapter it will never resolve to.
15
17
  class Dialect
18
+ autoload :MysqlishJsonFunctions,
19
+ "active_record/refined/dialect/mysqlish_json_functions"
16
20
  autoload :Sqlite, "active_record/refined/dialect/sqlite"
17
21
  autoload :Postgresql, "active_record/refined/dialect/postgresql"
18
22
  autoload :MysqlCompat, "active_record/refined/dialect/mysql_compat"
@@ -84,10 +88,23 @@ module ActiveRecord
84
88
 
85
89
  # The scalar and datetime functions a family spells differently, or has
86
90
  # none of. A name it does not list it spells like the method, upper
87
- # cased; a nil says it has no equivalent, and the block raises. The base
88
- # -- an unclassified adapter -- keeps every standard name.
89
- FUNCTIONS = {}.freeze
90
-
91
+ # cased; a nil says it has no equivalent, and the block raises. The
92
+ # base -- an unclassified adapter -- keeps the names most families
93
+ # share and nils the ones that are one or two families' own: printf
94
+ # FORMAT, whose name MySQL hands to a different function entirely,
95
+ # RAND, DATE_TRUNC, NOW, LOG2 and the three bit aggregates. A family
96
+ # that has one of them says so in a table of its own, since the tables
97
+ # shadow rather than merge.
98
+ FUNCTIONS = {
99
+ format: nil, rand: nil, date_trunc: nil, now: nil, log2: nil,
100
+ bit_and: nil, bit_or: nil, bit_xor: nil,
101
+ }.freeze
102
+
103
+ # The name this family spells a function with, asked for as the block
104
+ # builds the call; {FUNCTIONS} is where the answer is looked up.
105
+ # @param name [Symbol] the method's own name, as {BlockContext} has it
106
+ # @return [String]
107
+ # @raise [NotImplementedError] where the family has no equivalent
91
108
  def function_name(name, model)
92
109
  functions = self.class::FUNCTIONS
93
110
  return name.to_s.upcase unless functions.key?(name)
@@ -96,9 +113,19 @@ module ActiveRecord
96
113
  "#{name} has no equivalent on #{model.connection_db_config.adapter}")
97
114
  end
98
115
 
116
+ # Whether the datetime value functions take a precision,
117
+ # `current_timestamp(3)`. SQLite and SQL Server take none.
99
118
  def datetime_precision_supported? = true
119
+
120
+ # Whether the family has EXTRACT. SQLite spells the fields as strftime
121
+ # formats and SQL Server as DATEPART, so neither answers to the name.
100
122
  def extract_supported? = true
123
+
124
+ # Whether a comparison can be quantified with ANY or ALL. SQLite has
125
+ # neither.
101
126
  def quantifiers_supported? = true
127
+
128
+ # Whether a FULL OUTER JOIN can be written. The MySQL family has none.
102
129
  def full_outer_join_supported? = true
103
130
 
104
131
  # The FILTER clause, which restricts an aggregate to the rows a condition
@@ -106,6 +133,16 @@ module ActiveRecord
106
133
  # built by the aggregate node.
107
134
  def filter_supported? = true
108
135
 
136
+ # The bitwise operators, & | ^ << >> and ~. No SQL standard has them,
137
+ # so unlike the capabilities above the base refuses and each family
138
+ # that has the operators says so itself -- which of the seven is every
139
+ # one but Oracle, whose single bit operation is the BITAND function.
140
+ def bitwise_operators_supported? = false
141
+
142
+ # The array comparisons -- @>, <@ and && against an array column.
143
+ # The type and its operators are PostgreSQL's alone.
144
+ def array_comparisons_supported? = false
145
+
109
146
  # A lateral join is allowed to stand unless the family refuses it here.
110
147
  def check_lateral(_model); end
111
148
 
@@ -118,10 +155,13 @@ module ActiveRecord
118
155
  "bit_count has no equivalent on #{model.connection_db_config.adapter}"
119
156
  end
120
157
 
121
- # The row an upsert could not insert. PostgreSQL and SQLite name it;
122
- # MySQL spells the same thing VALUES(column) and overrides.
123
- def excluded(column, _model)
124
- AST::Column.new(:excluded, column)
158
+ # The row an upsert could not insert. `excluded` is PostgreSQL's name
159
+ # for it, which SQLite took over and the MySQL family spells
160
+ # VALUES(column), so each of the three carries its own; the families
161
+ # without an upsert have nothing for the name to stand in.
162
+ def excluded(_column, model)
163
+ raise NotImplementedError,
164
+ "excluded has no equivalent on #{model.connection_db_config.adapter}"
125
165
  end
126
166
 
127
167
  # true? / false? and their negations. The standard spells them with the
@@ -157,26 +197,32 @@ module ActiveRecord
157
197
  Arel::Nodes::InfixOperation.new(subtract ? :- : :+, date, interval))
158
198
  end
159
199
 
160
- # XOR, which no two families spell alike. The standard is the two
161
- # operations it is made of, naming each operand twice, as SQLite needs;
162
- # PostgreSQL and the MySQL family have an operator and override.
200
+ # XOR, which no two families spell alike. The default is the two
201
+ # operations it is made of, naming each operand twice -- built only
202
+ # from the & and | a family has just claimed through
203
+ # {#bitwise_operators_supported?}, so wherever it can be reached at
204
+ # all it works. Of the five that claim them, SQLite alone keeps it;
205
+ # the rest have an operator of their own and override.
163
206
  def bitwise_xor(left, right)
164
207
  Arel::Nodes::Subtraction.new(
165
208
  Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
166
209
  Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
167
210
  end
168
211
 
169
- # --- Reading JSON. The defaults are what an unclassified adapter gets,
170
- # which is SQLite's operators for a path and its functions elsewhere.
171
-
172
- # dig / dig_text. The standard is SQLite's -> and ->>, whose ->> keeps
173
- # the value's type, so dig_text casts to text for a portable comparison.
174
- def json_path(document, dollar_path, _steps, json_value, _model)
175
- extracted = Arel::Nodes::InfixOperation.new(
176
- json_value ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
177
- return extracted if json_value
178
- Arel::Nodes::NamedFunction.new(
179
- "CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
212
+ # --- Reading JSON. No two families spell it alike and the standard
213
+ # names only part of it, so the base refuses each piece and every
214
+ # family carries its own; what an unclassified adapter would get
215
+ # from any one family's spelling is a query that means nothing on
216
+ # the next.
217
+
218
+ # dig / dig_text. Nothing reaches every family: SQLite and PostgreSQL
219
+ # have operators of their own, MySQL its functions, and the two that
220
+ # spell it as SQL:2016's JSON_VALUE and JSON_QUERY -- Oracle and SQL
221
+ # Server -- differ over what a scalar leaf comes back as. Every
222
+ # family overrides.
223
+ def json_path(_document, _dollar_path, _steps, _json_value, model)
224
+ raise NotImplementedError,
225
+ "dig has no equivalent on #{model.connection_db_config.adapter}"
180
226
  end
181
227
 
182
228
  # A Ruby value on the JSON side of a comparison belongs to a JSON type;
@@ -187,69 +233,94 @@ module ActiveRecord
187
233
  "#{model.connection_db_config.adapter}; dig_text gives the value"
188
234
  end
189
235
 
236
+ # contains?: whether the document holds what is given. The standard has
237
+ # no equivalent; PostgreSQL has @> and the MySQL family JSON_CONTAINS,
238
+ # and both override.
190
239
  def json_contains(_document, _json, model)
191
240
  raise NotImplementedError,
192
241
  "contains? has no equivalent on #{model.connection_db_config.adapter}"
193
242
  end
194
243
 
195
- def json_has_key(document, _name, path, _model)
196
- Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
244
+ # key?: whether the object has the key. SQL:2016 spells it
245
+ # JSON_EXISTS, which of the five only Oracle answers to; PostgreSQL has
246
+ # the ? operator, the MySQL family JSON_CONTAINS_PATH, SQLite json_type
247
+ # at the path and SQL Server JSON_PATH_EXISTS, so every family
248
+ # overrides.
249
+ #
250
+ # The key arrives spelled both ways -- bare in `name`, as a $ path in
251
+ # `path` -- since a family reads it as one or as the other.
252
+ def json_has_key(_document, _name, _path, model)
253
+ raise NotImplementedError,
254
+ "key? has no equivalent on #{model.connection_db_config.adapter}"
197
255
  end
198
256
 
199
- def json_keys(document, _model)
200
- Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
257
+ # keys: the keys of an object as a JSON array. JSON_KEYS is the MySQL
258
+ # family's own; SQLite and PostgreSQL gather theirs through a subquery
259
+ # over their key-listing functions, and Oracle and SQL Server would
260
+ # reach them only through table unnests not written here.
261
+ def json_keys(_document, model)
262
+ raise NotImplementedError,
263
+ "keys has no equivalent on #{model.connection_db_config.adapter}"
201
264
  end
202
265
 
203
266
  # --- Writing JSON. A Ruby document or boolean has to be told apart from
204
267
  # a bare scalar, and embedded as the JSON it spells.
205
268
 
269
+ # The test the writing hooks share: a Hash, an Array or a boolean is a
270
+ # document, and anything else a bare scalar.
206
271
  def json_document_value?(value)
207
272
  value.is_a?(::Hash) || value.is_a?(::Array) || value == true || value == false
208
273
  end
209
274
 
210
275
  # A Ruby document or boolean written where one of the JSON functions
211
- # wants JSON. The standard marks the literal with JSON_EXTRACT($);
212
- # SQLite has json() and Oracle FORMAT JSON, and both override.
213
- def json_argument(value, _model)
214
- json = Arel::Nodes.build_quoted(JSON.generate(value))
215
- Arel::Nodes::NamedFunction.new("JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
276
+ # wants JSON. Every family marks the literal its own way --
277
+ # JSON_EXTRACT($) on MySQL, json() on SQLite, FORMAT JSON on Oracle,
278
+ # JSON_QUERY on SQL Server -- and none of the marks is another's.
279
+ def json_argument(_value, model)
280
+ raise NotImplementedError,
281
+ "a document written as JSON has no equivalent on " \
282
+ "#{model.connection_db_config.adapter}"
216
283
  end
217
284
 
218
- # bury: setting a value at a path. The standard is JSON_SET; PostgreSQL
219
- # has jsonb_set and Oracle JSON_TRANSFORM, and both override.
220
- def json_set(document, _steps, dollar_path, value, expression, model)
221
- Arel::Nodes::NamedFunction.new(
222
- "JSON_SET",
223
- [document, Arel::Nodes.build_quoted(dollar_path),
224
- json_set_value(value, expression, model)])
285
+ # bury: setting a value at a path. The standard has no editing
286
+ # functions at all: JSON_SET is {MysqlishJsonFunctions}', jsonb_set
287
+ # PostgreSQL's, JSON_TRANSFORM Oracle's and JSON_MODIFY SQL Server's.
288
+ def json_set(_document, _steps, _dollar_path, _value, _expression, model)
289
+ raise NotImplementedError,
290
+ "bury has no equivalent on #{model.connection_db_config.adapter}"
225
291
  end
226
292
 
227
- # except: removing keys. The standard removes a path apiece with
228
- # JSON_REMOVE; PostgreSQL subtracts an array of keys and Oracle removes
229
- # through JSON_TRANSFORM, and both override.
230
- def json_remove(document, dollar_paths, _steps, _model)
231
- Arel::Nodes::NamedFunction.new(
232
- "JSON_REMOVE",
233
- [document, *dollar_paths.map { |path| Arel::Nodes.build_quoted(path) }])
293
+ # except: removing keys, for which the standard likewise has nothing.
294
+ # {MysqlishJsonFunctions} removes a path apiece with JSON_REMOVE,
295
+ # PostgreSQL subtracts an array of keys, Oracle and SQL Server edit
296
+ # through JSON_TRANSFORM and JSON_MODIFY.
297
+ def json_remove(_document, _dollar_paths, _steps, model)
298
+ raise NotImplementedError,
299
+ "except has no equivalent on #{model.connection_db_config.adapter}"
234
300
  end
235
301
 
236
- # json_array / json_object built in the row. The standard says JSON_ARRAY
237
- # and JSON_OBJECT with the values (and keys alternating); PostgreSQL has
238
- # the jsonb_build_* pair and Oracle a keyword syntax, and both override.
239
- def json_build(kind, keys, args, _model)
240
- Arel::Nodes::NamedFunction.new(
241
- kind == :array ? "JSON_ARRAY" : "JSON_OBJECT", json_build_body(kind, keys, args))
302
+ # json_array / json_object built in the row. JSON_ARRAY(a, b) is
303
+ # SQL:2016, but JSON_OBJECT is where the syntaxes part -- the standard
304
+ # pairs each key as KEY k VALUE v, {MysqlishJsonFunctions} alternates
305
+ # them, SQL Server writes k : v -- so the pair travels together and
306
+ # each family says its own; SQL Server's is not written here yet.
307
+ def json_build(kind, _keys, _args, model)
308
+ raise NotImplementedError,
309
+ "json_#{kind} has no equivalent on #{model.connection_db_config.adapter}"
242
310
  end
243
311
 
244
- # A document or boolean built into json_array/json_object. The standard
245
- # marks it JSON as bury does; PostgreSQL casts to jsonb and overrides.
312
+ # A document or boolean built into json_array/json_object. The default
313
+ # rides through {#json_argument}, and refuses or serves with it;
314
+ # PostgreSQL casts to jsonb and overrides.
246
315
  def json_build_argument(value, model)
247
316
  json_argument(value, model)
248
317
  end
249
318
 
250
319
  # json_arrayagg / json_objectagg gather rows into a document. The
251
- # standard names are JSON_ARRAYAGG and JSON_OBJECTAGG; SQLite and
252
- # PostgreSQL have their own and override.
320
+ # names are SQL:2016's own, which the MySQL family and Oracle answer
321
+ # to, so unlike the rest of JSON the base keeps them; SQLite and
322
+ # PostgreSQL have names of their own, and SQL Server, which has no
323
+ # JSON aggregates, refuses.
253
324
  def json_aggregate_name(kind)
254
325
  kind == :arrayagg ? "JSON_ARRAYAGG" : "JSON_OBJECTAGG"
255
326
  end
@@ -286,6 +357,8 @@ module ActiveRecord
286
357
  # --- Grouping. GROUPING SETS, ROLLUP and CUBE, which the standard has
287
358
  # none of; PostgreSQL has all three and the MySQL family rollup alone.
288
359
 
360
+ # Whether the family has the kind of grouping asked for, one of
361
+ # `:grouping_sets`, `:rollup` and `:cube`.
289
362
  def grouping_supported?(_kind) = false
290
363
 
291
364
  # The MySQL family spells rollup WITH ROLLUP, trailing the group list
@@ -293,16 +366,9 @@ module ActiveRecord
293
366
  def grouping_by_with_rollup? = false
294
367
 
295
368
  protected
296
- # The value beside a path in JSON_SET: an expression as it is, a
297
- # document or boolean as JSON, a bare scalar quoted.
298
- def json_set_value(value, expression, model)
299
- return expression if expression
300
- return Arel::Nodes.build_quoted(value) unless json_document_value?(value)
301
- json_argument(value, model)
302
- end
303
-
304
- # The arguments to JSON_ARRAY/JSON_OBJECT: an array's values as they
305
- # are, an object's keys alternating with them.
369
+ # The values of JSON_ARRAY as they are, JSON_OBJECT's keys
370
+ # alternating with theirs -- the shape MysqlishJsonFunctions and
371
+ # PostgreSQL's jsonb_build_* pair both take.
306
372
  def json_build_body(kind, keys, args)
307
373
  return args if kind == :array
308
374
  keys.zip(args).flat_map { |key, arg| [Arel::Nodes.build_quoted(key), arg] }