activerecord-refined 0.4.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,9 @@ module ActiveRecord
9
9
  NAME = /[[:alpha:]_][[:alnum:]_$]*/
10
10
  ALIAS_NAME = /\A#{NAME}\z/
11
11
  FUNCTION_NAME = /\A#{NAME}(\.#{NAME})?\z/
12
+ # A SQL type as cast writes it: words, at most parenthesized with
13
+ # lengths -- double precision, decimal(10,2).
14
+ TYPE_NAME = /\A[[:alpha:]_][[:alnum:]_ ]*(\(\d+(, ?\d+)?\))?\z/
12
15
 
13
16
  def self.check_name(name, pattern, what)
14
17
  return name if pattern.match?(name.to_s)
@@ -60,26 +63,50 @@ module ActiveRecord
60
63
  Match.new(self, pattern, negated: true)
61
64
  end
62
65
 
66
+ # `!` negates any predicate, so these are here for the four that SQL
67
+ # spells for itself: IS NOT NULL rather than NOT (... IS NULL), and
68
+ # likewise NOT IN and NOT LIKE. They mean the same thing either way,
69
+ # including when the column is NULL; what they save is the reading.
63
70
  def null?
64
71
  Comparison.new(self, :==, nil)
65
72
  end
66
73
 
74
+ def not_null?
75
+ Comparison.new(self, :!=, nil)
76
+ end
77
+
67
78
  def in?(values)
68
79
  In.new(self, values)
69
80
  end
70
81
 
82
+ def not_in?(values)
83
+ In.new(self, values, negated: true)
84
+ end
85
+
71
86
  def between?(min, max)
72
87
  In.new(self, min..max)
73
88
  end
74
89
 
90
+ def not_between?(min, max)
91
+ In.new(self, min..max, negated: true)
92
+ end
93
+
75
94
  def like?(pattern)
76
95
  Like.new(self, pattern)
77
96
  end
78
97
 
98
+ def not_like?(pattern)
99
+ Like.new(self, pattern, negated: true)
100
+ end
101
+
79
102
  def ilike?(pattern)
80
103
  Like.new(self, pattern, nil, case_sensitive: false)
81
104
  end
82
105
 
106
+ def not_ilike?(pattern)
107
+ Like.new(self, pattern, nil, case_sensitive: false, negated: true)
108
+ end
109
+
83
110
  # Case-insensitive equality, folded on both sides rather than left to
84
111
  # the collation, so it means the same thing on every adapter.
85
112
  def casecmp?(value)
@@ -218,6 +245,15 @@ module ActiveRecord
218
245
  else operand
219
246
  end
220
247
  end
248
+
249
+ # Resolves a function argument: a column or an expression as above,
250
+ # anything else a value to be quoted.
251
+ def to_arel_argument(arg, table)
252
+ case arg
253
+ when Node, Symbol then to_arel_operand(arg, table)
254
+ else Arel::Nodes.build_quoted(arg)
255
+ end
256
+ end
221
257
  end
222
258
 
223
259
  class Predicate < Node
@@ -234,6 +270,29 @@ module ActiveRecord
234
270
  end
235
271
  end
236
272
 
273
+ # A literal standing where an expression would: `select { value(0).as(:depth) }`.
274
+ #
275
+ # Values reach the SQL quoted wherever they appear as an operand, but the
276
+ # top of a select list is ActiveRecord's, and a bare string there is SQL
277
+ # rather than a string. Saying `value` is how you ask for the other
278
+ # meaning, and it carries the predications with it, so a literal can be
279
+ # compared and combined like anything else.
280
+ class Value < Node
281
+ include Predications
282
+ include Arithmetics
283
+ include Aggregations
284
+
285
+ attr_reader :value
286
+
287
+ def initialize(value)
288
+ @value = value
289
+ end
290
+
291
+ def to_arel(_table)
292
+ Arel::Nodes.build_quoted(value)
293
+ end
294
+ end
295
+
237
296
  class Column < Node
238
297
  include Predications
239
298
  include Arithmetics
@@ -351,16 +410,80 @@ module ActiveRecord
351
410
  end
352
411
 
353
412
  def to_arel(table)
354
- arel_args = args.map do |arg|
355
- case arg
356
- when Node, Symbol then to_arel_operand(arg, table)
357
- else Arel::Nodes.build_quoted(arg)
358
- end
359
- end
413
+ arel_args = args.map {|arg| to_arel_argument(arg, table) }
360
414
  Arel::Nodes::NamedFunction.new(name, arel_args)
361
415
  end
362
416
  end
363
417
 
418
+ # EXTRACT(field FROM expr). The field is grammar rather than a value --
419
+ # a keyword the adapter reads bare -- so it has to be a plain name,
420
+ # which Arel upcases on the way out.
421
+ class Extract < Node
422
+ include Predications
423
+ include Arithmetics
424
+
425
+ attr_reader :field, :operand
426
+
427
+ def initialize(field, operand)
428
+ @field = AST.check_name(field, ALIAS_NAME, "extract field")
429
+ @operand = operand
430
+ end
431
+
432
+ def to_arel(table)
433
+ Arel::Nodes::Extract.new(to_arel_argument(operand, table), field.to_s)
434
+ end
435
+ end
436
+
437
+ # CAST(expr AS type). The type is grammar too, written into the SQL as
438
+ # given -- it is the adapter's own name for the type, and whether it
439
+ # exists is the database's to say -- so it has to look like one:
440
+ # a plain name, at most parenthesized with lengths.
441
+ class Cast < Node
442
+ include Predications
443
+ include Arithmetics
444
+
445
+ attr_reader :operand, :sql_type
446
+
447
+ def initialize(operand, sql_type)
448
+ @operand = operand
449
+ @sql_type = AST.check_name(sql_type, TYPE_NAME, "SQL type")
450
+ end
451
+
452
+ def to_arel(table)
453
+ Arel::Nodes::NamedFunction.new(
454
+ "CAST",
455
+ [Arel::Nodes::As.new(to_arel_argument(operand, table),
456
+ Arel::Nodes::SqlLiteral.new(sql_type.to_s))])
457
+ end
458
+ end
459
+
460
+ # CURRENT_TIMESTAMP and its relatives, what the SQL grammar calls a
461
+ # datetime value function. The grammar has them bare, and PostgreSQL
462
+ # and SQLite reject them written as calls, so unlike Function the name
463
+ # is emitted without parentheses. A precision is the one thing that
464
+ # does go into parentheses, and it is written into the SQL as given, so
465
+ # only an Integer is accepted.
466
+ class DatetimeValueFunction < Node
467
+ include Predications
468
+ include Arithmetics
469
+
470
+ attr_reader :name, :precision
471
+
472
+ def initialize(name, precision = nil)
473
+ unless precision.nil? || precision.is_a?(Integer)
474
+ raise ArgumentError,
475
+ "#{precision.inspect} is not an Integer precision"
476
+ end
477
+ @name = name
478
+ @precision = precision
479
+ end
480
+
481
+ def to_arel(_table)
482
+ Arel::Nodes::SqlLiteral.new(
483
+ precision ? "#{name}(#{precision})" : name)
484
+ end
485
+ end
486
+
364
487
  # A plain SQL comparison. The value is passed through as it is, so a Range
365
488
  # or an Array compares against a PostgreSQL range or array column, the way
366
489
  # ActiveRecord's own force_equality? types do.
@@ -406,19 +529,21 @@ module ActiveRecord
406
529
  # IN for a list of values, BETWEEN for a range, IN (SELECT ...) for a
407
530
  # relation.
408
531
  class In < Predicate
409
- attr_reader :operand, :values
532
+ attr_reader :operand, :values, :negated
410
533
 
411
- def initialize(operand, values)
534
+ def initialize(operand, values, negated: false)
412
535
  @operand = operand
413
536
  @values = values
537
+ @negated = negated
414
538
  end
415
539
 
416
540
  def to_arel(table)
417
541
  arel_operand = to_arel_operand(operand, table)
418
- case values
419
- when Range then arel_operand.between(values)
420
- when ActiveRecord::Relation then arel_operand.in(subquery(values))
421
- else arel_operand.in(values)
542
+ if values.is_a?(Range)
543
+ arel_operand.public_send(negated ? :not_between : :between, values)
544
+ else
545
+ arg = values.is_a?(ActiveRecord::Relation) ? subquery(values) : values
546
+ arel_operand.public_send(negated ? :not_in : :in, arg)
422
547
  end
423
548
  end
424
549
 
@@ -479,19 +604,23 @@ module ActiveRecord
479
604
  inject {|left, right| Or.new(left, right) }
480
605
  end
481
606
 
482
- attr_reader :operand, :pattern, :escape, :case_sensitive
607
+ attr_reader :operand, :pattern, :escape, :case_sensitive, :negated
483
608
 
484
- def initialize(operand, pattern, escape = nil, case_sensitive: true)
609
+ def initialize(operand, pattern, escape = nil, case_sensitive: true,
610
+ negated: false)
485
611
  @operand = operand
486
612
  @pattern = pattern
487
613
  @escape = escape
488
614
  @case_sensitive = case_sensitive
615
+ @negated = negated
489
616
  end
490
617
 
491
618
  def to_arel(table)
492
619
  # Arel matches case-insensitively unless told otherwise, which is
493
620
  # what picks ILIKE over LIKE on PostgreSQL.
494
- to_arel_operand(operand, table).matches(pattern, escape, case_sensitive)
621
+ to_arel_operand(operand, table).
622
+ public_send(negated ? :does_not_match : :matches,
623
+ pattern, escape, case_sensitive)
495
624
  end
496
625
  end
497
626
 
@@ -22,6 +22,18 @@ module ActiveRecord
22
22
  AST::Column.new(self, column_name)
23
23
  end
24
24
  end
25
+
26
+ # Shorthand for `value(0).as(:depth)` and the like. Numbers only: a
27
+ # string in a select list already means SQL rather than a string, so
28
+ # giving String this would make the same literal mean two things
29
+ # depending on whether it had been sent a message.
30
+ [Integer, Float].each do |klass|
31
+ refine klass do
32
+ def as(alias_name)
33
+ AST::As.new(AST::Value.new(self), alias_name)
34
+ end
35
+ end
36
+ end
25
37
  end
26
38
 
27
39
  class BlockContext
@@ -55,13 +67,20 @@ module ActiveRecord
55
67
  # Availability was checked by calling each one; the SQLite figures
56
68
  # assume the math functions its build usually enables.
57
69
  SCALAR_FUNCTIONS = {
58
- abs: {}, ceil: {}, coalesce: {}, concat: {}, exp: {}, floor: {},
59
- length: {}, ln: {}, log: {}, lower: {}, ltrim: {}, mod: {},
60
- nullif: {}, power: {}, replace: {}, round: {}, rtrim: {}, sqrt: {},
61
- substr: {}, trim: {}, upper: {},
70
+ abs: {}, acos: {}, asin: {}, atan: {}, atan2: {}, ceil: {},
71
+ coalesce: {}, concat: {}, cos: {}, degrees: {}, exp: {}, floor: {},
72
+ length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
73
+ mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
74
+ round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
75
+ tan: {}, trim: {}, upper: {},
62
76
  char_length: {sqlite: 'LENGTH'},
63
77
  greatest: {sqlite: 'MAX'},
64
78
  least: {sqlite: 'MIN'},
79
+ # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
80
+ log2: {postgresql: nil},
81
+ # MySQL's TRUNCATE insists on the second argument, where the others
82
+ # default it to zero; SQLite's trunc takes only the one.
83
+ trunc: {mysql: 'TRUNCATE'},
65
84
  now: {sqlite: nil},
66
85
  date_trunc: {sqlite: nil, mysql: nil},
67
86
  # Named for Kernel#rand, which it also takes back: a block calling
@@ -83,7 +102,65 @@ module ActiveRecord
83
102
  }.freeze
84
103
 
85
104
  SCALAR_FUNCTIONS.each_key do |name|
86
- define_method(name) {|*args| AST::Function.new(function_name(name), args) }
105
+ define_method(name) do |*args|
106
+ AST::Function.new(function_name(name, SCALAR_FUNCTIONS), args)
107
+ end
108
+ end
109
+
110
+ # The datetime value functions, as the SQL grammar calls them. These
111
+ # the grammar has bare -- PostgreSQL and SQLite reject them written with
112
+ # parentheses -- and the one thing that does go into parentheses is an
113
+ # optional precision, current_timestamp(3), which current_date never
114
+ # takes and SQLite never accepts. The table reads like
115
+ # SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what
116
+ # now means, reaching SQLite where now does not.
117
+ DATETIME_VALUE_FUNCTIONS = {
118
+ current_date: {},
119
+ current_time: {},
120
+ current_timestamp: {},
121
+ localtime: {sqlite: nil},
122
+ localtimestamp: {sqlite: nil},
123
+ }.freeze
124
+
125
+ def current_date
126
+ AST::DatetimeValueFunction.new(
127
+ function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
128
+ end
129
+
130
+ (DATETIME_VALUE_FUNCTIONS.keys - [:current_date]).each do |name|
131
+ define_method(name) do |precision = nil|
132
+ # Built first so that a precision of the wrong type is an
133
+ # ArgumentError on every adapter, before SQLite gets to say it takes
134
+ # none at all.
135
+ node = AST::DatetimeValueFunction.new(
136
+ function_name(name, DATETIME_VALUE_FUNCTIONS), precision)
137
+ if precision && adapter_family == :sqlite
138
+ raise NotImplementedError,
139
+ "#{name} takes no precision on #{@model.connection_db_config.adapter}"
140
+ end
141
+ node
142
+ end
143
+ end
144
+
145
+ # EXTRACT(field FROM expr). The field is a keyword, not a value, so it
146
+ # has to be a plain name; the node checks it. SQLite spells all of
147
+ # this as strftime formats, which no renaming carries, so it raises
148
+ # there -- after the node is built, so that a bad field is an
149
+ # ArgumentError on every adapter.
150
+ def extract(field, expr)
151
+ node = AST::Extract.new(field, expr)
152
+ if adapter_family == :sqlite
153
+ raise NotImplementedError,
154
+ "extract has no equivalent on #{@model.connection_db_config.adapter}"
155
+ end
156
+ node
157
+ end
158
+
159
+ # CAST(expr AS type). The type is the adapter's own name for it,
160
+ # checked for shape by the node; whether it exists is the database's to
161
+ # say.
162
+ def cast(expr, type)
163
+ AST::Cast.new(expr, type)
87
164
  end
88
165
 
89
166
  # Escape hatch for functions without a method of their own. The name is
@@ -99,10 +176,21 @@ module ActiveRecord
99
176
  AST::Exists.new(relation)
100
177
  end
101
178
 
179
+ # A literal where an expression is expected, quoted like any other value:
180
+ #
181
+ # select { [:id, value(0).as(:depth)] }
182
+ #
183
+ # Needed because the top of a select list is ActiveRecord's, and a bare
184
+ # string there is SQL rather than a string. Numbers have a shorthand --
185
+ # `0.as(:depth)` -- since nothing else could be meant by one.
186
+ def value(literal)
187
+ AST::Value.new(literal)
188
+ end
189
+
102
190
  private
103
191
 
104
- def function_name(name)
105
- spellings = SCALAR_FUNCTIONS.fetch(name)
192
+ def function_name(name, functions)
193
+ spellings = functions.fetch(name)
106
194
  return name.to_s.upcase unless spellings.key?(adapter_family)
107
195
  spellings.fetch(adapter_family) ||
108
196
  raise(NotImplementedError,
@@ -165,9 +253,8 @@ module ActiveRecord
165
253
  end
166
254
 
167
255
  # A symbol names a table, which ActiveRecord's own from only takes as a
168
- # string. With `as` it is selected under another name, which is how a
169
- # CTE stands in for the model's own table:
170
- # with_recursive(tree: [...]).from(:tree, as: :nodes)
256
+ # string. With `as` it is selected under another name; when that name
257
+ # is the model's own, from_cte says the same thing without repeating it.
171
258
  def from(value, subquery_name = nil, as: nil)
172
259
  unless value.is_a?(Symbol)
173
260
  if as
@@ -180,6 +267,18 @@ module ActiveRecord
180
267
  super(arel_table, subquery_name)
181
268
  end
182
269
 
270
+ # Selects a CTE in place of the model's own table. The alias is not a
271
+ # choice -- ActiveRecord keeps qualifying columns with the table name,
272
+ # so the model's is the only name that works -- which is why it is
273
+ # taken from the model rather than asked for:
274
+ # with_recursive(tree: [...]).from_cte(:tree)
275
+ def from_cte(name)
276
+ unless name.is_a?(Symbol)
277
+ raise ArgumentError, "from_cte takes the CTE's name as a symbol"
278
+ end
279
+ from(name, as: klass.table_name)
280
+ end
281
+
183
282
  # `as` names the table within the query, which is what makes a self
184
283
  # join expressible: joins(:employees, as: :managers) { ... }.
185
284
  def joins(*args, as: nil, &block)
@@ -1,5 +1,5 @@
1
1
  module Activerecord
2
2
  module Refined
3
- VERSION = '0.4.0'
3
+ VERSION = '0.5.1'
4
4
  end
5
5
  end
@@ -5,3 +5,7 @@ require 'active_record/refined/ast'
5
5
  require 'active_record/refined'
6
6
 
7
7
  ActiveRecord::QueryMethods.prepend ActiveRecord::Refined::QueryMethods
8
+
9
+ # The methods above are ActiveRecord's own, so a model already forwards them
10
+ # to its relation. from_cte is new, and has to be added to that list itself.
11
+ ActiveRecord::Base.singleton_class.delegate :from_cte, to: :all