fix model saving

This commit is contained in:
Dylan Knutson
2025-07-14 07:36:50 +00:00
parent 30b017906f
commit 4933e63f31
4 changed files with 150 additions and 172 deletions

View File

@@ -8,6 +8,7 @@ require "active_support"
require "active_support/concern"
require "active_model/attribute_set"
require_relative "aux_table/auto_join_queries"
require_relative "aux_table/migration_extensions"
module ActiveRecord
module AuxTable
@@ -87,7 +88,7 @@ module ActiveRecord
aux_table_configurations[table_name] = config =
generate_aux_model_class(table_name)
setup_schema_loading_hook(table_name)
setup_auto_join_queries(table_name)
setup_auto_join_queries(config)
config
end
@@ -160,6 +161,19 @@ module ActiveRecord
aux_config.define_aux_attribute_delegate(self, "#{column_name}=")
end
%i[save save!].each do |method_name|
save_method = self.instance_method(method_name)
self.define_method(method_name) do |*args|
result = save_method.bind(self).call(*args)
result &&=
self
.association(aux_config.aux_association_name)
.target
.send(method_name)
result
end
end
%i[_read_attribute read_attribute].each do |method_name|
# override _read_attribute to delegate auxiliary columns to the auxiliary table
read_attribute_method = self.instance_method(method_name)
@@ -240,19 +254,17 @@ module ActiveRecord
sig { params(table_name: Symbol).returns(AuxTableConfig) }
def generate_aux_model_class(table_name)
# Generate class name (e.g., :car_aux => "CarAux")
class_name = table_name.to_s.camelize
aux_class_name = table_name.to_s.camelize
aux_association_name = table_name.to_s.singularize.to_sym
# Ensure the class name doesn't conflict with existing constants
if Object.const_defined?(class_name)
Kernel.raise ArgumentError, "Class #{class_name} already exists"
if Object.const_defined?(aux_class_name)
Kernel.raise ArgumentError, "Class #{aux_class_name} already exists"
end
# Get the base class name for the foreign key (e.g., Vehicle -> vehicle_id)
# In STI, all subclasses share the same table, so we need the base class
base_class = self.base_class
base_class_name = base_class.name.underscore
foreign_key = "#{base_class_name}_id".to_sym
foreign_key = "base_table_id".to_sym
# Get the current class for the association
main_class = self
@@ -260,10 +272,11 @@ module ActiveRecord
primary_key = :id
# Create the auxiliary model class
model_class =
aux_class =
Class.new(ActiveRecord::Base) do
# Set the table name
self.table_name = table_name.to_s
self.primary_key = "base_table_id"
# Define the association back to the specific STI subclass
# Foreign key points to base STI table (e.g., vehicle_id)
@@ -280,19 +293,26 @@ module ActiveRecord
# set up has_one association to the auxiliary table
self.has_one(
aux_association_name,
class_name: class_name,
class_name: aux_class_name,
foreign_key:,
primary_key:,
inverse_of: main_association_name,
autosave: true
inverse_of: main_association_name
# autosave: true
)
after_create do
aux_model = association(aux_association_name).target
aux_model.base_table_id = self.id
aux_model.save!
true
end
# Set the constant to make the class accessible
Object.const_set(class_name, model_class)
Object.const_set(aux_class_name, aux_class)
AuxTableConfig.new(
table_name:,
model_class:,
model_class: aux_class,
aux_association_name:,
main_association_name:,
foreign_key:,

View File

@@ -4,13 +4,12 @@
module ActiveRecord
module AuxTable
module AutoJoinQueries
def setup_auto_join_queries(aux_table_name)
association_name = aux_table_name.to_s.singularize.to_sym
def setup_auto_join_queries(aux_config)
ActiveRecord::AuxTable::AutoJoinQueries.setup_query_extensions!(
self,
association_name
aux_config,
with_bind_attribute: false
)
self
end
# Get all aux column names for this model
@@ -19,12 +18,13 @@ module ActiveRecord
return [] unless config&.model_class
config.model_class.column_names.reject do |col|
%w[id created_at updated_at].include?(col) || col.end_with?("_id")
%w[base_table_id created_at updated_at].include?(col)
end
end
# Split conditions into main table and aux table conditions
def split_conditions(conditions, aux_columns)
def split_conditions(conditions, association_name)
aux_columns = self.get_aux_column_names(association_name)
main_conditions = {}
aux_conditions = {}
@@ -39,14 +39,19 @@ module ActiveRecord
[main_conditions, aux_conditions]
end
def self.setup_query_extensions!(on, association_name)
def self.setup_query_extensions!(
on,
aux_config,
with_bind_attribute: true
)
association_name = aux_config.aux_association_name
on.define_singleton_method(:where) do |*args|
if args.first.is_a?(Hash)
relation = self.eager_load(association_name)
self.apply_split_conditions!(relation, args)
ActiveRecord::AuxTable::AutoJoinQueries.setup_query_extensions!(
relation,
association_name
aux_config
)
relation
else
@@ -54,6 +59,35 @@ module ActiveRecord
end
end
unscoped_method = on.method(:unscoped)
on.define_singleton_method(:unscoped) do
relation = unscoped_method.call
ActiveRecord::AuxTable::AutoJoinQueries.setup_query_extensions!(
relation,
aux_config
)
relation
end
if with_bind_attribute
bind_attribute_method = on.method(:bind_attribute)
on.define_singleton_method(:bind_attribute) do |name, value, &block|
aux_column_names = self.get_aux_column_names(association_name)
if aux_column_names.include?(name.to_s)
attr = aux_config.model_class.arel_table[name]
bind =
aux_config.model_class.predicate_builder.build_bind_attribute(
attr.name,
value
)
block.call(attr, bind)
else
bind_attribute_method.call(name, value, &block)
end
end
end
on.define_singleton_method(:find_by) do |*args|
relation = self.eager_load(association_name)
self.apply_split_conditions!(relation, args)
@@ -62,9 +96,8 @@ module ActiveRecord
on.define_singleton_method(:apply_split_conditions!) do |relation, args|
conditions = args.first
aux_columns = self.get_aux_column_names(association_name)
main_conditions, aux_conditions =
self.split_conditions(conditions, aux_columns)
self.split_conditions(conditions, association_name)
relation.where!(main_conditions) if main_conditions.any?
if aux_conditions.any?
relation.where!(association_name => aux_conditions)
@@ -74,6 +107,21 @@ module ActiveRecord
on.define_singleton_method(:find) do |*args|
self.eager_load(association_name).find(*args)
end
on.define_singleton_method(:exists?) do |*args|
conditions = args.first || {}
main_conditions, aux_conditions =
self.split_conditions(conditions, association_name)
puts "checking with conditions: #{main_conditions} / #{aux_conditions}"
relation = self.select("1").joins(association_name)
relation.where!(main_conditions) if main_conditions.any?
if aux_conditions.any?
relation.where!(association_name => aux_conditions)
end
relation.first.present?
end
end
end
end

View File

@@ -0,0 +1,26 @@
# typed: false
# frozen_string_literal: true
module ActiveRecord
module AuxTable
module MigrationExtensions
def create_aux_table(base_table, name, **options)
aux_table_name = "#{base_table}_#{name}_aux"
create_table(aux_table_name, id: false, **options) do |t|
t.references :base_table,
primary_key: true,
null: false,
foreign_key: {
to_table: base_table,
validate: true
}
yield t
end
end
end
end
end
class ActiveRecord::Migration
include ActiveRecord::AuxTable::MigrationExtensions
end