declare_schema 0.8.0.pre.5 → 0.10.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +1 -1
  3. data/CHANGELOG.md +21 -1
  4. data/Gemfile.lock +1 -1
  5. data/README.md +91 -13
  6. data/lib/declare_schema.rb +46 -0
  7. data/lib/declare_schema/dsl.rb +39 -0
  8. data/lib/declare_schema/extensions/active_record/fields_declaration.rb +23 -4
  9. data/lib/declare_schema/model.rb +51 -59
  10. data/lib/declare_schema/model/column.rb +2 -0
  11. data/lib/declare_schema/model/field_spec.rb +11 -8
  12. data/lib/declare_schema/model/habtm_model_shim.rb +1 -1
  13. data/lib/declare_schema/version.rb +1 -1
  14. data/lib/generators/declare_schema/migration/migrator.rb +22 -33
  15. data/lib/generators/declare_schema/support/model.rb +4 -4
  16. data/spec/lib/declare_schema/api_spec.rb +7 -7
  17. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +41 -15
  18. data/spec/lib/declare_schema/field_spec_spec.rb +73 -22
  19. data/spec/lib/declare_schema/generator_spec.rb +3 -3
  20. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +78 -26
  21. data/spec/lib/declare_schema/migration_generator_spec.rb +1989 -815
  22. data/spec/lib/declare_schema/model/column_spec.rb +47 -17
  23. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +146 -57
  24. data/spec/lib/declare_schema/model/habtm_model_shim_spec.rb +3 -3
  25. data/spec/lib/declare_schema/model/index_definition_spec.rb +188 -77
  26. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +75 -11
  27. data/spec/lib/declare_schema_spec.rb +101 -0
  28. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +12 -2
  29. metadata +7 -6
  30. data/test_responses.txt +0 -2
@@ -44,6 +44,8 @@ module DeclareSchema
44
44
  if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
45
45
  types[:text][:limit] ||= 0xffff
46
46
  types[:binary][:limit] ||= 0xffff
47
+
48
+ types[:varbinary] ||= { name: "varbinary" } # TODO: :varbinary is an Invoca addition to Rails; make it a configurable option
47
49
  end
48
50
  end
49
51
  end
@@ -47,9 +47,9 @@ module DeclareSchema
47
47
  end
48
48
 
49
49
  def initialize(model, name, type, position: 0, **options)
50
- defined_primary_key = model.defined_primary_key
50
+ _defined_primary_key = model._defined_primary_key
51
51
 
52
- name.to_s == defined_primary_key and raise ArgumentError, "you may not provide a field spec for the primary key #{name.inspect}"
52
+ name.to_s == _defined_primary_key and raise ArgumentError, "you may not provide a field spec for the primary key #{name.inspect}"
53
53
 
54
54
  @model = model
55
55
  @name = name.to_sym
@@ -58,18 +58,21 @@ module DeclareSchema
58
58
  @position = position
59
59
  @options = options.dup
60
60
 
61
- @options.has_key?(:null) or @options[:null] = false
61
+ @options.has_key?(:null) or @options[:null] = ::DeclareSchema.default_null
62
+ @options[:null].nil? and raise "null: must be provided for field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_null is set to 'nil'; do you want `null: false`?"
62
63
 
63
64
  case @type
64
65
  when :text
65
66
  if self.class.mysql_text_limits?
66
67
  @options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
67
- @options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
68
+ @options[:limit] ||= ::DeclareSchema.default_text_limit or
69
+ raise("limit: must be provided for :text field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_text_limit is set to 'nil'; do you want `limit: 0xffff_ffff`?")
70
+ @options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit])
68
71
  else
69
72
  @options.delete(:limit)
70
73
  end
71
74
  when :string
72
- @options[:limit] or raise "limit: must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
75
+ @options[:limit] ||= ::DeclareSchema.default_string_limit or raise "limit: must be provided for :string field #{model}##{@name}: #{@options.inspect} since ::DeclareSchema#default_string_limit is set to 'nil'; do you want `limit: 255`?"
73
76
  when :bigint
74
77
  @type = :integer
75
78
  @options[:limit] = 8
@@ -96,15 +99,15 @@ module DeclareSchema
96
99
 
97
100
  if @type.in?([:text, :string])
98
101
  if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
99
- @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
100
- @options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
102
+ @options[:charset] ||= model.table_options[:charset] || ::DeclareSchema.default_charset
103
+ @options[:collation] ||= model.table_options[:collation] || ::DeclareSchema.default_collation
101
104
  else
102
105
  @options.delete(:charset)
103
106
  @options.delete(:collation)
104
107
  end
105
108
  else
106
109
  @options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
107
- @options[:collation] and warne("collation may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
110
+ @options[:collation] and warn("collation may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
108
111
  end
109
112
 
110
113
  @options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
@@ -47,7 +47,7 @@ module DeclareSchema
47
47
  false # no single-column primary key in database
48
48
  end
49
49
 
50
- def defined_primary_key
50
+ def _defined_primary_key
51
51
  false # no single-column primary key declared
52
52
  end
53
53
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.8.0.pre.5"
4
+ VERSION = "0.10.0"
5
5
  end
@@ -9,29 +9,14 @@ module Generators
9
9
  class Migrator
10
10
  class Error < RuntimeError; end
11
11
 
12
- DEFAULT_CHARSET = "utf8mb4"
13
- DEFAULT_COLLATION = "utf8mb4_bin"
14
-
15
12
  @ignore_models = []
16
13
  @ignore_tables = []
17
14
  @before_generating_migration_callback = nil
18
15
  @active_record_class = ActiveRecord::Base
19
- @default_charset = DEFAULT_CHARSET
20
- @default_collation = DEFAULT_COLLATION
21
16
 
22
17
  class << self
23
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints
24
- attr_reader :active_record_class, :default_charset, :default_collation, :before_generating_migration_callback
25
-
26
- def default_charset=(charset)
27
- charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
28
- @default_charset = charset
29
- end
30
-
31
- def default_collation=(collation)
32
- collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
33
- @default_collation = collation
34
- end
18
+ attr_accessor :ignore_models, :ignore_tables
19
+ attr_reader :active_record_class, :before_generating_migration_callback
35
20
 
36
21
  def active_record_class
37
22
  @active_record_class.is_a?(Class) or @active_record_class = @active_record_class.to_s.constantize
@@ -58,6 +43,9 @@ module Generators
58
43
  block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
59
44
  @before_generating_migration_callback = block
60
45
  end
46
+
47
+ delegate :default_charset=, :default_collation=, :default_charset, :default_collation, to: ::DeclareSchema
48
+ deprecate :default_charset=, :default_collation=, :default_charset, :default_collation, deprecator: ActiveSupport::Deprecation.new('1.0', 'declare_schema')
61
49
  end
62
50
 
63
51
  def initialize(ambiguity_resolver = {})
@@ -279,18 +267,19 @@ module Generators
279
267
  end
280
268
 
281
269
  #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
282
- #{create_indexes(model).join("\n") unless Migrator.disable_indexing}
283
- #{create_constraints(model).join("\n") unless Migrator.disable_indexing}
270
+ #{create_indexes(model).join("\n") if ::DeclareSchema.default_generate_indexing}
271
+ #{create_constraints(model).join("\n") if ::DeclareSchema.default_generate_foreign_keys}
284
272
  EOS
285
273
  end
286
274
 
287
275
  def create_table_options(model, disable_auto_increment)
288
- if model.primary_key.blank? || disable_auto_increment
276
+ primary_key = model._defined_primary_key
277
+ if primary_key.blank? || disable_auto_increment
289
278
  "id: false"
290
- elsif model.primary_key == "id"
279
+ elsif primary_key == "id"
291
280
  "id: :bigint"
292
281
  else
293
- "primary_key: :#{model.primary_key}"
282
+ "primary_key: :#{primary_key}"
294
283
  end
295
284
  end
296
285
 
@@ -299,8 +288,8 @@ module Generators
299
288
  {}
300
289
  else
301
290
  {
302
- charset: model.table_options[:charset] || Migrator.default_charset,
303
- collation: model.table_options[:collation] || Migrator.default_collation
291
+ charset: model.table_options[:charset] || ::DeclareSchema.default_charset,
292
+ collation: model.table_options[:collation] || ::DeclareSchema.default_collation
304
293
  }
305
294
  end
306
295
  end
@@ -323,18 +312,18 @@ module Generators
323
312
  new_table_name = model.table_name
324
313
 
325
314
  db_columns = model.connection.columns(current_table_name).index_by(&:name)
326
- key_missing = db_columns[model.primary_key].nil? && model.primary_key.present?
327
- if model.primary_key.present?
328
- db_columns.delete(model.primary_key)
315
+ key_missing = db_columns[model._defined_primary_key].nil? && model._defined_primary_key.present?
316
+ if model._defined_primary_key.present?
317
+ db_columns.delete(model._defined_primary_key)
329
318
  end
330
319
 
331
320
  model_column_names = model.field_specs.keys.map(&:to_s)
332
321
  db_column_names = db_columns.keys.map(&:to_s)
333
322
 
334
323
  to_add = model_column_names - db_column_names
335
- to_add += [model.primary_key] if key_missing && model.primary_key.present?
324
+ to_add += [model._defined_primary_key] if key_missing && model._defined_primary_key.present?
336
325
  to_remove = db_column_names - model_column_names
337
- to_remove -= [model.primary_key.to_sym] if model.primary_key.present?
326
+ to_remove -= [model._defined_primary_key.to_sym] if model._defined_primary_key.present?
338
327
 
339
328
  to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
340
329
 
@@ -414,7 +403,7 @@ module Generators
414
403
  end
415
404
 
416
405
  def change_indexes(model, old_table_name, to_remove)
417
- Migrator.disable_constraints and return [[], []]
406
+ ::DeclareSchema.default_generate_indexing or return [[], []]
418
407
 
419
408
  new_table_name = model.table_name
420
409
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
@@ -455,7 +444,7 @@ module Generators
455
444
 
456
445
  def change_foreign_key_constraints(model, old_table_name)
457
446
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
458
- Migrator.disable_indexing and return [[], []]
447
+ ::DeclareSchema.default_generate_foreign_keys or return [[], []]
459
448
 
460
449
  new_table_name = model.table_name
461
450
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
@@ -584,8 +573,8 @@ module Generators
584
573
  # TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
585
574
  def fix_mysql_charset_and_collation(dumped_schema)
586
575
  if !dumped_schema['options: ']
587
- dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
588
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
576
+ dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{::DeclareSchema.default_charset} "+
577
+ "COLLATE=#{::DeclareSchema.default_collation}\",")
589
578
  end
590
579
  default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
591
580
  default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
@@ -69,11 +69,11 @@ module DeclareSchema
69
69
  def declare_model_fields_and_associations
70
70
  buffer = ::DeclareSchema::Support::IndentedBuffer.new(indent: 2)
71
71
  buffer.newline!
72
- buffer << 'fields do'
72
+ buffer << 'declare_schema do'
73
73
  buffer.indent! do
74
74
  field_attributes.each do |attribute|
75
- decl = "%-#{max_attribute_length}s" % attribute.name + ' ' +
76
- attribute.type.to_sym.inspect +
75
+ decl = "%-#{max_attribute_length}s" % attribute.type + ' ' +
76
+ attribute.name.to_sym.inspect +
77
77
  case attribute.type.to_s
78
78
  when 'string'
79
79
  ', limit: 255'
@@ -113,7 +113,7 @@ module DeclareSchema
113
113
  end
114
114
 
115
115
  def max_attribute_length
116
- attributes.map { |attribute| attribute.name.length }.max
116
+ attributes.map { |attribute| attribute.type.length }.max
117
117
  end
118
118
 
119
119
  def field_attributes
@@ -20,9 +20,9 @@ RSpec.describe 'DeclareSchema API' do
20
20
  expect_model_definition_to_eq('advert', <<~EOS)
21
21
  class Advert < #{active_record_base_class}
22
22
 
23
- fields do
24
- title :string, limit: 255
25
- body :text
23
+ declare_schema do
24
+ string :title, limit: 255
25
+ text :body
26
26
  end
27
27
 
28
28
  end
@@ -91,8 +91,8 @@ RSpec.describe 'DeclareSchema API' do
91
91
  class AdvertWithRequiredTitle < ActiveRecord::Base
92
92
  self.table_name = 'adverts'
93
93
 
94
- fields do
95
- title :string, :required, limit: 255
94
+ declare_schema do
95
+ string :title, :required, limit: 255
96
96
  end
97
97
  end
98
98
 
@@ -110,8 +110,8 @@ RSpec.describe 'DeclareSchema API' do
110
110
  class AdvertWithUniqueTitle < ActiveRecord::Base
111
111
  self.table_name = 'adverts'
112
112
 
113
- fields do
114
- title :string, :unique, limit: 255
113
+ declare_schema do
114
+ string :title, :unique, limit: 255
115
115
  end
116
116
  end
117
117
 
@@ -3,29 +3,55 @@
3
3
  require_relative '../../../lib/declare_schema/field_declaration_dsl'
4
4
 
5
5
  RSpec.describe DeclareSchema::FieldDeclarationDsl do
6
- before do
7
- load File.expand_path('prepare_testapp.rb', __dir__)
6
+ let(:model) { TestModel.new }
7
+ subject { declared_class.new(model) }
8
8
 
9
- class TestModel < ActiveRecord::Base
10
- fields do
11
- name :string, limit: 127
9
+ context 'Using fields' do
10
+ before do
11
+ load File.expand_path('prepare_testapp.rb', __dir__)
12
12
 
13
- timestamps
13
+ class TestModel < ActiveRecord::Base
14
+ fields do
15
+ name :string, limit: 127
16
+
17
+ timestamps
18
+ end
14
19
  end
15
20
  end
16
- end
17
21
 
18
- let(:model) { TestModel.new }
19
- subject { declared_class.new(model) }
22
+ it 'has fields' do
23
+ expect(TestModel.field_specs).to be_kind_of(Hash)
24
+ expect(TestModel.field_specs.keys).to eq(['name', 'created_at', 'updated_at'])
25
+ expect(TestModel.field_specs.values.map(&:type)).to eq([:string, :datetime, :datetime])
26
+ end
20
27
 
21
- it 'has fields' do
22
- expect(TestModel.field_specs).to be_kind_of(Hash)
23
- expect(TestModel.field_specs.keys).to eq(['name', 'created_at', 'updated_at'])
24
- expect(TestModel.field_specs.values.map(&:type)).to eq([:string, :datetime, :datetime])
28
+ it 'stores limits' do
29
+ expect(TestModel.field_specs['name'].limit).to eq(127), TestModel.field_specs['name'].inspect
30
+ end
25
31
  end
26
32
 
27
- it 'stores limits' do
28
- expect(TestModel.field_specs['name'].limit).to eq(127), TestModel.field_specs['name'].inspect
33
+ context 'Using declare_schema' do
34
+ before do
35
+ load File.expand_path('prepare_testapp.rb', __dir__)
36
+
37
+ class TestModel < ActiveRecord::Base
38
+ declare_schema do
39
+ string :name, limit: 127
40
+
41
+ timestamps
42
+ end
43
+ end
44
+ end
45
+
46
+ it 'has fields' do
47
+ expect(TestModel.field_specs).to be_kind_of(Hash)
48
+ expect(TestModel.field_specs.keys).to eq(['name', 'created_at', 'updated_at'])
49
+ expect(TestModel.field_specs.values.map(&:type)).to eq([:string, :datetime, :datetime])
50
+ end
51
+
52
+ it 'stores limits' do
53
+ expect(TestModel.field_specs['name'].limit).to eq(127), TestModel.field_specs['name'].inspect
54
+ end
29
55
  end
30
56
 
31
57
  # TODO: fill out remaining tests
@@ -6,7 +6,7 @@ rescue LoadError
6
6
  end
7
7
 
8
8
  RSpec.describe DeclareSchema::Model::FieldSpec do
9
- let(:model) { double('model', table_options: {}, defined_primary_key: 'id') }
9
+ let(:model) { double('model', table_options: {}, _defined_primary_key: 'id') }
10
10
  let(:col_spec) { double('col_spec', type: :string) }
11
11
 
12
12
  before do
@@ -61,6 +61,15 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
61
61
  expect(subject.schema_attributes(col_spec)).to eq(type: :string, limit: 100, null: true)
62
62
  end
63
63
  end
64
+
65
+ it 'raises error when default_string_limit option is nil when not explicitly set in field spec' do
66
+ if defined?(Mysql2)
67
+ expect(::DeclareSchema).to receive(:default_string_limit) { nil }
68
+ expect do
69
+ described_class.new(model, :title, :string, null: true, charset: 'utf8mb4', position: 0)
70
+ end.to raise_error(/limit: must be provided for :string field/)
71
+ end
72
+ end
64
73
  end
65
74
 
66
75
  describe 'text' do
@@ -84,36 +93,66 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
84
93
  end
85
94
  end
86
95
 
87
- describe 'decimal' do
88
- it 'allows precision: and scale:' do
89
- subject = described_class.new(model, :quantity, :decimal, precision: 8, scale: 10, null: true, position: 3)
90
- expect(subject.schema_attributes(col_spec)).to eq(type: :decimal, precision: 8, scale: 10, null: true)
96
+ describe 'limit' do
97
+ it 'uses default_text_limit option when not explicitly set in field spec' do
98
+ allow(::DeclareSchema).to receive(:default_text_limit) { 100 }
99
+ subject = described_class.new(model, :title, :text, null: true, charset: 'utf8mb4', position: 2)
100
+ if defined?(Mysql2)
101
+ expect(subject.schema_attributes(col_spec)).to eq(type: :text, limit: 255, null: true, charset: 'utf8mb4', collation: 'utf8mb4_bin')
102
+ else
103
+ expect(subject.schema_attributes(col_spec)).to eq(type: :text, null: true)
104
+ end
91
105
  end
92
106
 
93
- it 'requires precision:' do
94
- expect_any_instance_of(described_class).to receive(:warn).with(/precision: required for :decimal type/)
95
- described_class.new(model, :quantity, :decimal, scale: 10, null: true, position: 3)
107
+ it 'raises error when default_text_limit option is nil when not explicitly set in field spec' do
108
+ if defined?(Mysql2)
109
+ expect(::DeclareSchema).to receive(:default_text_limit) { nil }
110
+ expect do
111
+ described_class.new(model, :title, :text, null: true, charset: 'utf8mb4', position: 2)
112
+ end.to raise_error(/limit: must be provided for :text field/)
113
+ end
96
114
  end
115
+ end
116
+ end
97
117
 
98
- it 'requires scale:' do
99
- expect_any_instance_of(described_class).to receive(:warn).with(/scale: required for :decimal type/)
100
- described_class.new(model, :quantity, :decimal, precision: 8, null: true, position: 3)
118
+ if defined?(Mysql2)
119
+ describe 'varbinary' do # TODO: :varbinary is an Invoca addition to Rails; make it a configurable option
120
+ it 'is supported' do
121
+ subject = described_class.new(model, :binary_dump, :varbinary, limit: 200, null: false, position: 2)
122
+ expect(subject.schema_attributes(col_spec)).to eq(type: :varbinary, limit: 200, null: false)
101
123
  end
102
124
  end
125
+ end
103
126
 
104
- [:integer, :bigint, :string, :text, :binary, :datetime, :date, :time].each do |t|
105
- describe t.to_s do
106
- let(:extra) { t == :string ? { limit: 100 } : {} }
127
+ describe 'decimal' do
128
+ it 'allows precision: and scale:' do
129
+ subject = described_class.new(model, :quantity, :decimal, precision: 8, scale: 10, null: true, position: 3)
130
+ expect(subject.schema_attributes(col_spec)).to eq(type: :decimal, precision: 8, scale: 10, null: true)
131
+ end
107
132
 
108
- it 'does not allow precision:' do
109
- expect_any_instance_of(described_class).to receive(:warn).with(/precision: only allowed for :decimal type/)
110
- described_class.new(model, :quantity, t, { precision: 8, null: true, position: 3 }.merge(extra))
111
- end unless t == :datetime
133
+ it 'requires precision:' do
134
+ expect_any_instance_of(described_class).to receive(:warn).with(/precision: required for :decimal type/)
135
+ described_class.new(model, :quantity, :decimal, scale: 10, null: true, position: 3)
136
+ end
112
137
 
113
- it 'does not allow scale:' do
114
- expect_any_instance_of(described_class).to receive(:warn).with(/scale: only allowed for :decimal type/)
115
- described_class.new(model, :quantity, t, { scale: 10, null: true, position: 3 }.merge(extra))
116
- end
138
+ it 'requires scale:' do
139
+ expect_any_instance_of(described_class).to receive(:warn).with(/scale: required for :decimal type/)
140
+ described_class.new(model, :quantity, :decimal, precision: 8, null: true, position: 3)
141
+ end
142
+ end
143
+
144
+ [:integer, :bigint, :string, :text, :binary, :datetime, :date, :time, (:varbinary if defined?(Mysql2))].compact.each do |t|
145
+ describe t.to_s do
146
+ let(:extra) { t == :string ? { limit: 100 } : {} }
147
+
148
+ it 'does not allow precision:' do
149
+ expect_any_instance_of(described_class).to receive(:warn).with(/precision: only allowed for :decimal type/)
150
+ described_class.new(model, :quantity, t, { precision: 8, null: true, position: 3 }.merge(extra))
151
+ end unless t == :datetime
152
+
153
+ it 'does not allow scale:' do
154
+ expect_any_instance_of(described_class).to receive(:warn).with(/scale: only allowed for :decimal type/)
155
+ described_class.new(model, :quantity, t, { scale: 10, null: true, position: 3 }.merge(extra))
117
156
  end
118
157
  end
119
158
  end
@@ -175,5 +214,17 @@ RSpec.describe DeclareSchema::Model::FieldSpec do
175
214
  it 'excludes non-sql options' do
176
215
  expect(subject.sql_options).to eq(limit: 4, null: true, default: 0)
177
216
  end
217
+
218
+ describe 'null' do
219
+ subject { described_class.new(model, :price, :integer, limit: 4, default: 0, position: 2, encrypt_using: ->(field) { field }) }
220
+ it 'uses default_null option when not explicitly set in field spec' do
221
+ expect(subject.sql_options).to eq(limit: 4, null: false, default: 0)
222
+ end
223
+
224
+ it 'raises error if default_null is set to nil when not explicitly set in field spec' do
225
+ expect(::DeclareSchema).to receive(:default_null) { nil }
226
+ expect { subject.sql_options }.to raise_error(/null: must be provided for field/)
227
+ end
228
+ end
178
229
  end
179
230
  end