Class: ActiveRecord::Refined::BlockContext

Inherits:
Object
  • Object
show all
Defined in:
lib/active_record/refined.rb

Constant Summary collapse

AGGREGATE_FUNCTIONS =
{
  sum: :sum, avg: :average, min: :minimum, max: :maximum,
}.freeze
SCALAR_FUNCTIONS =

Scalar functions, defined as real methods so that a typo is a NoMethodError and a name Kernel also answers to (format, hash, test) cannot quietly mean something else.

The value lists the adapters that differ: a string is what the function is called there, nil says the adapter has no equivalent. An adapter that is not listed spells it like the method. The families are what the entries key on, so trilogy reads the mysql column.

Availability was checked by calling each one; the SQLite figures assume the math functions its build usually enables.

{
  abs: {}, acos: {}, asin: {}, atan: {}, atan2: {}, ceil: {},
  coalesce: {}, concat: {}, cos: {}, degrees: {}, exp: {}, floor: {},
  length: {}, ln: {}, log: {}, log10: {}, lower: {}, ltrim: {},
  mod: {}, nullif: {}, pi: {}, power: {}, radians: {}, replace: {},
  round: {}, rtrim: {}, sign: {}, sin: {}, sqrt: {}, substr: {},
  tan: {}, trim: {}, upper: {},
  char_length: {sqlite: 'LENGTH'},
  greatest: {sqlite: 'MAX'},
  least: {sqlite: 'MIN'},
  # PostgreSQL spells log2(x) as log(2, x), which no renaming carries.
  log2: {postgresql: nil},
  # MySQL's TRUNCATE insists on the second argument, where the others
  # default it to zero; SQLite's trunc takes only the one.
  trunc: {mysql: 'TRUNCATE'},
  now: {sqlite: nil},
  # The bit aggregates, which PostgreSQL and MySQL spell alike and
  # SQLite has none of.  PostgreSQL gained bit_xor in 14.
  bit_and: {sqlite: nil}, bit_or: {sqlite: nil}, bit_xor: {sqlite: nil},
  date_trunc: {sqlite: nil, mysql: nil},
  # Named for Kernel#rand, which it also takes back: a block calling
  # rand would otherwise get Ruby's and never reach the database.
  rand: {sqlite: 'RANDOM', postgresql: 'RANDOM'},
  # Two different functions share this name: printf formatting here, and
  # on MySQL the one that puts separators in a number, which reads a
  # printf template as the number zero rather than complaining.  The
  # name keeps the one meaning; fn(:format, ...) reaches MySQL's.
  format: {mysql: nil},
}.freeze
DATETIME_VALUE_FUNCTIONS =

The datetime value functions, as the SQL grammar calls them. These the grammar has bare -- PostgreSQL and SQLite reject them written with parentheses -- and the one thing that does go into parentheses is an optional precision, current_timestamp(3), which current_date never takes and SQLite never accepts. The table reads like SCALAR_FUNCTIONS; current_timestamp is the portable spelling of what now means, reaching SQLite where now does not.

{
  current_date: {},
  current_time: {},
  current_timestamp: {},
  localtime: {sqlite: nil},
  localtimestamp: {sqlite: nil},
}.freeze

Instance Method Summary collapse

Constructor Details

#initialize(model) ⇒ BlockContext

The model is only consulted to learn which adapter the query is being built for, which is what decides how a scalar function is spelled.



41
42
43
# File 'lib/active_record/refined.rb', line 41

def initialize(model)
  @model = model
end

Instance Method Details

#all(relation) ⇒ Object



246
247
248
# File 'lib/active_record/refined.rb', line 246

def all(relation)
  quantified('ALL', relation)
end

#any(relation) ⇒ Object

ANY and ALL quantify a comparison over a subquery, which is what a scalar subquery cannot do: it has to return the one row.

Post.where { :likes > any(Post.published.select(:likes)) }
Post.where { :likes >= all(Post.select(:likes)) }

== any is IN and != all is NOT IN, so what these add is the four comparisons IN has no spelling for.



242
243
244
# File 'lib/active_record/refined.rb', line 242

def any(relation)
  quantified('ANY', relation)
end

#bit_count(expr) ⇒ Object

BIT_COUNT. MySQL counts the bits of a number; PostgreSQL counts those of a bit string, so the argument is cast, and to bit(64) because that is what makes a negative come back as MySQL has it -- 64 bits of two's complement rather than as many as the column happens to be wide.



219
220
221
222
223
224
225
226
227
228
# File 'lib/active_record/refined.rb', line 219

def bit_count(expr)
  case adapter_family
  when :mysql then AST::Function.new('BIT_COUNT', [expr])
  when :postgresql
    AST::Function.new('BIT_COUNT', [AST::Cast.new(expr, 'bit(64)')])
  else
    raise NotImplementedError,
      "bit_count has no equivalent on #{@model.connection_db_config.adapter}"
  end
end

#case(operand = nil) ⇒ Object

CASE. case is a keyword, so Ruby only reaches this one through the receiver -- self.case -- which is why the two shapes have shorthands that do not need it: :age.when(...) for the form with an operand, and case_when for the form where each when carries its own condition.

self.case(:age).when(10).then(1).else(0)
self.case.when { :age >= 60 }.then { :age - 60 }


278
279
280
# File 'lib/active_record/refined.rb', line 278

def case(operand = nil)
  AST::Case.new(operand)
end

#case_when(value = nil, &block) ⇒ Object

The searched CASE, started at its first when:

case_when { :age >= 60 }.then { :age - 60 }.else(0)


285
286
287
# File 'lib/active_record/refined.rb', line 285

def case_when(value = nil, &block)
  AST::Case.new.when(value, &block)
end

#cast(expr, type) ⇒ Object

CAST(expr AS type). The type is the adapter's own name for it, checked for shape by the node; whether it exists is the database's to say.



176
177
178
# File 'lib/active_record/refined.rb', line 176

def cast(expr, type)
  AST::Cast.new(expr, type)
end

#count(column, distinct: false) ⇒ Object



53
54
55
# File 'lib/active_record/refined.rb', line 53

def count(column, distinct: false)
  AST::Aggregate.new(column, :count, distinct: distinct)
end

#cube(*columns) ⇒ Object



169
170
171
# File 'lib/active_record/refined.rb', line 169

def cube(*columns)
  grouping(:cube, columns)
end

#current_dateObject



119
120
121
122
# File 'lib/active_record/refined.rb', line 119

def current_date
  AST::DatetimeValueFunction.new(
    function_name(:current_date, DATETIME_VALUE_FUNCTIONS))
end

#excluded(column) ⇒ Object

The row an upsert could not insert, for the block upsert_all takes. PostgreSQL and SQLite give it a name; MySQL spells the same thing VALUES(column), which takes the column bare.



264
265
266
267
268
269
# File 'lib/active_record/refined.rb', line 264

def excluded(column)
  return AST::Column.new(:excluded, column) unless adapter_family == :mysql

  quoted = @model.with_connection {|c| c.quote_column_name(column) }
  AST::Function.new('VALUES', [Arel::Nodes::SqlLiteral.new(quoted)])
end

#exists?(relation) ⇒ Boolean

Returns:

  • (Boolean)


230
231
232
# File 'lib/active_record/refined.rb', line 230

def exists?(relation)
  AST::Exists.new(relation)
end

#extract(field, expr) ⇒ Object

EXTRACT(field FROM expr). The field is a keyword, not a value, so it has to be a plain name; the node checks it. SQLite spells all of this as strftime formats, which no renaming carries, so it raises there -- after the node is built, so that a bad field is an ArgumentError on every adapter.



144
145
146
147
148
149
150
151
# File 'lib/active_record/refined.rb', line 144

def extract(field, expr)
  node = AST::Extract.new(field, expr)
  if adapter_family == :sqlite
    raise NotImplementedError,
      "extract has no equivalent on #{@model.connection_db_config.adapter}"
  end
  node
end

#fn(name, *args) ⇒ Object

Escape hatch for functions without a method of their own. The name is emitted as written, so a case-sensitive one can be spelled exactly, and for that reason it has to be a plain name, optionally qualified by a schema; anything else is refused rather than written into the SQL.



210
211
212
213
# File 'lib/active_record/refined.rb', line 210

def fn(name, *args)
  AST::Function.new(
    AST.check_name(name, AST::FUNCTION_NAME, "function name").to_s, args)
end

#grouping_sets(*sets) ⇒ Object

GROUP BY GROUPING SETS / ROLLUP / CUBE, which PostgreSQL has and the others do not -- MySQL's WITH ROLLUP says one of the three and says it somewhere else in the clause. Arel has the nodes and writes them for PostgreSQL alone, so what it would raise elsewhere says nothing; this says it here, as extract does, while the block is being read.

Sale.group { grouping_sets([:region], [:product], []) }
Sale.group { rollup(:region, :product) }


161
162
163
# File 'lib/active_record/refined.rb', line 161

def grouping_sets(*sets)
  grouping(:grouping_sets, sets)
end

#lag(expr, offset = 1, default = nil) ⇒ Object

The offset is written out rather than left to default, so that a default value cannot end up where the offset belongs.



198
199
200
# File 'lib/active_record/refined.rb', line 198

def lag(expr, offset = 1, default = nil)
  AST::WindowFunction.new('LAG', default.nil? ? [expr, offset] : [expr, offset, default])
end

#lead(expr, offset = 1, default = nil) ⇒ Object



202
203
204
# File 'lib/active_record/refined.rb', line 202

def lead(expr, offset = 1, default = nil)
  AST::WindowFunction.new('LEAD', default.nil? ? [expr, offset] : [expr, offset, default])
end

#nth_value(expr, nth) ⇒ Object



192
193
194
# File 'lib/active_record/refined.rb', line 192

def nth_value(expr, nth)
  AST::WindowFunction.new('NTH_VALUE', [expr, nth])
end

#rollup(*columns) ⇒ Object



165
166
167
# File 'lib/active_record/refined.rb', line 165

def rollup(*columns)
  grouping(:rollup, columns)
end

#value(literal) ⇒ Object

A literal where an expression is expected, quoted like any other value:

select { [:id, value(0).as(:depth)] }

Needed because the top of a select list is ActiveRecord's, and a bare string there is SQL rather than a string. Numbers have a shorthand -- 0.as(:depth) -- since nothing else could be meant by one.



257
258
259
# File 'lib/active_record/refined.rb', line 257

def value(literal)
  AST::Value.new(literal)
end