Class: ActiveRecord::Refined::Dialect

Inherits:
Object
  • Object
show all
Defined in:
lib/active_record/refined/dialect.rb,
lib/active_record/refined/dialect/mysql.rb,
lib/active_record/refined/dialect/oracle.rb,
lib/active_record/refined/dialect/sqlite.rb,
lib/active_record/refined/dialect/mariadb.rb,
lib/active_record/refined/dialect/postgresql.rb,
lib/active_record/refined/dialect/sql_server.rb,
lib/active_record/refined/dialect/mysql_compat.rb

Overview

One class per family of SQL spellings, resolved from the model's adapter and asked, rather than branched on, for whatever a query builds differently from one database to the next. The base is the standard spelling an unclassified adapter keeps; each subclass overrides only where its family departs from it.

The families are loaded as they are met: an application on PostgreSQL never loads the Oracle class, whose adapter it will never resolve to.

Defined Under Namespace

Classes: Mariadb, Mysql, MysqlCompat, Oracle, Postgresql, SqlServer, Sqlite

Constant Summary collapse

FUNCTIONS =

The scalar and datetime functions a family spells differently, or has none of. A name it does not list it spells like the method, upper cased; a nil says it has no equivalent, and the block raises. The base -- an unclassified adapter -- keeps every standard name.

{}.freeze

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.for(model) ⇒ Object

The dialect a model's queries are built for. An adapter nobody has registered keeps the standard spellings and is left to say for itself what it cannot do. The instances are cached by class rather than by adapter, so a re-registration takes effect on the next query.



56
57
58
59
# File 'lib/active_record/refined/dialect.rb', line 56

def for(model)
  klass = class_for(model.connection_db_config.adapter, model)
  @instances.compute_if_absent(klass) { klass.new }
end

.register(adapter, dialect = nil, &block) ⇒ Object

Names an adapter's dialect. An adapter's gem, or an application, registers its own -- a Dialect subclass overriding only where its family departs from the standard:

ActiveRecord::Refined::Dialect.register("exampledb", ExampleDialect)

A block registers an adapter whose dialect only the connection can name, as mysql2's is either MySQL's or MariaDB's: it receives the model and returns the class. The built-in families register with blocks too, which is what leaves each autoloaded until an adapter first resolves to it.



44
45
46
47
48
49
# File 'lib/active_record/refined/dialect.rb', line 44

def register(adapter, dialect = nil, &block)
  unless dialect || block
    raise ArgumentError, "register takes a dialect class or a block"
  end
  @registry[adapter.to_s] = dialect || block
end

Instance Method Details

#add_interval(date, amount, unit, subtract, _date_only) ⇒ Object

A date moved by a duration: :due_on + 3.days. The standard adds an interval literal, x + INTERVAL '3' DAY, which PostgreSQL and the MySQL family both read; SQLite, SQL Server and Oracle each spell the move their own way and override. The amount is a whole number and the unit one of six names, both checked by the node, so both are written into the SQL as they are. date_only says the operand is a date rather than a datetime, which only SQLite has to be told.



154
155
156
157
158
# File 'lib/active_record/refined/dialect.rb', line 154

def add_interval(date, amount, unit, subtract, _date_only)
  interval = Arel::Nodes::SqlLiteral.new("INTERVAL '#{amount}' #{unit.to_s.upcase}")
  Arel::Nodes::Grouping.new(
    Arel::Nodes::InfixOperation.new(subtract ? :- : :+, date, interval))
end

#bit_count(_expr, model) ⇒ Object

BIT_COUNT of a number. The standard has no equivalent; the families that do override.

Raises:

  • (NotImplementedError)


116
117
118
119
# File 'lib/active_record/refined/dialect.rb', line 116

def bit_count(_expr, model)
  raise NotImplementedError,
    "bit_count has no equivalent on #{model.connection_db_config.adapter}"
end

#bitwise_xor(left, right) ⇒ Object

XOR, which no two families spell alike. The standard is the two operations it is made of, naming each operand twice, as SQLite needs; PostgreSQL and the MySQL family have an operator and override.



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

def bitwise_xor(left, right)
  Arel::Nodes::Subtraction.new(
    Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseOr.new(left, right)),
    Arel::Nodes::Grouping.new(Arel::Nodes::BitwiseAnd.new(left, right)))
end

#check_json_aggregate_window(_source, _model) ⇒ Object

A family that cannot take these two as window functions refuses one.



263
# File 'lib/active_record/refined/dialect.rb', line 263

def check_json_aggregate_window(_source, _model); end

#check_lateral(_model) ⇒ Object

A lateral join is allowed to stand unless the family refuses it here.



110
# File 'lib/active_record/refined/dialect.rb', line 110

def check_lateral(_model); end

#check_string_aggregate_window(_model) ⇒ Object

A family that cannot take string_agg as a window function refuses one.



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

def check_string_aggregate_window(_model); end

#collate(operand, name, _model) ⇒ Object

COLLATE, which every family spells expr COLLATE name -- the name a bare identifier, no Arel node for it. Written bare it has to be a plain one, so it is checked here; PostgreSQL quotes it and widens what it takes, overriding. PostgreSQL also folds an unquoted name to lower case, where its built-in names are upper -- "C", "POSIX" -- another reason it quotes rather than inheriting this.



141
142
143
144
145
# File 'lib/active_record/refined/dialect.rb', line 141

def collate(operand, name, _model)
  AST.check_name(name, AST::COLLATION_NAME, "collation name")
  Arel::Nodes::InfixOperation.new(
    "COLLATE", operand, Arel::Nodes::SqlLiteral.new(name))
end

#datetime_precision_supported?Boolean

Returns:

  • (Boolean)


99
# File 'lib/active_record/refined/dialect.rb', line 99

def datetime_precision_supported? = true

#excluded(column, _model) ⇒ Object

The row an upsert could not insert. PostgreSQL and SQLite name it; MySQL spells the same thing VALUES(column) and overrides.



123
124
125
# File 'lib/active_record/refined/dialect.rb', line 123

def excluded(column, _model)
  AST::Column.new(:excluded, column)
end

#extract_supported?Boolean

Returns:

  • (Boolean)


100
# File 'lib/active_record/refined/dialect.rb', line 100

def extract_supported? = true

#filter_supported?Boolean

The FILTER clause, which restricts an aggregate to the rows a condition holds for. A family without it gets the CASE that means the same, built by the aggregate node.

Returns:

  • (Boolean)


107
# File 'lib/active_record/refined/dialect.rb', line 107

def filter_supported? = true

#full_outer_join_supported?Boolean

Returns:

  • (Boolean)


102
# File 'lib/active_record/refined/dialect.rb', line 102

def full_outer_join_supported? = true

#function_name(name, model) ⇒ Object



91
92
93
94
95
96
97
# File 'lib/active_record/refined/dialect.rb', line 91

def function_name(name, model)
  functions = self.class::FUNCTIONS
  return name.to_s.upcase unless functions.key?(name)
  functions.fetch(name) ||
    raise(NotImplementedError,
          "#{name} has no equivalent on #{model.connection_db_config.adapter}")
end

#grouping_by_with_rollup?Boolean

The MySQL family spells rollup WITH ROLLUP, trailing the group list rather than wrapping a list of its own, and overrides.

Returns:

  • (Boolean)


293
# File 'lib/active_record/refined/dialect.rb', line 293

def grouping_by_with_rollup? = false

#grouping_supported?(_kind) ⇒ Boolean

--- Grouping. GROUPING SETS, ROLLUP and CUBE, which the standard has none of; PostgreSQL has all three and the MySQL family rollup alone.

Returns:

  • (Boolean)


289
# File 'lib/active_record/refined/dialect.rb', line 289

def grouping_supported?(_kind) = false

#json_aggregate_filter_supported?Boolean

These two keep a NULL as JSON null rather than passing over it, so the CASE that stands in for FILTER would leave one in the document; a family without FILTER for them refuses it.

Returns:

  • (Boolean)


260
# File 'lib/active_record/refined/dialect.rb', line 260

def json_aggregate_filter_supported? = true

#json_aggregate_name(kind) ⇒ Object

json_arrayagg / json_objectagg gather rows into a document. The standard names are JSON_ARRAYAGG and JSON_OBJECTAGG; SQLite and PostgreSQL have their own and override.



253
254
255
# File 'lib/active_record/refined/dialect.rb', line 253

def json_aggregate_name(kind)
  kind == :arrayagg ? "JSON_ARRAYAGG" : "JSON_OBJECTAGG"
end

#json_argument(value, _model) ⇒ Object

A Ruby document or boolean written where one of the JSON functions wants JSON. The standard marks the literal with JSON_EXTRACT($); SQLite has json() and Oracle FORMAT JSON, and both override.



213
214
215
216
# File 'lib/active_record/refined/dialect.rb', line 213

def json_argument(value, _model)
  json = Arel::Nodes.build_quoted(JSON.generate(value))
  Arel::Nodes::NamedFunction.new("JSON_EXTRACT", [json, Arel::Nodes.build_quoted("$")])
end

#json_build(kind, keys, args, _model) ⇒ Object

json_array / json_object built in the row. The standard says JSON_ARRAY and JSON_OBJECT with the values (and keys alternating); PostgreSQL has the jsonb_build_* pair and Oracle a keyword syntax, and both override.



239
240
241
242
# File 'lib/active_record/refined/dialect.rb', line 239

def json_build(kind, keys, args, _model)
  Arel::Nodes::NamedFunction.new(
    kind == :array ? "JSON_ARRAY" : "JSON_OBJECT", json_build_body(kind, keys, args))
end

#json_build_argument(value, model) ⇒ Object

A document or boolean built into json_array/json_object. The standard marks it JSON as bury does; PostgreSQL casts to jsonb and overrides.



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

def json_build_argument(value, model)
  json_argument(value, model)
end

#json_contains(_document, _json, model) ⇒ Object

Raises:

  • (NotImplementedError)


190
191
192
193
# File 'lib/active_record/refined/dialect.rb', line 190

def json_contains(_document, _json, model)
  raise NotImplementedError,
    "contains? has no equivalent on #{model.connection_db_config.adapter}"
end

#json_document_value?(value) ⇒ Boolean

--- Writing JSON. A Ruby document or boolean has to be told apart from a bare scalar, and embedded as the JSON it spells.

Returns:

  • (Boolean)


206
207
208
# File 'lib/active_record/refined/dialect.rb', line 206

def json_document_value?(value)
  value.is_a?(::Hash) || value.is_a?(::Array) || value == true || value == false
end

#json_has_key(document, _name, path, _model) ⇒ Object



195
196
197
# File 'lib/active_record/refined/dialect.rb', line 195

def json_has_key(document, _name, path, _model)
  Arel::Nodes::NamedFunction.new("json_type", [document, path]).not_eq(nil)
end

#json_keys(document, _model) ⇒ Object



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

def json_keys(document, _model)
  Arel::Nodes::NamedFunction.new("JSON_KEYS", [document])
end

#json_list_by_element?Boolean

A JSON value in an IN list or a range is compared per element on the MySQL family, which overrides; the standard leaves it to the IN.

Returns:

  • (Boolean)


284
# File 'lib/active_record/refined/dialect.rb', line 284

def json_list_by_element? = false

#json_literal(_json, model) ⇒ Object

A Ruby value on the JSON side of a comparison belongs to a JSON type; a family without one refuses it.

Raises:

  • (NotImplementedError)


184
185
186
187
188
# File 'lib/active_record/refined/dialect.rb', line 184

def json_literal(_json, model)
  raise NotImplementedError,
    "a JSON comparison has no equivalent on " \
    "#{model.connection_db_config.adapter}; dig_text gives the value"
end

#json_path(document, dollar_path, _steps, json_value, _model) ⇒ Object

dig / dig_text. The standard is SQLite's -> and ->>, whose ->> keeps the value's type, so dig_text casts to text for a portable comparison.



174
175
176
177
178
179
180
# File 'lib/active_record/refined/dialect.rb', line 174

def json_path(document, dollar_path, _steps, json_value, _model)
  extracted = Arel::Nodes::InfixOperation.new(
    json_value ? :"->" : :"->>", document, Arel::Nodes.build_quoted(dollar_path))
  return extracted if json_value
  Arel::Nodes::NamedFunction.new(
    "CAST", [Arel::Nodes::As.new(extracted, Arel::Nodes::SqlLiteral.new("text"))])
end

#json_remove(document, dollar_paths, _steps, _model) ⇒ Object

except: removing keys. The standard removes a path apiece with JSON_REMOVE; PostgreSQL subtracts an array of keys and Oracle removes through JSON_TRANSFORM, and both override.



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

def json_remove(document, dollar_paths, _steps, _model)
  Arel::Nodes::NamedFunction.new(
    "JSON_REMOVE",
    [document, *dollar_paths.map { |path| Arel::Nodes.build_quoted(path) }])
end

#json_set(document, _steps, dollar_path, value, expression, model) ⇒ Object

bury: setting a value at a path. The standard is JSON_SET; PostgreSQL has jsonb_set and Oracle JSON_TRANSFORM, and both override.



220
221
222
223
224
225
# File 'lib/active_record/refined/dialect.rb', line 220

def json_set(document, _steps, dollar_path, value, expression, model)
  Arel::Nodes::NamedFunction.new(
    "JSON_SET",
    [document, Arel::Nodes.build_quoted(dollar_path),
     json_set_value(value, expression, model)])
end

#quantifiers_supported?Boolean

Returns:

  • (Boolean)


101
# File 'lib/active_record/refined/dialect.rb', line 101

def quantifiers_supported? = true

#string_agg(operand, separator, orders, _string, model) ⇒ Object

string_agg: the strings of a group joined into one. The standard's is LISTAGG(x, ', ') WITHIN GROUP (ORDER BY ...), which Oracle reads, and the ORDER BY is not optional there: an aggregate asked for no order is given the values' own, which costs the caller nothing to have. PostgreSQL and SQLite carry the ORDER BY inside the call, SQL Server takes the WITHIN GROUP only when there is an order, and the MySQL family has GROUP_CONCAT; every one of them overrides. string says the operand is a column declared one, which PostgreSQL alone asks.



273
274
275
276
277
# File 'lib/active_record/refined/dialect.rb', line 273

def string_agg(operand, separator, orders, _string, model)
  orders = [operand] if orders.empty?
  Arel.sql("LISTAGG(#{compile(operand, model)}, #{quote(separator, model)}) " \
           "WITHIN GROUP (ORDER BY #{compile_list(orders, model)})")
end

#truth_value(operand, value, negated, _model) ⇒ Object

true? / false? and their negations. The standard spells them with the boolean IS [NOT] TRUE/FALSE, which keeps a NULL out of the plain form and in of the negation; a family without a boolean type overrides.



130
131
132
133
# File 'lib/active_record/refined/dialect.rb', line 130

def truth_value(operand, value, negated, _model)
  literal = value ? Arel::Nodes::True.new : Arel::Nodes::False.new
  Arel::Nodes::InfixOperation.new(negated ? "IS NOT" : "IS", operand, literal)
end