# frozen-string-literal: true module Sequel # The +ASTTransformer+ class is designed to handle the abstract syntax trees # that Sequel uses internally and produce modified copies of them. By itself # it only produces a straight copy. It's designed to be subclassed and have # subclasses returned modified copies of the specific nodes that need to # be modified. class ASTTransformer # Return +obj+ or a potentially transformed version of it. def transform(obj) v(obj) end private # Recursive version that handles all of Sequel's internal object types # and produces copies of them. def v(o) case o when Symbol, Numeric, String, Class, TrueClass, FalseClass, NilClass o when Array o.map{|x| v(x)} when Hash h = {} o.each{|k, val| h[v(k)] = v(val)} h when SQL::NumericExpression if o.op == :extract o.class.new(o.op, o.args[0], v(o.args[1])) else o.class.new(o.op, *v(o.args)) end when SQL::ComplexExpression o.class.new(o.op, *v(o.args)) when SQL::Identifier SQL::Identifier.new(v(o.value)) when SQL::QualifiedIdentifier SQL::QualifiedIdentifier.new(v(o.table), v(o.column)) when SQL::OrderedExpression SQL::OrderedExpression.new(v(o.expression), o.descending, :nulls=>o.nulls) when SQL::AliasedExpression SQL::AliasedExpression.new(v(o.expression), o.alias, o.columns) when SQL::CaseExpression args = [v(o.conditions), v(o.default)] args << v(o.expression) if o.expression? SQL::CaseExpression.new(*args) when SQL::Cast SQL::Cast.new(v(o.expr), o.type) when SQL::Function h = {} o.opts.each do |k, val| h[k] = v(val) end SQL::Function.new!(o.name, v(o.args), h) when SQL::Subscript SQL::Subscript.new(v(o.f), v(o.sub)) when SQL::Window opts = o.opts.dup opts[:partition] = v(opts[:partition]) if opts[:partition] opts[:order] = v(opts[:order]) if opts[:order] SQL::Window.new(opts) when SQL::PlaceholderLiteralString args = if o.args.is_a?(Hash) h = {} o.args.each{|k,val| h[k] = v(val)} h else v(o.args) end SQL::PlaceholderLiteralString.new(o.str, args, o.parens) when SQL::JoinOnClause SQL::JoinOnClause.new(v(o.on), o.join_type, v(o.table_expr)) when SQL::JoinUsingClause SQL::JoinUsingClause.new(v(o.using), o.join_type, v(o.table_expr)) when SQL::JoinClause SQL::JoinClause.new(o.join_type, v(o.table_expr)) when SQL::DelayedEvaluation SQL::DelayedEvaluation.new(lambda{|ds| v(o.call(ds))}) when SQL::Wrapper SQL::Wrapper.new(v(o.value)) else o end end end # Handles qualifying existing datasets, so that unqualified columns # in the dataset are qualified with a given table name. class Qualifier < ASTTransformer # Store the dataset to use as the basis for qualification, # and the table used to qualify unqualified columns. def initialize(ds, table) @ds = ds @table = table end private # Turn SQL::Identifiers and symbols that aren't implicitly # qualified into SQL::QualifiedIdentifiers. For symbols that # are not implicitly qualified by are implicitly aliased, return an # SQL::AliasedExpressions with a qualified version of the symbol. def v(o) case o when Symbol t, column, aliaz = @ds.send(:split_symbol, o) if t o elsif aliaz SQL::AliasedExpression.new(SQL::QualifiedIdentifier.new(@table, SQL::Identifier.new(column)), aliaz) else SQL::QualifiedIdentifier.new(@table, o) end when SQL::Identifier SQL::QualifiedIdentifier.new(@table, o) when SQL::QualifiedIdentifier, SQL::JoinClause # Return these directly, so we don't accidentally qualify symbols in them. o else super end end end # +Unbinder+ is used to take a dataset filter and return a modified version # that unbinds already bound values and returns a dataset with bound value # placeholders and a hash of bind values. You can then prepare the dataset # and use the bound variables to execute it with the same values. # # This class only does a limited form of unbinding where the variable names # and values can be associated unambiguously. The only cases it handles # are SQL::ComplexExpression with an operator in +UNBIND_OPS+, a # first argument that's an instance of a member of +UNBIND_KEY_CLASSES+, and # a second argument that's an instance of a member of +UNBIND_VALUE_CLASSES+. # # So it can handle cases like: # # DB.filter(:a=>1).exclude(:b=>2).where{c > 3} # # But it cannot handle cases like: # # DB.filter(:a + 1 < 0) class Unbinder < ASTTransformer # The SQL::ComplexExpression operates that will be considered # for transformation. UNBIND_OPS = [:'=', :'!=', :<, :>, :<=, :>=] # The key classes (first argument of the ComplexExpression) that will # considered for transformation. UNBIND_KEY_CLASSES = [Symbol, SQL::Identifier, SQL::QualifiedIdentifier] # The value classes (second argument of the ComplexExpression) that # will be considered for transformation. UNBIND_VALUE_CLASSES = [Numeric, String, Date, Time] # The hash of bind variables that were extracted from the dataset filter. attr_reader :binds # Intialize an empty +binds+ hash. def initialize @binds = {} end private # Create a suitable bound variable key for the object, which should be # an instance of one of the +UNBIND_KEY_CLASSES+. def bind_key(obj) case obj when Symbol obj when String obj.to_sym when SQL::Identifier bind_key(obj.value) when SQL::QualifiedIdentifier :"#{bind_key(obj.table)}.#{bind_key(obj.column)}" else raise Error, "unhandled object in Sequel::Unbinder#bind_key: #{obj}" end end # Handle SQL::ComplexExpression instances with suitable ops # and arguments, substituting the value with a bound variable placeholder # and assigning it an entry in the +binds+ hash with a matching key. def v(o) if o.is_a?(SQL::ComplexExpression) && UNBIND_OPS.include?(o.op) l, r = o.args l = l.value if l.is_a?(Sequel::SQL::Wrapper) r = r.value if r.is_a?(Sequel::SQL::Wrapper) if UNBIND_KEY_CLASSES.any?{|c| l.is_a?(c)} && UNBIND_VALUE_CLASSES.any?{|c| r.is_a?(c)} && !r.is_a?(LiteralString) key = bind_key(l) if (old = binds[key]) && old != r raise UnbindDuplicate, "two different values for #{key.inspect}: #{[r, old].inspect}" end binds[key] = r SQL::ComplexExpression.new(o.op, l, :"$#{key}") else super end else super end end end end