# frozen-string-literal: true
module Sequel
# The Sequel::BasicObject class is just like the
# default +BasicObject+ class, except that missing constants are resolved in
# +Object+. This allows the virtual row support to work with classes
# without prefixing them with ::, such as:
#
# DB[:bonds].where{maturity_date > Time.now}
class BasicObject < ::BasicObject
# Lookup missing constants in ::Object
def self.const_missing(name)
::Object.const_get(name)
end
end
class LiteralString < ::String
end
# Time subclass that gets literalized with only the time value, so it operates
# like a standard SQL time type. This type does not support timezones, by design,
# so it will not work correctly with time with time zone types.
class SQLTime < ::Time
@date = nil
class << self
# Set the date used for SQLTime instances.
attr_writer :date
# Use the date explicitly set, or the current date if there is not a
# date set.
def date
@date || now
end
# Set the correct date and timezone when parsing times.
def parse(*)
t = super
utc = Sequel.application_timezone == :utc
d = @date
if d || utc
meth = utc ? :utc : :local
d ||= t
t = public_send(meth, d.year, d.month, d.day, t.hour, t.min, t.sec, t.usec)
end
t
end
# Create a new SQLTime instance given an hour, minute, second, and usec.
def create(hour, minute, second, usec = 0)
t = date
meth = Sequel.application_timezone == :utc ? :utc : :local
public_send(meth, t.year, t.month, t.day, hour, minute, second, usec)
end
end
# Show that this is an SQLTime, and the time represented
def inspect
"#<#{self.class} #{to_s}>"
end
# Return a string in HH:MM:SS format representing the time.
def to_s(*args)
if args.empty?
strftime('%H:%M:%S')
else
# Superclass may have defined a method that takes a format string,
# and we shouldn't override in that case.
super
end
end
end
# The SQL module holds classes whose instances represent SQL fragments.
# It also holds modules that are used by these classes.
module SQL
# Base class for all SQL expression objects.
class Expression
@comparison_attrs = [].freeze
class << self
# All attributes used for equality and hash methods.
attr_reader :comparison_attrs
# Expression objects are assumed to be value objects, where their
# attribute values can't change after assignment. In order to make
# it easy to define equality and hash methods, subclass
# instances assume that the only values that affect the results of
# such methods are the values of the object's attributes.
def attr_reader(*args)
super
comparison_attrs.concat(args)
end
# Copy the comparison_attrs into the subclass.
def inherited(subclass)
super
subclass.instance_variable_set(:@comparison_attrs, comparison_attrs.dup)
end
private
# Create a to_s instance method that takes a dataset, and calls
# the method provided on the dataset with args as the argument (self by default).
# Used to DRY up some code.
#
# Do not call this method with untrusted input, as that can result in
# arbitrary code execution.
def to_s_method(meth, args=:self) # :nodoc:
class_eval("def to_s_append(ds, sql) ds.#{meth}_append(sql, #{args}) end", __FILE__, __LINE__)
@comparison_attrs.freeze
end
end
# Make clone/dup return self, since Expression objects are supposed to
# be frozen value objects
def clone
self
end
alias dup clone
# Alias of eql?
def ==(other)
eql?(other)
end
# Returns true if the receiver is the same expression as the
# the +other+ expression.
def eql?(other)
other.is_a?(self.class) && !self.class.comparison_attrs.find{|a| public_send(a) != other.public_send(a)}
end
# Make sure that the hash value is the same if the attributes are the same.
def hash
([self.class] + self.class.comparison_attrs.map{|x| public_send(x)}).hash
end
# Show the class name and instance variables for the object.
def inspect
"#<#{self.class} #{instance_variables.map{|iv| "#{iv}=>#{instance_variable_get(iv).inspect}"}.join(', ')}>"
end
end
# Represents a SQL expression, with a given operator and one
# or more attributes (which may also be ComplexExpressions, forming
# a tree). This class is the backbone of Sequel's ruby expression DSL.
#
# This is an abstract class that is not that useful by itself. The
# subclasses +BooleanExpression+, +NumericExpression+, and +StringExpression+
# define the behavior of the DSL via operators.
class ComplexExpression < Expression
# A hash of the opposite for each operator symbol, used for inverting
# objects.
OPERTATOR_INVERSIONS = {:AND => :OR, :OR => :AND, :< => :>=, :> => :<=,
:<= => :>, :>= => :<, :'=' => :'!=' , :'!=' => :'=', :LIKE => :'NOT LIKE',
:'NOT LIKE' => :LIKE, :~ => :'!~', :'!~' => :~, :IN => :'NOT IN',
:'NOT IN' => :IN, :IS => :'IS NOT', :'IS NOT' => :IS, :'~*' => :'!~*',
:'!~*' => :'~*', :NOT => :NOOP, :NOOP => :NOT, :ILIKE => :'NOT ILIKE',
:'NOT ILIKE'=>:ILIKE}.freeze
# Standard mathematical operators used in +NumericMethods+
MATHEMATICAL_OPERATORS = [:+, :-, :/, :*, :**].freeze
# Bitwise mathematical operators used in +BitwiseMethods+
BITWISE_OPERATORS = [:&, :|, :^, :<<, :>>, :%].freeze
# Operators that check for equality
EQUALITY_OPERATORS = [:'=', :'!='].freeze
# Inequality operators used in +InequalityMethods+
INEQUALITY_OPERATORS = [:<, :>, :<=, :>=].freeze
# Hash of ruby operator symbols to SQL operators, used in +BooleanMethods+
BOOLEAN_OPERATOR_METHODS = {:& => :AND, :| =>:OR}.freeze
# Operators that use IN/NOT IN for inclusion/exclusion
IN_OPERATORS = [:IN, :'NOT IN'].freeze
# Operators that use IS, used for special casing to override literal true/false values
IS_OPERATORS = [:IS, :'IS NOT'].freeze
# Operators that do pattern matching via regular expressions
REGEXP_OPERATORS = [:~, :'!~', :'~*', :'!~*'].freeze
# Operators that do pattern matching via LIKE
LIKE_OPERATORS = [:LIKE, :'NOT LIKE', :ILIKE, :'NOT ILIKE'].freeze
# Operator symbols that take exactly two arguments
TWO_ARITY_OPERATORS = (EQUALITY_OPERATORS + INEQUALITY_OPERATORS + IS_OPERATORS + IN_OPERATORS + REGEXP_OPERATORS + LIKE_OPERATORS + [:**]).freeze
# Operator symbols that take one or more arguments
N_ARITY_OPERATORS = ([:AND, :OR, :'||'] + MATHEMATICAL_OPERATORS + BITWISE_OPERATORS - [:**]).freeze
# Operator symbols that are associative
ASSOCIATIVE_OPERATORS = [:AND, :OR, :'||', :+, :*, :&, :|].freeze
# Operator symbols that take only a single argument
ONE_ARITY_OPERATORS = [:NOT, :NOOP, :'B~'].freeze
# Custom expressions that may have different syntax on different databases
CUSTOM_EXPRESSIONS = [:extract].freeze
# The operator symbol for this object
attr_reader :op
# An array of args for this object
attr_reader :args
# Set the operator symbol and arguments for this object to the ones given.
# Convert all args that are hashes or arrays of two element arrays to +BooleanExpressions+,
# other than the second arg for an IN/NOT IN operator.
# Raise an +Error+ if the operator doesn't allow boolean input and a boolean argument is given.
# Raise an +Error+ if the wrong number of arguments for a given operator is used.
def initialize(op, *args)
orig_args = args
args = args.map{|a| Sequel.condition_specifier?(a) ? SQL::BooleanExpression.from_value_pairs(a) : a}
case op
when *N_ARITY_OPERATORS
raise(Error, "The #{op} operator requires at least 1 argument") unless args.length >= 1
args.map!{|a| a.is_a?(self.class) && a.op == :NOOP ? a.args.first : a}
if ASSOCIATIVE_OPERATORS.include?(op)
old_args = args
args = []
old_args.each{|a| a.is_a?(self.class) && a.op == op ? args.concat(a.args) : args.push(a)}
end
when *TWO_ARITY_OPERATORS
raise(Error, "The #{op} operator requires precisely 2 arguments") unless args.length == 2
# With IN/NOT IN, even if the second argument is an array of two element arrays,
# don't convert it into a boolean expression, since it's definitely being used
# as a value list.
args[1] = orig_args[1] if IN_OPERATORS.include?(op)
when *ONE_ARITY_OPERATORS
raise(Error, "The #{op} operator requires a single argument") unless args.length == 1
when *CUSTOM_EXPRESSIONS
# nothing
else
raise(Error, "Invalid operator #{op}")
end
@op = op
@args = args.freeze
freeze
end
to_s_method :complex_expression_sql, '@op, @args'
end
# The base class for expressions that can be used in multiple places in
# an SQL query.
class GenericExpression < Expression
end
# Includes an +as+ method that creates an SQL alias.
module AliasMethods
# Create an SQL alias (+AliasedExpression+) of the receiving column or expression to the given alias.
#
# Sequel.function(:func).as(:alias) # func() AS "alias"
# Sequel.function(:func).as(:alias, [:col_alias1, :col_alias2]) # func() AS "alias"("col_alias1", "col_alias2")
def as(aliaz, columns=nil)
AliasedExpression.new(self, aliaz, columns)
end
end
# This defines the bitwise methods: &, |, ^, ~, <<, and >>. Because these
# methods overlap with the standard +BooleanMethods methods+, and they only
# make sense for integers, they are only included in +NumericExpression+.
#
# Sequel[:a].sql_number & :b # "a" & "b"
# Sequel[:a].sql_number | :b # "a" | "b"
# Sequel[:a].sql_number ^ :b # "a" ^ "b"
# Sequel[:a].sql_number << :b # "a" << "b"
# Sequel[:a].sql_number >> :b # "a" >> "b"
# ~Sequel[:a].sql_number # ~"a"
module BitwiseMethods
ComplexExpression::BITWISE_OPERATORS.each do |o|
module_eval("def #{o}(o) NumericExpression.new(#{o.inspect}, self, o) end", __FILE__, __LINE__)
end
# Do the bitwise compliment of the self
#
# ~(Sequel[:a].sql_number) # ~"a"
def ~
NumericExpression.new(:'B~', self)
end
end
# This module includes the boolean/logical AND (&), OR (|) and NOT (~) operators
# that are defined on objects that can be used in a boolean context in SQL.
#
# Sequel[:a] & Sequel[:b] # "a" AND "b"
# Sequel[:a] | Sequel[:b] # "a" OR "b"
# ~Sequel[:a] # NOT "a"
#
# One exception to this is when a NumericExpression or Integer is the argument
# to & or |, in which case a bitwise method will be used:
#
# Sequel[:a] & 1 # "a" & 1
# Sequel[:a] | (Sequel[:b] + 1) # "a" | ("b" + 1)
module BooleanMethods
ComplexExpression::BOOLEAN_OPERATOR_METHODS.each do |m, o|
module_eval(<<-END, __FILE__, __LINE__+1)
def #{m}(o)
case o
when NumericExpression, Integer
NumericExpression.new(#{m.inspect}, self, o)
else
BooleanExpression.new(#{o.inspect}, self, o)
end
end
END
end
# Create a new BooleanExpression with NOT, representing the inversion of whatever self represents.
#
# ~Sequel[:a] # NOT :a
def ~
BooleanExpression.invert(self)
end
end
# These methods make it easier to create Sequel expressions without
# using the core extensions.
module Builders
# Create an SQL::AliasedExpression for the given expression and alias.
#
# Sequel.as(:column, :alias) # "column" AS "alias"
# Sequel.as(:column, :alias, [:col_alias1, :col_alias2]) # "column" AS "alias"("col_alias1", "col_alias2")
def as(exp, aliaz, columns=nil)
SQL::AliasedExpression.new(exp, aliaz, columns)
end
# Order the given argument ascending.
# Options:
#
# :nulls :: Set to :first to use NULLS FIRST (so NULL values are ordered
# before other values), or :last to use NULLS LAST (so NULL values
# are ordered after other values).
#
# Sequel.asc(:a) # a ASC
# Sequel.asc(:b, nulls: :last) # b ASC NULLS LAST
def asc(arg, opts=OPTS)
SQL::OrderedExpression.new(arg, false, opts)
end
# Return an SQL::Blob that holds the same data as this string.
# Blobs provide proper escaping of binary data. If given a blob, returns it
# directly.
def blob(s)
if s.is_a?(SQL::Blob)
s
else
SQL::Blob.new(s)
end
end
# Return an SQL::CaseExpression created with the given arguments.
# The first argument are the WHEN/THEN conditions,
# specified as an array or a hash. The second argument is the
# ELSE default value. The third optional argument is the
# CASE expression.
#
# Sequel.case({a: 1}, 0) # SQL: CASE WHEN a THEN 1 ELSE 0 END
# Sequel.case({a: 1}, 0, :b) # SQL: CASE b WHEN a THEN 1 ELSE 0 END
# Sequel.case({{a: [2,3]} => 1}, 0) # SQL: CASE WHEN a IN (2, 3) THEN 1 ELSE 0 END
# Sequel.case([[{a: [2,3]}, 1]], 0) # SQL: CASE WHEN a IN (2, 3) THEN 1 ELSE 0 END
def case(*args)
SQL::CaseExpression.new(*args)
end
# Cast the reciever to the given SQL type. You can specify a ruby class as a type,
# and it is handled similarly to using a database independent type in the schema methods.
#
# Sequel.cast(:a, :integer) # CAST(a AS integer)
# Sequel.cast(:a, String) # CAST(a AS varchar(255))
def cast(arg, sql_type)
SQL::Cast.new(arg, sql_type)
end
# Cast the reciever to the given SQL type (or the database's default Integer type if none given),
# and return the result as a +NumericExpression+, so you can use the bitwise operators
# on the result.
#
# Sequel.cast_numeric(:a) # CAST(a AS integer)
# Sequel.cast_numeric(:a, Float) # CAST(a AS double precision)
def cast_numeric(arg, sql_type = nil)
cast(arg, sql_type || Integer).sql_number
end
# Cast the reciever to the given SQL type (or the database's default String type if none given),
# and return the result as a +StringExpression+, so you can use +
# directly on the result for SQL string concatenation.
#
# Sequel.cast_string(:a) # CAST(a AS varchar(255))
# Sequel.cast_string(:a, :text) # CAST(a AS text)
def cast_string(arg, sql_type = nil)
cast(arg, sql_type || String).sql_string
end
# Return an emulated function call for getting the number of characters
# in the argument:
#
# Sequel.char_length(:a) # char_length(a) -- Most databases
# Sequel.char_length(:a) # length(a) -- SQLite
def char_length(arg)
SQL::Function.new!(:char_length, [arg], :emulate=>true)
end
# Do a deep qualification of the argument using the qualifier. This recurses into
# nested structures.
#
# Sequel.deep_qualify(:table, :column) # "table"."column"
# Sequel.deep_qualify(:table, Sequel[:column] + 1) # "table"."column" + 1
# Sequel.deep_qualify(:table, Sequel[:a].like('b')) # "table"."a" LIKE 'b' ESCAPE '\'
def deep_qualify(qualifier, expr)
Sequel::Qualifier.new(qualifier).transform(expr)
end
# Return a delayed evaluation that uses the passed block. This is used
# to delay evaluations of the code to runtime. For example, with
# the following code:
#
# ds = DB[:table].where{column > Time.now}
#
# The filter is fixed to the time that where was called. Unless you are
# only using the dataset once immediately after creating it, that's
# probably not desired. If you just want to set it to the time when the
# query is sent to the database, you can wrap it in Sequel.delay:
#
# ds = DB[:table].where{column > Sequel.delay{Time.now}}
#
# Note that for dates and timestamps, you are probably better off using
# Sequel::CURRENT_DATE and Sequel::CURRENT_TIMESTAMP instead of this
# generic delayed evaluation facility.
def delay(&block)
raise(Error, "Sequel.delay requires a block") unless block
SQL::DelayedEvaluation.new(block)
end
# Order the given argument descending.
# Options:
#
# :nulls :: Set to :first to use NULLS FIRST (so NULL values are ordered
# before other values), or :last to use NULLS LAST (so NULL values
# are ordered after other values).
#
# Sequel.desc(:a) # b DESC
# Sequel.desc(:b, nulls: :first) # b DESC NULLS FIRST
def desc(arg, opts=OPTS)
SQL::OrderedExpression.new(arg, true, opts)
end
# Wraps the given object in an appropriate Sequel wrapper.
# If the given object is already a Sequel object, return it directly.
# For condition specifiers (hashes and arrays of two pairs), true, and false,
# return a boolean expressions. For numeric objects, return a numeric
# expression. For strings, return a string expression. For procs or when
# the method is passed a block, evaluate it as a virtual row and wrap it
# appropriately. In all other cases, use a generic wrapper.
#
# This method allows you to construct SQL expressions that are difficult
# to construct via other methods. For example:
#
# Sequel.expr(1) - :a # SQL: (1 - a)
#
# On the Sequel module, this is aliased as #[], for easier use:
#
# Sequel[1] - :a # SQL: (1 - a)
def expr(arg=(no_arg=true), &block)
if defined?(yield)
if no_arg
return expr(block)
else
raise Error, 'cannot provide both an argument and a block to Sequel.expr'
end
elsif no_arg
raise Error, 'must provide either an argument or a block to Sequel.expr'
end
case arg
when Symbol
t, c, a = Sequel.split_symbol(arg)
arg = if t
SQL::QualifiedIdentifier.new(t, c)
else
SQL::Identifier.new(c)
end
if a
arg = SQL::AliasedExpression.new(arg, a)
end
arg
when SQL::Expression, LiteralString, SQL::Blob
arg
when Hash
SQL::BooleanExpression.from_value_pairs(arg, :AND)
when Array
if condition_specifier?(arg)
SQL::BooleanExpression.from_value_pairs(arg, :AND)
else
SQL::Wrapper.new(arg)
end
when Numeric
SQL::NumericExpression.new(:NOOP, arg)
when String
SQL::StringExpression.new(:NOOP, arg)
when TrueClass, FalseClass
SQL::BooleanExpression.new(:NOOP, arg)
when Proc
expr(virtual_row(&arg))
else
SQL::Wrapper.new(arg)
end
end
# Extract a datetime_part (e.g. year, month) from the given
# expression:
#
# Sequel.extract(:year, :date) # extract(year FROM "date")
def extract(datetime_part, exp)
SQL::NumericExpression.new(:extract, datetime_part, exp)
end
# Returns a Sequel::SQL::Function with the function name
# and the given arguments.
#
# Sequel.function(:now) # SQL: now()
# Sequel.function(:substr, :a, 1) # SQL: substr(a, 1)
def function(name, *args)
SQL::Function.new(name, *args)
end
# Return the argument wrapped as an SQL::Identifier.
#
# Sequel.identifier(:a) # "a"
def identifier(name)
SQL::Identifier.new(name)
end
# Return a Sequel::SQL::StringExpression representing an SQL string made up of the
# concatenation of the given array's elements. If an argument is passed,
# it is used in between each element of the array in the SQL
# concatenation.
#
# Sequel.join([:a]) # SQL: a
# Sequel.join([:a, :b]) # SQL: a || b
# Sequel.join([:a, 'b']) # SQL: a || 'b'
# Sequel.join(['a', :b], ' ') # SQL: 'a' || ' ' || b
def join(args, joiner=nil)
raise Error, 'argument to Sequel.join must be an array' unless args.is_a?(Array)
if joiner
args = args.zip([joiner]*args.length).flatten
args.pop
end
return SQL::StringExpression.new(:NOOP, '') if args.empty?
args = args.map do |a|
case a
when Symbol, ::Sequel::SQL::Expression, ::Sequel::LiteralString, TrueClass, FalseClass, NilClass
a
else
a.to_s
end
end
SQL::StringExpression.new(:'||', *args)
end
# Create a BooleanExpression case insensitive (if the database supports it) pattern match of the receiver with
# the given patterns. See SQL::StringExpression.like.
#
# Sequel.ilike(:a, 'A%') # "a" ILIKE 'A%' ESCAPE '\'
def ilike(*args)
SQL::StringExpression.like(*(args << {:case_insensitive=>true}))
end
# Create a SQL::BooleanExpression case sensitive (if the database supports it) pattern match of the receiver with
# the given patterns. See SQL::StringExpression.like.
#
# Sequel.like(:a, 'A%') # "a" LIKE 'A%' ESCAPE '\'
def like(*args)
SQL::StringExpression.like(*args)
end
# Converts a string into a Sequel::LiteralString, in order to override string
# literalization, e.g.:
#
# DB[:items].where(abc: 'def').sql #=>
# "SELECT * FROM items WHERE (abc = 'def')"
#
# DB[:items].where(abc: Sequel.lit('def')).sql #=>
# "SELECT * FROM items WHERE (abc = def)"
#
# You can also provide arguments, to create a Sequel::SQL::PlaceholderLiteralString:
#
# DB[:items].select{|o| o.count(Sequel.lit('DISTINCT ?', :a))}.sql #=>
# "SELECT count(DISTINCT a) FROM items"
def lit(s, *args)
if args.empty?
if s.is_a?(LiteralString)
s
else
LiteralString.new(s)
end
else
SQL::PlaceholderLiteralString.new(s, args)
end
end
# Return a Sequel::SQL::BooleanExpression created from the condition
# specifier, matching none of the conditions.
#
# Sequel.negate(a: true) # SQL: a IS NOT TRUE
# Sequel.negate([[:a, true]]) # SQL: a IS NOT TRUE
# Sequel.negate([[:a, 1], [:b, 2]]) # SQL: ((a != 1) AND (b != 2))
def negate(arg)
if condition_specifier?(arg)
SQL::BooleanExpression.from_value_pairs(arg, :AND, true)
else
raise Error, 'must pass a conditions specifier to Sequel.negate'
end
end
# Return a Sequel::SQL::BooleanExpression created from the condition
# specifier, matching any of the conditions.
#
# Sequel.or(a: true) # SQL: a IS TRUE
# Sequel.or([[:a, true]]) # SQL: a IS TRUE
# Sequel.or([[:a, 1], [:b, 2]]) # SQL: ((a = 1) OR (b = 2))
def or(arg)
if condition_specifier?(arg)
SQL::BooleanExpression.from_value_pairs(arg, :OR, false)
else
raise Error, 'must pass a conditions specifier to Sequel.or'
end
end
# Create a qualified identifier with the given qualifier and identifier
#
# Sequel.qualify(:table, :column) # "table"."column"
# Sequel.qualify(:schema, :table) # "schema"."table"
# Sequel.qualify(:table, :column).qualify(:schema) # "schema"."table"."column"
def qualify(qualifier, identifier)
SQL::QualifiedIdentifier.new(qualifier, identifier)
end
# Return an SQL::Subscript with the given arguments, representing an
# SQL array access.
#
# Sequel.subscript(:array, 1) # array[1]
# Sequel.subscript(:array, 1, 2) # array[1, 2]
# Sequel.subscript(:array, [1, 2]) # array[1, 2]
# Sequel.subscript(:array, 1..2) # array[1:2]
# Sequel.subscript(:array, 1...3) # array[1:2]
def subscript(exp, *subs)
SQL::Subscript.new(exp, subs.flatten)
end
# Return an emulated function call for trimming a string of spaces from
# both sides (similar to ruby's String#strip).
#
# Sequel.trim(:a) # trim(a) -- Most databases
# Sequel.trim(:a) # ltrim(rtrim(a)) -- Microsoft SQL Server
def trim(arg)
SQL::Function.new!(:trim, [arg], :emulate=>true)
end
# Return a SQL::ValueList created from the given array. Used if the array contains
# all two element arrays and you want it treated as an SQL value list (IN predicate)
# instead of as a conditions specifier (similar to a hash). This is not necessary if you are using
# this array as a value in a filter, but may be necessary if you are using it as a
# value with placeholder SQL:
#
# DB[:a].where([:a, :b]=>[[1, 2], [3, 4]]) # SQL: (a, b) IN ((1, 2), (3, 4))
# DB[:a].where('(a, b) IN ?', [[1, 2], [3, 4]]) # SQL: (a, b) IN ((1 = 2) AND (3 = 4))
# DB[:a].where('(a, b) IN ?', Sequel.value_list([[1, 2], [3, 4]])) # SQL: (a, b) IN ((1, 2), (3, 4))
def value_list(arg)
raise Error, 'argument to Sequel.value_list must be an array' unless arg.is_a?(Array)
SQL::ValueList.new(arg)
end
end
# Holds methods that are used to cast objects to different SQL types.
module CastMethods
# Cast the reciever to the given SQL type. You can specify a ruby class as a type,
# and it is handled similarly to using a database independent type in the schema methods.
#
# Sequel.function(:func).cast(:integer) # CAST(func() AS integer)
# Sequel.function(:func).cast(String) # CAST(func() AS varchar(255))
def cast(sql_type)
Cast.new(self, sql_type)
end
# Cast the reciever to the given SQL type (or the database's default Integer type if none given),
# and return the result as a +NumericExpression+, so you can use the bitwise operators
# on the result.
#
# Sequel.function(:func).cast_numeric # CAST(func() AS integer)
# Sequel.function(:func).cast_numeric(Float) # CAST(func() AS double precision)
def cast_numeric(sql_type = nil)
Cast.new(self, sql_type || Integer).sql_number
end
# Cast the reciever to the given SQL type (or the database's default String type if none given),
# and return the result as a +StringExpression+, so you can use +
# directly on the result for SQL string concatenation.
#
# Sequel.function(:func).cast_string # CAST(func() AS varchar(255))
# Sequel.function(:func).cast_string(:text) # CAST(func() AS text)
def cast_string(sql_type = nil)
Cast.new(self, sql_type || String).sql_string
end
end
# Adds methods that allow you to treat an object as an instance of a specific
# +ComplexExpression+ subclass.
module ComplexExpressionMethods
# Extract a datetime part (e.g. year, month) from self:
#
# Sequel[:date].extract(:year) # extract(year FROM "date")
#
# Also has the benefit of returning the result as a
# NumericExpression instead of a generic ComplexExpression.
def extract(datetime_part)
NumericExpression.new(:extract, datetime_part, self)
end
# Return a BooleanExpression representation of +self+.
def sql_boolean
BooleanExpression.new(:NOOP, self)
end
# Return a NumericExpression representation of +self+.
#
# ~Sequel[:a] # NOT "a"
# ~(Sequel[:a].sql_number) # ~"a"
def sql_number
NumericExpression.new(:NOOP, self)
end
# Return a StringExpression representation of +self+.
#
# Sequel[:a] + :b # "a" + "b"
# Sequel[:a].sql_string + :b # "a" || "b"
def sql_string
StringExpression.new(:NOOP, self)
end
end
# This module includes the inequality methods (>, <, >=, <=) that are defined on objects that can be
# used in a numeric or string context in SQL.
#
# Sequel[:a] > :b # a > "b"
# Sequel[:a] < :b # a > "b"
# Sequel[:a] >= :b # a >= "b"
# Sequel[:a] <= :b # a <= "b"
module InequalityMethods
ComplexExpression::INEQUALITY_OPERATORS.each do |o|
module_eval("def #{o}(o) BooleanExpression.new(#{o.inspect}, self, o) end", __FILE__, __LINE__)
end
end
# This module includes the standard mathematical methods (+, -, *, and /)
# that are defined on objects that can be used in a numeric context in SQL
# (+Symbol+, +LiteralString+, and +SQL::GenericExpression+).
#
# Sequel[:a] + :b # "a" + "b"
# Sequel[:a] - :b # "a" - "b"
# Sequel[:a] * :b # "a" * "b"
# Sequel[:a] / :b # "a" / "b"
#
# One exception to this is if + is called with a +String+ or +StringExpression+,
# in which case the || operator is used instead of the + operator:
#
# Sequel[:a] + 'b' # "a" || 'b'
module NumericMethods
(ComplexExpression::MATHEMATICAL_OPERATORS - [:+]).each do |o|
module_eval("def #{o}(o) NumericExpression.new(#{o.inspect}, self, o) end", __FILE__, __LINE__)
end
# If the argument given is Numeric, treat it as a NumericExpression,
# allowing code such as:
#
# 1 + Sequel[:x] # SQL: (1 + x)
# Sequel.expr{1 - x(y)} # SQL: (1 - x(y))
def coerce(other)
if other.is_a?(Numeric)
[SQL::NumericExpression.new(:NOOP, other), self]
elsif defined?(super)
super
else
[self, other]
end
end
# Use || as the operator when called with StringExpression and String instances,
# and the + operator for LiteralStrings and all other types.
def +(ce)
case ce
when LiteralString
NumericExpression.new(:+, self, ce)
when StringExpression, String
StringExpression.new(:'||', self, ce)
else
NumericExpression.new(:+, self, ce)
end
end
end
# This module includes methods for overriding the =~ method for SQL equality,
# inclusion, and pattern matching. It returns the same result that Sequel would
# return when using a hash with a single entry, where the receiver was the key
# and the argument was the value. Example:
#
# Sequel[:a] =~ 1 # (a = 1)
# Sequel[:a] =~ [1, 2] # (a IN [1, 2])
# Sequel[:a] =~ nil # (a IS NULL)
#
# This also adds the !~ method, for easily setting up not equals,
# exclusion, and inverse pattern matching. This is the same as as inverting the
# result of the =~ method
#
# Sequel[:a] !~ 1 # (a != 1)
# Sequel[:a] !~ [1, 2] # (a NOT IN [1, 2])
# Sequel[:a] !~ nil # (a IS NOT NULL)
module PatternMatchMethods
# Set up an equality, inclusion, or pattern match operation, based on the type
# of the argument.
def =~(other)
BooleanExpression.send(:from_value_pair, self, other)
end
def !~(other)
~(self =~ other)
end
end
# This adds methods to create SQL expressions using operators:
#
# Sequel.+(1, :a) # (1 + a)
# Sequel.-(1, :a) # (1 - a)
# Sequel.*(1, :a) # (1 * a)
# Sequel./(1, :a) # (1 / a)
# Sequel.&(:b, :a) # (b AND a)
# Sequel.|(:b, :a) # (b OR a)
module OperatorBuilders
{'::Sequel::SQL::NumericExpression'=>{'+'=>'+', '-'=>'-', '*'=>'*', '/'=>'/'},
'::Sequel::SQL::BooleanExpression'=>{'&'=>'AND', '|'=>'OR'}}.each do |klass, ops|
ops.each do |m, op|
class_eval(<<-END, __FILE__, __LINE__ + 1)
def #{m}(*args)
if (args.length == 1)
if (v = args.first).class.is_a?(#{klass})
v
else
#{klass}.new(:NOOP, v)
end
else
#{klass}.new(:#{op}, *args)
end
end
END
end
end
# Return NumericExpression for the exponentiation:
#
# Sequel.**(2, 3) # SQL: power(2, 3)
def **(a, b)
SQL::NumericExpression.new(:**, a, b)
end
# Invert the given expression. Returns a Sequel::SQL::BooleanExpression
# created from this argument, not matching all of the conditions.
#
# Sequel.~(nil) # SQL: NOT NULL
# Sequel.~([[:a, true]]) # SQL: a IS NOT TRUE
# Sequel.~([[:a, 1], [:b, [2, 3]]]) # SQL: a != 1 OR b NOT IN (2, 3)
def ~(arg)
if condition_specifier?(arg)
SQL::BooleanExpression.from_value_pairs(arg, :OR, true)
else
SQL::BooleanExpression.invert(arg)
end
end
end
# Methods that create +OrderedExpressions+, used for sorting by columns
# or more complex expressions.
module OrderMethods
# Mark the receiving SQL column as sorting in an ascending fashion (generally a no-op).
# Options:
#
# :nulls :: Set to :first to use NULLS FIRST (so NULL values are ordered
# before other values), or :last to use NULLS LAST (so NULL values
# are ordered after other values).
def asc(opts=OPTS)
OrderedExpression.new(self, false, opts)
end
# Mark the receiving SQL column as sorting in a descending fashion.
# Options:
#
# :nulls :: Set to :first to use NULLS FIRST (so NULL values are ordered
# before other values), or :last to use NULLS LAST (so NULL values
# are ordered after other values).
def desc(opts=OPTS)
OrderedExpression.new(self, true, opts)
end
end
# Includes a +qualify+ and [] methods that create QualifiedIdentifiers, used for qualifying column
# names with a table or table names with a schema, and the * method for returning all columns in
# the identifier if no arguments are given.
module QualifyingMethods
# If no arguments are given, return an SQL::ColumnAll:
#
# Sequel[:a].* # a.*
def *(ce=(arg=false;nil))
if arg == false
Sequel::SQL::ColumnAll.new(self)
else
super(ce)
end
end
# Qualify the receiver with the given +qualifier+ (table for column/schema for table).
#
# Sequel[:column].qualify(:table) # "table"."column"
# Sequel[:table].qualify(:schema) # "schema"."table"
# Sequel.qualify(:table, :column).qualify(:schema) # "schema"."table"."column"
def qualify(qualifier)
QualifiedIdentifier.new(qualifier, self)
end
# Qualify the receiver with the given +qualifier+ (table for column/schema for table).
#
# Sequel[:table][:column] # "table"."column"
# Sequel[:schema][:table] # "schema"."table"
# Sequel[:schema][:table][:column] # "schema"."table"."column"
def [](identifier)
QualifiedIdentifier.new(self, identifier)
end
end
# This module includes the +like+ and +ilike+ methods used for pattern matching that are defined on objects that can be
# used in a string context in SQL (+Symbol+, +LiteralString+, SQL::GenericExpression).
module StringMethods
# Create a +BooleanExpression+ case insensitive pattern match of the receiver
# with the given patterns. See StringExpression.like.
#
# Sequel[:a].ilike('A%') # "a" ILIKE 'A%' ESCAPE '\'
def ilike(*ces)
StringExpression.like(self, *(ces << {:case_insensitive=>true}))
end
# Create a +BooleanExpression+ case sensitive (if the database supports it) pattern match of the receiver with
# the given patterns. See StringExpression.like.
#
# Sequel[:a].like('A%') # "a" LIKE 'A%' ESCAPE '\'
def like(*ces)
StringExpression.like(self, *ces)
end
end
# This module includes the + method. It is included in +StringExpression+ and can be included elsewhere
# to allow the use of the + operator to represent concatenation of SQL Strings:
module StringConcatenationMethods
# Return a +StringExpression+ representing the concatenation of the receiver
# with the given argument.
#
# Sequel[:x].sql_string + :y # => "x" || "y"
def +(ce)
StringExpression.new(:'||', self, ce)
end
end
# This module includes the +sql_subscript+ method, representing SQL array accesses.
module SubscriptMethods
# Return a Subscript with the given arguments, representing an
# SQL array access.
#
# Sequel[:array].sql_subscript(1) # array[1]
# Sequel[:array].sql_subscript(1, 2) # array[1, 2]
# Sequel[:array].sql_subscript([1, 2]) # array[1, 2]
# Sequel[:array].sql_subscript(1..2) # array[1:2]
# Sequel[:array].sql_subscript(1...3) # array[1:2]
def sql_subscript(*sub)
Subscript.new(self, sub.flatten)
end
end
# Represents an aliasing of an expression to a given alias.
class AliasedExpression < Expression
# The expression to alias
attr_reader :expression
# The alias to use for the expression.
attr_reader :alias
# The columns aliases (derived column list) to use, for when the aliased expression is
# a record or set of records (such as a dataset).
attr_reader :columns
# Create an object with the given expression, alias, and optional column aliases.
def initialize(expression, aliaz, columns=nil)
@expression = expression
@alias = aliaz
@columns = columns
freeze
end
to_s_method :aliased_expression_sql
end
# +Blob+ is used to represent binary data in the Ruby environment that is
# stored as a blob type in the database. Sequel represents binary data as a Blob object because
# most database engines require binary data to be escaped differently than regular strings.
class Blob < ::String
include SQL::AliasMethods
include SQL::CastMethods
class << self
# Alias new to call for usage in conversion procs
alias call new
end
# Return a LiteralString with the same content if no args are given, otherwise
# return a SQL::PlaceholderLiteralString with the current string and the given args.
def lit(*args)
args.empty? ? LiteralString.new(self) : SQL::PlaceholderLiteralString.new(self, args)
end
# Return a string showing that this is a blob, the size, and the some or all of the content,
# depending on the size.
def inspect
size = length
content = if size > 20
"start=#{self[0...10].to_s.inspect} end=#{self[-10..-1].to_s.inspect}"
else
"content=#{super}"
end
"#<#{self.class}:0x#{"%x" % object_id} bytes=#{size} #{content}>"
end
# Returns +self+, since it is already a blob.
def to_sequel_blob
self
end
end
# Subclass of +ComplexExpression+ where the expression results
# in a boolean value in SQL.
class BooleanExpression < ComplexExpression
include BooleanMethods
# Take pairs of values (e.g. a hash or array of two element arrays)
# and converts it to a +BooleanExpression+. The operator and args
# used depends on the case of the right (2nd) argument:
#
# 0..10 :: left >= 0 AND left <= 10
# [1,2] :: left IN (1,2)
# nil :: left IS NULL
# true :: left IS TRUE
# false :: left IS FALSE
# /as/ :: left ~ 'as'
# :blah :: left = blah
# 'blah' :: left = 'blah'
#
# If multiple arguments are given, they are joined with the op given (AND
# by default, OR possible). If negate is set to true,
# all subexpressions are inverted before used. Therefore, the following
# expressions are equivalent:
#
# ~from_value_pairs(hash)
# from_value_pairs(hash, :OR, true)
def self.from_value_pairs(pairs, op=:AND, negate=false)
pairs = pairs.map{|l,r| from_value_pair(l, r)}
pairs.map!{|ce| invert(ce)} if negate
pairs.length == 1 ? pairs[0] : new(op, *pairs)
end
# Return a BooleanExpression based on the right side of the pair.
def self.from_value_pair(l, r)
case r
when Range
unless r.begin.nil?
begin_expr = new(:>=, l, r.begin)
end
unless r.end.nil?
end_expr = new(r.exclude_end? ? :< : :<=, l, r.end)
end
if begin_expr
if end_expr
new(:AND, begin_expr, end_expr)
else
begin_expr
end
elsif end_expr
end_expr
else
new(:'=', 1, 1)
end
when ::Array
r = r.dup.freeze unless r.frozen?
new(:IN, l, r)
when ::String
r = r.dup.freeze unless r.frozen?
new(:'=', l, r)
when ::Sequel::Dataset
new(:IN, l, r)
when NegativeBooleanConstant
new(:"IS NOT", l, r.constant)
when BooleanConstant
new(:IS, l, r.constant)
when NilClass, TrueClass, FalseClass
new(:IS, l, r)
when Regexp
StringExpression.like(l, r)
when DelayedEvaluation
Sequel.delay{|ds| from_value_pair(l, r.call(ds))}
when Dataset::PlaceholderLiteralizer::Argument
r.transform{|v| from_value_pair(l, v)}
else
new(:'=', l, r)
end
end
private_class_method :from_value_pair
# Invert the expression, if possible. If the expression cannot
# be inverted, raise an error. An inverted expression should match everything that the
# uninverted expression did not match, and vice-versa, except for possible issues with
# SQL NULL (i.e. 1 == NULL is NULL and 1 != NULL is also NULL).
#
# BooleanExpression.invert(:a) # NOT "a"
def self.invert(ce)
case ce
when BooleanExpression
case op = ce.op
when :AND, :OR
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.map{|a| BooleanExpression.invert(a)})
when :IN, :"NOT IN"
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.dup)
else
if ce.args.length == 2
case ce.args[1]
when Function, LiteralString, PlaceholderLiteralString
# Special behavior to not push down inversion in this case because doing so
# can result in incorrect behavior for ANY/SOME/ALL operators.
BooleanExpression.new(:NOT, ce)
else
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.dup)
end
else
BooleanExpression.new(OPERTATOR_INVERSIONS[op], *ce.args.dup)
end
end
when StringExpression, NumericExpression
raise(Sequel::Error, "cannot invert #{ce.inspect}")
when Constant
CONSTANT_INVERSIONS[ce] || raise(Sequel::Error, "cannot invert #{ce.inspect}")
else
BooleanExpression.new(:NOT, ce)
end
end
# Always use an AND operator for & on BooleanExpressions
def &(ce)
BooleanExpression.new(:AND, self, ce)
end
# Always use an OR operator for | on BooleanExpressions
def |(ce)
BooleanExpression.new(:OR, self, ce)
end
# Return self instead of creating a new object to save on memory.
def sql_boolean
self
end
end
# Represents an SQL CASE expression, used for conditional branching in SQL.
class CaseExpression < GenericExpression
# An array of all two pairs with the first element specifying the
# condition and the second element specifying the result if the
# condition matches.
attr_reader :conditions
# The default value if no conditions match.
attr_reader :default
# An optional expression to test the conditions against
attr_reader :expression
# Create an object with the given conditions and
# default value, and optional expression. An expression can be provided to
# test each condition against, instead of having
# all conditions represent their own boolean expression.
def initialize(conditions, default, expression=(no_expression=true; nil))
raise(Sequel::Error, 'CaseExpression conditions must be a hash or array of all two pairs') unless Sequel.condition_specifier?(conditions)
@conditions = conditions.to_a.dup.freeze
@default = default
@expression = expression
@no_expression = no_expression
freeze
end
# Whether to use an expression for this CASE expression.
def expression?
!@no_expression
end
# Merge the CASE expression into the conditions, useful for databases that
# don't support CASE expressions.
def with_merged_expression
if expression?
e = expression
CaseExpression.new(conditions.map{|c, r| [::Sequel::SQL::BooleanExpression.new(:'=', e, c), r]}, default)
else
self
end
end
to_s_method :case_expression_sql
end
# Represents a cast of an SQL expression to a specific type.
class Cast < GenericExpression
# The expression to cast
attr_reader :expr
# The type to which to cast the expression
attr_reader :type
# Set the expression and type for the cast
def initialize(expr, type)
@expr = expr
@type = type
freeze
end
to_s_method :cast_sql, '@expr, @type'
end
# Represents all columns in a given table, table.* in SQL
class ColumnAll < Expression
# The table containing the columns being selected
attr_reader :table
# Create an object with the given table
def initialize(table)
@table = table
freeze
end
to_s_method :column_all_sql
end
class ComplexExpression
include AliasMethods
include CastMethods
include OrderMethods
include PatternMatchMethods
include SubscriptMethods
# Return a BooleanExpression with the same op and args.
def sql_boolean
BooleanExpression.new(op, *args)
end
# Return a NumericExpression with the same op and args.
def sql_number
NumericExpression.new(op, *args)
end
# Return a StringExpression with the same op and args.
def sql_string
StringExpression.new(op, *args)
end
end
# Represents constants or psuedo-constants (e.g. +CURRENT_DATE+) in SQL.
class Constant < GenericExpression
# The underlying constant related to this object.
attr_reader :constant
# Create a constant with the given value
def initialize(constant)
@constant = constant
freeze
end
to_s_method :constant_sql, '@constant'
end
# Represents boolean constants such as +NULL+, +TRUE+, and +FALSE+.
class BooleanConstant < Constant
to_s_method :boolean_constant_sql, '@constant'
end
# Represents inverse boolean constants (currently only +NOTNULL+). A
# special class to allow for special behavior.
class NegativeBooleanConstant < Constant
to_s_method :negative_boolean_constant_sql, '@constant'
end
# Holds default generic constants that can be referenced. These
# are included in the Sequel top level module and are also available
# in this module which can be required at the top level to get
# direct access to the constants.
module Constants
CURRENT_DATE = Constant.new(:CURRENT_DATE)
CURRENT_TIME = Constant.new(:CURRENT_TIME)
CURRENT_TIMESTAMP = Constant.new(:CURRENT_TIMESTAMP)
DEFAULT = Constant.new(:DEFAULT)
SQLTRUE = TRUE = BooleanConstant.new(true)
SQLFALSE = FALSE = BooleanConstant.new(false)
NULL = BooleanConstant.new(nil)
NOTNULL = NegativeBooleanConstant.new(nil)
end
class ComplexExpression
# A hash of the opposite for each constant, used for inverting constants.
CONSTANT_INVERSIONS = {Constants::TRUE=>Constants::FALSE, Constants::FALSE=>Constants::TRUE,
Constants::NULL=>Constants::NOTNULL, Constants::NOTNULL=>Constants::NULL}.freeze
end
# Represents a delayed evaluation, encapsulating a callable
# object which returns the value to use when called.
class DelayedEvaluation < GenericExpression
# A callable object that returns the value of the evaluation
# when called.
attr_reader :callable
# Set the callable object
def initialize(callable)
@callable = callable
freeze
end
# Call the underlying callable and return the result. If the
# underlying callable only accepts a single argument, call it
# with the given dataset.
def call(ds)
if @callable.respond_to?(:arity) && @callable.arity == 1
@callable.call(ds)
else
@callable.call
end
end
to_s_method :delayed_evaluation_sql
end
# Represents an SQL function call.
class Function < GenericExpression
WILDCARD = LiteralString.new('*').freeze
DISTINCT = ["DISTINCT ".freeze].freeze
COMMA_ARRAY = [LiteralString.new(', ').freeze].freeze
# The SQL function to call
attr_reader :name
# The array of arguments to pass to the function (may be blank)
attr_reader :args
# Options for this function
attr_reader :opts
# Set the name and args for the function
def initialize(name, *args)
_initialize(name, args, OPTS)
end
# Set the name, args, and options, for internal use only.
def self.new!(name, args, opts) # :nodoc:
allocate.send(:_initialize, name, args, opts)
end
# If no arguments are given, return a new function with the wildcard prepended to the arguments.
#
# Sequel.function(:count).* # count(*)
def *(ce=(arg=false;nil))
if arg == false
raise Error, "Cannot apply * to functions with arguments" unless args.empty?
with_opts(:"*"=>true)
else
super(ce)
end
end
# Return a new function with DISTINCT before the method arguments.
#
# Sequel.function(:count, :col).distinct # count(DISTINCT col)
def distinct
with_opts(:distinct=>true)
end
# Return a new function with FILTER added to it, for filtered
# aggregate functions:
#
# Sequel.function(:foo, :col).filter(a: 1) # foo(col) FILTER (WHERE (a = 1))
def filter(*args, &block)
if args.length == 1
args = args.first
else
args.freeze
end
with_opts(:filter=>args, :filter_block=>block)
end
# Return a function which will use LATERAL when literalized:
#
# Sequel.function(:foo, :col).lateral # LATERAL foo(col)
def lateral
with_opts(:lateral=>true)
end
# Return a new function where the function will be ordered. Only useful for aggregate
# functions that are order dependent.
#
# Sequel.function(:foo, :a).order(:a, Sequel.desc(:b)) # foo(a ORDER BY a, b DESC)
def order(*args)
with_opts(:order=>args.freeze)
end
# Return a new function with an OVER clause (making it a window function).
# See Sequel::SQL::Window for the list of options +over+ can receive.
#
# Sequel.function(:row_number).over(partition: :col) # row_number() OVER (PARTITION BY col)
def over(window=OPTS)
raise Error, "function already has a window applied to it" if opts[:over]
window = Window.new(window) unless window.is_a?(Window)
with_opts(:over=>window)
end
# Return a new function where the function name will be quoted if the database supports
# quoted functions:
#
# Sequel.function(:foo).quoted # "foo"()
def quoted
with_opts(:quoted=>true)
end
# Return a new function where the function name will not be quoted even
# if the database supports quoted functions:
#
# Sequel[:foo][:bar].function.unquoted # foo.bar()
def unquoted
with_opts(:quoted=>false)
end
# Return a new function that will use WITH ORDINALITY to also return
# a row number for every row the function returns:
#
# Sequel.function(:foo).with_ordinality # foo() WITH ORDINALITY
def with_ordinality
with_opts(:with_ordinality=>true)
end
# Return a new function that uses WITHIN GROUP ordered by the given expression,
# useful for ordered-set and hypothetical-set aggregate functions:
#
# Sequel.function(:rank, :a).within_group(:b, :c)
# # rank(a) WITHIN GROUP (ORDER BY b, c)
def within_group(*expressions)
with_opts(:within_group=>expressions.freeze)
end
to_s_method :function_sql
private
# Set name, args, and opts
def _initialize(name, args, opts)
@name = name
@args = args.freeze
@opts = opts.freeze
freeze
end
# Return a new function call with the given opts merged into the current opts.
def with_opts(opts)
self.class.new!(name, args, @opts.merge(opts))
end
end
class GenericExpression
include AliasMethods
include BooleanMethods
include CastMethods
include ComplexExpressionMethods
include InequalityMethods
include NumericMethods
include OrderMethods
include PatternMatchMethods
include StringMethods
include SubscriptMethods
end
# Represents an identifier (column, table, schema, etc.).
class Identifier < GenericExpression
include QualifyingMethods
# The identifier to reference
attr_reader :value
# Set the identifier to the given argument
def initialize(value)
@value = value
freeze
end
# Create a Function using this identifier as the functions name, with
# the given args.
def function(*args)
Function.new(self, *args)
end
to_s_method :quote_identifier, '@value'
end
# Represents an SQL JOIN clause, used for joining tables.
class JoinClause < Expression
# The type of join to do
attr_reader :join_type
# The expression representing the table/set related to the JOIN.
# Is an AliasedExpression if the JOIN uses an alias.
attr_reader :table_expr
# Create an object with the given join_type and table expression.
def initialize(join_type, table_expr)
@join_type = join_type
@table_expr = table_expr
freeze
end
# The table/set related to the JOIN, without any alias.
def table
if @table_expr.is_a?(AliasedExpression)
@table_expr.expression
else
@table_expr
end
end
# The table alias to use for the JOIN , or nil if the
# JOIN does not alias the table.
def table_alias
if @table_expr.is_a?(AliasedExpression)
@table_expr.alias
end
end
# The column aliases to use for the JOIN , or nil if the
# JOIN does not use a derived column list.
def column_aliases
if @table_expr.is_a?(AliasedExpression)
@table_expr.columns
end
end
to_s_method :join_clause_sql
end
# Represents an SQL JOIN clause with ON conditions.
class JoinOnClause < JoinClause
# The conditions for the join
attr_reader :on
# Create an object with the ON conditions and call super with the
# remaining args.
def initialize(on, *args)
@on = on
super(*args)
end
to_s_method :join_on_clause_sql
end
# Represents an SQL JOIN clause with USING conditions.
class JoinUsingClause < JoinClause
# The columns that appear in both tables that should be equal
# for the conditions to match.
attr_reader :using
# Create an object with the given USING conditions and call super
# with the remaining args.
def initialize(cols, *args)
@using = cols
super(*args)
end
to_s_method :join_using_clause_sql
end
# Represents a literal string with placeholders and arguments.
# This is necessary to ensure delayed literalization of the arguments
# required for the prepared statement support and for database-specific
# literalization.
class PlaceholderLiteralString < GenericExpression
# The literal string containing placeholders. This can also be an array
# of strings, where each arg in args goes between the string elements.
attr_reader :str
# The arguments that will be subsituted into the placeholders.
# Either an array of unnamed placeholders (which will be substituted in
# order for ? characters), or a hash of named placeholders (which will be
# substituted for :key phrases).
attr_reader :args
# Whether to surround the expression with parantheses
attr_reader :parens
# Create an object with the given string, placeholder arguments, and parens flag.
def initialize(str, args, parens=false)
@str = str
@args = args.is_a?(Array) && args.length == 1 && (v = args[0]).is_a?(Hash) ? v : args
@parens = parens
freeze
end
# Return a copy of the that will be surrounded by parantheses.
def with_parens
@parens ? self : self.class.new(@str, @args, true)
end
to_s_method :placeholder_literal_string_sql
end
# Subclass of +ComplexExpression+ where the expression results
# in a numeric value in SQL.
class NumericExpression < ComplexExpression
include BitwiseMethods
include NumericMethods
include InequalityMethods
# Always use + for + operator for NumericExpressions.
def +(ce)
NumericExpression.new(:+, self, ce)
end
# Return self instead of creating a new object to save on memory.
def sql_number
self
end
end
# Represents a column/expression to order the result set by.
class OrderedExpression < Expression
INVERT_NULLS = {:first=>:last, :last=>:first}.freeze
# The expression to order the result set by.
attr_reader :expression
# Whether the expression should order the result set in a descending manner
attr_reader :descending
# Whether to sort NULLS FIRST/LAST
attr_reader :nulls
# Set the expression and descending attributes to the given values.
# Options:
#
# :nulls :: Can be :first/:last for NULLS FIRST/LAST.
def initialize(expression, descending = true, opts=OPTS)
@expression = expression
@descending = descending
@nulls = opts[:nulls]
freeze
end
# Return a copy that is ordered ASC
def asc
OrderedExpression.new(@expression, false, :nulls=>@nulls)
end
# Return a copy that is ordered DESC
def desc
OrderedExpression.new(@expression, true, :nulls=>@nulls)
end
# Return an inverted expression, changing ASC to DESC and NULLS FIRST to NULLS LAST.
def invert
OrderedExpression.new(@expression, !@descending, :nulls=>INVERT_NULLS.fetch(@nulls, @nulls))
end
to_s_method :ordered_expression_sql
end
# Represents a qualified identifier (column with table or table with schema).
class QualifiedIdentifier < GenericExpression
include QualifyingMethods
# The table/schema qualifying the reference
attr_reader :table
# The column/table referenced
attr_reader :column
# Set the table and column to the given arguments
def initialize(table, column)
@table = convert_identifier(table)
@column = convert_identifier(column)
freeze
end
# Create a Function using this identifier as the functions name, with
# the given args.
def function(*args)
Function.new(self, *args)
end
to_s_method :qualified_identifier_sql, "@table, @column"
private
# Automatically convert SQL::Identifiers to strings
def convert_identifier(identifier)
case identifier
when SQL::Identifier
identifier.value.to_s
else
identifier
end
end
end
# Subclass of +ComplexExpression+ where the expression results
# in a text/string/varchar value in SQL.
class StringExpression < ComplexExpression
include StringMethods
include StringConcatenationMethods
include InequalityMethods
# Map of [regexp, case_insenstive] to +ComplexExpression+ operator symbol
LIKE_MAP = {[true, true]=>:'~*', [true, false]=>:~, [false, true]=>:ILIKE, [false, false]=>:LIKE}.freeze
LIKE_MAP.each_key(&:freeze)
# Creates a SQL pattern match exprssion. left (l) is the SQL string we
# are matching against, and ces are the patterns we are matching.
# The match succeeds if any of the patterns match (SQL OR).
#
# If a regular expression is used as a pattern, an SQL regular expression will be
# used, which is currently only supported on some databases. Be aware
# that SQL regular expression syntax is similar to ruby
# regular expression syntax, but it not exactly the same, especially for
# advanced regular expression features. Sequel just uses the source of the
# ruby regular expression verbatim as the SQL regular expression string.
#
# If any other object is used as a regular expression, the SQL LIKE operator will
# be used, and should be supported by most databases.
#
# The pattern match will be case insensitive if the last argument is a hash
# with a key of :case_insensitive that is not false or nil. Also,
# if a case insensitive regular expression is used (//i), that particular
# pattern which will always be case insensitive.
#
# StringExpression.like(:a, 'a%') # ("a" LIKE 'a%' ESCAPE '\')
# StringExpression.like(:a, 'a%', case_insensitive: true) # ("a" ILIKE 'a%' ESCAPE '\')
# StringExpression.like(:a, 'a%', /^a/i) # (("a" LIKE 'a%' ESCAPE '\') OR ("a" ~* '^a'))
def self.like(l, *ces)
l, lre, lci = like_element(l)
lci = (ces.last.is_a?(Hash) ? ces.pop : OPTS)[:case_insensitive] ? true : lci
ces.map! do |ce|
r, rre, rci = like_element(ce)
BooleanExpression.new(LIKE_MAP[[lre||rre, lci||rci]], l, r)
end
ces.length == 1 ? ces[0] : BooleanExpression.new(:OR, *ces)
end
# Returns a three element array, made up of:
# * The object to use
# * Whether it is a regular expression
# * Whether it is case insensitive
def self.like_element(re) # :nodoc:
if re.is_a?(Regexp)
[re.source, true, re.casefold?]
else
[re, false, false]
end
end
private_class_method :like_element
# Return self instead of creating a new object to save on memory.
def sql_string
self
end
end
# Represents an SQL array access, with multiple possible arguments.
class Subscript < GenericExpression
# The SQL array column
attr_reader :expression
alias f expression
# The array of subscripts to use (should be an array of numbers)
attr_reader :sub
# Set the array column and subscripts to the given arguments
def initialize(expression, sub)
@expression = expression
@sub = sub
freeze
end
# Create a new +Subscript+ appending the given subscript(s)
# to the current array of subscripts.
#
# Sequel[:a].sql_subscript(2) # a[2]
# Sequel[:a].sql_subscript(2) | 1 # a[2, 1]
def |(sub)
Subscript.new(@expression, @sub + Array(sub))
end
# Create a new +Subscript+ by accessing a subarray of a multidimensional
# array.
#
# Sequel[:a].sql_subscript(2) # a[2]
# Sequel[:a].sql_subscript(2)[1] # a[2][1]
def [](sub)
Subscript.new(self, Array(sub))
end
to_s_method :subscript_sql
end
# Represents an SQL value list (IN/NOT IN predicate value). Added so it is possible to deal with a
# ruby array of two element arrays as an SQL value list instead of an ordered
# hash-like conditions specifier.
class ValueList < ::Array
# Show that this is a value list and not just an array
def inspect
"#<#{self.class} #{super}>"
end
end
# The purpose of the +VirtualRow+ class is to allow the easy creation of SQL identifiers and functions,
# in a way that leads to more compact code.
#
# An instance of this class is yielded to the block supplied to Dataset#where, Dataset#order, and Dataset#select
# (and the other methods that accept a block and pass it to one of those methods).
# If the block doesn't take an argument, the block is instance_execed in the context of
# an instance of this class.
#
# +VirtualRow+ uses +method_missing+ to return either an +Identifier+, +Function+
# depending on how it is called.
#
# +Function+ :: Returned if any arguments are supplied, using the method name
# as the function name, and the arguments as the function arguments.
# +Identifier+ :: Returned otherwise, using the method name.
#
# If splitting symbols has been enabled (not the default), then method calls without
# arguments will return +QualifiedIdentifier+ instances if the method call includes a
# double underscore.
#
# Examples:
#
# ds = DB[:t]
#
# # Argument yielded to block
# ds.where{|r| r.name < 2} # SELECT * FROM t WHERE (name < 2)
#
# # Block without argument (instance_exec)
# ds.where{name < 2} # SELECT * FROM t WHERE (name < 2)
#
# # Functions
# ds.where{is_active(1, 'arg2')} # SELECT * FROM t WHERE is_active(1, 'arg2')
# ds.select{version.function} # SELECT version() FROM t
# ds.select{count.function.*} # SELECT count(*) FROM t
# ds.select{count(col1).distinct} # SELECT count(DISTINCT col1) FROM t
#
# # Math Operators
# ds.select{|o| o.+(1, :a).as(:b)} # SELECT (1 + a) AS b FROM t
# ds.select{|o| o.-(2, :a).as(:b)} # SELECT (2 - a) AS b FROM t
# ds.select{|o| o.*(3, :a).as(:b)} # SELECT (3 * a) AS b FROM t
# ds.select{|o| o./(4, :a).as(:b)} # SELECT (4 / a) AS b FROM t
#
# # Boolean Operators
# ds.where{|o| o.&({a: 1}, :b)} # SELECT * FROM t WHERE ((a = 1) AND b)
# ds.where{|o| o.|({a: 1}, :b)} # SELECT * FROM t WHERE ((a = 1) OR b)
# ds.where{|o| o.~(a: 1)} # SELECT * FROM t WHERE (a != 1)
# ds.where{|o| o.~(a: 1, b: 2)} # SELECT * FROM t WHERE ((a != 1) OR (b != 2))
#
# # Inequality Operators
# ds.where{|o| o.>(1, :a)} # SELECT * FROM t WHERE (1 > a)
# ds.where{|o| o.<(2, :a)} # SELECT * FROM t WHERE (2 < a)
# ds.where{|o| o.>=(3, :a)} # SELECT * FROM t WHERE (3 >= a)
# ds.where{|o| o.<=(4, :a)} # SELECT * FROM t WHERE (4 <= a)
#
# For a more detailed explanation, see the {Virtual Rows guide}[rdoc-ref:doc/virtual_rows.rdoc].
class VirtualRow < BasicObject
include OperatorBuilders
%w'> < >= <='.each do |op|
class_eval(<<-END, __FILE__, __LINE__ + 1)
def #{op}(*args)
SQL::BooleanExpression.new(:#{op}, *args)
end
END
end
def initialize
freeze
end
m = Module.new do
# Return an +Identifier+, +QualifiedIdentifier+, or +Function+, depending
# on arguments and whether a block is provided. Does not currently call the block.
# See the class level documentation.
def method_missing(m, *args)
if args.empty?
if Sequel.split_symbols?
table, column = m.to_s.split('__', 2)
column ? QualifiedIdentifier.new(table, column) : Identifier.new(m)
else
Identifier.new(m)
end
else
Function.new(m, *args)
end
end
end
include m
Sequel::VIRTUAL_ROW = new
end
# A +Window+ is part of a window function specifying the window over which a window function operates.
#
# Sequel::SQL::Window.new(partition: :col1)
# # (PARTITION BY col1)
# Sequel::SQL::Window.new(partition: [:col2, :col3])
# # (PARTITION BY col2, col3)
#
# Sequel::SQL::Window.new(order: :col4)
# # (ORDER BY col4)
# Sequel::SQL::Window.new(order: [:col5, Sequel.desc(:col6)])
# # (ORDER BY col5, col6 DESC)
#
# Sequel::SQL::Window.new(partition: :col7, frame: :all)
# # (PARTITION BY col7 ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)
# Sequel::SQL::Window.new(partition: :col7, frame: :rows)
# # (PARTITION BY col7 ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
# Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: current})
# # (PARTITION BY col7 RANGE CURRENT ROW)
# Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 1, end: 1})
# # (PARTITION BY col7 RANGE BETWEEN 1 PRECEDING AND 1 FOLLOWING)
# Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 2, end: [1, :preceding]})
# # (PARTITION BY col7 RANGE BETWEEN 2 PRECEDING AND 1 PRECEDING)
# Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: 1, end: [2, :following]})
# # (PARTITION BY col7 RANGE BETWEEN 1 FOLLOWING AND 2 FOLLOWING)
# Sequel::SQL::Window.new(partition: :col7, frame: {type: :range, start: :preceding, exclude: :current})
# # (PARTITION BY col7 RANGE UNBOUNDED PRECEDING EXCLUDE CURRENT ROW)
#
# Sequel::SQL::Window.new(window: :named_window) # you can create a named window with Dataset#window
# # (named_window)
class Window < Expression
# The options for this window. Options currently supported:
# :frame :: if specified, should be :all, :rows, :range, :groups, a String, or a Hash.
# :all :: Always operates over all rows in the partition
# :rows :: Includes rows in the partition up to and including the current row
# :range, :groups :: Includes rows in the partition up to and including the current group
# String :: Used as literal SQL code, try to avoid
# Hash :: Hash of options for the frame:
# :type :: The type of frame, must be :rows, :range, or :groups (required)
# :start :: The start of the frame (required). Possible values:
# :preceding :: UNBOUNDED PRECEDING
# :following :: UNBOUNDED FOLLOWING
# :current :: CURRENT ROW
# String, Numeric, or Cast :: Used as the offset of rows/values preceding
# Array :: Must have two elements, with first element being String, Numeric, or
# Cast and second element being :preceding or :following
# :end :: The end of the frame. Can be left out. If present, takes the same values as
# :start, except that when a String, Numeric, or Hash, it is used as the offset
# for rows following
# :exclude :: Which rows to exclude. Possible values are :current, :ties, :group
# :no_others.
# :order :: order on the column(s) given
# :partition :: partition/group on the column(s) given
# :window :: base results on a previously specified named window
attr_reader :opts
# Set the options to the options given
def initialize(opts=OPTS)
@opts = opts.frozen? ? opts : Hash[opts].freeze
freeze
end
to_s_method :window_sql, '@opts'
end
# A +Wrapper+ is a simple way to wrap an existing object so that it supports
# the Sequel DSL.
class Wrapper < GenericExpression
# The underlying value wrapped by this object.
attr_reader :value
# Set the value wrapped by the object.
def initialize(value)
@value = value
freeze
end
to_s_method :literal, '@value'
end
end
# +LiteralString+ is used to represent literal SQL expressions. A
# +LiteralString+ is copied verbatim into an SQL statement. Instances of
# +LiteralString+ can be created by calling Sequel.lit.
class LiteralString
include SQL::OrderMethods
include SQL::ComplexExpressionMethods
include SQL::BooleanMethods
include SQL::NumericMethods
include SQL::StringMethods
include SQL::InequalityMethods
include SQL::AliasMethods
include SQL::CastMethods
# Show that the current string is a literal string in addition to the output.
def inspect
"#<#{self.class} #{super}>"
end
# Return self if no args are given, otherwise return a SQL::PlaceholderLiteralString
# with the current string and the given args.
def lit(*args)
args.empty? ? self : SQL::PlaceholderLiteralString.new(self, args)
end
# Convert a literal string to a SQL::Blob.
def to_sequel_blob
SQL::Blob.new(self)
end
end
include SQL::Constants
extend SQL::Builders
extend SQL::OperatorBuilders
end