Compare commits

...

2 Commits

Author SHA1 Message Date
Dylan Knutson
8f610b8fa7 allow redefinition of methods 2025-07-26 00:37:14 +00:00
Dylan Knutson
6df1fe8053 remove demo_functionality 2025-07-25 17:19:22 -07:00
4 changed files with 85 additions and 199 deletions

View File

@@ -1,188 +0,0 @@
#!/usr/bin/env ruby
require "bundler/setup"
require_relative "lib/has_aux_table"
Bundler.require(:default, :development)
ActiveRecord::Base.establish_connection(
adapter: "sqlite3",
database: ":memory:"
)
# ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Schema.define do
# Create base table and aux table in the same block
create_base_table :users do |t|
t.create_aux :fa do |t|
t.string :username, index: true
t.string :url_name, index: true
end
t.timestamps
end
# Change base table later to add an aux table
change_base_table :users do |t|
t.create_aux :e621 do |t|
t.integer :e621_id, index: true
t.string :username, index: true
end
end
create_base_table :posts do |t|
t.timestamp :posted_at, index: true
t.timestamps
end
# Directly create aux tables
create_aux_table :posts, :fa do |t|
t.references :creator,
foreign_key: {
to_table: :users_fa_aux,
primary_key: :base_table_id
}
t.integer :category
t.integer :fa_id, index: true
t.string :title
t.string :species
end
create_aux_table :posts, :e621 do |t|
t.references :creator,
foreign_key: {
to_table: :users_e621_aux,
primary_key: :base_table_id
}
t.integer :e621_id, index: true
t.string :md5, index: true
end
create_table :user_post_fav_joins, id: false do |t|
t.references :user, null: false, foreign_key: { to_table: :users }
t.references :post, null: false, foreign_key: { to_table: :posts }
t.timestamps
end
end
class UserPostFavJoin < ActiveRecord::Base
belongs_to :user
belongs_to :post
end
class User < ActiveRecord::Base
include HasAuxTable
has_many :user_post_fav_joins, dependent: :destroy
has_many :favorite_posts, through: :user_post_fav_joins, source: :post
has_many :created_posts, class_name: "Post", inverse_of: :creator
end
class FaUser < User
aux_table :fa
validates :url_name, presence: true
has_many :created_posts, class_name: "FaPost", inverse_of: :creator
end
class E621User < User
aux_table :e621
validates :username, presence: true
has_many :created_posts, class_name: "E621Post", inverse_of: :creator
end
class Post < ActiveRecord::Base
include HasAuxTable
has_many :user_post_fav_joins, dependent: :destroy
has_many :favoriting_users, through: :user_post_fav_joins, source: :user
validates_presence_of :posted_at
end
class FaPost < Post
aux_table :fa
belongs_to :creator, class_name: "FaUser", inverse_of: :created_posts
validates :fa_id, presence: true, uniqueness: true
validates :title, presence: true
enum :category, { image: 0, video: 1, text: 2 }
end
class E621Post < Post
aux_table :e621
validates :e621_id, presence: true
belongs_to :creator, class_name: "E621User", inverse_of: :created_posts
end
fa_user = FaUser.create!(username: "Alice", url_name: "alice")
fa_user_id = fa_user.id
raise if fa_user.id.nil?
raise unless fa_user.persisted?
raise unless fa_user.username == "Alice"
raise unless fa_user.url_name == "alice"
fa_user.reload
fa_user_found = FaUser.find_by(username: "Alice")
raise unless fa_user_found.id == fa_user_id
raise unless fa_user_found.username == "Alice"
raise unless fa_user_found.url_name == "alice"
fa_user_found = FaUser.find(fa_user_id)
raise unless fa_user_found.id == fa_user_id
raise unless fa_user_found.username == "Alice"
raise unless fa_user_found.url_name == "alice"
fa_post =
fa_user.created_posts.create!(
fa_id: 12_345,
title: "Test Post",
species: "Cat",
posted_at: 1.day.ago
)
raise unless fa_post.persisted?
raise unless fa_post.creator == fa_user
raise unless fa_post.creator_id == fa_user.id
fa_posts_all = FaPost.all.to_a
raise unless fa_posts_all.size == 1
raise unless fa_posts_all.first.creator == fa_user
raise unless fa_posts_all.first.creator_id == fa_user.id
raise unless FaPost.exists?(fa_id: 12_345)
raise if FaPost.exists?(fa_id: 12_346)
posts = fa_user.created_posts
fa_post2 = posts.find_by(fa_id: 12_345)
raise unless fa_post2.id == fa_post.id
e621_user = E621User.create!(username: "bob", e621_id: 67_890)
raise unless e621_user.persisted?
raise unless e621_user.username == "bob"
raise unless e621_user.e621_id == 67_890
e621_user_found = E621User.find_by(username: "bob")
raise unless e621_user_found.id == e621_user.id
raise unless e621_user_found.username == "bob"
raise unless e621_user_found.e621_id == 67_890
e621_post =
e621_user.created_posts.create!(
e621_id: 102_938,
md5: "DEADBEEF" * 4,
posted_at: 2.weeks.ago
)
raise unless e621_post.persisted?
raise unless e621_post.creator == e621_user
raise unless e621_post.creator_id == e621_user.id
e621_user.favorite_posts << e621_post
raise unless e621_user.favorite_posts.size == 1
raise unless e621_user.favorite_posts.first == e621_post
raise unless e621_user.favorite_posts.first.id == e621_post.id
e621_fav_joins = e621_user.user_post_fav_joins
raise unless e621_fav_joins.size == 1
raise unless e621_fav_joins.first.user == e621_user
raise unless e621_fav_joins.first.post == e621_post
raise unless e621_fav_joins.first.post_id == e621_post.id
e621_posts_all = E621Post.all.to_a
raise unless e621_posts_all.size == 1
raise unless e621_posts_all.first.creator == e621_user
raise unless e621_posts_all.first.creator_id == e621_user.id

View File

@@ -51,18 +51,26 @@ module HasAuxTable
end
# Main DSL method for defining auxiliary tables
sig { params(aux_name: T.any(String, Symbol)).returns(AuxTableConfig) }
def aux_table(aux_name)
sig do
params(
aux_name: T.any(String, Symbol),
allow_redefining: T.nilable(T.any(Symbol, T::Array[Symbol]))
).returns(AuxTableConfig)
end
def aux_table(aux_name, allow_redefining: nil)
@aux_table_configs ||=
T.let({}, T.nilable(T::Hash[Symbol, AuxTableConfig]))
allow_redefining = [allow_redefining].flatten.compact
aux_name = aux_name.to_sym
if @aux_table_configs.key?(aux_name)
Kernel.raise ArgumentError,
"Auxiliary '#{aux_name}' on #{self.name} (table '#{self.table_name}') already exists"
end
@aux_table_configs[aux_name] = config = generate_aux_config(aux_name)
@aux_table_configs[aux_name] = config =
generate_aux_config(aux_name, allow_redefining)
setup_attribute_types_hook!(config)
setup_load_schema_hook!(config)
setup_initialize_hook!(config)
@@ -85,8 +93,12 @@ module HasAuxTable
private
# Generate auxiliary model class dynamically
sig { params(aux_name: Symbol).returns(AuxTableConfig) }
def generate_aux_config(aux_name)
sig do
params(aux_name: Symbol, allow_redefining: T::Array[Symbol]).returns(
AuxTableConfig
)
end
def generate_aux_config(aux_name, allow_redefining)
main_class = T.cast(self, T.class_of(ActiveRecord::Base))
main_table = main_class.table_name
@@ -108,7 +120,8 @@ module HasAuxTable
AuxTableConfig.from_models(
main_class:,
aux_class:,
aux_association_name:
aux_association_name:,
allow_redefining:
)
# Define the association back to the specific STI subclass
@@ -217,7 +230,8 @@ module HasAuxTable
# Generate attribute accessors for each auxiliary column
config.aux.columns_hash.each do |column_name, column|
column_name = column_name.to_sym
if self.method_defined?(column_name.to_sym)
if self.method_defined?(column_name.to_sym) &&
!config.allow_redefining.include?(column_name.to_sym)
raise "invariant: method #{column_name} already defined"
end
[

View File

@@ -9,15 +9,21 @@ module HasAuxTable
const :aux_association_name, Symbol
const :main, ModelClassHelper
const :aux, ModelClassHelper
const :allow_redefining, T::Array[Symbol]
sig do
params(
main_class: T.class_of(ActiveRecord::Base),
aux_class: T.class_of(ActiveRecord::Base),
aux_association_name: Symbol
aux_association_name: Symbol,
allow_redefining: T::Array[Symbol]
).returns(AuxTableConfig)
end
def self.from_models(main_class:, aux_class:, aux_association_name:)
def self.from_models(
main_class:,
aux_class:,
aux_association_name:,
allow_redefining: []
)
primary_key = aux_class.primary_key
aux_rejected_column_names = [
primary_key,
@@ -37,7 +43,8 @@ module HasAuxTable
ModelClassHelper.new(
klass: aux_class,
rejected_column_names: aux_rejected_column_names
)
),
allow_redefining:
)
end

View File

@@ -1182,4 +1182,57 @@ RSpec.describe HasAuxTable do
expect(patient.doctors.count).to eq(1)
end
end
describe "allowing redefining of methods" do
it "allows method redefining with `allow_method_redefinition`" do
ActiveRecord::Schema.define do
create_base_table :test_model2s do |t|
t.string :on_base
t.create_aux :specific do |t|
t.string :on_aux
end
end
end
class TestModel2 < ActiveRecord::Base
include HasAuxTable
def on_base
"on_base #{super} #{id}"
end
end
expect {
class TestModel2A < TestModel2
aux_table :specific, allow_redefining: :on_base
def on_base
"2a_on_base_override #{super}"
end
def on_aux
"2a_on_aux_override #{super}"
end
end
}.not_to raise_error
expect {
class TestModel2B < TestModel2
aux_table :specific, allow_redefining: :on_base
end
}.not_to raise_error
base_model = TestModel2.create!(on_base: "base")
expect(base_model.on_base).to eq("on_base base #{base_model.id}")
specific_a = TestModel2A.create!(on_base: "2a_base", on_aux: "2a_aux")
expect(specific_a.on_base).to eq(
"2a_on_base_override on_base 2a_base #{specific_a.id}"
)
expect(specific_a.on_aux).to eq("2a_on_aux_override 2a_aux")
specific_b = TestModel2B.create!(on_base: "2b_base", on_aux: "2b_aux")
expect(specific_b.on_base).to eq("on_base 2b_base #{specific_b.id}")
expect(specific_b.on_aux).to eq("2b_aux")
end
end
end