class ROM::SQL::Attribute

Extended schema attributes tailored for SQL databases

@api public

Constants

META_KEYS
NONSTANDARD_EQUALITY_VALUES
OPERATORS
QualifyError

Error raised when an attribute cannot be qualified

Public Class Methods

[](type, options = EMPTY_HASH) click to toggle source

@api private

# File lib/rom/sql/attribute.rb, line 33
def [](type, options = EMPTY_HASH)
  fetch_or_store([type, options]) { new(type, **options) }
end

Public Instance Methods

!() click to toggle source

Negate the attribute's sql expression

@example

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

@return [Attribute]

@api public

# File lib/rom/sql/attribute.rb, line 224
def !
  ~self
end
=~(other) click to toggle source

Return a new attribute with an equality expression

@example

users.where { email =~ 1 }

@return [Attribute]

@api public

# File lib/rom/sql/attribute.rb, line 198
def =~(other)
  meta(sql_expr: sql_expr =~ binary_operation_arg(other))
end
canonical() click to toggle source

Return a new attribute in its canonical form

@api public

# 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) click to toggle source

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.

@example

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

@param [Hash] mapping mapping between SQL expressions @return [SQL::Attribute]

@api public

# 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 = ' ') click to toggle source

Create a CONCAT function from the attribute

@example with default separator (' ')

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

@example with custom separator

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

@param [SQL::Attribute] other

@return [SQL::Function]

@api public

# File lib/rom/sql/attribute.rb, line 283
def concat(other, sep = ' ')
  Function.new(type).concat(self, sep, other)
end
foreign_key() click to toggle source

Return a new attribute marked as a FK

@return [SQL::Attribute]

@api public

# File lib/rom/sql/attribute.rb, line 141
def foreign_key
  meta(foreign_key: true)
end
func(&block) click to toggle source

Create a function DSL from the attribute

@example

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

@return [SQL::Function]

@api public

# File lib/rom/sql/attribute.rb, line 266
def func(&block)
  ProjectionDSL.new(name => self).call(&block).first
end
in(*args) click to toggle source

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.

@example

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))

@param [Array<Object>] args A range or a list of values for an inclusion check

@api public

# 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
indexed() click to toggle source

Returns a new attribute marked as indexed

@api public

# File lib/rom/sql/attribute.rb, line 322
def indexed
  meta(index: true)
end
indexed?() click to toggle source

@api public

# File lib/rom/sql/attribute.rb, line 315
def indexed?
  meta[:index].equal?(true)
end
is(other) click to toggle source

Return a boolean expression with an equality operator

@example

users.where { id.is(1) }

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

@param [Object] other Any SQL-compatible object type

@api public

# File lib/rom/sql/attribute.rb, line 186
def is(other)
  self =~ other
end
joined() click to toggle source

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

@return [SQL::Attribute] Original attribute marked as joined

@api public

# File lib/rom/sql/attribute.rb, line 93
def joined
  meta(joined: true)
end
joined?() click to toggle source

Return if an attribute was used in a join

@example

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

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

@return [Boolean]

@api public

# File lib/rom/sql/attribute.rb, line 108
def joined?
  meta[:joined].equal?(true)
end
meta_options_ast() click to toggle source

@api private

Calls superclass method
# File lib/rom/sql/attribute.rb, line 327
def meta_options_ast
  meta = super
  meta[:index] = true if indexed?
  meta
end
not(other) click to toggle source

Return a boolean expression with a negated equality operator

@example

users.where { id.not(1) }

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

@param [Object] other Any SQL-compatible object type

@api public

# File lib/rom/sql/attribute.rb, line 212
def not(other)
  !is(other)
end
qualifiable?() click to toggle source

Return if an attribute is qualifiable

@return [Boolean]

@api public

# File lib/rom/sql/attribute.rb, line 132
def qualifiable?
  !source.nil?
end
qualified(table_alias = nil) click to toggle source

Return a new attribute marked as qualified

@example

users[:id].aliased(:user_id)

@return [SQL::Attribute]

@api public

# 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?() click to toggle source

Return if an attribute type is qualified

@example

id = users[:id].qualify

id.qualified?
# => true

@return [Boolean]

@api public

# 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) click to toggle source

Return a new attribute that is aliased and marked as qualified

Intended to be used when passing attributes to `dataset#select`

@return [SQL::Attribute]

@api public

# 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
sql_literal(ds) click to toggle source

Sequel calls this method to coerce an attribute into SQL string

@param [Sequel::Dataset] ds

@api private

# File lib/rom/sql/attribute.rb, line 292
def sql_literal(ds)
  ds.literal(sql_expr)
end
to_sql_name() click to toggle source

Sequel column representation

@return [Sequel::SQL::AliasedExpression,Sequel::SQL::Identifier]

@api private

# 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_sym() click to toggle source

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

@example

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

@return [Symbol]

@api public

# 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
unwrap() click to toggle source

Removes metadata from the type

@api private

# 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) click to toggle source

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

@param [Object] value any SQL-serializable value @return [SQL::Attribute]

@api public

# File lib/rom/sql/attribute.rb, line 350
def value(value)
  meta(sql_expr: Sequel[value])
end

Private Instance Methods

__cmp__(op, other) click to toggle source

A simple wrapper for the boolean expression constructor where the left part is the attribute value

@api private

# File lib/rom/sql/attribute.rb, line 405
def __cmp__(op, other)
  Sequel::SQL::BooleanExpression.new(op, self, binary_operation_arg(other))
end
binary_operation_arg(value) click to toggle source

Preprocess input value for binary operations

@api private

# File lib/rom/sql/attribute.rb, line 412
def binary_operation_arg(value)
  case value
  when Sequel::SQL::Expression
    value
  else
    type[value]
  end
end
extensions() click to toggle source

@api private

# File lib/rom/sql/attribute.rb, line 433
def extensions
  TypeExtensions[type]
end
method_missing(meth, *args, &block) click to toggle source

Delegate to sql expression if it responds to a given method

@api private

Calls superclass method
# 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
sql_expr() click to toggle source

Return Sequel Expression object for an attribute

@api private

# File lib/rom/sql/attribute.rb, line 382
def sql_expr
  @sql_expr ||= (meta[:sql_expr] || to_sql_name)
end
table_name() click to toggle source

Return source table name or its alias

@api private

# File lib/rom/sql/attribute.rb, line 424
def table_name
  if qualified? && meta[:qualified].is_a?(Symbol)
    meta[:qualified]
  else
    source.dataset
  end
end