Class: ROM::SQL::Attribute

Inherits:
Attribute
  • Object
show all
Extended by:
Dry::Core::Cache
Includes:
AttributeAliasing, AttributeWrapping
Defined in:
lib/rom/sql/attribute.rb,
lib/rom/sql/extensions/postgres/types/json.rb,
lib/rom/sql/extensions/postgres/types/array.rb,
lib/rom/sql/extensions/postgres/types/ltree.rb

Overview

Extended schema attributes tailored for SQL databases

Constant Summary collapse

OPERATORS =
%i[>= <= > <].freeze
NONSTANDARD_EQUALITY_VALUES =
[true, false, nil].freeze
META_KEYS =
%i[index foreign_key target sql_expr qualified].freeze
QualifyError =

Error raised when an attribute cannot be qualified

Class.new(StandardError)

Class Method Summary collapse

Instance Method Summary collapse

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(meth, *args, &block) ⇒ Object (private)

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Delegate to sql expression if it responds to a given method


389
390
391
392
393
394
395
396
397
398
399
# File 'lib/rom/sql/attribute.rb', line 389

def method_missing(meth, *args, &block)
  if OPERATORS.include?(meth)
    __cmp__(meth, args[0])
  elsif extensions.key?(meth)
    extensions[meth].(type, sql_expr, *args, &block)
  elsif sql_expr.respond_to?(meth)
    meta(sql_expr: sql_expr.__send__(meth, *args, &block))
  else
    super
  end
end

Class Method Details

.[](type, options = EMPTY_HASH) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.


33
34
35
# File 'lib/rom/sql/attribute.rb', line 33

def [](type, options = EMPTY_HASH)
  fetch_or_store([type, options]) { new(type, **options) }
end

Instance Method Details

#!Attribute

Negate the attribute's sql expression

Examples:

users.where(!users[:id].is(1))

Returns:


224
225
226
# File 'lib/rom/sql/attribute.rb', line 224

def !
  ~self
end

#+(value) ⇒ SQL::Attribute<Types::LTree>

Concatenate two LTree values Translates to ||

Examples:

people.select { (ltree_tags + ROM::Types::Values::TreePath.new('Moscu')).as(:ltree_tags) }.where { name.is('Jade Doe') }
people.select { (ltree_tags + 'Moscu').as(:ltree_tags) }.where { name.is('Jade Doe') }

Parameters:

  • keys (LTree, String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 113

#=~(other) ⇒ Attribute

Return a new attribute with an equality expression

Examples:

users.where { email =~ 1 }

Returns:


198
199
200
# File 'lib/rom/sql/attribute.rb', line 198

def =~(other)
  meta(sql_expr: sql_expr =~ binary_operation_arg(other))
end

#aliased(alias_name) ⇒ SQL::Attribute Also known as: as Originally defined in module AttributeAliasing

Return a new attribute with an alias

Examples:

users[:id].aliased(:user_id)

Returns:

#aliased_projection?TrueClass, FalseClass Originally defined in module AttributeAliasing

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Return true if this attribute is an aliased projection

Examples:

class Tasks < ROM::Relation[:memory]
  schema do
    attribute :user_id, Types::Integer, alias: :id
    attribute :name, Types::String
  end
end

Users.schema[:user_id].aliased?
# => true
Users.schema[:user_id].aliased_projection?
# => false

Users.schema[:user_id].qualified_projection.aliased?
# => true
Users.schema[:user_id].qualified_projection.aliased_projection?
# => true

Returns:

  • (TrueClass, FalseClass)

#any(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the array includes a value Translates to the ANY operator

Parameters:

  • value (Object)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/array.rb', line 21

#ascendant(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree is a ascendant of the LTree value Translates to the @> operator

Examples:

people.select(:name).where { ltree_tags.ascendant('Bottom.Cities') }

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 83

#canonicalObject

Return a new attribute in its canonical form


41
42
43
44
45
46
47
# File 'lib/rom/sql/attribute.rb', line 41

def canonical
  if aliased?
    with(alias: nil).meta(sql_expr: nil)
  else
    self
  end
end

#case(mapping) ⇒ SQL::Attribute

Build a case expression based on attribute. See SQL::Function#case when you don't have a specific expression after the CASE keyword. Pass the :else keyword to provide the catch-all case, it's mandatory because of the Sequel's API used underneath.

Examples:

users.select_append { id.case(1 => `'first'`, else: `'other'`).as(:first_or_not) }

Parameters:

  • mapping (Hash)

    mapping between SQL expressions

Returns:


366
367
368
369
370
371
372
373
374
375
# File 'lib/rom/sql/attribute.rb', line 366

def case(mapping)
  mapping = mapping.dup
  otherwise = mapping.delete(:else) do
    raise ArgumentError, 'provide the default case using the :else keyword'
  end

  type = mapping.values[0].type

  Attribute[type].meta(sql_expr: ::Sequel.case(mapping, otherwise, self))
end

#concat(other, sep = ' ') ⇒ SQL::Function

Create a CONCAT function from the attribute

Examples:

with default separator (' ')

users[:id].concat(users[:name])

with custom separator

users[:id].concat(users[:name], '-')

Parameters:

Returns:


283
284
285
# File 'lib/rom/sql/attribute.rb', line 283

def concat(other, sep = ' ')
  Function.new(type).concat(self, sep, other)
end

#contain(other) ⇒ SQL::Attribute<Types::Bool>

Check whether the array includes another array Translates to the @> operator

Parameters:

  • other (Array)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 2

#contain_ancestor(value) ⇒ SQL::Attribute<Types::PG::Bool>

Does LTree array contain an ancestor of ltree Translates to @>

Examples:

people.select(:name).where { parents_tags.contain_ancestor('Top.Building.EmpireState.381')}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 123

#contain_any_ltextquery(value) ⇒ SQL::Attribute<Types::Bool>

Does LTree array contain any path matching ltxtquery Translates to @

Examples:

people.select(:name).where { parents_tags.contain_any_ltextquery('Parks')}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 110

#contain_ascendant(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree is a ascendant of the LTree values Translates to the @> operator

Examples:

people.select(:name).where { ltree_tags.contain_ascendant(['Bottom.Cities']) }
people.select(:name).where { ltree_tags.contain_ascendant('Bottom.Cities, Bottom.Parks') }

Parameters:

  • value (Array<String>, String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 69

#contain_descendant(value) ⇒ SQL::Attribute<Types::PG::Bool>

Does LTree array contain an descendant of ltree Translates to <@

Examples:

people.select(:name).where { parents_tags.contain_descendant('Top.Building.EmpireState.381')}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 42

#contained_by(other) ⇒ SQL::Attribute<Types::Bool>

Check whether the array is contained by another array Translates to the <@ operator

Parameters:

  • other (Array)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 17

#delete(*path) ⇒ SQL::Attribute<Types::PG::JSONB>

Deletes the specified value by key, index, or path Translates to - or #- depending on the number of arguments

Examples:

people.select { data.delete('age').as(:data_without_age) }
people.select { fields.delete(0).as(:fields_without_first) }
people.select { fields.delete(-1).as(:fields_without_last) }
people.select { data.delete('deeply', 'nested', 'value').as(:data) }
people.select { fields.delete('0', 'name').as(:data) }

Parameters:

  • path (Array<String>)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 118

#descendant(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree is a descendant of the LTree value Translates to the <@ operator

Examples:

people.select(:name).where { ltree_tags.descendant('Bottom.Cities') }

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 56

#find_ancestor(value) ⇒ SQL::Attribute<Types::PG::LTree>

Return first LTree array entry that is an ancestor of ltree, NULL if none Translates to [email protected]>

Examples:

people.select(:name).where { parents_tags.find_ancestor('Left.Parks').not(nil)}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 149

#find_descendant(value) ⇒ SQL::Attribute<Types::PG::LTree>

Return first LTree array entry that is an descendant of ltree, NULL if none Translates to ?<@

Examples:

people.select(:name).where { parents_tags.find_descendant('Left.Parks').not(nil)}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 162

#foreign_keySQL::Attribute

Return a new attribute marked as a FK

Returns:


141
142
143
# File 'lib/rom/sql/attribute.rb', line 141

def foreign_key
  meta(foreign_key: true)
end

#func(&block) ⇒ SQL::Function

Create a function DSL from the attribute

Examples:

users[:id].func { integer::count(id).as(:count) }

Returns:


266
267
268
# File 'lib/rom/sql/attribute.rb', line 266

def func(&block)
  ProjectionDSL.new(name => self).call(&block).first
end

#get(idx) ⇒ SQL::Attribute

Get element by index (PG uses 1-based indexing)

Parameters:

  • idx (Integer)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 30

#get_text(*path) ⇒ SQL::Attribute<Types::String>

Extract the JSON value as text using at the specified path Translates to ->> or #>> depending on the number of arguments

Examples:

people.select { data.get('age').as(:person_age) }
people.select { fields.get(0).as(:first_field) }
people.select { fields.get('0', 'value').as(:first_field_value) }

Parameters:

  • path (Array<Integer>, Array<String>)

    Path to extract

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 45

#has_all_keys(*keys) ⇒ SQL::Attribute<Types::Bool>

Does the JSON value have all the specified top-level keys Translates to ?&

Examples:

people.where { data.has_all_keys('age', 'height') }

Parameters:

  • keys (Array<String>)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 86

#has_any_key(*keys) ⇒ SQL::Attribute<Types::Bool>

Does the JSON value have any of the specified top-level keys Translates to ?|

Examples:

people.where { data.has_any_key('age', 'height') }

Parameters:

  • keys (Array<String>)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 73

#has_key(key) ⇒ SQL::Attribute<Types::Bool>

Does the JSON value have the specified top-level key Translates to ?

Examples:

people.where { data.has_key('age') }

Parameters:

  • key (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 60

#in(*args) ⇒ Object

Return a boolean expression with an inclusion test

If the single argument passed to the method is a Range object then the resulting expression will restrict the attribute value with range's bounds. Upper bound condition will be inclusive/non-inclusive depending on the range type.

If more than one argument is passed to the method or the first argument is not Range then the result will be a simple IN check.

Examples:

users.where { id.in(1..100) | created_at(((Time.now - 86400)..Time.now)) }
users.where { id.in(1, 2, 3) }
users.where(users[:id].in(1, 2, 3))

Parameters:

  • args (Array<Object>)

    A range or a list of values for an inclusion check


246
247
248
249
250
251
252
253
254
255
256
# File 'lib/rom/sql/attribute.rb', line 246

def in(*args)
  if args.first.is_a?(Range)
    range = args.first
    lower_cond = __cmp__(:>=, range.begin)
    upper_cond = __cmp__(range.exclude_end? ? :< : :<=, range.end)

    Sequel::SQL::BooleanExpression.new(:AND, lower_cond, upper_cond)
  else
    __cmp__(:IN, args)
  end
end

#indexedObject

Returns a new attribute marked as indexed


322
323
324
# File 'lib/rom/sql/attribute.rb', line 322

def indexed
  meta(index: true)
end

#indexed?Boolean

Returns:

  • (Boolean)

315
316
317
# File 'lib/rom/sql/attribute.rb', line 315

def indexed?
  meta[:index].equal?(true)
end

#is(other) ⇒ Object

Return a boolean expression with an equality operator

Examples:

users.where { id.is(1) }

users.where(users[:id].is(1))

Parameters:

  • other (Object)

    Any SQL-compatible object type


186
187
188
# File 'lib/rom/sql/attribute.rb', line 186

def is(other)
  self =~ other
end

#join(delimiter, null_repr) ⇒ SQL::Attribute<Types::String>

Convert the array to a string by joining values with a delimiter (empty stirng by default) and optional filler for NULL values Translates to an array_to_string call

Parameters:

  • delimiter (Object)
  • null_repr (Object)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/array.rb', line 67

#joinedSQL::Attribute

Return a new attribute marked as joined

Whenever you join two schemas, the right schema's attribute will be marked as joined using this method

Returns:


93
94
95
# File 'lib/rom/sql/attribute.rb', line 93

def joined
  meta(joined: true)
end

#joined?Boolean

Return if an attribute was used in a join

Examples:

schema = users.schema.join(tasks.schema)

schema[:id, :tasks].joined?
# => true

Returns:

  • (Boolean)

108
109
110
# File 'lib/rom/sql/attribute.rb', line 108

def joined?
  meta[:joined].equal?(true)
end

#lengthSQL::Attribute<Types::Integer>

Return array size

Returns:


# File 'lib/rom/sql/extensions/postgres/types/array.rb', line 41

#match(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree match a lquery value Translates to the ~ operator

Examples:

people.select(:name).where { ltree_tags.match('Bottom.Cities') }

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 2

#match_any(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree match any of the lquery values Translates to the ? operator

Examples:

people.select(:name).where { ltree_tags.match_any(['Bottom', 'Bottom.Cities.*']) }
people.select(:name).where { ltree_tags.match_any('Bottom,Bottom.Cities.*') }

Parameters:

  • value (Array, String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 15

#match_any_lquery(value) ⇒ SQL::Attribute<Types::PG::LTree>

Return first LTree array entry that matches lquery, NULL if none Translates to ?~

Examples:

people.select(:name).where { parents_tags.match_any_lquery('Right.*').not(nil)}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 175

#match_any_ltextquery(value) ⇒ SQL::Attribute<Types::PG::LTree>

Return first LTree array entry that matches ltextquery, NULL if none Translates to [email protected]

Examples:

people.select(:name).where { parents_tags.match_any_ltextquery('EmpireState').not(nil)}

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 188

#match_ltextquery(value) ⇒ SQL::Attribute<Types::Bool>

Check whether the LTree match a ltextquery Translates to the @ operator

Examples:

people.select(:name).where { ltree_tags.match_ltextquery('Countries & Brasil') }

Parameters:

  • value (String)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/ltree.rb', line 29

#merge(value) ⇒ SQL::Attribute<Types::PG::JSONB>

Concatenate two JSON values Translates to ||

Examples:

people.select { data.merge(fetched_at: Time.now).as(:data) }
people.select { (fields + [name: 'height', value: 165]).as(:fields) }

Parameters:

  • value (Hash, Array)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/json.rb', line 99

#meta_options_astObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.


327
328
329
330
331
# File 'lib/rom/sql/attribute.rb', line 327

def meta_options_ast
  meta = super
  meta[:index] = true if indexed?
  meta
end

#not(other) ⇒ Object

Return a boolean expression with a negated equality operator

Examples:

users.where { id.not(1) }

users.where(users[:id].not(1))

Parameters:

  • other (Object)

    Any SQL-compatible object type


212
213
214
# File 'lib/rom/sql/attribute.rb', line 212

def not(other)
  !is(other)
end

#overlaps(other) ⇒ SQL::Attribute<Types::Bool>

Check whether the arrays have common values Translates to &&

Parameters:

  • other (Array)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/array.rb', line 48

#qualifiable?Boolean

Return if an attribute is qualifiable

Returns:

  • (Boolean)

132
133
134
# File 'lib/rom/sql/attribute.rb', line 132

def qualifiable?
  !source.nil?
end

#qualified(table_alias = nil) ⇒ SQL::Attribute

Return a new attribute marked as qualified

Examples:

users[:id].aliased(:user_id)

Returns:


57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/rom/sql/attribute.rb', line 57

def qualified(table_alias = nil)
  return self if qualified? && table_alias.nil?
  return meta(qualified: false) unless qualifiable?

  case sql_expr
  when Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier, Sequel::SQL::QualifiedIdentifier
    attr = meta(qualified: table_alias || true)
    attr.meta(sql_expr: attr.to_sql_name)
  else
    raise QualifyError, "can't qualify #{name.inspect} (#{sql_expr.inspect})"
  end
end

#qualified?Boolean

Return if an attribute type is qualified

Examples:

id = users[:id].qualify

id.qualified?
# => true

Returns:

  • (Boolean)

123
124
125
# File 'lib/rom/sql/attribute.rb', line 123

def qualified?
  meta[:qualified].equal?(true) || meta[:qualified].is_a?(Symbol)
end

#qualified_projection(table_alias = nil) ⇒ SQL::Attribute

Return a new attribute that is aliased and marked as qualified

Intended to be used when passing attributes to dataset#select

Returns:


77
78
79
80
81
82
83
# File 'lib/rom/sql/attribute.rb', line 77

def qualified_projection(table_alias = nil)
  if aliased?
    qualified(table_alias).aliased(self.alias)
  else
    qualified(table_alias)
  end
end

#remove_value(value) ⇒ SQL::Attribute<Types::PG::Array>

Remove elements by value

Parameters:

  • value (Object)

Returns:


# File 'lib/rom/sql/extensions/postgres/types/array.rb', line 58

#sql_literal(ds) ⇒ Object

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sequel calls this method to coerce an attribute into SQL string

Parameters:

  • ds (Sequel::Dataset)

292
293
294
# File 'lib/rom/sql/attribute.rb', line 292

def sql_literal(ds)
  ds.literal(sql_expr)
end

#to_sql_nameSequel::SQL::AliasedExpression, Sequel::SQL::Identifier

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Sequel column representation

Returns:

  • (Sequel::SQL::AliasedExpression, Sequel::SQL::Identifier)

301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/rom/sql/attribute.rb', line 301

def to_sql_name
  @_to_sql_name ||=
    if qualified? && aliased_projection?
      Sequel.qualify(table_name, name).as(self.alias)
    elsif qualified?
      Sequel.qualify(table_name, name)
    elsif aliased_projection?
      Sequel.as(name, self.alias)
    else
      Sequel[name]
    end
end

#to_symSymbol

Return symbol representation of an attribute

This uses convention from sequel where double underscore in the name is used for qualifying, and triple underscore means aliasing

Examples:

users[:id].qualified.to_sym
# => :users__id

users[:id].as(:user_id).to_sym
# => :id___user_id

users[:id].qualified.as(:user_id).to_sym
# => :users__id___user_id

Returns:

  • (Symbol)

163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/rom/sql/attribute.rb', line 163

def to_sym
  @_to_sym ||=
    if qualified? && aliased?
      :"#{table_name}__#{name}___#{meta[:alias]}"
    elsif qualified?
      :"#{table_name}__#{name}"
    elsif aliased?
      :"#{name}___#{meta[:alias]}"
    else
      name
    end
end

#unwrapObject

This method is part of a private API. You should avoid using this method if possible, as it may be removed or be changed in the future.

Removes metadata from the type


336
337
338
339
340
341
# File 'lib/rom/sql/attribute.rb', line 336

def unwrap
  cleaned_meta = meta.reject { |k, _| META_KEYS.include?(k) }
  type = optional? ? right : self.type

  self.class.new(type.with(meta: cleaned_meta), **options)
end

#value(value) ⇒ SQL::Attribute

Wrap a value with the type, it allows using attribute and type specific methods on literals and things like this

Parameters:

  • value (Object)

    any SQL-serializable value

Returns:


350
351
352
# File 'lib/rom/sql/attribute.rb', line 350

def value(value)
  meta(sql_expr: Sequel[value])
end

#wrapped(name = source.dataset) ⇒ Attribute Originally defined in module AttributeWrapping

Return attribute type wrapped for the specified relation name

Parameters:

  • name (Symbol) (defaults to: source.dataset)

    The name of the source relation (defaults to source.dataset)

Returns:

#wrapped?Boolean Originally defined in module AttributeWrapping

Return if the attribute type is from a wrapped relation

Wrapped attributes are used when two schemas from different relations are merged together. This way we can identify them easily and handle correctly in places like auto-mapping.

Returns:

  • (Boolean)