Files
has_aux_table/spec/has_aux_table_spec.rb
2025-07-24 05:19:46 +00:00

1138 lines
36 KiB
Ruby

# typed: false
# frozen_string_literal: true
RSpec.describe HasAuxTable do
# Car class will be defined after schema setup
it "has a version number" do
expect(HasAuxTable::VERSION).not_to be nil
end
it "can create STI records" do
car = Car.create!(name: "Toyota Camry", type: "Car")
expect(car).to be_persisted
boat = Boat.create!(name: "Yacht", type: "Boat")
expect(boat).to be_persisted
end
it "is a clean test environment" do
expect(Vehicle.count).to eq(0)
end
describe "column reporting" do
it "reports columns of the base class" do
expect(Vehicle.inspect).to include("name")
end
it "reports the correct columns on the string repr of the class" do
expect(Car.inspect).to include("fuel_type")
end
it "does not include the aux table foreign key" do
expect(Car.inspect).not_to include("base_table_id")
end
it "reports created_at, updated_at timestamp columns at the end of the list" do
expect(Car.inspect).to match(/\bfuel_type\b.+\bupdated_at\b/)
expect(Car.inspect).to match(/\bname\b.+\bupdated_at\b/)
end
it "includes columns in instances of the model" do
car = Car.create!(name: "Honda Civic")
expect(car.inspect).to include("fuel_type")
expect(car.inspect).to include("engine_size")
expect(car.inspect).to include("created_at")
expect(car.inspect).to include("updated_at")
end
it "puts _at columns at the end of the list on instances" do
car = Car.create!(name: "Honda Civic")
expect(car.inspect).to match(/\bfuel_type\b.+\bupdated_at\b/)
expect(car.inspect).to match(/\bname\b.+\bupdated_at\b/)
end
end
it "can be created with .new" do
car = Car.new
car.name = "Honda Civic"
car.engine_size = 1.8
car.save!
expect(car.fuel_type).to be_nil
expect(car.engine_size).to eq(1.8)
expect(car.name).to eq("Honda Civic")
end
it "has the right #attributes" do
car =
Car.create!(name: "Honda Civic", fuel_type: "gasoline", engine_size: 2.0)
expect(car.attributes).to match(
hash_including(
"type" => "Car",
"id" => car.id,
"name" => "Honda Civic",
"fuel_type" => "gasoline",
"engine_size" => be_within(0.001).of(2.0),
"created_at" => be_within(0.001).of(car.created_at),
"updated_at" => be_within(0.001).of(car.updated_at)
)
)
end
it "reads attributes with read_attribute" do
car = Car.create!(name: "Honda Civic", fuel_type: "gasoline")
expect(car.read_attribute("name")).to eq("Honda Civic")
expect(car.read_attribute(:name)).to eq("Honda Civic")
expect(car.read_attribute("fuel_type")).to eq("gasoline")
expect(car.read_attribute(:fuel_type)).to eq("gasoline")
end
it "can be created as the base class" do
vehicle = Vehicle.create(type: "Vehicle", name: "big tractor")
expect(vehicle.attributes).to match(
hash_including(
"type" => "Vehicle",
"id" => vehicle.id,
"name" => "big tractor",
"created_at" => be_within(0.001).of(vehicle.created_at),
"updated_at" => be_within(0.001).of(vehicle.updated_at)
)
)
end
it "can be created through an association" do
lot = VehicleLot.create(name: "lot1")
lot.vehicles.create { |b| b.name = "vehicle1" }
lot.save!
lot.reload
expect(lot.vehicles.count).to eq(1)
expect(lot.vehicles.first.name).to eq("vehicle1")
end
it "can set association on aux record" do
driver = Driver.create!(name: "John Doe", license_number: 12_345)
car = Car.create!(name: "Honda Civic")
driver.car = car
expect(driver.car).to eq(car)
expect(driver.car_id).to eq(car.id)
driver.save!
driver = Driver.find(driver.id)
expect(driver.car).to eq(car)
end
it "defined_enums returns the correct values" do
engine_types = {
"turbofan" => 0,
"turboprop" => 1,
"piston" => 2,
"electric" => 3
}
expect(Plane.defined_enums).to eq({ "engine_type" => engine_types })
expect(Plane.engine_types).to eq(engine_types)
end
it "works with enums" do
plane = Plane.create!(name: "Boeing 747", engine_type: :turbofan)
expect(plane.engine_type).to eq("turbofan")
plane.engine_type = "piston"
expect(plane.engine_type).to eq("piston")
plane.save!
expect(plane.engine_type).to eq("piston")
expect(plane.piston?).to be_truthy
plane.turboprop!
expect(plane.engine_type).to eq("turboprop")
end
describe "validations" do
it "validates the main record" do
driver = Driver.create!(name: "John Doe", license_number: 12_345)
expect(driver.valid?).to be_truthy
driver.name = nil
expect(driver.valid?).to be_falsey
end
it "validates through an association" do
car = Car.create!(name: "Honda Civic")
car.drivers.create!(name: "John Doe", license_number: 12_345)
end
end
describe "#changed?" do
it "returns true if the main record changes" do
car = Car.create!(name: "Honda Civic")
expect(car.changed?).to be_falsey
car.name = "Toyota Camry"
expect(car.changed?).to be_truthy
end
it "returns true if the aux record changes" do
car = Car.create!(name: "Honda Civic")
expect(car.changed?).to be_falsey
car.fuel_type = "hybrid"
expect(car.changed?).to be_truthy
end
end
describe "#changed_attributes" do
# changed_attributes returns a hash with the original values of the attribute
it "returns the changed attributes of the main record" do
car = Car.create!(name: "Honda Civic")
expect(car.changed_attributes).to eq({})
car.name = "Toyota Camry"
expect(car.changed_attributes).to eq({ "name" => "Honda Civic" })
end
it "returns the changed attributes of the aux record when original is nil" do
car = Car.create!(name: "Honda Civic")
expect(car.changed_attributes).to eq({})
car.fuel_type = "hybrid"
expect(car.changed_attributes).to eq({ "fuel_type" => nil })
end
it "returns the changed attributes of the aux record when original is not nil" do
car = Car.create!(name: "Honda Civic", fuel_type: "gasoline")
expect(car.changed_attributes).to eq({})
car.fuel_type = "hybrid"
expect(car.changed_attributes).to eq({ "fuel_type" => "gasoline" })
end
end
describe "database integration" do
it "provides automatic attribute accessors for auxiliary table columns" do
vehicle = Car.create!(name: "Honda Civic")
# Test getter methods (should return nil initially)
expect(vehicle.fuel_type).to be_nil
expect(vehicle.engine_size).to be_nil
# Test presence check methods
expect(vehicle.fuel_type?).to be_falsey
expect(vehicle.engine_size?).to be_falsey
# Test setter methods (should create auxiliary record automatically)
vehicle.fuel_type = "hybrid"
vehicle.engine_size = 1.8
# Test that values are set correctly
expect(vehicle.fuel_type).to eq("hybrid")
expect(vehicle.engine_size).to eq(1.8)
# Test presence check methods after setting values
expect(vehicle.fuel_type?).to be_truthy
expect(vehicle.engine_size?).to be_truthy
# Save and reload to verify persistence
vehicle.save!
reloaded_vehicle = Car.find(vehicle.id)
expect(reloaded_vehicle.fuel_type).to eq("hybrid")
expect(reloaded_vehicle.engine_size).to eq(1.8)
end
it "allows saving the model with auxiliary columns" do
car = Car.create!(name: "Honda Civic")
num_queries =
SpecHelper.count_queries do
car.fuel_type = "hybrid"
car.engine_size = 1.8
car.save!
end
expect(num_queries).to eq(1)
end
end
describe "query extensions" do
before do
# Create test data
@car1 =
Car.create!(
name: "Toyota Prius",
type: "Car",
fuel_type: "hybrid",
engine_size: 1.8
)
@car2 =
Car.create!(
name: "Honda Civic",
type: "Car",
fuel_type: "gasoline",
engine_size: 2.0
)
@car3 =
Car.create!(
name: "Tesla Model 3",
type: "Car",
fuel_type: "electric",
engine_size: 0.0
)
end
describe "find method with automatic joins" do
it "automatically includes auxiliary table joins for find" do
# Find should automatically include joins to load auxiliary data
found_car = Car.find(@car1.id)
# Auxiliary attributes should be accessible
expect(found_car.fuel_type).to eq("hybrid")
expect(found_car.engine_size).to eq(1.8)
expect(found_car.name).to eq("Toyota Prius")
end
it "works with multiple IDs" do
cars = Car.find([@car1.id, @car2.id])
expect(cars.length).to eq(2)
# All cars should have auxiliary data loaded
prius = cars.find { |c| c.name == "Toyota Prius" }
civic = cars.find { |c| c.name == "Honda Civic" }
expect(prius.fuel_type).to eq("hybrid")
expect(civic.fuel_type).to eq("gasoline")
end
end
describe "find_by method with automatic joins" do
it "automatically includes auxiliary table joins for find_by" do
# Find_by should automatically include joins for auxiliary data
found_car = Car.find_by(name: "Toyota Prius")
expect(found_car).to be_present
expect(found_car.fuel_type).to eq("hybrid")
expect(found_car.engine_size).to eq(1.8)
end
it "works with auxiliary columns in find_by" do
# This should work with auxiliary columns due to automatic join
found_car = Car.find_by(fuel_type: "hybrid")
expect(found_car).to be_present
expect(found_car.name).to eq("Toyota Prius")
expect(found_car.fuel_type).to eq("hybrid")
end
it "returns nil when no record found" do
found_car = Car.find_by(fuel_type: "diesel")
expect(found_car).to be_nil
end
it "works with find_by!" do
expect { Car.find_by!(fuel_type: "diesel") }.to raise_error(
ActiveRecord::RecordNotFound
)
end
end
describe "where method with automatic joins" do
it "automatically handles auxiliary columns in where clauses" do
# Query with auxiliary column should automatically include join
hybrid_cars = Car.where(fuel_type: "hybrid")
expect(hybrid_cars.length).to eq(1)
expect(hybrid_cars.first.name).to eq("Toyota Prius")
expect(hybrid_cars.first.fuel_type).to eq("hybrid")
end
it "works with multiple auxiliary columns" do
# Query with multiple auxiliary columns
efficient_cars = Car.where(fuel_type: "hybrid", engine_size: 1.8)
expect(efficient_cars.length).to eq(1)
expect(efficient_cars.first.name).to eq("Toyota Prius")
end
it "handles range queries on auxiliary columns" do
# Range query on auxiliary column
small_engine_cars = Car.where(engine_size: 0.0..1.9)
expect(small_engine_cars.length).to eq(2)
car_names = small_engine_cars.map(&:name).sort
expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
end
it "supports mixed queries with main table and auxiliary table columns" do
# Mixed query with both main table and auxiliary table columns
prius_hybrids = Car.where(name: "Toyota Prius", fuel_type: "hybrid")
expect(prius_hybrids.length).to eq(1)
expect(prius_hybrids.first.name).to eq("Toyota Prius")
expect(prius_hybrids.first.fuel_type).to eq("hybrid")
end
it "handles IN queries on auxiliary columns" do
# IN query on auxiliary column
eco_cars = Car.where(fuel_type: %w[hybrid electric])
expect(eco_cars.length).to eq(2)
car_names = eco_cars.map(&:name).sort
expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
end
it "doesn't add joins for queries without auxiliary columns" do
toyota_cars = Car.where(name: "Toyota Prius")
expect(toyota_cars.length).to eq(1)
expect(toyota_cars.first.name).to eq("Toyota Prius")
expect(toyota_cars.first.fuel_type).to eq("hybrid")
end
it "works with chained where clauses" do
efficient_cars = Car.where(fuel_type: "hybrid").where(engine_size: 1.8)
expect(efficient_cars.length).to eq(1)
expect(efficient_cars.first.name).to eq("Toyota Prius")
end
it "supports complex query combinations" do
# Complex query with OR conditions
cars = Car.where(fuel_type: "hybrid").or(Car.where(engine_size: 0.0))
expect(cars.length).to eq(2)
car_names = cars.map(&:name).sort
expect(car_names).to eq(["Tesla Model 3", "Toyota Prius"])
end
it "works when sql is passed to where" do
cars = Car.where("fuel_type = 'hybrid'")
expect(cars.length).to eq(1)
expect(cars.first.name).to eq("Toyota Prius")
end
it "works for .not queries" do
cars = Car.where.not(fuel_type: "hybrid")
expect(cars.length).to eq(2)
expect(cars.map(&:name)).to eq(["Honda Civic", "Tesla Model 3"])
end
end
describe "query performance and optimization" do
it "loads auxiliary data in single query with joins" do
# This test ensures we're using joins rather than N+1 queries
cars = Car.where(fuel_type: "gasoline")
# Should have loaded auxiliary data via join
expect(cars.length).to eq(1)
expect(cars.first.name).to eq("Honda Civic")
expect(cars.first.fuel_type).to eq("gasoline")
expect(cars.first.engine_size).to eq(2.0)
end
describe "query count validation" do
# These tests validate the performance optimizations using eager_load
# All query methods now use single queries with proper LEFT OUTER JOINs
it "loads single model with auxiliary data in one query using find" do
query_count =
SpecHelper.count_queries do
car = Car.find(@car1.id)
# Access auxiliary attributes to ensure they're loaded
car.fuel_type
car.engine_size
end
expect(query_count).to eq(1)
end
it "loads single model with auxiliary data in one query using find_by" do
query_count =
SpecHelper.count_queries do
car = Car.find_by(name: "Toyota Prius")
# Access auxiliary attributes to ensure they're loaded
car.fuel_type
car.engine_size
end
expect(query_count).to eq(1)
end
it "loads multiple models with auxiliary data in one query using where" do
query_count =
SpecHelper.count_queries do
cars = Car.where(fuel_type: %w[hybrid electric])
# Access auxiliary attributes for all cars
cars.each do |car|
car.fuel_type
car.engine_size
end
end
expect(query_count).to eq(1)
end
it "avoids N+1 queries when loading multiple models" do
# Create additional test data
5.times do |i|
Car.create!(
name: "Test Car #{i}",
fuel_type: "gasoline",
engine_size: 1.5
)
end
cars = nil
query_count =
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|
car.fuel_type
car.engine_size
car.name
end
end
expect(query_count).to eq(1) # Single query regardless of how many cars are loaded
expect(cars.length).to be >= 1 # At least the original Honda Civic plus new cars
end
it "uses single query when ordering by auxiliary columns" do
query_count =
SpecHelper.count_queries do
cars = Car.where(engine_size: 1.0..3.0).order(:engine_size)
# Access all attributes
cars.each do |car|
car.name
car.fuel_type
car.engine_size
end
end
expect(query_count).to eq(1)
end
it "uses single query for complex auxiliary column queries" do
query_count =
SpecHelper.count_queries do
cars =
Car.where(fuel_type: "hybrid").or(Car.where(engine_size: 0.0))
# Access all attributes
cars.each do |car|
car.name
car.fuel_type
car.engine_size
end
end
expect(query_count).to eq(1)
end
it "uses single query when finding by auxiliary columns" do
query_count =
SpecHelper.count_queries do
car = Car.find_by(fuel_type: "hybrid", name: "Toyota Prius")
# Access all attributes
car.name
car.fuel_type
car.engine_size
end
expect(query_count).to eq(1)
end
it "doesn't trigger additional queries when accessing auxiliary attributes after load" do
# First load the car
car = Car.find(@car1.id)
# Now count queries when accessing auxiliary attributes
query_count =
SpecHelper.count_queries do
car.fuel_type
car.engine_size
car.fuel_type? # presence check
car.engine_size? # presence check
end
# Currently this should be 0 since auxiliary record is already loaded
expect(query_count).to eq(0) # No additional queries should be triggered
end
it "handles mixed queries with main and auxiliary columns in single query" do
query_count =
SpecHelper.count_queries do
cars = Car.where(name: "Toyota Prius", fuel_type: "hybrid")
cars.each do |car|
car.name
car.fuel_type
car.engine_size
end
end
expect(query_count).to eq(1)
end
it "uses single query for range queries on auxiliary columns" do
query_count =
SpecHelper.count_queries do
cars = Car.where(engine_size: 0.0..1.9)
cars.each do |car|
car.name
car.fuel_type
car.engine_size
end
end
expect(query_count).to eq(1)
end
it "maintains single query performance with limit and offset" do
query_count =
SpecHelper.count_queries do
car = Car.where(fuel_type: %w[hybrid electric]).limit(1).first
# Access auxiliary attributes
car.fuel_type
car.engine_size
end
expect(query_count).to eq(1)
end
end
it "works with order clauses on auxiliary columns" do
# Order by auxiliary column
cars_by_engine = Car.where(engine_size: 1.0..3.0).order(:engine_size)
expect(cars_by_engine.length).to eq(2)
expect(cars_by_engine.first.name).to eq("Toyota Prius")
expect(cars_by_engine.second.name).to eq("Honda Civic")
end
it "supports limit and offset with auxiliary columns" do
# Limit with auxiliary column query
first_hybrid = Car.where(fuel_type: %w[hybrid electric]).limit(1).first
expect(first_hybrid).to be_present
expect(["Toyota Prius", "Tesla Model 3"]).to include(first_hybrid.name)
end
end
describe "edge cases and error handling" do
it "handles queries with non-existent auxiliary columns gracefully" do
# This should not break and should fall back to normal query behavior
expect { Car.where(non_existent_column: "value").first }.to raise_error(
ActiveRecord::StatementInvalid
)
end
it "works with empty where conditions" do
# Empty where should not cause issues
cars = Car.where({})
expect(cars.length).to eq(3)
end
it "handles nil values in auxiliary columns" do
# Create a car with nil auxiliary values
Car.create!(name: "Incomplete Car", type: "Car")
Car.create!(
name: "Complete Car",
fuel_type: "gasoline",
engine_size: 2.0
)
# Query for cars with nil fuel_type
incomplete_cars = Car.where(fuel_type: nil)
expect(incomplete_cars.length).to eq(1)
expect(incomplete_cars.first.name).to eq("Incomplete Car")
end
end
end
describe "column overlap validation" do
it "raises error when auxiliary table defines column that exists in main table" do
# Create a test schema with overlapping columns
ActiveRecord::Schema.define do
create_base_table :test_overlap_mains do |t|
t.string :name
t.string :description
t.create_aux :overlap do |t|
t.string :name
t.string :extra_data
end
t.timestamps
end
end
# Define models that will trigger the validation
class TestOverlapMain < ActiveRecord::Base
include HasAuxTable
end
expect {
class TestOverlapChild < TestOverlapMain
aux_table :overlap
end
# Trigger schema loading to activate validation
TestOverlapChild.load_schema
}.to raise_error(ArgumentError, /defines column\(s\) 'name'/)
end
it "ignores system columns and foreign keys when checking for overlaps" do
# Create a test schema where system columns are duplicated (which should be allowed)
ActiveRecord::Schema.define do
create_base_table :test_system_cols_main do |t|
t.string :name
t.create_aux :has_timestamps do |t|
t.timestamps
end
t.timestamps
end
end
class TestSystemColsMain < ActiveRecord::Base
include HasAuxTable
end
expect {
class TestSystemColsChild < TestSystemColsMain
aux_table :has_timestamps
end
}.not_to raise_error
end
end
describe "methods that depend on relation" do
before(:each) do
@car =
Car.create!(name: "Toyota Prius", fuel_type: "hybrid", engine_size: 1.5)
end
describe "destroy" do
it "destroys the main record" do
expect { @car.destroy }.to change { Car.count }.by(-1)
end
it "destroys the aux record" do
expect { @car.destroy }.to change {
Object.const_get(:VehiclesCarAux).count
}.by(-1)
end
end
describe "associations" do
it "can create a driver through the association" do
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect(driver.car).to eq(@car)
expect(driver.car_id).to eq(@car.id)
expect(driver.car.fuel_type).to eq("hybrid")
expect(driver.car.engine_size).to eq(1.5)
end
it "executes the hooks when creating through the association" do
driver =
@car
.drivers
.create!(name: "John Doe") do |driver|
driver.license_number = 123_456
end
expect(driver.license_number).to eq(123_456)
end
it "can create a driver directly" do
driver =
Driver.create!(car: @car, name: "John Doe", license_number: 123_456)
expect(driver.car).to eq(@car)
expect(driver.car_id).to eq(@car.id)
expect(driver.car.fuel_type).to eq("hybrid")
expect(driver.car.engine_size).to eq(1.5)
end
it "can be accessed through the association" do
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect(@car.drivers).to eq([driver])
end
it "can be destroyed through the association" do
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect { driver.destroy }.to change { @car.reload.drivers.count }.by(-1)
end
it "can be queried through the association" do
driver = @car.drivers.create!(name: "John Doe", license_number: 123_456)
expect(@car.drivers.where(name: "John Doe")).to eq([driver])
drivers = @car.drivers
d = drivers.find_by!(license_number: 123_456)
expect(d.id).to eq(driver.id)
d = drivers.find_by(license_number: 123_456)
expect(d.id).to eq(driver.id)
end
it "can have the association queried when fk is on the main table" do
lot = VehicleLot.create!(name: "Lot 1")
nolot_car = @car
lot_car = Car.create!(name: "Car 1", vehicle_lot: lot)
expect(Car.where(vehicle_lot: lot)).to eq([lot_car])
expect(Car.where(vehicle_lot: nil)).to eq([nolot_car])
end
it "can have the association queried when fk is on the aux table" do
driver1 =
Driver.create!(name: "John Doe", license_number: 123, car: @car)
driver2 = Driver.create!(name: "Jane Goodall", license_number: 456)
nodriver_car = Car.create!(name: "No Driver Car")
expect(Driver.where(car: @car)).to eq([driver1])
expect(Driver.where(car: nil)).to eq([driver2])
expect(Driver.where(car: nodriver_car)).to eq([])
end
describe "custom foreign and primary keys" do
before(:each) do
@e1 =
ModelECustom.create!(
pk_base_id: 1,
fk_base_id: 2,
pk_aux_id: 3,
fk_aux_id: 4
)
@e2 =
ModelECustom.create!(
pk_base_id: 5,
fk_base_id: 6,
pk_aux_id: 7,
fk_aux_id: 8
)
end
describe "belongs_to association" do
it "works between base and base" do
# e1.fk_base_id <- e2.pk_base_id
@e1.base_to_base = @e2
@e1.save!
@e2.save!
expect(@e1.fk_base_id).to eq(5)
expect(@e2.pk_base_id).to eq(5)
expect(ModelECustom.where(base_to_base: @e2)).to eq([@e1])
expect(ModelECustom.where(base_to_base: @e1)).to eq([])
end
it "works between base and aux" do
# e1.fk_base_id <- e2.pk_aux_id
@e1.base_to_aux = @e2
@e1.save!
@e2.save!
expect(@e1.fk_base_id).to eq(7)
expect(@e2.pk_aux_id).to eq(7)
expect(ModelECustom.where(base_to_aux: @e2)).to eq([@e1])
expect(ModelECustom.where(base_to_aux: @e1)).to eq([])
end
it "works between aux and base" do
# e1.fk_aux_id <- e2.pk_base_id
@e1.aux_to_base = @e2
@e1.save!
@e2.save!
expect(@e1.fk_aux_id).to eq(5)
expect(@e2.pk_base_id).to eq(5)
expect(ModelECustom.where(aux_to_base: @e2)).to eq([@e1])
expect(ModelECustom.where(aux_to_base: @e1)).to eq([])
end
it "works between aux and aux" do
# e1.fk_aux_id <- e2.pk_aux_id
@e1.aux_to_aux = @e2
@e1.save!
@e2.save!
expect(@e1.fk_aux_id).to eq(7)
expect(@e2.pk_aux_id).to eq(7)
expect(ModelECustom.where(aux_to_aux: @e2)).to eq([@e1])
expect(ModelECustom.where(aux_to_aux: @e1)).to eq([])
end
end
describe "has_one association" do
it "works between base and base" do
# e1.pk_base_id -> e2.fk_base_id
@e1.owned_base_to_base = @e2
@e1.save!
@e2.save!
expect(@e1.pk_base_id).to eq(1)
expect(@e2.fk_base_id).to eq(1)
expect(ModelECustom.where(owned_base_to_base: @e2)).to eq([@e1])
expect(ModelECustom.where(owned_base_to_base: @e1)).to eq([])
end
it "works between base and aux" do
# e1.pk_base_id -> e2.fk_aux_id
@e1.owned_base_to_aux = @e2
@e1.save!
@e2.save!
expect(@e1.pk_base_id).to eq(1)
expect(@e2.fk_aux_id).to eq(1)
expect(ModelECustom.where(owned_base_to_aux: @e2)).to eq([@e1])
expect(ModelECustom.where(owned_base_to_aux: @e1)).to eq([])
end
it "works between aux and base" do
# e1.pk_aux_id -> e2.fk_base_id
@e1.owned_aux_to_base = @e2
@e1.save!
@e2.save!
expect(@e1.pk_aux_id).to eq(3)
expect(@e2.fk_base_id).to eq(3)
expect(ModelECustom.where(owned_aux_to_base: @e2)).to eq([@e1])
expect(ModelECustom.where(owned_aux_to_base: @e1)).to eq([])
end
it "works between aux and aux" do
# e1.pk_aux_id -> e2.fk_aux_id
@e1.owned_aux_to_aux = @e2
@e1.save!
@e2.save!
expect(@e1.pk_aux_id).to eq(3)
expect(@e2.fk_aux_id).to eq(3)
expect(ModelECustom.where(owned_aux_to_aux: @e2)).to eq([@e1])
expect(ModelECustom.where(owned_aux_to_aux: @e1)).to eq([])
end
end
end
end
describe "#reload" do
it "discards changes to aux attributes when reloading the model" do
@car.fuel_type = "gasoline"
@car.reload
expect(@car.fuel_type).to eq("hybrid")
end
it "discards changes to main attributes when reloading the model" do
@car.name = "Honda Civic"
@car.reload
expect(@car.name).to eq("Toyota Prius")
end
it "can be saved after reloading" do
@car.reload
@car.name = "Honda Civic"
@car.save!
car = Car.find(@car.id)
expect(car.name).to eq("Honda Civic")
end
it "reloads the right value" do
car2 = Car.find(@car.id)
expect(@car.name).to eq("Toyota Prius")
@car.name = "Honda Civic"
@car.fuel_type = "gasoline"
@car.save!
@car.reload
expect(@car.name).to eq("Honda Civic")
expect(@car.fuel_type).to eq("gasoline")
car2.reload
expect(car2.name).to eq("Honda Civic")
expect(car2.fuel_type).to eq("gasoline")
end
it "reloads with one query" do
num_queries = SpecHelper.count_queries { @car.reload }
expect(num_queries).to eq(1)
end
it "reloads associations" do
expect(@car.drivers.length).to eq(0)
Driver.create!(car: @car, name: "Billy Kid", license_number: 123_456)
expect(@car.drivers.length).to eq(0)
expect(@car.drivers.count).to eq(1)
@car.reload
expect(@car.drivers.length).to eq(1)
expect(@car.drivers.count).to eq(1)
end
end
describe "#exists?" do
it "works when present with base table attributes" do
expect(Car.exists?(id: @car.id)).to be_truthy
end
it "works when missing with with base table attributes" do
expect(Car.exists?(id: 9999)).to be_falsey
end
it "works when present with aux table attributes" do
expect(Car.exists?(fuel_type: "hybrid")).to be_truthy
end
it "works when missing with aux table attributes" do
expect(Car.exists?(fuel_type: "diesel")).to be_falsey
end
end
end
it "loads the aux data separately when loaded from main class" do
car =
Car.create!(name: "Honda Civic", fuel_type: "gasoline", engine_size: 2.0)
boat = Boat.create!(name: "Yacht", only_freshwater: true)
expect(Vehicle.count).to eq(2)
expect(Car.count).to eq(1)
expect(Boat.count).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(SpecHelper.count_queries { boat = Vehicle.find(boat.id) }).to eq(1)
expect(boat.only_freshwater).to eq(true)
end
it "unscoped can take a block" do
car = Car.create!(name: "Honda Civic", fuel_type: "gasoline")
car = Car.unscoped { Car.find(car.id) }
expect(car.fuel_type).to eq("gasoline")
end
describe "namespaced models" do
it "works with namespaced models" do
fork1 =
Kitchen::Fork.create!(name: "Fork", material: "metal", num_tongs: 3)
expect(fork1.material).to eq("metal")
expect(fork1.num_tongs).to eq(3)
end
end
it "can redefine constants" do
class TestModel < ActiveRecord::Base
include HasAuxTable
end
class TestModelSpecific < TestModel
aux_table :specific
end
Object.send(:remove_const, :TestModelSpecific)
expect {
class TestModelSpecific < TestModel
aux_table :specific
end
}.not_to raise_error
end
describe "counter cache" do
def verify_counter_cache(model, assoc_name, expected_count)
expect(model.send("#{assoc_name}_count")).to eq(expected_count)
expect(model.send(assoc_name).count).to eq(expected_count || 0)
model.reload
expect(model.send("#{assoc_name}_count")).to eq(expected_count)
expect(model.send(assoc_name).count).to eq(expected_count || 0)
end
let(:reader) { Reader.create!(name: "John Doe", reading_speed: 100) }
let(:book) do
Book.create!(title: "The Great Gatsby", author: "F. Scott Fitzgerald")
end
it "updates counter caches that are on the aux model" do
verify_counter_cache(reader, :read_book_joins, nil)
reader.read_books << book
verify_counter_cache(reader, :read_book_joins, 1)
end
it "updates counter caches on a non-aux model" do
verify_counter_cache(book, :read_book_joins, nil)
reader.read_books << book
verify_counter_cache(book, :read_book_joins, 1)
end
it "has_one is a base class, belongs_to is a subclass, created via subclass" do
a = ModelA.create!(a_field1: "a_0")
verify_counter_cache(a, :model_bs, nil)
5.times do |i|
ModelB.create!(model_a: a, b_field1: "b_#{i}")
verify_counter_cache(a, :model_bs, i + 1)
end
end
it "has_one is a base class, belongs_to is a subclass, created via association" do
a = ModelA.create!(a_field1: "a_0")
verify_counter_cache(a, :model_bs, nil)
5.times do |i|
a.model_bs.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_bs, i + 1)
end
end
it "has_one is a subclass, belongs_to is a subclass, created via subclass" do
a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0")
verify_counter_cache(a, :model_b2s, nil)
5.times do |i|
a.model_b2s.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_b2s, i + 1)
end
end
it "has_one is a subclass, belongs_to is a subclass, created via association" do
a = ModelA2.create!(a_field1: "a2_0", a2_field1: "a2_0")
verify_counter_cache(a, :model_b2s, nil)
5.times do |i|
a.model_b2s.create!(b_field1: "b_#{i}")
verify_counter_cache(a, :model_b2s, i + 1)
end
end
it "has_one is subclass, belongs_to is a vanilla class" do
a = ModelA1.create!(a_field1: "a1_0", a1_field1: "a1_0")
verify_counter_cache(a, :model_cs, nil)
5.times do |i|
a.model_cs.create!(c_field1: "c_#{i}")
verify_counter_cache(a, :model_cs, i + 1)
end
end
it "is a join table connecting two base classes" do
a = ModelA.create!(a_field1: "a1_0")
ds = 5.times.map { ModelD.create! }
verify_counter_cache(a, :ad_joins, nil)
ds.each { |d| verify_counter_cache(d, :ad_joins, nil) }
ds.each_with_index do |d, i|
a.ad_joins.create!(model_d: d)
verify_counter_cache(a, :ad_joins, i + 1)
verify_counter_cache(d, :ad_joins, 1)
end
end
it "is a join table connecting two subclasses" do
a = ModelA1.create!(a_field1: "a1_0", a1_field1: "a1_0")
ds = 3.times.map { |i| ModelD1.create!(d1_field1: "d1_#{i}") }
verify_counter_cache(a, :ad_joins, nil)
ds.each { |d| verify_counter_cache(d, :ad_joins, nil) }
ds.each_with_index do |d, i|
a.ad_joins.create!(model_d: d)
verify_counter_cache(a, :ad_joins, i + 1)
verify_counter_cache(d, :ad_joins, 1)
end
end
end
describe "joins model with aux tables" do
it "can create a join record" do
doctor = Person.create!(name: "Dr. John Doe")
patient = Person.create!(name: "Jane Doe")
assoc = doctor.patients
assoc << patient
expect(doctor.patients.count).to eq(1)
expect(patient.doctors.count).to eq(1)
end
end
end