lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-0.1.0 vs lib/torque/postgresql/auxiliary_statement.rb in torque-postgresql-0.1.1

- old
+ new

@@ -1,21 +1,20 @@ +require_relative 'auxiliary_statement/settings' + module Torque module PostgreSQL class AuxiliaryStatement - # The settings collector class - Settings = Collector.new(:attributes, :join, :join_type, :query) - 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, :join_attributes, :table, :query, - :join_type].each do |attribute| + [:exposed_attributes, :selected_attributes, :query, :join_attributes, + :join_type, :requires].each do |attribute| define_method(attribute) do setup instance_variable_get("@#{attribute}") end end @@ -25,32 +24,73 @@ const = name.to_s.camelize << '_' << self.name.demodulize return base.const_get(const) if base.const_defined?(const) 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] + return klass.new(options) unless klass.nil? + raise ArgumentError, <<-MSG.strip + There's no '#{statement}' auxiliary statement defined for #{base.class.name}. + MSG + 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 + # 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 if setup? + @query = nil end # Get the base class associated to this statement def base self.parent end + # Get the name of the base class + def base_name + base.name + 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 + # Get the arel table of the base class def base_table - base.arel_table + @base_table ||= base.arel_table end # Get the arel table of the query def query_table - query.arel_table + @query_table ||= query.arel_table end + # Project a column on a given table, or use the column table + def project(column, arel_table = nil) + if column.to_s.include?('.') + table_name, column = column.to_s.split('.') + arel_table = Arel::Table.new(table_name) + end + + arel_table ||= table + arel_table[column.to_s] + end + private # Just setup the class if it's not setup def setup setup! unless setup? end @@ -60,11 +100,11 @@ defined?(@query) && @query end # Setup the class def setup! - # attributes key + # attributes key: # Provides a map of attributes to be exposed to the main query. # # For instace, if the statement query has an 'id' column that you # want it to be accessed on the main query as 'item_id', # you can use: @@ -72,11 +112,11 @@ # # If its statement has more tables, and you want to expose those # fields, then: # attributes 'table.name': :item_name # - # join_type key + # join_type key: # Changes the type of the join and set the constraints # # The left side of the hash is the source table column, the right # side is the statement table column, now it's only accepting '=' # constraints @@ -85,106 +125,193 @@ # join 'post.id': :'user.last_post_id' # # It's possible to change the default type of join # join :left, id: :user_id # - # join key + # join key: # Changes the type of the join # - # query key + # query key: # Save the query command to be performand - settings = Settings.new + # + # requires key: + # Indicates dependencies with another statements + # + # polymorphic key: + # Indicates a polymorphic relationship, with will affect the way the + # auto join works, by giving a polymorphic connection + settings = Settings.new(self) @config.call(settings) - table_name = self.name.demodulize.split('_').first.underscore @join_type = settings.join_type || :inner - @table = Arel::Table.new(table_name) + @requires = Array[settings.requires].flatten.compact @query = settings.query + # Manually set the query table when it's not an relation query + @query_table = settings.query_table unless relation_query?(@query) + + # Reset all the used attributes @selected_attributes = [] @exposed_attributes = [] @join_attributes = [] - # Iterate the attributes settings - # Attributes (left => right) - # left -> query.selected_attributes AS right - # right -> table.exposed_attributes - settings.attributes.each do |left, right| + # Generate attributes projections + attributes_projections(settings.attributes) + + # 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 + 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 - # Iterate the join settings - # Join (left => right) - # left -> base.join_attributes.eq(right) - # right -> table.selected_attributes - if settings.join.nil? - check_auto_join - else - settings.join.each do |left, right| - @selected_attributes << project(right, query_table) - @join_attributes << project(left, base_table).eq(project(right)) - end + # 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 end # Check if it's possible to identify the connection between the main # query and the statement query - def check_auto_join - foreign_key = base.name.foreign_key + # + # 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) - primary_key = project(base.primary_key, base_table) - @selected_attributes << project(foreign_key, query_table) - @join_attributes << primary_key.eq(project(foreign_key)) + joins_projections(base.primary_key => foreign_key) + if polymorphic.present? + foreign_type = project(foreign_key.gsub(/_id$/, '_type'), query_table) + @selected_attributes << foreign_type + @join_attributes << foreign_type.eq(base_name) + end end end - - # Project a column on a given table, or use the column table - def project(column, table = @table) - if column.to_s.include?('.') - table, column = column.split('.') - table = Arel::Table.new(table) - end - - table[column] - end end + delegate :exposed_attributes, :join_attributes, :selected_attributes, :join_type, :table, + :query_table, :base_table, :requires, :project, :relation_query?, to: :class + # Start a new auxiliary statement giving extra options def initialize(*args) - @options = args.extract_options! + options = args.extract_options! + uses_key = Torque::PostgreSQL.config.auxiliary_statement.send_arguments_key + + @join = options.fetch(:join, {}) + @uses = options.fetch(uses_key, []) + @select = options.fetch(:select, {}) + @join_type = options.fetch(:join_type, join_type) end # Get the columns that will be selected for this statement def columns - self.class.exposed_attributes + exposed_attributes + @select.values.map(&method(:project)) end # Build the statement on the given arel and return the WITH statement - def build_arel(arel) - klass = self.class - query = klass.query.select(*klass.selected_attributes) + def build_arel(arel, base) + list = [] + # Process dependencies + if requires.present? + requires.each do |dependent| + next if base.auxiliary_statements.key?(dependent) + + instance = AuxiliaryStatement.instantiate(dependent, base) + base.auxiliary_statements[dependent] = instance + list << instance.build_arel(arel, base) + end + end + # Build the join for this statement - arel.join(klass.table, arel_join).on(*klass.join_attributes) + arel.join(table, arel_join).on(*join_columns) # Return the subquery for this statement - Arel::Nodes::As.new(klass.table, query.send(:build_arel)) + list << Arel::Nodes::As.new(table, mount_query) end private # Get the class of the join on arel def arel_join - case @options.fetch(:join_type, self.class.join_type) + 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.gsub(/^ +| +$|\n/, '') + raise ArgumentError, <<-MSG.strip 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 + uses = @uses.map(&klass.parent.connection.method(:quote)) + + # Call a proc to get the query + if query.respond_to?(:call) + query = query.call(*uses) + uses = [] + end + + # Prepare the query depending on its type + if query.is_a?(String) + Arel::Nodes::SqlLiteral.new("(#{query})" % uses) + elsif relation_query?(query) + query.select(*select_columns).send(:build_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}. + 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) + end + 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) end end end end