Compare commits

...

2 Commits

Author SHA1 Message Date
Dylan Knutson
8e1c193801 spec refactor 2025-07-18 05:51:24 +00:00
Dylan Knutson
ca9eae138a relax ancestor requirements 2025-07-18 05:31:42 +00:00
5 changed files with 179 additions and 170 deletions

View File

@@ -32,12 +32,7 @@ module HasAuxTable
module ClassMethods
extend T::Sig
extend T::Helpers
requires_ancestor { Kernel }
requires_ancestor { T.class_of(BasicObject) }
requires_ancestor { T.class_of(ActiveRecord::Base) }
requires_ancestor { ActiveRecord::ModelSchema::ClassMethods }
requires_ancestor { ActiveRecord::Associations::ClassMethods }
requires_ancestor { ActiveModel::Attributes::ClassMethods }
include RelationExtensions

View File

@@ -2,4 +2,4 @@
.
--ignore=/tmp/
--ignore=/vendor/bundle
--enable-experimental-requires-ancestor
--enable-experimental-requires-ancestor

View File

@@ -1,155 +1,8 @@
# typed: false
# frozen_string_literal: true
RSpec.describe HasAuxTable do
LOG_QUERIES = false
# Helper method to count queries
def count_queries(&block)
query_count = 0
query_callback =
lambda { |name, start, finish, message_id, values| query_count += 1 }
ActiveSupport::Notifications.subscribed(
query_callback,
"sql.active_record"
) do
if LOG_QUERIES
old_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::DEBUG
end
block.call
ActiveRecord::Base.logger = old_logger if LOG_QUERIES
end
query_count
end
before(:all) do
# Set up the database schema for testing
ActiveRecord::Schema.define do
create_table :vehicle_lots do |t|
t.string :name
t.timestamps
end
create_base_table :vehicles do |t|
t.string :name
t.references :vehicle_lot, foreign_key: { to_table: :vehicle_lots }
t.timestamps
t.create_aux :car do |t|
t.string :fuel_type
t.decimal :engine_size, precision: 3, scale: 1
end
t.create_aux :boat do |t|
t.boolean :only_freshwater
end
end
create_base_table :people do |t|
t.string :name
t.timestamps
t.create_aux :driver do |t|
t.integer :license_number
t.references :car,
null: false,
foreign_key: {
to_table: :vehicles_car_aux,
primary_key: :base_table_id
}
end
t.create_aux :captain do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
t.create_aux :passenger do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
end
create_base_table :utensils do |t|
t.string :name
t.string :material
t.timestamps
t.create_aux :fork do |t|
t.integer :num_tongs
end
t.create_aux :spoon do |t|
t.string :curvature
end
end
end
class Vehicle < ActiveRecord::Base
include HasAuxTable
belongs_to :vehicle_lot
end
class VehicleLot < ActiveRecord::Base
has_many :vehicles
end
class Car < Vehicle
aux_table :car
has_many :drivers, inverse_of: :car
end
class Boat < Vehicle
aux_table :boat
has_many :passengers, inverse_of: :boat
belongs_to :captain, inverse_of: :boat
end
class Person < ActiveRecord::Base
include HasAuxTable
self.table_name = "people"
end
class Driver < Person
aux_table :driver
belongs_to :car, inverse_of: :drivers
end
class Captain < Person
aux_table :captain
has_one :boat, inverse_of: :captain
end
class Passenger < Person
aux_table :passenger
belongs_to :boat, inverse_of: :passengers
end
module Kitchen
class Utensil < ActiveRecord::Base
include HasAuxTable
end
class Fork < Utensil
aux_table :fork
end
class Spoon < Utensil
aux_table :spoon
end
end
end
before(:all) { SpecHelper.initialize_spec_schema! }
# Car class will be defined after schema setup
@@ -270,7 +123,7 @@ RSpec.describe HasAuxTable do
it "allows saving the model with auxiliary columns" do
car = Car.create!(name: "Honda Civic")
num_queries =
count_queries do
SpecHelper.count_queries do
car.fuel_type = "hybrid"
car.engine_size = 1.8
car.save!
@@ -450,7 +303,7 @@ RSpec.describe HasAuxTable do
it "loads single model with auxiliary data in one query using find" do
query_count =
count_queries do
SpecHelper.count_queries do
car = Car.find(@car1.id)
# Access auxiliary attributes to ensure they're loaded
car.fuel_type
@@ -462,7 +315,7 @@ RSpec.describe HasAuxTable do
it "loads single model with auxiliary data in one query using find_by" do
query_count =
count_queries do
SpecHelper.count_queries do
car = Car.find_by(name: "Toyota Prius")
# Access auxiliary attributes to ensure they're loaded
car.fuel_type
@@ -474,7 +327,7 @@ RSpec.describe HasAuxTable do
it "loads multiple models with auxiliary data in one query using where" do
query_count =
count_queries do
SpecHelper.count_queries do
cars = Car.where(fuel_type: %w[hybrid electric])
# Access auxiliary attributes for all cars
cars.each do |car|
@@ -499,7 +352,7 @@ RSpec.describe HasAuxTable do
cars = nil
query_count =
count_queries do
SpecHelper.count_queries do
cars = Car.where(fuel_type: "gasoline")
# Access auxiliary attributes for all cars - should not trigger additional queries
cars.each do |car|
@@ -515,7 +368,7 @@ RSpec.describe HasAuxTable do
it "uses single query when ordering by auxiliary columns" do
query_count =
count_queries do
SpecHelper.count_queries do
cars = Car.where(engine_size: 1.0..3.0).order(:engine_size)
# Access all attributes
cars.each do |car|
@@ -530,7 +383,7 @@ RSpec.describe HasAuxTable do
it "uses single query for complex auxiliary column queries" do
query_count =
count_queries do
SpecHelper.count_queries do
cars =
Car.where(fuel_type: "hybrid").or(Car.where(engine_size: 0.0))
# Access all attributes
@@ -546,7 +399,7 @@ RSpec.describe HasAuxTable do
it "uses single query when finding by auxiliary columns" do
query_count =
count_queries do
SpecHelper.count_queries do
car = Car.find_by(fuel_type: "hybrid", name: "Toyota Prius")
# Access all attributes
car.name
@@ -563,7 +416,7 @@ RSpec.describe HasAuxTable do
# Now count queries when accessing auxiliary attributes
query_count =
count_queries do
SpecHelper.count_queries do
car.fuel_type
car.engine_size
car.fuel_type? # presence check
@@ -576,7 +429,7 @@ RSpec.describe HasAuxTable do
it "handles mixed queries with main and auxiliary columns in single query" do
query_count =
count_queries do
SpecHelper.count_queries do
cars = Car.where(name: "Toyota Prius", fuel_type: "hybrid")
cars.each do |car|
car.name
@@ -590,7 +443,7 @@ RSpec.describe HasAuxTable do
it "uses single query for range queries on auxiliary columns" do
query_count =
count_queries do
SpecHelper.count_queries do
cars = Car.where(engine_size: 0.0..1.9)
cars.each do |car|
car.name
@@ -604,7 +457,7 @@ RSpec.describe HasAuxTable do
it "maintains single query performance with limit and offset" do
query_count =
count_queries do
SpecHelper.count_queries do
car = Car.where(fuel_type: %w[hybrid electric]).limit(1).first
# Access auxiliary attributes
car.fuel_type
@@ -814,7 +667,7 @@ RSpec.describe HasAuxTable do
end
it "reloads with one query" do
num_queries = count_queries { @car.reload }
num_queries = SpecHelper.count_queries { @car.reload }
expect(num_queries).to eq(1)
end
end
@@ -846,12 +699,12 @@ RSpec.describe HasAuxTable do
expect(Car.count).to eq(1)
expect(Boat.count).to eq(1)
expect(count_queries { car = Vehicle.find(car.id) }).to eq(1)
expect(SpecHelper.count_queries { car = Vehicle.find(car.id) }).to eq(1)
expect(car.fuel_type).to eq("gasoline")
expect(car.engine_size).to eq(2.0)
expect(car.name).to eq("Honda Civic")
expect(count_queries { boat = Vehicle.find(boat.id) }).to eq(1)
expect(SpecHelper.count_queries { boat = Vehicle.find(boat.id) }).to eq(1)
expect(boat.only_freshwater).to eq(true)
end

View File

@@ -1,3 +1,4 @@
# typed: strict
# frozen_string_literal: true
require "active_record"
@@ -29,3 +30,107 @@ RSpec.configure do |config|
end
end
end
module SpecHelper
extend T::Sig
extend T::Helpers
LOG_QUERIES = T.let(false, T::Boolean)
# Helper method to count queries
sig { params(block: T.proc.void).returns(Integer) }
def self.count_queries(&block)
query_count = 0
query_callback =
lambda { |name, start, finish, message_id, values| query_count += 1 }
ActiveSupport::Notifications.subscribed(
query_callback,
"sql.active_record"
) do
if LOG_QUERIES
old_logger = ActiveRecord::Base.logger
ActiveRecord::Base.logger = Logger.new(STDOUT)
ActiveRecord::Base.logger.level = Logger::DEBUG
end
block.call
ActiveRecord::Base.logger = old_logger if LOG_QUERIES
end
query_count
end
sig { void }
def self.initialize_spec_schema!
# Set up the database schema for testing
ActiveRecord::Schema.define do
create_table :vehicle_lots do |t|
t.string :name
t.timestamps
end
create_base_table :vehicles do |t|
t.string :name
t.references :vehicle_lot, foreign_key: { to_table: :vehicle_lots }
t.timestamps
t.create_aux :car do |t|
t.string :fuel_type
t.decimal :engine_size, precision: 3, scale: 1
end
t.create_aux :boat do |t|
t.boolean :only_freshwater
end
end
create_base_table :people do |t|
t.string :name
t.timestamps
t.create_aux :driver do |t|
t.integer :license_number
t.references :car,
null: false,
foreign_key: {
to_table: :vehicles_car_aux,
primary_key: :base_table_id
}
end
t.create_aux :captain do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
t.create_aux :passenger do |t|
t.references :boat,
null: false,
foreign_key: {
to_table: :vehicles_boat_aux,
primary_key: :base_table_id
}
end
end
create_base_table :utensils do |t|
t.string :name
t.string :material
t.timestamps
t.create_aux :fork do |t|
t.integer :num_tongs
end
t.create_aux :spoon do |t|
t.string :curvature
end
end
require_relative "spec_models"
end
end
end

56
spec/spec_models.rb Normal file
View File

@@ -0,0 +1,56 @@
# typed: strict
# frozen_string_literal: true
class Vehicle < ActiveRecord::Base
include HasAuxTable
belongs_to :vehicle_lot
end
class VehicleLot < ActiveRecord::Base
has_many :vehicles
end
class Car < Vehicle
aux_table :car
has_many :drivers, inverse_of: :car
end
class Boat < Vehicle
aux_table :boat
has_many :passengers, inverse_of: :boat
belongs_to :captain, inverse_of: :boat
end
class Person < ActiveRecord::Base
include HasAuxTable
self.table_name = "people"
end
class Driver < Person
aux_table :driver
belongs_to :car, inverse_of: :drivers
end
class Captain < Person
aux_table :captain
has_one :boat, inverse_of: :captain
end
class Passenger < Person
aux_table :passenger
belongs_to :boat, inverse_of: :passengers
end
module Kitchen
class Utensil < ActiveRecord::Base
include HasAuxTable
end
class Fork < Utensil
aux_table :fork
end
class Spoon < Utensil
aux_table :spoon
end
end