declare_schema 0.8.0.pre.4 → 0.10.0.pre.dc.1

Sign up to get free protection for your applications and to get access to all the features.
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 +40 -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 +12 -9
  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
@@ -78,7 +81,7 @@ module DeclareSchema
78
81
  Column.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect} not found in #{Column.native_types.inspect} for adapter #{ActiveRecord::Base.connection.class.name}"
79
82
 
80
83
  if @type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
81
- @options[:limit] ||= Column.native_types[@type][:limit]
84
+ @options[:limit] ||= Column.native_types.dig(@type, :limit)
82
85
  else
83
86
  @type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@type} in field #{model}##{@name}")
84
87
  @options.delete(:limit)
@@ -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.4"
4
+ VERSION = "0.10.0.pre.dc.1"
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