Class: ActiveRecord::Refined::BlockContext
- Inherits:
-
Object
- Object
- ActiveRecord::Refined::BlockContext
- 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
- #all(relation) ⇒ Object
-
#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.
-
#bit_count(expr) ⇒ Object
BIT_COUNT.
-
#case(operand = nil) ⇒ Object
CASE.
-
#case_when(value = nil, &block) ⇒ Object
The searched CASE, started at its first when:.
-
#cast(expr, type) ⇒ Object
CAST(expr AS type).
- #count(column, distinct: false) ⇒ Object
- #cube(*columns) ⇒ Object
- #current_date ⇒ Object
-
#excluded(column) ⇒ Object
The row an upsert could not insert, for the block upsert_all takes.
- #exists?(relation) ⇒ Boolean
-
#extract(field, expr) ⇒ Object
EXTRACT(field FROM expr).
-
#fn(name, *args) ⇒ Object
Escape hatch for functions without a method of their own.
-
#grouping_sets(*sets) ⇒ Object
GROUP BY GROUPING SETS / ROLLUP / CUBE.
-
#initialize(model) ⇒ BlockContext
constructor
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.
-
#json_array(*values) ⇒ Object
A JSON document built in the row: json_array from the values given, json_object from a Ruby hash whose values are expressions.
-
#json_arrayagg(value) ⇒ Object
Rows gathered into one JSON document: json_arrayagg collects a value from each row into an array, json_objectagg a key and a value into an object.
- #json_object(pairs = {}) ⇒ Object
- #json_objectagg(key, value) ⇒ Object
-
#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.
- #lead(expr, offset = 1, default = nil) ⇒ Object
- #nth_value(expr, nth) ⇒ Object
-
#op(operator, left, right) ⇒ Object
The same escape hatch for operators: op("&&", :tags, "ruby,sql").
- #rollup(*columns) ⇒ Object
-
#sql(statement, *binds) ⇒ Object
SQL as written, asked for by name:.
-
#value(literal) ⇒ Object
A literal where an expression is expected, quoted like any other value:.
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.
52 53 54 |
# File 'lib/active_record/refined.rb', line 52 def initialize(model) @model = model end |
Instance Method Details
#all(relation) ⇒ Object
284 285 286 |
# File 'lib/active_record/refined.rb', line 284 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.
280 281 282 |
# File 'lib/active_record/refined.rb', line 280 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.
257 258 259 260 261 262 263 264 265 266 |
# File 'lib/active_record/refined.rb', line 257 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 }
325 326 327 |
# File 'lib/active_record/refined.rb', line 325 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)
332 333 334 |
# File 'lib/active_record/refined.rb', line 332 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.
209 210 211 |
# File 'lib/active_record/refined.rb', line 209 def cast(expr, type) AST::Cast.new(expr, type) end |
#count(column, distinct: false) ⇒ Object
64 65 66 |
# File 'lib/active_record/refined.rb', line 64 def count(column, distinct: false) AST::Aggregate.new(column, :count, distinct: distinct) end |
#cube(*columns) ⇒ Object
202 203 204 |
# File 'lib/active_record/refined.rb', line 202 def cube(*columns) grouping(:cube, columns) end |
#current_date ⇒ Object
151 152 153 154 |
# File 'lib/active_record/refined.rb', line 151 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.
311 312 313 314 315 316 |
# File 'lib/active_record/refined.rb', line 311 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
268 269 270 |
# File 'lib/active_record/refined.rb', line 268 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.
176 177 178 179 180 181 182 183 |
# File 'lib/active_record/refined.rb', line 176 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.
243 244 245 246 |
# File 'lib/active_record/refined.rb', line 243 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. PostgreSQL has all three; the MySQL family has WITH ROLLUP, which says rollup and only rollup, trailing the group list -- the node spells it there. 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) }
194 195 196 |
# File 'lib/active_record/refined.rb', line 194 def grouping_sets(*sets) grouping(:grouping_sets, sets) end |
#json_array(*values) ⇒ Object
A JSON document built in the row: json_array from the values given, json_object from a Ruby hash whose values are expressions.
81 82 83 |
# File 'lib/active_record/refined.rb', line 81 def json_array(*values) AST::JsonBuild.new(:array, values) end |
#json_arrayagg(value) ⇒ Object
Rows gathered into one JSON document: json_arrayagg collects a value from each row into an array, json_objectagg a key and a value into an object.
71 72 73 |
# File 'lib/active_record/refined.rb', line 71 def json_arrayagg(value) AST::JsonAggregate.new(:arrayagg, [value]) end |
#json_object(pairs = {}) ⇒ Object
85 86 87 |
# File 'lib/active_record/refined.rb', line 85 def json_object(pairs = {}) AST::JsonBuild.new(:object, pairs) end |
#json_objectagg(key, value) ⇒ Object
75 76 77 |
# File 'lib/active_record/refined.rb', line 75 def json_objectagg(key, value) AST::JsonAggregate.new(:objectagg, [key, value]) 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.
231 232 233 |
# File 'lib/active_record/refined.rb', line 231 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
235 236 237 |
# File 'lib/active_record/refined.rb', line 235 def lead(expr, offset = 1, default = nil) AST::WindowFunction.new("LEAD", default.nil? ? [expr, offset] : [expr, offset, default]) end |
#nth_value(expr, nth) ⇒ Object
225 226 227 |
# File 'lib/active_record/refined.rb', line 225 def nth_value(expr, nth) AST::WindowFunction.new("NTH_VALUE", [expr, nth]) end |
#op(operator, left, right) ⇒ Object
The same escape hatch for operators: op("&&", :tags, "ruby,sql").
249 250 251 |
# File 'lib/active_record/refined.rb', line 249 def op(operator, left, right) AST::Operation.new(operator, left, right) end |
#rollup(*columns) ⇒ Object
198 199 200 |
# File 'lib/active_record/refined.rb', line 198 def rollup(*columns) grouping(:rollup, columns) end |
#sql(statement, *binds) ⇒ Object
SQL as written, asked for by name:
where { sql("length(name) > ?", 10) }
The one way a string means SQL inside a block. ? and :name placeholders take quoted values, through sanitize_sql_array.
294 295 296 |
# File 'lib/active_record/refined.rb', line 294 def sql(statement, *binds) AST::Sql.new(statement, binds) end |
#value(literal) ⇒ Object
A literal where an expression is expected, quoted like any other value:
select { [:id, value(0).as(:depth)] }
Numbers and strings have a shorthand -- 0.as(:depth) -- so this is
the spelling for the rest: true, nil, a date.
304 305 306 |
# File 'lib/active_record/refined.rb', line 304 def value(literal) AST::Value.new(literal) end |