sequel 5.3.0 → 5.4.0

Sign up to get free protection for your applications and to get access to all the features.
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)