1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788
|
require_relative "spec_helper"
describe "class_table_inheritance plugin" do
before do
@db = Sequel.mock(:numrows=>1, :autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string, :allow_null=>false}], [:kind, {:type=>:string}]],
:managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer, :allow_null=>false}] ],
:executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
:staff=>[[:id, {:type=>:integer}], [:manager_id, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.extend_datasets do
def columns
{[:employees]=>[:id, :name, :kind],
[:managers]=>[:id, :num_staff],
[:executives]=>[:id, :num_managers],
[:staff]=>[:id, :manager_id],
[:employees, :managers]=>[:id, :name, :kind, :num_staff],
[:employees, :managers, :executives]=>[:id, :name, :kind, :num_staff, :num_managers],
[:employees, :staff]=>[:id, :name, :kind, :manager_id],
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
base = Sequel::Model(@db)
base.plugin :auto_validations if @use_auto_validations
class ::Employee < base
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
private
def _save_refresh; @values[:id] = 1 end
plugin :class_table_inheritance, :key=>:kind, :table_map=>{:Staff=>:staff}
end
class ::Manager < Employee
one_to_many :staff_members, :class=>:Staff
end
class ::Executive < Manager
end
class ::Ceo < Executive
end
class ::Staff < Employee
many_to_one :manager
end
class ::Intern < Employee
end
@ds = Employee.dataset
@db.sqls
end
after do
[:Intern, :Ceo, :Executive, :Manager, :Staff, :Employee].each{|s| Object.send(:remove_const, s)}
end
it "should freeze CTI information when freezing model class" do
Employee.freeze
Employee.cti_models.frozen?.must_equal true
Employee.cti_tables.frozen?.must_equal true
Employee.cti_instance_dataset.frozen?.must_equal true
Employee.cti_table_columns.frozen?.must_equal true
Employee.cti_table_map.frozen?.must_equal true
end
it "should not attempt to use prepared statements" do
Manager.plugin :prepared_statements
Manager.load(:id=>1, :kind=>'Manager', :num_staff=>2).save
@db.sqls.must_equal ["UPDATE employees SET kind = 'Manager' WHERE (id = 1)", "UPDATE managers SET num_staff = 2 WHERE (id = 1)"]
Employee.plugin :prepared_statements
Employee.load(:id=>2, :kind=>'Employee').save
@db.sqls.must_equal ["UPDATE employees SET kind = 'Employee' WHERE (id = 2)"]
end
it "#cti_models.first should be the model that loaded the plugin" do
Executive.cti_models.first.must_equal Employee
end
it "should have simple_table = nil for all subclasses" do
Manager.simple_table.must_be_nil
Executive.simple_table.must_be_nil
Ceo.simple_table.must_be_nil
Staff.simple_table.must_be_nil
Intern.simple_table.must_be_nil
end
it "should have working row_proc if using set_dataset in subclass to remove columns" do
Manager.set_dataset(Manager.dataset.select(*(Manager.columns - [:blah])))
Manager.dataset = Manager.dataset.with_fetch(:id=>1, :kind=>'Ceo')
Manager[1].must_equal Ceo.load(:id=>1, :kind=>'Ceo')
end
it "should use a subquery in subclasses" do
Employee.dataset.sql.must_equal 'SELECT * FROM employees'
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees'
Executive.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id)) AS employees'
Ceo.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (employees.kind IN (\'Ceo\'))) AS employees'
Staff.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id)) AS employees'
Intern.dataset.sql.must_equal 'SELECT * FROM employees WHERE (employees.kind IN (\'Intern\'))'
end
it "should use a empty empty string for class name for anonymous subclasses" do
Class.new(Employee).dataset.sql.must_equal "SELECT * FROM employees WHERE (employees.kind IN (''))"
Class.new(Manager).dataset.sql.must_equal "SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id) WHERE (employees.kind IN (''))) AS employees"
Class.new(Executive).dataset.sql.must_equal "SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (employees.kind IN (''))) AS employees"
Class.new(Ceo).dataset.sql.must_equal "SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (employees.kind IN (''))) AS employees"
Class.new(Staff).dataset.sql.must_equal "SELECT * FROM (SELECT employees.id, employees.name, employees.kind, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id) WHERE (employees.kind IN (''))) AS employees"
Class.new(Intern).dataset.sql.must_equal "SELECT * FROM (SELECT * FROM employees WHERE (employees.kind IN (''))) AS employees"
end
it "should return rows with the correct class based on the polymorphic_key value" do
@ds.with_fetch([{:kind=>'Employee'}, {:kind=>'Manager'}, {:kind=>'Executive'}, {:kind=>'Ceo'}, {:kind=>'Staff'}, {:kind=>'Intern'}]).all.collect{|x| x.class}.must_equal [Employee, Manager, Executive, Ceo, Staff, Intern]
end
it "should return rows with the correct class based on the polymorphic_key value for subclasses" do
Manager.dataset.with_fetch([{:kind=>'Manager'}, {:kind=>'Executive'}, {:kind=>'Ceo'}]).all.collect{|x| x.class}.must_equal [Manager, Executive, Ceo]
end
it "should have refresh return all columns in subclass after loading from superclass" do
Employee.dataset = Employee.dataset.with_fetch([{:id=>1, :name=>'A', :kind=>'Ceo'}])
Ceo.dataset = Ceo.dataset.with_fetch([{:id=>1, :name=>'A', :kind=>'Ceo', :num_staff=>3, :num_managers=>2}])
a = Employee.first
a.class.must_equal Ceo
a.values.must_equal(:id=>1, :name=>'A', :kind=>'Ceo')
a.refresh.values.must_equal(:id=>1, :name=>'A', :kind=>'Ceo', :num_staff=>3, :num_managers=>2)
@db.sqls.must_equal ["SELECT * FROM employees LIMIT 1",
"SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id) WHERE (employees.kind IN ('Ceo'))) AS employees WHERE (id = 1) LIMIT 1"]
end
describe "with auto_validations plugin" do
before(:all) do
@use_auto_validations = true
end
it "should work" do
e = Employee.new
e.valid?.must_equal false
e.errors.must_equal(:name=>["is not present"])
e = Manager.new
e.valid?.must_equal false
e.errors.must_equal(:name=>["is not present"], :num_staff=>["is not present"])
e = Executive.new
e.valid?.must_equal false
e.errors.must_equal(:name=>["is not present"], :num_staff=>["is not present"])
end
end
it "should return rows with the current class if sti_key is nil" do
Employee.plugin :class_table_inheritance
Employee.dataset.with_fetch([{:kind=>'Employee'}, {:kind=>'Manager'}, {:kind=>'Executive'}, {:kind=>'Ceo'}, {:kind=>'Staff'}, {:kind=>'Intern'}]).all.map{|x| x.class}.must_equal [Employee, Employee, Employee, Employee, Employee, Employee]
end
it "should return rows with the current class if sti_key is nil in subclasses" do
Employee.plugin :class_table_inheritance
Object.send(:remove_const, :Executive)
Object.send(:remove_const, :Manager)
class ::Manager < Employee; end
class ::Executive < Manager; end
Manager.dataset.with_fetch([{:kind=>'Manager'}, {:kind=>'Executive'}]).all.map{|x| x.class}.must_equal [Manager, Manager]
end
it "should handle a model map with integer values" do
Employee.plugin :class_table_inheritance, :key=>:kind, :model_map=>{0=>:Employee, 1=>:Manager, 2=>:Executive, 3=>:Ceo, 4=>:Intern}
Object.send(:remove_const, :Intern)
Object.send(:remove_const, :Ceo)
Object.send(:remove_const, :Executive)
Object.send(:remove_const, :Manager)
class ::Intern < Employee; end
class ::Manager < Employee; end
class ::Executive < Manager; end
class ::Ceo < Executive; end
Employee.dataset = Employee.dataset.with_fetch([{:kind=>nil},{:kind=>0},{:kind=>1}, {:kind=>2}, {:kind=>3}, {:kind=>4}])
Employee.all.collect{|x| x.class}.must_equal [Employee, Employee, Manager, Executive, Ceo, Intern]
Manager.dataset = Manager.dataset.with_fetch([{:kind=>nil},{:kind=>0},{:kind=>1}, {:kind=>2}, {:kind=>3}])
Manager.all.collect{|x| x.class}.must_equal [Manager, Employee, Manager, Executive, Ceo]
end
it "should fallback to the main class if the given class does not exist" do
@ds.with_fetch([{:kind=>'Employee'}, {:kind=>'Manager'}, {:kind=>'Blah'}, {:kind=>'Staff'}]).all.map{|x| x.class}.must_equal [Employee, Manager, Employee, Staff]
end
it "should fallback to the main class if the given class does not exist in subclasses" do
Manager.dataset.with_fetch([{:kind=>'Manager'}, {:kind=>'Executive'}, {:kind=>'Ceo'}, {:kind=>'Blah'}]).all.map{|x| x.class}.must_equal [Manager, Executive, Ceo, Manager]
end
it "should sets the model class name for the key when creating new parent class records" do
Employee.create
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Employee')"]
end
it "should sets the model class name for the key when creating new class records for subclass without separate table" do
Intern.create
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Intern')"]
end
it "should sets the model class name for the key when creating new subclass records" do
Ceo.create
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Ceo')",
"INSERT INTO managers (id) VALUES (1)",
"INSERT INTO executives (id) VALUES (1)"]
end
it "should ignore existing sti_key value when creating new records" do
Employee.create(:kind=>'Manager')
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Employee')"]
end
it "should ignore existing sti_key value in subclasses" do
Manager.create(:kind=>'Executive')
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Manager')",
"INSERT INTO managers (id) VALUES (1)"]
end
it "should handle validations on the type column field" do
o = Employee.new
def o.validate
errors.add(:kind, 'not present') unless kind
end
o.valid?.must_equal true
end
it "should set the type column field even when not validating" do
Employee.new.save(:validate=>false)
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Employee')"]
end
it "should handle type field matching current class when saving" do
Employee.create(:kind=>'Employee')
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Employee')"]
end
it "should convert type field matching subclass using different table when saving" do
Employee.create(:kind=>'Manager')
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Employee')"]
end
it "should keep type field matching subclass using same table when saving" do
Employee.create(:kind=>'Intern')
@db.sqls.must_equal ["INSERT INTO employees (kind) VALUES ('Intern')"]
end
it "should allow specifying a map of names to tables to override implicit mapping" do
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees'
Staff.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id)) AS employees'
end
it "should lazily load attributes for columns in subclass tables" do
Manager.dataset = Manager.dataset.with_fetch(:id=>1, :name=>'J', :kind=>'Ceo', :num_staff=>2)
m = Manager[1]
@db.sqls.must_equal ['SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees WHERE (id = 1) LIMIT 1']
@db.fetch = {:num_managers=>3}
m.must_be_kind_of Ceo
m.num_managers.must_equal 3
@db.sqls.must_equal ['SELECT employees.num_managers FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id)) AS employees WHERE (employees.id = 1) LIMIT 1']
m.values.must_equal(:id=>1, :name=>'J', :kind=>'Ceo', :num_staff=>2, :num_managers=>3)
end
it "should lazily load columns in middle classes correctly when loaded from parent class" do
Employee.dataset = Employee.dataset.with_fetch(:id=>1, :kind=>'Ceo')
@db.fetch = [[:num_staff=>2]]
e = Employee[1]
e.must_be_kind_of(Ceo)
@db.sqls.must_equal ["SELECT * FROM employees WHERE (id = 1) LIMIT 1"]
e.num_staff.must_equal 2
@db.sqls.must_equal ["SELECT employees.num_staff FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees WHERE (employees.id = 1) LIMIT 1"]
end
it "should eagerly load lazily columns in subclasses when loaded from parent class" do
Employee.dataset = Employee.dataset.with_fetch(:id=>1, :kind=>'Ceo')
@db.fetch = [[{:id=>1, :num_staff=>2}], [{:id=>1, :num_managers=>3}]]
e = Employee.all.first
e.must_be_kind_of(Ceo)
@db.sqls.must_equal ["SELECT * FROM employees"]
e.num_staff.must_equal 2
@db.sqls.must_equal ["SELECT employees.id, employees.num_staff FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees WHERE (employees.id IN (1))"]
e.num_managers.must_equal 3
@db.sqls.must_equal ['SELECT employees.id, employees.num_managers FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id)) AS employees WHERE (employees.id IN (1))']
end
it "should include schema for columns for tables for ancestor classes" do
Employee.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string, :allow_null=>false}, :kind=>{:type=>:string})
Manager.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string, :allow_null=>false}, :kind=>{:type=>:string}, :num_staff=>{:type=>:integer, :allow_null=>false})
Executive.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string, :allow_null=>false}, :kind=>{:type=>:string}, :num_staff=>{:type=>:integer, :allow_null=>false}, :num_managers=>{:type=>:integer})
Staff.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string, :allow_null=>false}, :kind=>{:type=>:string}, :manager_id=>{:type=>:integer})
Intern.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string, :allow_null=>false}, :kind=>{:type=>:string})
end
it "should use the correct primary key (which should have the same name in all subclasses)" do
[Employee, Manager, Executive, Ceo, Staff, Intern].each{|c| c.primary_key.must_equal :id}
end
it "should have table_name return the table name of the most specific table" do
Employee.table_name.must_equal :employees
Manager.table_name.must_equal :employees
Executive.table_name.must_equal :employees
Ceo.table_name.must_equal :employees
Staff.table_name.must_equal :employees
Intern.table_name.must_equal :employees
end
it "should delete the correct rows from all tables when deleting" do
Employee.load(:id=>1).delete
@db.sqls.must_equal ["DELETE FROM employees WHERE (id = 1)"]
Intern.load(:id=>1).delete
@db.sqls.must_equal ["DELETE FROM employees WHERE (id = 1)"]
Ceo.load(:id=>1).delete
@db.sqls.must_equal ["DELETE FROM executives WHERE (id = 1)", "DELETE FROM managers WHERE (id = 1)", "DELETE FROM employees WHERE (id = 1)"]
end
it "should not allow deletion of frozen object" do
[Ceo, Executive, Employee, Manager, Intern].each do |c|
o = c.load(:id=>1)
o.freeze
proc{o.delete}.must_raise(Sequel::Error)
@db.sqls.must_equal []
end
end
it "should insert the correct rows into all tables when inserting into parent class" do
Employee.create(:name=>'E')
@db.sqls.must_equal ["INSERT INTO employees (name, kind) VALUES ('E', 'Employee')"]
end
it "should insert the correct rows into all tables when inserting into subclass without separate table" do
Intern.create(:name=>'E')
@db.sqls.must_equal ["INSERT INTO employees (name, kind) VALUES ('E', 'Intern')"]
end
it "should insert the correct rows into all tables when inserting" do
Ceo.create(:num_managers=>3, :num_staff=>2, :name=>'E')
@db.sqls.must_equal ["INSERT INTO employees (name, kind) VALUES ('E', 'Ceo')",
"INSERT INTO managers (id, num_staff) VALUES (1, 2)",
"INSERT INTO executives (id, num_managers) VALUES (1, 3)"]
end
it "should insert the correct rows into all tables when inserting when insert_select is supported" do
[Executive, Manager, Employee].each do |klass|
klass.instance_variable_set(:@cti_instance_dataset, klass.cti_instance_dataset.with_extend do
def supports_insert_select?; true; end
def insert_select(v)
db.run(insert_sql(v) + " RETURNING *")
v.merge(:id=>1)
end
end)
end
Ceo.create(:num_managers=>3, :num_staff=>2, :name=>'E')
@db.sqls.must_equal ["INSERT INTO employees (name, kind) VALUES ('E', 'Ceo') RETURNING *",
"INSERT INTO managers (id, num_staff) VALUES (1, 2) RETURNING *",
"INSERT INTO executives (id, num_managers) VALUES (1, 3) RETURNING *"]
end
it "should insert the correct rows into all tables with a given primary key" do
e = Ceo.new(:num_managers=>3, :num_staff=>2, :name=>'E')
e.id = 2
e.save
@db.sqls.must_equal ["INSERT INTO employees (id, name, kind) VALUES (2, 'E', 'Ceo')",
"INSERT INTO managers (id, num_staff) VALUES (2, 2)",
"INSERT INTO executives (id, num_managers) VALUES (2, 3)"]
end
it "should update the correct rows in all tables when updating parent class" do
Employee.load(:id=>2).update(:name=>'E')
@db.sqls.must_equal ["UPDATE employees SET name = 'E' WHERE (id = 2)"]
end
it "should update the correct rows in all tables when updating subclass without separate table" do
Intern.load(:id=>2).update(:name=>'E')
@db.sqls.must_equal ["UPDATE employees SET name = 'E' WHERE (id = 2)"]
end
it "should update the correct rows in all tables when updating" do
Ceo.load(:id=>2).update(:num_managers=>3, :num_staff=>2, :name=>'E')
@db.sqls.must_equal ["UPDATE employees SET name = 'E' WHERE (id = 2)", "UPDATE managers SET num_staff = 2 WHERE (id = 2)", "UPDATE executives SET num_managers = 3 WHERE (id = 2)"]
end
it "should update only tables with changes when updating" do
obj = Ceo.load(:id=>2, :num_managers=>3, :num_staff=>2, :name=>'E')
obj.update(:num_staff=>3)
@db.sqls.must_equal ["UPDATE managers SET num_staff = 3 WHERE (id = 2)"]
end
it "should raise error if one of the updates does not update a single row" do
@db.numrows = [1, 0]
proc{Ceo.load(:id=>2).update(:num_managers=>3, :num_staff=>2, :name=>'E')}.must_raise Sequel::NoExistingObject
@db.sqls.must_equal ["UPDATE employees SET name = 'E' WHERE (id = 2)", "UPDATE managers SET num_staff = 2 WHERE (id = 2)"]
end
it "should handle many_to_one relationships correctly" do
Manager.dataset = Manager.dataset.with_fetch(:id=>3, :name=>'E', :kind=>'Ceo', :num_managers=>3)
Staff.load(:manager_id=>3).manager.must_equal Ceo.load(:id=>3, :name=>'E', :kind=>'Ceo', :num_managers=>3)
@db.sqls.must_equal ['SELECT * FROM (SELECT employees.id, employees.name, employees.kind, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees WHERE (id = 3) LIMIT 1']
end
it "should handle one_to_many relationships correctly" do
Staff.dataset = Staff.dataset.with_fetch(:id=>1, :name=>'S', :kind=>'Staff', :manager_id=>3)
Ceo.load(:id=>3).staff_members.must_equal [Staff.load(:id=>1, :name=>'S', :kind=>'Staff', :manager_id=>3)]
@db.sqls.must_equal ['SELECT * FROM (SELECT employees.id, employees.name, employees.kind, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id)) AS employees WHERE (employees.manager_id = 3)']
end
end
describe "class_table_inheritance plugin without sti_key with :alias option" do
before do
@db = Sequel.mock(:numrows=>1, :autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}]],
:managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}]],
:executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
:staff=>[[:id, {:type=>:integer}], [:manager_id, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.extend_datasets do
def columns
{[:employees]=>[:id, :name],
[:managers]=>[:id, :num_staff],
[:executives]=>[:id, :num_managers],
[:staff]=>[:id, :manager_id],
[:employees, :managers]=>[:id, :name, :num_staff],
[:employees, :managers, :executives]=>[:id, :name, :num_staff, :num_managers],
[:employees, :staff]=>[:id, :name, :manager_id],
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
class ::Employee < Sequel::Model(@db)
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance, :table_map=>{:Staff=>:staff}, :alias=>:emps
end
class ::Manager < Employee
one_to_many :staff_members, :class=>:Staff
end
class ::Executive < Manager
end
class ::Staff < Employee
many_to_one :manager
end
@ds = Employee.dataset
@db.sqls
end
after do
Object.send(:remove_const, :Executive)
Object.send(:remove_const, :Manager)
Object.send(:remove_const, :Staff)
Object.send(:remove_const, :Employee)
end
it "should have simple_table = nil for all subclasses" do
Manager.simple_table.must_be_nil
Executive.simple_table.must_be_nil
Staff.simple_table.must_be_nil
end
it "should have working row_proc if using set_dataset in subclass to remove columns" do
Manager.set_dataset(Manager.dataset.select(*(Manager.columns - [:blah])))
Manager.dataset = Manager.dataset.with_fetch(:id=>1)
Manager[1].must_equal Manager.load(:id=>1)
end
it "should use a joined dataset in subclasses" do
Employee.dataset.sql.must_equal 'SELECT * FROM employees'
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS emps'
Executive.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, managers.num_staff, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id)) AS emps'
Staff.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id)) AS emps'
end
it "should return rows with the current class if sti_key is nil" do
Employee.plugin(:class_table_inheritance)
Employee.dataset = Employee.dataset.with_fetch([{}])
Employee.first.class.must_equal Employee
end
it "should include schema for columns for tables for ancestor classes" do
Employee.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string})
Manager.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :num_staff=>{:type=>:integer})
Executive.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :num_staff=>{:type=>:integer}, :num_managers=>{:type=>:integer})
Staff.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :manager_id=>{:type=>:integer})
end
it "should use the correct primary key (which should have the same name in all subclasses)" do
[Employee, Manager, Executive, Staff].each{|c| c.primary_key.must_equal :id}
end
it "should have table_name return the table name of the most specific table" do
Employee.table_name.must_equal :employees
Manager.table_name.must_equal :emps
Executive.table_name.must_equal :emps
Staff.table_name.must_equal :emps
end
it "should delete the correct rows from all tables when deleting" do
Executive.load(:id=>1).delete
@db.sqls.must_equal ["DELETE FROM executives WHERE (id = 1)", "DELETE FROM managers WHERE (id = 1)", "DELETE FROM employees WHERE (id = 1)"]
end
it "should not allow deletion of frozen object" do
o = Executive.load(:id=>1)
o.freeze
proc{o.delete}.must_raise(Sequel::Error)
@db.sqls.must_equal []
end
it "should insert the correct rows into all tables when inserting" do
Executive.create(:num_managers=>3, :num_staff=>2, :name=>'E')
@db.sqls.must_equal ["INSERT INTO employees (name) VALUES ('E')",
"INSERT INTO managers (id, num_staff) VALUES (1, 2)",
"INSERT INTO executives (id, num_managers) VALUES (1, 3)"]
end
it "should insert the correct rows into all tables with a given primary key" do
e = Executive.new(:num_managers=>3, :num_staff=>2, :name=>'E')
e.id = 2
e.save
@db.sqls.must_equal ["INSERT INTO employees (id, name) VALUES (2, 'E')",
"INSERT INTO managers (id, num_staff) VALUES (2, 2)",
"INSERT INTO executives (id, num_managers) VALUES (2, 3)"]
end
it "should update the correct rows in all tables when updating" do
Executive.load(:id=>2).update(:num_managers=>3, :num_staff=>2, :name=>'E')
@db.sqls.must_equal ["UPDATE employees SET name = 'E' WHERE (id = 2)", "UPDATE managers SET num_staff = 2 WHERE (id = 2)", "UPDATE executives SET num_managers = 3 WHERE (id = 2)"]
end
it "should handle many_to_one relationships correctly" do
Manager.dataset = Manager.dataset.with_fetch(:id=>3, :name=>'E', :num_staff=>3)
Staff.load(:manager_id=>3).manager.must_equal Manager.load(:id=>3, :name=>'E', :num_staff=>3)
@db.sqls.must_equal ['SELECT * FROM (SELECT employees.id, employees.name, managers.num_staff FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS emps WHERE (id = 3) LIMIT 1']
end
it "should handle one_to_many relationships correctly" do
Staff.dataset = Staff.dataset.with_fetch(:id=>1, :name=>'S', :manager_id=>3)
Executive.load(:id=>3).staff_members.must_equal [Staff.load(:id=>1, :name=>'S', :manager_id=>3)]
@db.sqls.must_equal ['SELECT * FROM (SELECT employees.id, employees.name, staff.manager_id FROM employees INNER JOIN staff ON (staff.id = employees.id)) AS emps WHERE (emps.manager_id = 3)']
end
end
describe "class_table_inheritance plugin with duplicate columns" do
it "should raise error if no columns are explicitly ignored" do
@db = Sequel.mock(:autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
:managers=>[[:id, {:type=>:integer}], [:name, {:type=>:string}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.extend_datasets do
def columns
{[:employees]=>[:id, :name, :kind],
[:managers]=>[:id, :name],
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
class ::Employee < Sequel::Model(@db)
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance
end
proc{class ::Manager < Employee; end}.must_raise Sequel::Error
end
describe "with certain sub-class columns ignored" do
before do
@db = Sequel.mock(:autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}], [:updated_at, {:type=>:datetime}]],
:managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}], [:updated_at, {:type=>:datetime}], [:another_duplicate_column, {:type=>:integer}]],
:executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}], [:updated_at, {:type=>:datetime}], [:another_duplicate_column, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.extend_datasets do
def columns
{[:employees]=>[:id, :name, :kind, :updated_at],
[:managers]=>[:id, :num_staff, :updated_at, :another_duplicate_column],
[:executives]=>[:id, :num_managers, :updated_at, :another_duplicate_column],
[:employees, :managers]=>[:id, :name, :kind, :updated_at, :num_staff],
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
class ::Employee < Sequel::Model(@db)
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance, :ignore_subclass_columns=>[:updated_at]
end
class ::Manager < Employee
Manager.cti_ignore_subclass_columns.push(:another_duplicate_column)
end
class ::Executive < Manager; end
end
it "should not use the ignored column in a sub-class subquery" do
Employee.dataset.sql.must_equal 'SELECT * FROM employees'
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, employees.updated_at, managers.num_staff, managers.another_duplicate_column FROM employees INNER JOIN managers ON (managers.id = employees.id)) AS employees'
Executive.dataset.sql.must_equal 'SELECT * FROM (SELECT employees.id, employees.name, employees.kind, employees.updated_at, managers.num_staff, managers.another_duplicate_column, executives.num_managers FROM employees INNER JOIN managers ON (managers.id = employees.id) INNER JOIN executives ON (executives.id = managers.id)) AS employees'
end
it "should include schema for columns for tables for ancestor classes" do
Employee.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :updated_at=>{:type=>:datetime})
Manager.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :updated_at=>{:type=>:datetime}, :num_staff=>{:type=>:integer}, :another_duplicate_column=>{:type=>:integer})
Executive.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :updated_at=>{:type=>:datetime}, :num_staff=>{:type=>:integer}, :another_duplicate_column=>{:type=>:integer}, :num_managers=>{:type=>:integer})
end
after do
Object.send(:remove_const, :Executive)
end
end
after do
Object.send(:remove_const, :Manager)
Object.send(:remove_const, :Employee)
end
end
describe "class_table_inheritance plugin with dataset defined with QualifiedIdentifier" do
before do
@db = Sequel.mock(:numrows=>1, :autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{Sequel[:hr][:employees]=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
Sequel[:hr][:managers]=>[[:id, {:type=>:integer}]],
Sequel[:hr][:staff]=>[[:id, {:type=>:integer}], [:manager_id, {:type=>:integer}]],
Sequel[:hr][:executives]=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.singleton_class.send(:alias_method, :schema, :schema)
@db.extend_datasets do
def columns
{[Sequel[:hr][:employees]]=>[:id, :name, :kind],
[Sequel[:hr][:managers]]=>[:id],
[Sequel[:hr][:staff]]=>[:id, :manager_id],
[Sequel[:hr][:employees], Sequel[:hr][:managers]]=>[:id, :name, :kind],
[Sequel[:hr][:employees], Sequel[:hr][:staff]]=>[:id, :name, :kind, :manager_id],
[Sequel[:hr][:employees], Sequel[:hr][:managers], Sequel[:hr][:executives]]=>[:id, :name, :kind, :manager_id, :num_managers],
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
end
after do
[:Manager, :Staff, :Employee, :Executive].each{|s| Object.send(:remove_const, s) if Object.const_defined?(s)}
end
describe "with table_map used to qualify subclasses" do
before do
::Employee = Class.new(Sequel::Model)
::Employee.db = @db
::Employee.set_dataset(Sequel[:hr][:employees])
class ::Employee
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance, :table_map=>{:Manager=>Sequel[:hr][:managers],:Staff=>Sequel[:hr][:staff]}
end
class ::Manager < Employee
one_to_many :staff_members, :class=>:Staff
end
class ::Staff < Employee
many_to_one :manager
end
end
it "should handle many_to_one relationships correctly" do
Manager.dataset = Manager.dataset.with_fetch(:id=>3, :name=>'E')
Staff.load(:manager_id=>3).manager.must_equal Manager.load(:id=>3, :name=>'E')
@db.sqls.must_equal ['SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind FROM hr.employees INNER JOIN hr.managers ON (hr.managers.id = hr.employees.id)) AS employees WHERE (id = 3) LIMIT 1']
end
it "should handle one_to_many relationships correctly" do
Staff.dataset = Staff.dataset.with_fetch(:id=>1, :name=>'S', :manager_id=>3)
Manager.load(:id=>3).staff_members.must_equal [Staff.load(:id=>1, :name=>'S', :manager_id=>3)]
@db.sqls.must_equal ['SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind, hr.staff.manager_id FROM hr.employees INNER JOIN hr.staff ON (hr.staff.id = hr.employees.id)) AS employees WHERE (employees.manager_id = 3)']
end
end
describe "without table_map or qualify_tables set" do
it "should use a non-qualified subquery in subclasses" do
def @db.schema(table, opts={})
{Sequel[:hr][:employees]=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
:managers=>[[:id, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
@db.extend_datasets do
def columns
{[Sequel[:hr][:employees]]=>[:id, :name, :kind],
[:managers]=>[:id],
[Sequel[:hr][:employees], :managers]=>[:id, :name, :kind]
}[opts[:from] + (opts[:join] || []).map{|x| x.table}]
end
end
::Employee = Class.new(Sequel::Model)
::Employee.db = @db
::Employee.set_dataset(Sequel[:hr][:employees])
class ::Employee
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance
end
class ::Manager < ::Employee
end
Employee.dataset.sql.must_equal 'SELECT * FROM hr.employees'
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind FROM hr.employees INNER JOIN managers ON (managers.id = hr.employees.id)) AS employees'
end
end
describe "with qualify_tables option set" do
it "should use a subquery with the same qualifier in subclasses" do
::Employee = Class.new(Sequel::Model)
::Employee.db = @db
::Employee.set_dataset(Sequel[:hr][:employees])
class ::Employee
private
def _save_refresh; @values[:id] = 1 end
def self.columns
dataset.columns || dataset.opts[:from].first.expression.columns
end
plugin :class_table_inheritance, :table_map=>{:Staff=>Sequel[:hr][:staff]}, qualify_tables: true
end
class ::Manager < ::Employee
one_to_many :staff_members, :class=>:Staff
end
class ::Staff < ::Employee
many_to_one :manager
end
class ::Executive < ::Manager
end
Employee.dataset.sql.must_equal 'SELECT * FROM hr.employees'
Manager.dataset.sql.must_equal 'SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind FROM hr.employees INNER JOIN hr.managers ON (hr.managers.id = hr.employees.id)) AS employees'
Staff.dataset.sql.must_equal 'SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind, hr.staff.manager_id FROM hr.employees INNER JOIN hr.staff ON (hr.staff.id = hr.employees.id)) AS employees'
Executive.dataset.sql.must_equal 'SELECT * FROM (SELECT hr.employees.id, hr.employees.name, hr.employees.kind, hr.executives.num_managers FROM hr.employees INNER JOIN hr.managers ON (hr.managers.id = hr.employees.id) INNER JOIN hr.executives ON (hr.executives.id = hr.managers.id)) AS employees'
end
end
end
describe "class_table_inheritance plugin with schema_caching extension" do
before do
@db = Sequel.mock(:autoid=>proc{|sql| 1})
def @db.supports_schema_parsing?() true end
def @db.schema(table, opts={})
{:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
:managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}] ],
:executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
}[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
end
end
after do
[:Executive, :Manager, :Employee, :Staff].each{|s| Object.send(:remove_const, s) if Object.const_defined?(s)}
end
it "should not query for columns if the schema cache is present and a table_map is given" do
class ::Employee < Sequel::Model(@db)
plugin :class_table_inheritance, :table_map=>{:Staff=>:employees, :Manager=>:managers, :Executive=>:executives}
end
class ::Staff < Employee; end
class ::Manager < Employee; end
class ::Executive < Manager; end
Employee.columns.must_equal [:id, :name, :kind]
Staff.columns.must_equal [:id, :name, :kind]
Manager.columns.must_equal [:id, :name, :kind, :num_staff]
Executive.columns.must_equal [:id, :name, :kind, :num_staff, :num_managers]
@db.sqls.must_equal []
end
it "should not query for columns if the schema cache is present and no table_map is given" do
class ::Employee < Sequel::Model(@db)
plugin :class_table_inheritance
end
class ::Manager < Employee; end
class ::Executive < Manager; end
@db.sqls.must_equal []
end
end
|