sequel 5.3.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG +30 -0
  3. data/bin/sequel +13 -0
  4. data/doc/cheat_sheet.rdoc +1 -0
  5. data/doc/dataset_filtering.rdoc +1 -1
  6. data/doc/querying.rdoc +8 -11
  7. data/doc/release_notes/5.4.0.txt +80 -0
  8. data/doc/testing.rdoc +2 -0
  9. data/lib/sequel/adapters/shared/db2.rb +6 -5
  10. data/lib/sequel/adapters/shared/mssql.rb +5 -8
  11. data/lib/sequel/adapters/shared/mysql.rb +4 -8
  12. data/lib/sequel/adapters/shared/oracle.rb +1 -1
  13. data/lib/sequel/adapters/shared/postgres.rb +5 -3
  14. data/lib/sequel/adapters/shared/sqlanywhere.rb +1 -6
  15. data/lib/sequel/adapters/shared/sqlite.rb +2 -0
  16. data/lib/sequel/database/connecting.rb +1 -1
  17. data/lib/sequel/database/schema_methods.rb +10 -1
  18. data/lib/sequel/dataset/query.rb +1 -2
  19. data/lib/sequel/extensions/date_arithmetic.rb +27 -10
  20. data/lib/sequel/extensions/datetime_parse_to_time.rb +37 -0
  21. data/lib/sequel/extensions/index_caching.rb +107 -0
  22. data/lib/sequel/extensions/null_dataset.rb +3 -1
  23. data/lib/sequel/extensions/pg_timestamptz.rb +26 -0
  24. data/lib/sequel/model/base.rb +2 -2
  25. data/lib/sequel/plugins/class_table_inheritance.rb +11 -3
  26. data/lib/sequel/plugins/json_serializer.rb +2 -2
  27. data/lib/sequel/plugins/xml_serializer.rb +1 -1
  28. data/lib/sequel/version.rb +1 -1
  29. data/spec/adapters/postgres_spec.rb +1 -1
  30. data/spec/adapters/spec_helper.rb +3 -0
  31. data/spec/adapters/sqlite_spec.rb +1 -1
  32. data/spec/bin_spec.rb +9 -0
  33. data/spec/core/connection_pool_spec.rb +2 -2
  34. data/spec/core/dataset_spec.rb +1 -6
  35. data/spec/extensions/class_table_inheritance_spec.rb +52 -2
  36. data/spec/extensions/date_arithmetic_spec.rb +15 -1
  37. data/spec/extensions/datetime_parse_to_time_spec.rb +169 -0
  38. data/spec/extensions/index_caching_spec.rb +66 -0
  39. data/spec/extensions/json_serializer_spec.rb +5 -0
  40. data/spec/extensions/null_dataset_spec.rb +5 -0
  41. data/spec/extensions/pg_extended_date_support_spec.rb +4 -0
  42. data/spec/extensions/pg_timestamptz_spec.rb +17 -0
  43. data/spec/extensions/xml_serializer_spec.rb +7 -0
  44. data/spec/integration/dataset_test.rb +6 -0
  45. data/spec/integration/prepared_statement_test.rb +1 -1
  46. data/spec/integration/schema_test.rb +19 -17
  47. data/spec/integration/spec_helper.rb +4 -0
  48. data/spec/model/record_spec.rb +28 -0
  49. metadata +11 -3
@@ -0,0 +1,37 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # This switches the default parsing of strings into Time values
4
+ # from using Time.parse to using DateTime.parse.to_time. This
5
+ # fixes issues when the times being parsed have no timezone
6
+ # information, the implicit timezone for the Database instance
7
+ # is set to +:utc+, and the timestamps being used include values
8
+ # not valid in the local timezone, such as during a daylight
9
+ # savings time switch.
10
+ #
11
+ # To load the extension:
12
+ #
13
+ # Sequel.extension :datetime_parse_to_time
14
+
15
+ #
16
+ module Sequel::DateTimeParseToTime
17
+ private
18
+
19
+ # Use DateTime.parse.to_time to do the conversion if the input a string and is assumed to
20
+ # be in UTC and there is no offset information in the string.
21
+ def convert_input_timestamp(v, input_timezone)
22
+ if v.is_a?(String) && datetime_class == Time && input_timezone == :utc && !Date._parse(v).has_key?(:offset)
23
+ t = DateTime.parse(v).to_time
24
+ case application_timezone
25
+ when nil, :local
26
+ t = t.localtime
27
+ end
28
+ t
29
+ else
30
+ super
31
+ end
32
+ rescue => e
33
+ raise convert_exception_class(e, Sequel::InvalidValue)
34
+ end
35
+ end
36
+
37
+ Sequel.extend(Sequel::DateTimeParseToTime)
@@ -0,0 +1,107 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The index_caching extension adds a few methods to Sequel::Database
4
+ # that make it easy to dump information about database indexes to a file,
5
+ # and load it from that file. Loading index information from a
6
+ # dumped file is faster than parsing it from the database, so this
7
+ # can save bootup time for applications with large numbers of index.
8
+ #
9
+ # Basic usage in application code:
10
+ #
11
+ # DB = Sequel.connect('...')
12
+ # DB.extension :index_caching
13
+ # DB.load_index_cache('/path/to/index_cache.dump')
14
+ #
15
+ # # load model files
16
+ #
17
+ # Then, whenever database indicies are modified, write a new cached
18
+ # file. You can do that with <tt>bin/sequel</tt>'s -X option:
19
+ #
20
+ # bin/sequel -X /path/to/index_cache.dump postgres://...
21
+ #
22
+ # Alternatively, if you don't want to dump the index information for
23
+ # all tables, and you don't worry about race conditions, you can
24
+ # choose to use the following in your application code:
25
+ #
26
+ # DB = Sequel.connect('...')
27
+ # DB.extension :index_caching
28
+ # DB.load_index_cache?('/path/to/index_cache.dump')
29
+ #
30
+ # # load model files
31
+ #
32
+ # DB.dump_index_cache?('/path/to/index_cache.dump')
33
+ #
34
+ # With this method, you just have to delete the index dump file if
35
+ # the schema is modified, and the application will recreate it for you
36
+ # using just the tables that your models use.
37
+ #
38
+ # Note that it is up to the application to ensure that the dumped
39
+ # index cache reflects the current state of the database. Sequel
40
+ # does no checking to ensure this, as checking would take time and the
41
+ # purpose of this code is to take a shortcut.
42
+ #
43
+ # The index cache is dumped in Marshal format, since it is the fastest
44
+ # and it handles all ruby objects used in the indexes hash. Because of this,
45
+ # you should not attempt to load from an untrusted file.
46
+ #
47
+ # Related module: Sequel::IndexCaching
48
+
49
+ #
50
+ module Sequel
51
+ module IndexCaching
52
+ # Set index cache to the empty hash.
53
+ def self.extended(db)
54
+ db.instance_variable_set(:@indexes, {})
55
+ end
56
+
57
+ # Remove the index cache for the given schema name
58
+ def remove_cached_schema(table)
59
+ k = quote_schema_table(table)
60
+ Sequel.synchronize{@indexes.delete(k)}
61
+ super
62
+ end
63
+
64
+ # Dump the index cache to the filename given in Marshal format.
65
+ def dump_index_cache(file)
66
+ File.open(file, 'wb'){|f| f.write(Marshal.dump(@indexes))}
67
+ nil
68
+ end
69
+
70
+ # Dump the index cache to the filename given unless the file
71
+ # already exists.
72
+ def dump_index_cache?(file)
73
+ dump_index_cache(file) unless File.exist?(file)
74
+ end
75
+
76
+ # Replace the index cache with the data from the given file, which
77
+ # should be in Marshal format.
78
+ def load_index_cache(file)
79
+ @indexes = Marshal.load(File.read(file))
80
+ nil
81
+ end
82
+
83
+ # Replace the index cache with the data from the given file if the
84
+ # file exists.
85
+ def load_index_cache?(file)
86
+ load_index_cache(file) if File.exist?(file)
87
+ end
88
+
89
+ # If no options are provided and there is cached index information for
90
+ # the table, return the cached information instead of querying the
91
+ # database.
92
+ def indexes(table, opts=OPTS)
93
+ return super unless opts.empty?
94
+
95
+ quoted_name = literal(table)
96
+ if v = Sequel.synchronize{@indexes[quoted_name]}
97
+ return v
98
+ end
99
+
100
+ result = super
101
+ Sequel.synchronize{@indexes[quoted_name] = result}
102
+ result
103
+ end
104
+ end
105
+
106
+ Database.register_extension(:index_caching, IndexCaching)
107
+ end
@@ -41,7 +41,9 @@ module Sequel
41
41
  module Nullifiable
42
42
  # Return a cloned nullified dataset.
43
43
  def nullify
44
- with_extend(NullDataset)
44
+ cached_dataset(:_nullify_ds) do
45
+ with_extend(NullDataset)
46
+ end
45
47
  end
46
48
  end
47
49
 
@@ -0,0 +1,26 @@
1
+ # frozen-string-literal: true
2
+ #
3
+ # The pg_timestamptz extension changes the default timestamp
4
+ # type for the database to be +timestamptz+ (+timestamp with time zone+)
5
+ # instead of +timestamp+ (+timestamp without time zone+). This is
6
+ # recommended if you are dealing with multiple timezones in your application.
7
+ #
8
+ # To load the extension into the database:
9
+ #
10
+ # DB.extension :pg_timestamptz
11
+ #
12
+ # Related module: Sequel::Postgres::Timestamptz
13
+
14
+ #
15
+ module Sequel
16
+ module Postgres
17
+ module Timestamptz
18
+ # Use timestamptz by default for generic timestamp value.
19
+ def type_literal_generic_datetime(column)
20
+ :timestamptz
21
+ end
22
+ end
23
+ end
24
+
25
+ Database.register_extension(:pg_timestamptz, Postgres::Timestamptz)
26
+ end
@@ -1635,8 +1635,8 @@ module Sequel
1635
1635
  # the record should be refreshed from the database.
1636
1636
  def _insert
1637
1637
  ds = _insert_dataset
1638
- if _use_insert_select?(ds) && (h = _insert_select_raw(ds))
1639
- _save_set_values(h)
1638
+ if _use_insert_select?(ds) && !(h = _insert_select_raw(ds)).nil?
1639
+ _save_set_values(h) if h
1640
1640
  nil
1641
1641
  else
1642
1642
  iid = _insert_raw(ds)
@@ -196,6 +196,8 @@ module Sequel
196
196
  # :key_chooser :: proc returning key for the provided model instance
197
197
  # :table_map :: Hash with class name symbols keys mapping to table name symbol values.
198
198
  # Overrides implicit table names.
199
+ # :ignore_subclass_columns :: Array with column names as symbols that are ignored
200
+ # on all sub-classes.
199
201
  def self.configure(model, opts = OPTS)
200
202
  SingleTableInheritance.configure model, opts[:key], opts
201
203
 
@@ -206,6 +208,7 @@ module Sequel
206
208
  @cti_table_columns = columns
207
209
  @cti_table_map = opts[:table_map] || {}
208
210
  @cti_alias = opts[:alias] || @dataset.first_source
211
+ @cti_ignore_subclass_columns = opts[:ignore_subclass_columns] || []
209
212
  end
210
213
  end
211
214
 
@@ -232,17 +235,22 @@ module Sequel
232
235
  # the implicit naming is incorrect.
233
236
  attr_reader :cti_table_map
234
237
 
238
+ # An array of columns that may be duplicated in sub-classes. The
239
+ # primary key column is always allowed to be duplicated
240
+ attr_reader :cti_ignore_subclass_columns
241
+
235
242
  # Freeze CTI information when freezing model class.
236
243
  def freeze
237
244
  @cti_models.freeze
238
245
  @cti_tables.freeze
239
246
  @cti_table_columns.freeze
240
247
  @cti_table_map.freeze
248
+ @cti_ignore_subclass_columns.freeze
241
249
 
242
250
  super
243
251
  end
244
252
 
245
- Plugins.inherited_instance_variables(self, :@cti_models=>nil, :@cti_tables=>nil, :@cti_table_columns=>nil, :@cti_instance_dataset=>nil, :@cti_table_map=>nil, :@cti_alias=>nil)
253
+ Plugins.inherited_instance_variables(self, :@cti_models=>nil, :@cti_tables=>nil, :@cti_table_columns=>nil, :@cti_instance_dataset=>nil, :@cti_table_map=>nil, :@cti_alias=>nil, :@cti_ignore_subclass_columns=>nil)
246
254
 
247
255
  def inherited(subclass)
248
256
  ds = sti_dataset
@@ -273,10 +281,10 @@ module Sequel
273
281
  if cti_tables.length == 1
274
282
  ds = ds.select(*self.columns.map{|cc| Sequel.qualify(cti_table_name, Sequel.identifier(cc))})
275
283
  end
276
- cols = columns - [pk]
284
+ cols = (columns - [pk]) - cti_ignore_subclass_columns
277
285
  dup_cols = cols & ds.columns
278
286
  unless dup_cols.empty?
279
- raise Error, "class_table_inheritance with duplicate column names (other than the primary key column) is not supported, make sure tables have unique column names"
287
+ raise Error, "class_table_inheritance with duplicate column names (other than the primary key column) is not supported, make sure tables have unique column names (duplicate columns: #{dup_cols}). If this is desired, specify these columns in the :ignore_subclass_columns option when initializing the plugin"
280
288
  end
281
289
  sel_app = cols.map{|cc| Sequel.qualify(table, Sequel.identifier(cc))}
282
290
  @sti_dataset = ds = ds.join(table, pk=>pk).select_append(*sel_app)
@@ -406,7 +406,7 @@ module Sequel
406
406
  end
407
407
  end
408
408
 
409
- res = if row_proc
409
+ res = if row_proc || @opts[:eager_graph]
410
410
  array = if opts[:array]
411
411
  opts = opts.dup
412
412
  opts.delete(:array)
@@ -414,7 +414,7 @@ module Sequel
414
414
  all
415
415
  end
416
416
  array.map{|obj| Literal.new(Sequel.object_to_json(obj, opts, &opts[:instance_block]))}
417
- else
417
+ else
418
418
  all
419
419
  end
420
420
 
@@ -389,7 +389,7 @@ module Sequel
389
389
  # as well as the :array_root_name option for specifying the name of
390
390
  # the root node that contains the nodes for all of the instances.
391
391
  def to_xml(opts=OPTS)
392
- raise(Sequel::Error, "Dataset#to_xml") unless row_proc
392
+ raise(Sequel::Error, "Dataset#to_xml") unless row_proc || @opts[:eager_graph]
393
393
  x = model.xml_builder(opts)
394
394
  name_proc = model.xml_serialize_name_proc(opts)
395
395
  array = if opts[:array]
@@ -5,7 +5,7 @@ module Sequel
5
5
  MAJOR = 5
6
6
  # The minor version of Sequel. Bumped for every non-patch level
7
7
  # release, generally around once a month.
8
- MINOR = 3
8
+ MINOR = 4
9
9
  # The tiny version of Sequel. Usually 0, only bumped for bugfix
10
10
  # releases that fix regressions from previous versions.
11
11
  TINY = 0
@@ -511,7 +511,7 @@ describe "A PostgreSQL dataset" do
511
511
 
512
512
  it "should support :using when altering a column's type" do
513
513
  @db.create_table!(:atest){Integer :t}
514
- @db[:atest].insert(1262304000)
514
+ @db[:atest].insert(1262404000)
515
515
  @db.alter_table(:atest){set_column_type :t, Time, :using=>Sequel.cast('epoch', Time) + Sequel.cast('1 second', :interval) * :t}
516
516
  @db[:atest].get(Sequel.extract(:year, :t)).must_equal 2010
517
517
  end
@@ -32,6 +32,9 @@ end
32
32
  IDENTIFIER_MANGLING = !!ENV['SEQUEL_IDENTIFIER_MANGLING'] unless defined?(IDENTIFIER_MANGLING)
33
33
  DB.extension(:identifier_mangling) if IDENTIFIER_MANGLING
34
34
 
35
+ DB.extension(:pg_timestamptz) if ENV['SEQUEL_PG_TIMESTAMPTZ']
36
+ DB.extension :index_caching if ENV['SEQUEL_INDEX_CACHING']
37
+
35
38
  if dch = ENV['SEQUEL_DUPLICATE_COLUMNS_HANDLER']
36
39
  DB.extension :duplicate_columns_handler
37
40
  DB.opts[:on_duplicate_columns] = dch.to_sym unless dch.empty?
@@ -554,7 +554,7 @@ describe "A SQLite database" do
554
554
  @db.add_index :test3, :b
555
555
  @db.add_index :test3, [:b, :a]
556
556
  @db.drop_column :test3, :b
557
- @db.indexes(:test3).must_equal(:test3_a_index=>{:unique=>false, :columns=>[:a]})
557
+ @db.indexes(:test3)[:test3_a_index].must_equal(:unique=>false, :columns=>[:a])
558
558
  end
559
559
 
560
560
  it "should have support for various #transaction modes" do
data/spec/bin_spec.rb CHANGED
@@ -183,6 +183,15 @@ END
183
183
  Marshal.load(File.read(TMP_FILE)).must_equal("`a`"=>[[:a, {:type=>:integer, :db_type=>"integer", :ruby_default=>nil, :allow_null=>true, :default=>nil, :primary_key=>false}]])
184
184
  end
185
185
 
186
+ it "-X should dump the index cache" do
187
+ bin(:args=>"-X #{TMP_FILE}").must_equal ''
188
+ Marshal.load(File.read(TMP_FILE)).must_equal({})
189
+ DB.create_table(:a){Integer :id}
190
+ DB.create_table(:b){Integer :b, index: {name: "idx_test", unique: true}}
191
+ bin(:args=>"-X #{TMP_FILE}").must_equal ''
192
+ Marshal.load(File.read(TMP_FILE)).must_equal("`a`"=>{}, "`b`"=>{:idx_test=>{:unique=>true, :columns=>[:b]}})
193
+ end
194
+
186
195
  it "-t should output full backtraces on error" do
187
196
  bin(:args=>'-c "lambda{lambda{lambda{raise \'foo\'}.call}.call}.call"', :stderr=>true).count("\n").must_be :<, 3
188
197
  bin(:args=>'-t -c "lambda{lambda{lambda{raise \'foo\'}.call}.call}.call"', :stderr=>true).count("\n").must_be :>, 3
@@ -510,7 +510,7 @@ ThreadedConnectionPoolSpecs = shared_description do
510
510
  @pool.hold{|cc| cc.must_equal c}
511
511
  @pool.hold do |cc|
512
512
  cc.must_equal c
513
- Thread.new{@pool.hold{|cc2| cc2.must_equal c2}}
513
+ Thread.new{@pool.hold{|cc2| _(cc2).must_equal c2}}.join
514
514
  end
515
515
  end
516
516
 
@@ -523,7 +523,7 @@ ThreadedConnectionPoolSpecs = shared_description do
523
523
  @pool.hold{|cc| cc.must_equal c}
524
524
  @pool.hold do |cc|
525
525
  cc.must_equal c2
526
- Thread.new{@pool.hold{|cc2| cc2.must_equal c}}
526
+ Thread.new{@pool.hold{|cc2| _(cc2).must_equal c}}.join
527
527
  end
528
528
  end
529
529
 
@@ -378,14 +378,9 @@ describe "Dataset#where" do
378
378
  @dataset.where(nil).sql.must_equal "SELECT * FROM test WHERE NULL"
379
379
  end
380
380
 
381
- deprecated "should handle nil block result has no existing filter" do
382
- @dataset.where{nil}.sql.must_equal "SELECT * FROM test"
383
- end
384
-
385
- # SEQUEL54
386
381
  it "should handle nil block result has no existing filter" do
387
382
  @dataset.where{nil}.sql.must_equal "SELECT * FROM test WHERE NULL"
388
- end if false
383
+ end
389
384
 
390
385
  it "should just clone if given an empty array or hash argument" do
391
386
  @dataset.where({}).sql.must_equal @dataset.sql
@@ -6,7 +6,7 @@ describe "class_table_inheritance plugin" do
6
6
  def @db.supports_schema_parsing?() true end
7
7
  def @db.schema(table, opts={})
8
8
  {:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}]],
9
- :managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}]],
9
+ :managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}] ],
10
10
  :executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}]],
11
11
  :staff=>[[:id, {:type=>:integer}], [:manager_id, {:type=>:integer}]],
12
12
  }[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
@@ -486,7 +486,7 @@ describe "class_table_inheritance plugin without sti_key with :alias option" do
486
486
  end
487
487
 
488
488
  describe "class_table_inheritance plugin with duplicate columns" do
489
- it "should raise error" do
489
+ it "should raise error if no columns are explicitly ignored" do
490
490
  @db = Sequel.mock(:autoid=>proc{|sql| 1})
491
491
  def @db.supports_schema_parsing?() true end
492
492
  def @db.schema(table, opts={})
@@ -510,6 +510,56 @@ describe "class_table_inheritance plugin with duplicate columns" do
510
510
  end
511
511
  proc{class ::Manager < Employee; end}.must_raise Sequel::Error
512
512
  end
513
+
514
+ describe "with certain sub-class columns ignored" do
515
+ before do
516
+ @db = Sequel.mock(:autoid=>proc{|sql| 1})
517
+ def @db.supports_schema_parsing?() true end
518
+ def @db.schema(table, opts={})
519
+ {:employees=>[[:id, {:primary_key=>true, :type=>:integer}], [:name, {:type=>:string}], [:kind, {:type=>:string}], [:updated_at, {:type=>:datetime}]],
520
+ :managers=>[[:id, {:type=>:integer}], [:num_staff, {:type=>:integer}], [:updated_at, {:type=>:datetime}], [:another_duplicate_column, {:type=>:integer}]],
521
+ :executives=>[[:id, {:type=>:integer}], [:num_managers, {:type=>:integer}], [:updated_at, {:type=>:datetime}], [:another_duplicate_column, {:type=>:integer}]],
522
+ }[table.is_a?(Sequel::Dataset) ? table.first_source_table : table]
523
+ end
524
+ @db.extend_datasets do
525
+ def columns
526
+ {[:employees]=>[:id, :name, :kind, :updated_at],
527
+ [:managers]=>[:id, :num_staff, :updated_at, :another_duplicate_column],
528
+ [:executives]=>[:id, :num_managers, :updated_at, :another_duplicate_column],
529
+ [:employees, :managers]=>[:id, :name, :kind, :updated_at, :num_staff],
530
+ }[opts[:from] + (opts[:join] || []).map{|x| x.table}]
531
+ end
532
+ end
533
+ class ::Employee < Sequel::Model(@db)
534
+ def _save_refresh; @values[:id] = 1 end
535
+ def self.columns
536
+ dataset.columns || dataset.opts[:from].first.expression.columns
537
+ end
538
+ plugin :class_table_inheritance, :ignore_subclass_columns=>[:updated_at]
539
+ end
540
+ class ::Manager < Employee
541
+ Manager.cti_ignore_subclass_columns.push(:another_duplicate_column)
542
+ end
543
+ class ::Executive < Manager; end
544
+ end
545
+
546
+ it "should not use the ignored column in a sub-class subquery" do
547
+ Employee.dataset.sql.must_equal 'SELECT * FROM employees'
548
+ 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'
549
+ 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'
550
+ end
551
+
552
+ it "should include schema for columns for tables for ancestor classes" do
553
+ Employee.db_schema.must_equal(:id=>{:primary_key=>true, :type=>:integer}, :name=>{:type=>:string}, :kind=>{:type=>:string}, :updated_at=>{:type=>:datetime})
554
+ 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})
555
+ 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})
556
+ end
557
+
558
+ after do
559
+ Object.send(:remove_const, :Executive)
560
+ end
561
+ end
562
+
513
563
  after do
514
564
  Object.send(:remove_const, :Manager)
515
565
  Object.send(:remove_const, :Employee)