lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-2.3.0 vs lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-2.4.0

- old
+ new

@@ -1,42 +1,46 @@ # frozen_string_literal: true require_relative 'auxiliary_statement/settings' +require_relative 'auxiliary_statement/recursive' module Torque module PostgreSQL class AuxiliaryStatement TABLE_COLUMN_AS_STRING = /\A(?:"?(\w+)"?\.)?"?(\w+)"?\z/.freeze class << self - attr_reader :config + attr_reader :config, :table_name # 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)) + + base.const_set(const, Class.new(self)).tap do |klass| + klass.instance_variable_set(:@table_name, name.to_s) + end end # Create a new instance of an auxiliary statement - def instantiate(statement, base, options = nil) + def instantiate(statement, base, **options) 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? + return klass.new(**options) unless klass.nil? 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 = [], join_sources = []) - klass = instantiate(statement, base, options) + def build(statement, base, bound_attributes = [], join_sources = [], **options) + klass = instantiate(statement, base, **options) result = klass.build(base) bound_attributes.concat(klass.bound_attributes) join_sources.concat(klass.join_sources) result @@ -54,11 +58,11 @@ end # 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) + klass = Class.new(self) if block_given? klass.instance_variable_set(:@table_name, table_or_settings) klass.configurator(block) elsif relation_query?(table_or_settings) @@ -87,41 +91,36 @@ # 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) + recursive = self < AuxiliaryStatement::Recursive + settings = Settings.new(base, instance, recursive) settings.instance_exec(settings, &@config) settings end # Get the arel version of the statement table def table @table ||= ::Arel::Table.new(table_name) end - - # Get the name of the table of the configurated statement - def table_name - @table_name ||= self.name.demodulize.split('_').first.underscore - end end delegate :config, :table, :table_name, :relation, :configure, :relation_query?, to: :class attr_reader :bound_attributes, :join_sources # Start a new auxiliary statement giving extra options - def initialize(*args) - options = args.extract_options! + def initialize(*, **options) args_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key @join = options.fetch(:join, {}) @args = options.fetch(args_key, {}) @where = options.fetch(:where, {}) @select = options.fetch(:select, {}) - @join_type = options.fetch(:join_type, nil) + @join_type = options[:join_type] @bound_attributes = [] @join_sources = [] end @@ -129,48 +128,46 @@ def build(base) @bound_attributes.clear @join_sources.clear # Prepare all the data for the statement - prepare(base) + prepare(base, configure(base, self)) # Add the join condition to the list @join_sources << build_join(base) # Return the statement with its dependencies [@dependencies, ::Arel::Nodes::As.new(table, build_query(base))] end private # Setup the statement using the class configuration - def prepare(base) - settings = configure(base, self) + def prepare(base, settings) requires = Array.wrap(settings.requires).flatten.compact @dependencies = ensure_dependencies(requires, base).flatten.compact @join_type ||= settings.join_type || :inner @query = settings.query # Call a proc to get the real query - if @query.methods.include?(:call) + if @query.respond_to?(:call) call_args = @query.try(:arity) === 0 ? [] : [OpenStruct.new(@args)] @query = @query.call(*call_args) @args = [] end - # Manually set the query table when it's not an relation query - @query_table = settings.query_table unless relation_query?(@query) + # Merge select attributes provided on the instance creation @select = settings.attributes.merge(@select) if settings.attributes.present? # 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 + break name if @query.klass.eql?(reflection.klass) end end end # Build the string or arel query @@ -232,19 +229,10 @@ 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 - # 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 - # Build the join based on the join type arel_join.new(table, table.create_on(conditions)) end # Get the class of the join on arel @@ -261,32 +249,43 @@ end end # 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 + # Add the columns necessary for the join + list = @join_sources.each_with_object(@select) do |join, hash| + join.right.expr.children.each do |item| + hash[item.left.name] = nil if item.left.relation.eql?(table) + end end + + # Add select columns to the query and get exposed columns + list.map do |left, right| + base.select_extra_values += [table[right.to_s]] unless right.nil? + next unless query_table + + col = project(left, query_table) + right.nil? ? col : col.as(right.to_s) + end.compact end # 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] + (list + with_options).map do |name, options| + dependent_klass = base.model.auxiliary_statements_list[name] raise ArgumentError, <<-MSG.squish if dependent_klass.nil? - The '#{dependent}' auxiliary statement dependency can't found on + The '#{name}' auxiliary statement dependency can't found on #{self.class.name}. MSG next if base.auxiliary_statements_values.any? do |cte| cte.is_a?(dependent_klass) end - AuxiliaryStatement.build(dependent, base, options, bound_attributes, join_sources) + options ||= {} + AuxiliaryStatement.build(name, base, bound_attributes, join_sources, **options) end end # Project a column on a given table, or use the column table def project(column, arel_table = nil)