lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-0.2.16 vs lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-1.0.0
- old
+ new
@@ -4,40 +4,43 @@
module PostgreSQL
class AuxiliaryStatement
TABLE_COLUMN_AS_STRING = /\A(?:"?(\w+)"?\.)?"?(\w+)"?\z/.freeze
class << self
- # These attributes require that the class is setup
- #
- # The attributes separation means
- # exposed_attributes -> Will be projected to the main query
- # selected_attributes -> Will be selected on the configurated query
- # join_attributes -> Will be used to join the the queries
- [:exposed_attributes, :selected_attributes, :query, :join_attributes,
- :join_type, :requires].each do |attribute|
- define_method(attribute) do
- setup
- instance_variable_get("@#{attribute}")
- end
- end
+ attr_reader :config
# Find or create the class that will handle statement
def lookup(name, base)
const = name.to_s.camelize << '_' << self.name.demodulize
return base.const_get(const, false) if base.const_defined?(const, false)
base.const_set(const, Class.new(AuxiliaryStatement))
end
# Create a new instance of an auxiliary statement
def instantiate(statement, base, options = nil)
- klass = base.auxiliary_statements_list[statement]
+ klass = while base < ActiveRecord::Base
+ list = base.auxiliary_statements_list
+ break list[statement] if list.present? && list.key?(statement)
+
+ base = base.superclass
+ end
+
return klass.new(options) unless klass.nil?
- raise ArgumentError, <<-MSG.strip
+ raise ArgumentError, <<-MSG.squish
There's no '#{statement}' auxiliary statement defined for #{base.class.name}.
MSG
end
+ # Fast access to statement build
+ def build(statement, base, options = nil, bound_attributes = [])
+ klass = instantiate(statement, base, options)
+ result = klass.build(base)
+
+ bound_attributes.concat(klass.bound_attributes)
+ result
+ end
+
# Identify if the query set may be used as a relation
def relation_query?(obj)
!obj.nil? && obj.respond_to?(:ancestors) && \
obj.ancestors.include?(ActiveRecord::Base)
end
@@ -45,25 +48,49 @@
# Identify if the query set may be used as arel
def arel_query?(obj)
!obj.nil? && obj.is_a?(::Arel::SelectManager)
end
- # Set a configuration block, if the class is already set up, just clean
- # the query and wait it to be setup again
- def configurator(block)
- @config = block
- @query = nil
+ # A way to create auxiliary statements outside of models configurations,
+ # being able to use on extensions
+ def create(table_or_settings, &block)
+ klass = Class.new(AuxiliaryStatement)
+
+ if block_given?
+ klass.instance_variable_set(:@table_name, table_or_settings)
+ klass.configurator(block)
+ elsif relation_query?(table_or_settings)
+ klass.configurator(query: table_or_settings)
+ else
+ klass.configurator(table_or_settings)
+ end
+
+ klass
end
- # Get the base class associated to this statement
- def base
- self.parent
+ # Set a configuration block or static hash
+ def configurator(config)
+ if config.is_a?(Hash)
+ # Map the aliases
+ config[:attributes] = config.delete(:select) if config.key?(:select)
+
+ # Create the struct that mocks a configuration result
+ config = OpenStruct.new(config)
+ table_name = config[:query]&.klass&.name&.underscore
+ instance_variable_set(:@table_name, table_name)
+ end
+
+ @config = config
end
- # Get the name of the base class
- def base_name
- base.name
+ # Run a configuration block or get the static configuration
+ def configure(base, instance)
+ return @config unless @config.respond_to?(:call)
+
+ settings = Settings.new(base, instance)
+ settings.instance_exec(settings, &@config)
+ settings
end
# Get the arel version of the statement table
def table
@table ||= ::Arel::Table.new(table_name)
@@ -71,229 +98,205 @@
# Get the name of the table of the configurated statement
def table_name
@table_name ||= self.name.demodulize.split('_').first.underscore
end
+ end
- # Get the arel table of the base class
- def base_table
- @base_table ||= base.arel_table
- end
+ delegate :config, :table, :table_name, :relation, :configure, :relation_query?,
+ to: :class
- # Get the arel table of the query
- def query_table
- @query_table ||= query.arel_table
- end
+ attr_reader :bound_attributes
- # Project a column on a given table, or use the column table
- def project(column, arel_table = nil)
- if column.respond_to?(:as)
- return column
- elsif (as_string = TABLE_COLUMN_AS_STRING.match(column.to_s))
- column = as_string[2]
- arel_table = ::Arel::Table.new(as_string[1]) unless as_string[1].nil?
- end
+ # Start a new auxiliary statement giving extra options
+ def initialize(*args)
+ options = args.extract_options!
+ args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
- arel_table ||= table
- arel_table[column.to_s]
- end
+ @join = options.fetch(:join, {})
+ @args = options.fetch(args_key, {})
+ @where = options.fetch(:where, {})
+ @select = options.fetch(:select, {})
+ @join_type = options.fetch(:join_type, nil)
+ @bound_attributes = []
+ end
- private
- # Just setup the class if it's not setup
- def setup
- setup! unless setup?
- end
+ # Build the statement on the given arel and return the WITH statement
+ def build(base)
+ prepare(base)
- # Check if the class is setup
- def setup?
- defined?(@query) && @query
- end
+ # Add the join condition to the list
+ base.joins_values += [build_join(base)]
- # Setup the class
- def setup!
- settings = Settings.new(self)
- settings.instance_exec(settings, &@config)
+ # Return the statement with its dependencies
+ [@dependencies, ::Arel::Nodes::As.new(table, build_query(base))]
+ end
- @join_type = settings.join_type || :inner
- @requires = Array[settings.requires].flatten.compact
- @query = settings.query
+ private
+ # Setup the statement using the class configuration
+ def prepare(base)
+ settings = configure(base, self)
+ requires = Array.wrap(settings.requires).flatten.compact
+ @dependencies = ensure_dependencies(requires, base).flatten.compact
- # Manually set the query table when it's not an relation query
- @query_table = settings.query_table unless relation_query?(@query)
+ @join_type ||= settings.join_type || :inner
+ @query = settings.query
- # Reset all the used attributes
- @selected_attributes = []
- @exposed_attributes = []
- @join_attributes = []
+ # Call a proc to get the real query
+ if @query.methods.include?(:call)
+ call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)]
+ @query = @query.call(*call_args)
+ @args = []
+ end
- # Generate attributes projections
- attributes_projections(settings.attributes)
+ # Manually set the query table when it's not an relation query
+ @query_table = settings.query_table unless relation_query?(@query)
+ @select = settings.attributes.merge(@select) if settings.attributes.present?
- # Generate join projections
- if settings.join.present?
- joins_projections(settings.join)
- elsif relation_query?(@query)
- check_auto_join(settings.polymorphic)
- else
- raise ArgumentError, <<-MSG.strip.gsub(/\n +/, ' ')
- You must provide the join columns when using '#{query.class.name}'
- as a query object on #{self.class.name}.
- MSG
+ # Merge join settings
+ if settings.join.present?
+ @join = settings.join.merge(@join)
+ elsif settings.through.present?
+ @association = settings.through.to_s
+ elsif relation_query?(@query)
+ @association = base.reflections.find do |name, reflection|
+ break name if @query.klass.eql? reflection.klass
end
end
+ end
- # Iterate the attributes settings
- # Attributes (left => right)
- # left -> query.selected_attributes AS right
- # right -> table.exposed_attributes
- def attributes_projections(list)
- list.each do |left, right|
- @exposed_attributes << project(right)
- @selected_attributes << project(left, query_table).as(right.to_s)
- end
- end
+ # Build the string or arel query
+ def build_query(base)
+ # Expose columns and get the list of the ones for select
+ columns = expose_columns(base, @query.try(:arel_table))
- # Iterate the join settings
- # Join (left => right)
- # left -> base.join_attributes.eq(right)
- # right -> table.selected_attributes
- def joins_projections(list)
- list.each do |left, right|
- @selected_attributes << project(right, query_table)
- @join_attributes << project(left, base_table).eq(project(right))
- end
+ # Prepare the query depending on its type
+ if @query.is_a?(String)
+ args = @args.map{ |k, v| [k, base.connection.quote(v)] }.to_h
+ ::Arel.sql("(#{@query})" % args)
+ elsif relation_query?(@query)
+ @query = @query.where(@where) if @where.present?
+ @bound_attributes.concat(@query.send(:bound_attributes))
+ @query.select(*columns).arel
+ else
+ raise ArgumentError, <<-MSG.squish
+ Only String and ActiveRecord::Base objects are accepted as query objects,
+ #{@query.class.name} given for #{self.class.name}.
+ MSG
end
+ end
- # Check if it's possible to identify the connection between the main
- # query and the statement query
- #
- # First, identify the foreign key column name, then check if it exists
- # on the query and then create the projections
- def check_auto_join(polymorphic)
- foreign_key = (polymorphic.present? ? polymorphic : base_name)
- foreign_key = foreign_key.to_s.foreign_key
- if query.columns_hash.key?(foreign_key)
- joins_projections(base.primary_key => foreign_key)
- if polymorphic.present?
- foreign_type = foreign_key.gsub(/_id$/, '_type')
- @selected_attributes << project(foreign_type, query_table)
- @join_attributes << project(foreign_type).eq(base_name)
- end
- end
- end
- end
+ # Build the join statement that will be sent to the main arel
+ def build_join(base)
+ conditions = table.create_and([])
+ builder = base.predicate_builder
+ foreign_table = base.arel_table
- delegate :exposed_attributes, :join_attributes, :selected_attributes, :join_type, :table,
- :query_table, :base_table, :requires, :project, :relation_query?, to: :class
+ # Check if it's necessary to load the join from an association
+ if @association.present?
+ association = base.reflections[@association]
- # Start a new auxiliary statement giving extra options
- def initialize(*args)
- options = args.extract_options!
- args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key
+ # Require source of a through reflection
+ if association.through_reflection?
+ base.joins(association.source_reflection_name)
- @join = options.fetch(:join, {})
- @args = options.fetch(args_key, {})
- @select = options.fetch(:select, {})
- @join_type = options.fetch(:join_type, join_type)
- end
+ # Changes the base of the connection to the reflection table
+ builder = association.klass.predicate_builder
+ foreign_table = ::Arel::Table.new(association.plural_name)
+ end
- # Get the columns that will be selected for this statement
- def columns
- exposed_attributes + @select.values.map(&method(:project))
- end
+ # Add the scopes defined by the reflection
+ if association.respond_to?(:join_scope)
+ args = [@query.arel_table]
+ args << base if association.method(:join_scope).arity.eql?(2)
+ @query.merge(association.join_scope(*args))
+ end
- # Build the statement on the given arel and return the WITH statement
- def build_arel(arel, base)
- # Build the join for this statement
- arel.join(table, arel_join).on(*join_columns)
+ # Add the join constraints
+ constraint = association.build_join_constraint(table, foreign_table)
+ constraint = constraint.children if constraint.is_a?(::Arel::Nodes::And)
+ conditions.children.concat(Array.wrap(constraint))
+ end
- # Return the subquery for this statement
- ::Arel::Nodes::As.new(table, mount_query)
- end
+ # Build all conditions for the join on statement
+ @join.inject(conditions.children) do |arr, (left, right)|
+ left = project(left, foreign_table)
+ item = right.is_a?(Symbol) ? project(right).eq(left) : builder.build(left, right)
+ arr.push(item)
+ end
- # Get the bound attributes from statement qeury
- def bound_attributes
- return [] unless relation_query?(self.class.query)
- self.class.query.send(:bound_attributes)
- end
+ # Raise an error when there's no join conditions
+ raise ArgumentError, <<-MSG.squish if conditions.children.empty?
+ You must provide the join columns when using '#{@query.class.name}'
+ as a query object on #{self.class.name}.
+ MSG
- # Ensure that all the dependencies are loaded in the base relation
- def ensure_dependencies!(base)
- requires.each do |dependent|
- dependent_klass = base.model.auxiliary_statements_list[dependent]
- next if base.auxiliary_statements_values.any? do |cte|
- cte.is_a?(dependent_klass)
+ # Expose join columns
+ if relation_query?(@query)
+ query_table = @query.arel_table
+ conditions.children.each do |item|
+ @query.select_values += [query_table[item.left.name]] \
+ if item.left.relation.eql?(table)
+ end
end
- instance = AuxiliaryStatement.instantiate(dependent, base)
- instance.ensure_dependencies!(base)
- base.auxiliary_statements_values += [instance]
+ # Build the join based on the join type
+ arel_join.new(table, table.create_on(conditions))
end
- end
- private
-
# Get the class of the join on arel
def arel_join
case @join_type
when :inner then ::Arel::Nodes::InnerJoin
when :left then ::Arel::Nodes::OuterJoin
when :right then ::Arel::Nodes::RightOuterJoin
when :full then ::Arel::Nodes::FullOuterJoin
else
- raise ArgumentError, <<-MSG.strip
+ raise ArgumentError, <<-MSG.squish
The '#{@join_type}' is not implemented as a join type.
MSG
end
end
- # Mount the query base on it's class
- def mount_query
- klass = self.class
- query = klass.query
- args = @args
-
- # Call a proc to get the query
- if query.methods.include?(:call)
- call_args = query.try(:arity) === 0 ? [] : [OpenStruct.new(args)]
- query = query.call(*call_args)
- args = []
+ # Mount the list of selected attributes
+ def expose_columns(base, query_table = nil)
+ # Add select columns to the query and get exposed columns
+ @select.map do |left, right|
+ base.select_extra_values += [table[right.to_s]]
+ project(left, query_table).as(right.to_s) if query_table
end
+ end
- # Prepare the query depending on its type
- if query.is_a?(String)
- args = args.map{ |k, v| [k, klass.parent.connection.quote(v)] }.to_h
- ::Arel::Nodes::SqlLiteral.new("(#{query})" % args)
- elsif relation_query?(query)
- query.select(*select_columns).arel
- else
- raise ArgumentError, <<-MSG.strip
- Only String and ActiveRecord::Base objects are accepted as query objects,
- #{query.class.name} given for #{self.class.name}.
+ # Ensure that all the dependencies are loaded in the base relation
+ def ensure_dependencies(list, base)
+ with_options = list.extract_options!.to_a
+ (list + with_options).map do |dependent, options|
+ dependent_klass = base.model.auxiliary_statements_list[dependent]
+
+ raise ArgumentError, <<-MSG.squish if dependent_klass.nil?
+ The '#{dependent}' auxiliary statement dependency can't found on
+ #{self.class.name}.
MSG
- end
- end
- # Mount the list of join attributes with the additional ones
- def join_columns
- join_attributes + @join.map do |left, right|
- if right.is_a?(Symbol)
- project(left, base_table).eq(project(right))
- else
- project(left).eq(right)
+ next if base.auxiliary_statements_values.any? do |cte|
+ cte.is_a?(dependent_klass)
end
+
+ AuxiliaryStatement.build(dependent, base, options, bound_attributes)
end
end
- # Mount the list of selected attributes with the additional ones
- def select_columns
- selected_attributes + @select.map do |left, right|
- project(left, query_table).as(right.to_s)
- end + @join.map do |left, right|
- column = right.is_a?(Symbol) ? right : left
- project(column, query_table)
+ # Project a column on a given table, or use the column table
+ def project(column, arel_table = nil)
+ if column.respond_to?(:as)
+ return column
+ elsif (as_string = TABLE_COLUMN_AS_STRING.match(column.to_s))
+ column = as_string[2]
+ arel_table = ::Arel::Table.new(as_string[1]) unless as_string[1].nil?
end
- end
+ arel_table ||= table
+ arel_table[column.to_s]
+ end
end
end
end