# 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") queries = capture_queries do car.fuel_type = "hybrid" car.engine_size = 1.8 car.save! end expect(queries.length).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 "works when using the same relation mulitiple times" do cars = Car.where(fuel_type: %w[hybrid electric]) expect(cars.count).to eq(2) expect(cars.find_by(name: "Toyota Prius").fuel_type).to eq("hybrid") 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 queries = capture_queries do car = Car.find(@car1.id) # Access auxiliary attributes to ensure they're loaded car.fuel_type car.engine_size end expect(queries.length).to eq(1) end it "loads single model with auxiliary data in one query using find_by" do queries = capture_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(queries.length).to eq(1) end it "loads multiple models with auxiliary data in one query using where" do queries = capture_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(queries.length).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 queries = capture_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(queries.length).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 queries = capture_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(queries.length).to eq(1) end it "uses single query for complex auxiliary column queries" do queries = capture_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(queries.length).to eq(1) end it "uses single query when finding by auxiliary columns" do queries = capture_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(queries.length).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 queries = capture_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(queries.length).to eq(0) # No additional queries should be triggered end it "handles mixed queries with main and auxiliary columns in single query" do queries = capture_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(queries.length).to eq(1) end it "uses single query for range queries on auxiliary columns" do queries = capture_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(queries.length).to eq(1) end it "maintains single query performance with limit and offset" do queries = capture_queries do car = Car.where(fuel_type: %w[hybrid electric]).limit(1).first # Access auxiliary attributes car.fuel_type car.engine_size end expect(queries.length).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 describe "has_many association" do it "works between base and base" do # e1.pk_base_id -> e2.fk_base_id @e1.owned_base_to_base_many << @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_many: @e2)).to eq( [@e1] ) expect(ModelECustom.where(owned_base_to_base_many: @e1)).to eq([]) end it "works between base and aux" do # e1.pk_base_id -> e2.fk_aux_id @e1.owned_base_to_aux_many << @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_many: @e2)).to eq([@e1]) expect(ModelECustom.where(owned_base_to_aux_many: @e1)).to eq([]) end it "works between aux and base" do # e1.pk_aux_id -> e2.fk_base_id @e1.owned_aux_to_base_many << @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_many: @e2)).to eq([@e1]) expect(ModelECustom.where(owned_aux_to_base_many: @e1)).to eq([]) end it "works between aux and aux" do # e1.pk_aux_id -> e2.fk_aux_id @e1.owned_aux_to_aux_many << @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_many: @e2)).to eq([@e1]) expect(ModelECustom.where(owned_aux_to_aux_many: @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 queries = capture_queries { @car.reload } expect(queries.length).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(capture_queries { car = Vehicle.find(car.id) }.length).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(capture_queries { boat = Vehicle.find(boat.id) }.length).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 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 describe "range queries" do ActiveRecord::Schema.define do create_base_table :test_range_models do |t| t.integer :base_field t.create_aux :specific do |t| t.integer :aux_field end end end class TestRangeModel < ActiveRecord::Base include HasAuxTable end class TestRangeModelSpecific < TestRangeModel aux_table :specific end before do @bases = (0..5).map { |i| TestRangeModel.create!(base_field: i) } @specifics = (0..5).map do |i| TestRangeModelSpecific.create!(base_field: i, aux_field: i) end end it "works with a from..to range" do expect(TestRangeModel.where(base_field: 1..5)).to eq( @bases[1..5] + @specifics[1..5] ) expect(TestRangeModelSpecific.where(base_field: 2..3)).to eq( @specifics[2..3] ) expect(TestRangeModelSpecific.where(aux_field: 2..3)).to eq( @specifics[2..3] ) expect(TestRangeModelSpecific.where(aux_field: 4..7)).to eq( @specifics[4..5] ) end it "works with a from.. range" do expect(TestRangeModel.where(base_field: 1..)).to eq( @bases[1..5] + @specifics[1..5] ) expect(TestRangeModelSpecific.where(aux_field: 1..)).to eq( @specifics[1..5] ) end it "works with a ..to range" do expect(TestRangeModel.where(base_field: ..4)).to eq( @bases[0..4] + @specifics[0..4] ) expect(TestRangeModelSpecific.where(aux_field: ..4)).to eq( @specifics[0..4] ) end end end