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,
lib/active_record/refined/dialect/mysqlish_json_functions.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. Where no spelling is anyone's standard -- most of JSON -- the base refuses instead, and every family carries its own.

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

Modules: MysqlishJsonFunctions 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 the names most families share and nils the ones that are one or two families' own: printf FORMAT, whose name MySQL hands to a different function entirely, RAND, DATE_TRUNC, NOW, LOG2 and the three bit aggregates. A family that has one of them says so in a table of its own, since the tables shadow rather than merge.

{
  format: nil, rand: nil, date_trunc: nil, now: nil, log2: nil,
  bit_and: nil, bit_or: nil, bit_xor: nil,
}.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.



60
61
62
63
# File 'lib/active_record/refined/dialect.rb', line 60

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.



48
49
50
51
52
53
# File 'lib/active_record/refined/dialect.rb', line 48

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.



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

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

#array_comparisons_supported?Boolean

The array comparisons -- @>, <@ and && against an array column. The type and its operators are PostgreSQL's alone.

Returns:

  • (Boolean)


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

def array_comparisons_supported? = false

#bit_count(_expr, model) ⇒ Object

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

Raises:

  • (NotImplementedError)


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

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

#bitwise_operators_supported?Boolean

The bitwise operators, & | ^ << >> and ~. No SQL standard has them, so unlike the capabilities above the base refuses and each family that has the operators says so itself -- which of the seven is every one but Oracle, whose single bit operation is the BITAND function.

Returns:

  • (Boolean)


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

def bitwise_operators_supported? = false

#bitwise_xor(left, right) ⇒ Object

XOR, which no two families spell alike. The default is the two operations it is made of, naming each operand twice -- built only from the & and | a family has just claimed through #bitwise_operators_supported?, so wherever it can be reached at all it works. Of the five that claim them, SQLite alone keeps it; the rest have an operator of their own and override.



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

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.



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

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.



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

def check_lateral(_model); end

#check_string_aggregate_window(_model) ⇒ Object

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



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

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.



181
182
183
184
185
# File 'lib/active_record/refined/dialect.rb', line 181

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

Whether the datetime value functions take a precision, current_timestamp(3). SQLite and SQL Server take none.

Returns:

  • (Boolean)


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

def datetime_precision_supported? = true

#excluded(_column, model) ⇒ Object

The row an upsert could not insert. excluded is PostgreSQL's name for it, which SQLite took over and the MySQL family spells VALUES(column), so each of the three carries its own; the families without an upsert have nothing for the name to stand in.

Raises:

  • (NotImplementedError)


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

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

#extract_supported?Boolean

Whether the family has EXTRACT. SQLite spells the fields as strftime formats and SQL Server as DATEPART, so neither answers to the name.

Returns:

  • (Boolean)


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

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)


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

def filter_supported? = true

#full_outer_join_supported?Boolean

Whether a FULL OUTER JOIN can be written. The MySQL family has none.

Returns:

  • (Boolean)


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

def full_outer_join_supported? = true

#function_name(name, model) ⇒ String

The name this family spells a function with, asked for as the block builds the call; FUNCTIONS is where the answer is looked up.

Parameters:

  • name (Symbol)

    the method's own name, as BlockContext has it

Returns:

  • (String)

Raises:

  • (NotImplementedError)

    where the family has no equivalent



108
109
110
111
112
113
114
# File 'lib/active_record/refined/dialect.rb', line 108

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)


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

def grouping_by_with_rollup? = false

#grouping_supported?(_kind) ⇒ Boolean

Whether the family has the kind of grouping asked for, one of :grouping_sets, :rollup and :cube.

Returns:

  • (Boolean)


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

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)


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

def json_aggregate_filter_supported? = true

#json_aggregate_name(kind) ⇒ Object

json_arrayagg / json_objectagg gather rows into a document. The names are SQL:2016's own, which the MySQL family and Oracle answer to, so unlike the rest of JSON the base keeps them; SQLite and PostgreSQL have names of their own, and SQL Server, which has no JSON aggregates, refuses.



324
325
326
# File 'lib/active_record/refined/dialect.rb', line 324

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. Every family marks the literal its own way -- JSON_EXTRACT($) on MySQL, json() on SQLite, FORMAT JSON on Oracle, JSON_QUERY on SQL Server -- and none of the marks is another's.

Raises:

  • (NotImplementedError)


279
280
281
282
283
# File 'lib/active_record/refined/dialect.rb', line 279

def json_argument(_value, model)
  raise NotImplementedError,
    "a document written as JSON has no equivalent on " \
    "#{model.connection_db_config.adapter}"
end

#json_build(kind, _keys, _args, model) ⇒ Object

json_array / json_object built in the row. JSON_ARRAY(a, b) is SQL:2016, but JSON_OBJECT is where the syntaxes part -- the standard pairs each key as KEY k VALUE v, MysqlishJsonFunctions alternates them, SQL Server writes k : v -- so the pair travels together and each family says its own; SQL Server's is not written here yet.

Raises:

  • (NotImplementedError)


307
308
309
310
# File 'lib/active_record/refined/dialect.rb', line 307

def json_build(kind, _keys, _args, model)
  raise NotImplementedError,
    "json_#{kind} has no equivalent on #{model.connection_db_config.adapter}"
end

#json_build_argument(value, model) ⇒ Object

A document or boolean built into json_array/json_object. The default rides through #json_argument, and refuses or serves with it; PostgreSQL casts to jsonb and overrides.



315
316
317
# File 'lib/active_record/refined/dialect.rb', line 315

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

#json_contains(_document, _json, model) ⇒ Object

contains?: whether the document holds what is given. The standard has no equivalent; PostgreSQL has @> and the MySQL family JSON_CONTAINS, and both override.

Raises:

  • (NotImplementedError)


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

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

#json_document_value?(value) ⇒ Boolean

The test the writing hooks share: a Hash, an Array or a boolean is a document, and anything else a bare scalar.

Returns:

  • (Boolean)


271
272
273
# File 'lib/active_record/refined/dialect.rb', line 271

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

key?: whether the object has the key. SQL:2016 spells it JSON_EXISTS, which of the five only Oracle answers to; PostgreSQL has the ? operator, the MySQL family JSON_CONTAINS_PATH, SQLite json_type at the path and SQL Server JSON_PATH_EXISTS, so every family overrides.

The key arrives spelled both ways -- bare in name, as a $ path in path -- since a family reads it as one or as the other.

Raises:

  • (NotImplementedError)


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

def json_has_key(_document, _name, _path, model)
  raise NotImplementedError,
    "key? has no equivalent on #{model.connection_db_config.adapter}"
end

#json_keys(_document, model) ⇒ Object

keys: the keys of an object as a JSON array. JSON_KEYS is the MySQL family's own; SQLite and PostgreSQL gather theirs through a subquery over their key-listing functions, and Oracle and SQL Server would reach them only through table unnests not written here.

Raises:

  • (NotImplementedError)


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

def json_keys(_document, model)
  raise NotImplementedError,
    "keys has no equivalent on #{model.connection_db_config.adapter}"
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)


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

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)


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

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. Nothing reaches every family: SQLite and PostgreSQL have operators of their own, MySQL its functions, and the two that spell it as SQL:2016's JSON_VALUE and JSON_QUERY -- Oracle and SQL Server -- differ over what a scalar leaf comes back as. Every family overrides.

Raises:

  • (NotImplementedError)


223
224
225
226
# File 'lib/active_record/refined/dialect.rb', line 223

def json_path(_document, _dollar_path, _steps, _json_value, model)
  raise NotImplementedError,
    "dig has no equivalent on #{model.connection_db_config.adapter}"
end

#json_remove(_document, _dollar_paths, _steps, model) ⇒ Object

except: removing keys, for which the standard likewise has nothing. MysqlishJsonFunctions removes a path apiece with JSON_REMOVE, PostgreSQL subtracts an array of keys, Oracle and SQL Server edit through JSON_TRANSFORM and JSON_MODIFY.

Raises:

  • (NotImplementedError)


297
298
299
300
# File 'lib/active_record/refined/dialect.rb', line 297

def json_remove(_document, _dollar_paths, _steps, model)
  raise NotImplementedError,
    "except has no equivalent on #{model.connection_db_config.adapter}"
end

#json_set(_document, _steps, _dollar_path, _value, _expression, model) ⇒ Object

bury: setting a value at a path. The standard has no editing functions at all: JSON_SET is MysqlishJsonFunctions', jsonb_set PostgreSQL's, JSON_TRANSFORM Oracle's and JSON_MODIFY SQL Server's.

Raises:

  • (NotImplementedError)


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

def json_set(_document, _steps, _dollar_path, _value, _expression, model)
  raise NotImplementedError,
    "bury has no equivalent on #{model.connection_db_config.adapter}"
end

#quantifiers_supported?Boolean

Whether a comparison can be quantified with ANY or ALL. SQLite has neither.

Returns:

  • (Boolean)


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

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.



344
345
346
347
348
# File 'lib/active_record/refined/dialect.rb', line 344

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.



170
171
172
173
# File 'lib/active_record/refined/dialect.rb', line 170

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