# frozen_string_literal: true
module Torque
module PostgreSQL
module Base
extend ActiveSupport::Concern
##
# :singleton-method: schema
# :call-seq: schema
#
# The schema to which the table belongs to.
included do
mattr_accessor :belongs_to_many_required_by_default, instance_accessor: false
class_attribute :schema, instance_writer: false
end
module ClassMethods
delegate :distinct_on, :with, :itself_only, :cast_records, to: :all
# Make sure that table name is an instance of TableName class
def reset_table_name
self.table_name = TableName.new(self, super)
end
# Whenever the base model is inherited, add a list of auxiliary
# statements like the one that loads inherited records' relname
def inherited(subclass)
super
subclass.class_attribute(:auxiliary_statements_list)
subclass.auxiliary_statements_list = {}
record_class = ActiveRecord::Relation._record_class_attribute
# Define helper methods to return the class of the given records
subclass.auxiliary_statement record_class do |cte|
ActiveSupport::Deprecation.warn(<<~MSG.squish)
Inheritance does not use this auxiliary statement and it can be removed.
You can replace it with `model.select_extra_values << 'tableoid::regclass'`.
MSG
pg_class = ::Arel::Table.new('pg_class')
arel_query = ::Arel::SelectManager.new(pg_class)
arel_query.project(pg_class['oid'], pg_class['relname'].as(record_class.to_s))
cte.query 'pg_class', arel_query.to_sql
cte.attributes col(record_class) => record_class
cte.join tableoid: :oid
end
# Define the dynamic attribute that returns the same information as
# the one provided by the auxiliary statement
subclass.dynamic_attribute(record_class) do
klass = self.class
next klass.table_name unless klass.physically_inheritances?
query = klass.unscoped.where(subclass.primary_key => id)
query.pluck(klass.arel_table['tableoid'].cast('regclass')).first
end
end
# Specifies a one-to-many association. The following methods for
# retrieval and query of collections of associated objects will be
# added:
#
# +collection+ is a placeholder for the symbol passed as the +name+
# argument, so belongs_to_many :tags would add among others
# tags.empty?.
#
# [collection]
# Returns a Relation of all the associated objects.
# An empty Relation is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by adding their ids to
# the array of ids on the parent object.
# Note that this operation instantly fires update SQL without waiting
# for the save or update call on the parent object, unless the parent
# object is a new record.
# This will also run validations and callbacks of associated
# object(s).
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their
# ids from the list on the parent object.
# Objects will be in addition destroyed if they're associated with
# dependent: :destroy, and deleted if they're associated
# with dependent: :delete_all.
# [collection.destroy(object, ...)]
# Removes one or more objects from the collection by running
# destroy on each record, regardless of any dependent option,
# ensuring callbacks are run. They will also be removed from the list
# on the parent object.
# [collection=objects]
# Replaces the collections content by deleting and adding objects as
# appropriate.
# [collection_singular_ids]
# Returns an array of the associated objects' ids
# [collection_singular_ids=ids]
# Replace the collection with the objects identified by the primary
# keys in +ids+. This method loads the models and calls
# collection=. See above.
# [collection.clear]
# Removes every object from the collection. This destroys the
# associated objects if they are associated with
# dependent: :destroy, deletes them directly from the
# database if dependent: :delete_all, otherwise just remove
# them from the list on the parent object.
# [collection.empty?]
# Returns +true+ if there are no associated objects.
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
# Finds an associated object according to the same rules as
# ActiveRecord::FinderMethods#find.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
# Uses the same rules as ActiveRecord::FinderMethods#exists?.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have
# been instantiated with +attributes+ and linked to this object by
# adding its +id+ to the list after saving.
# [collection.create(attributes = {})]
# Returns a new object of the collection type that has been
# instantiated with +attributes+, linked to this object by adding its
# +id+ to the list after performing the save (if it passed the
# validation).
# [collection.create!(attributes = {})]
# Does the same as collection.create, but raises
# ActiveRecord::RecordInvalid if the record is invalid.
# [collection.reload]
# Returns a Relation of all of the associated objects, forcing a
# database read. An empty Relation is returned if none are found.
#
# === Example
#
# A Video class declares belongs_to_many :tags,
# which will add:
# * Video#tags (similar to Tag.where([id] && tag_ids))
# * Video#tags<<
# * Video#tags.delete
# * Video#tags.destroy
# * Video#tags=
# * Video#tag_ids
# * Video#tag_ids=
# * Video#tags.clear
# * Video#tags.empty?
# * Video#tags.size
# * Video#tags.find
# * Video#tags.exists?(name: 'ACME')
# * Video#tags.build
# * Video#tags.create
# * Video#tags.create!
# * Video#tags.reload
# The declaration can also include an +options+ hash to specialize the
# behavior of the association.
#
# === Options
# [:class_name]
# Specify the class name of the association. Use it only if that name
# can't be inferred from the association name. So belongs_to_many
# :tags will by default be linked to the +Tag+ class, but if the
# real class name is +SpecialTag+, you'll have to specify it with this
# option.
# [:foreign_key]
# Specify the foreign key used for the association. By default this is
# guessed to be the name of this class in lower-case and "_ids"
# suffixed. So a Video class that makes a #belongs_to_many association
# with Tag will use "tag_ids" as the default :foreign_key.
#
# It is a good idea to set the :inverse_of option as well.
# [:primary_key]
# Specify the name of the column to use as the primary key for the
# association. By default this is +id+.
# [:dependent]
# Controls what happens to the associated objects when their owner is
# destroyed. Note that these are implemented as callbacks, and Rails
# executes callbacks in order. Therefore, other similar callbacks may
# affect the :dependent behavior, and the :dependent
# behavior may affect other callbacks.
# [:touch]
# If true, the associated objects will be touched (the updated_at/on
# attributes set to current time) when this record is either saved or
# destroyed. If you specify a symbol, that attribute will be updated
# with the current time in addition to the updated_at/on attribute.
# Please note that with touching no validation is performed and only
# the +after_touch+, +after_commit+ and +after_rollback+ callbacks are
# executed.
# [:optional]
# When set to +true+, the association will not have its presence
# validated.
# [:required]
# When set to +true+, the association will also have its presence
# validated. This will validate the association itself, not the id.
# You can use +:inverse_of+ to avoid an extra query during validation.
# NOTE: required is set to false by default and is
# deprecated. If you want to have association presence validated,
# use required: true.
# [:default]
# Provide a callable (i.e. proc or lambda) to specify that the
# association should be initialized with a particular record before
# validation.
# [:inverse_of]
# Specifies the name of the #has_many association on the associated
# object that is the inverse of this #belongs_to_many association.
# See ActiveRecord::Associations::ClassMethods's overview on
# Bi-directional associations for more detail.
#
# Option examples:
# belongs_to_many :tags, dependent: :nullify
# belongs_to_many :tags, required: true, touch: true
# belongs_to_many :tags, default: -> { Tag.default }
def belongs_to_many(name, scope = nil, **options, &extension)
klass = Associations::Builder::BelongsToMany
reflection = klass.build(self, name, scope, options, &extension)
::ActiveRecord::Reflection.add_reflection(self, name, reflection)
end
# Allow extra keyword arguments to be sent to +InsertAll+
def upsert_all(attributes, **xargs)
xargs = xargs.merge(on_duplicate: :update)
::ActiveRecord::InsertAll.new(self, attributes, **xargs).execute
end
protected
# Allow optional select attributes to be loaded manually when they are
# not present. This is associated with auxiliary statement, which
# permits columns that can be loaded through CTEs, be loaded
# individually for a single record
#
# For instance, if you have a statement that can load an user's last
# comment content, by querying the comments using an auxiliary
# statement.
# subclass.auxiliary_statement :last_comment do |cte|
# cte.query Comment.order(:user_id, id: :desc)
# .distinct_on(:user_id)
# cte.attributes col(:content) => :last_comment
# cte.join_type :left
# end
#
# In case you don't use 'with(:last_comment)', you can do the
# following.
# dynamic_attribute(:last_comment) do
# comments.order(id: :desc).first.content
# end
#
# This means that any auxiliary statements can have their columns
# granted even when they are not used
def dynamic_attribute(name, &block)
define_method(name) do
return read_attribute(name) if has_attribute?(name)
result = self.instance_exec(&block)
type_klass = ActiveRecord::Type.respond_to?(:default_value) \
? ActiveRecord::Type.default_value \
: self.class.connection.type_map.send(:default_value)
@attributes[name.to_s] = ActiveRecord::Relation::QueryAttribute.new(
name.to_s, result, type_klass,
)
read_attribute(name)
end
end
# Creates a new auxiliary statement (CTE) under the base class
# 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:
# attributes id: :item_id, 'MAX(id)' => :max_id,
# col(:id).minimum => :min_id
#
# If its statement has more tables, and you want to expose those
# fields, then:
# attributes 'table.name': :item_name
#
# 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
# join id: :user_id
# join id: :'user.id'
# join 'post.id': :'user.last_post_id'
#
# It's possible to change the default type of join
# join :left, id: :user_id
#
# join key:
# Changes the type of the join
#
# query key:
# Save the query command to be performand
#
# 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
def auxiliary_statement(table, &block)
klass = AuxiliaryStatement.lookup(table, self)
auxiliary_statements_list[table.to_sym] = klass
klass.configurator(block)
end
alias cte auxiliary_statement
# Creates a new recursive auxiliary statement (CTE) under the base
# Very similar to the regular auxiliary statement, but with two-part
# query where one is executed first and the second recursively
def recursive_auxiliary_statement(table, &block)
klass = AuxiliaryStatement::Recursive.lookup(table, self)
auxiliary_statements_list[table.to_sym] = klass
klass.configurator(block)
end
alias recursive_cte recursive_auxiliary_statement
end
end
::ActiveRecord::Base.include(Base)
end
end