declare_schema 0.8.0.pre.3 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -47,11 +47,9 @@ module DeclareSchema
47
47
  end
48
48
 
49
49
  def initialize(model, name, type, position: 0, **options)
50
- # TODO: TECH-5116
51
- # Invoca change - searching for the primary key was causing an additional database read on every model load. Assume
52
- # "id" which works for invoca.
53
- # raise ArgumentError, "you cannot provide a field spec for the primary key" if name == model.primary_key
54
- name == "id" and raise ArgumentError, "you cannot provide a field spec for the primary key"
50
+ _defined_primary_key = model._defined_primary_key
51
+
52
+ name.to_s == _defined_primary_key and raise ArgumentError, "you may not provide a field spec for the primary key #{name.inspect}"
55
53
 
56
54
  @model = model
57
55
  @name = name.to_sym
@@ -60,18 +58,21 @@ module DeclareSchema
60
58
  @position = position
61
59
  @options = options.dup
62
60
 
63
- @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`?"
64
63
 
65
64
  case @type
66
65
  when :text
67
66
  if self.class.mysql_text_limits?
68
67
  @options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
69
- @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])
70
71
  else
71
72
  @options.delete(:limit)
72
73
  end
73
74
  when :string
74
- @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`?"
75
76
  when :bigint
76
77
  @type = :integer
77
78
  @options[:limit] = 8
@@ -80,7 +81,7 @@ module DeclareSchema
80
81
  Column.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect} not found in #{Column.native_types.inspect} for adapter #{ActiveRecord::Base.connection.class.name}"
81
82
 
82
83
  if @type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
83
- @options[:limit] ||= Column.native_types[@type][:limit]
84
+ @options[:limit] ||= Column.native_types.dig(@type, :limit)
84
85
  else
85
86
  @type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@type} in field #{model}##{@name}")
86
87
  @options.delete(:limit)
@@ -98,15 +99,15 @@ module DeclareSchema
98
99
 
99
100
  if @type.in?([:text, :string])
100
101
  if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
101
- @options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
102
- @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
103
104
  else
104
105
  @options.delete(:charset)
105
106
  @options.delete(:collation)
106
107
  end
107
108
  else
108
109
  @options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
109
- @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}")
110
111
  end
111
112
 
112
113
  @options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'index_definition'
4
+
3
5
  module DeclareSchema
4
6
  module Model
5
7
  class ForeignKeyDefinition
@@ -15,10 +17,10 @@ module DeclareSchema
15
17
  @child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
16
18
  @parent_table_name = options[:parent_table]&.to_s
17
19
  @foreign_key_name = options[:foreign_key]&.to_s || @foreign_key
18
- @index_name = options[:index_name]&.to_s || model.connection.index_name(model.table_name, column: @foreign_key_name)
19
20
 
20
- # Empty constraint lets mysql generate the name
21
- @constraint_name = options[:constraint_name]&.to_s || @index_name&.to_s || ''
21
+ @constraint_name = options[:constraint_name]&.to_s ||
22
+ options[:index_name]&.to_s ||
23
+ IndexDefinition.index_name(@foreign_key_name)
22
24
  @on_delete_cascade = options[:dependent] == :delete
23
25
  end
24
26
 
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DeclareSchema
4
+ module Model
5
+ class HabtmModelShim
6
+ class << self
7
+ def from_reflection(refl)
8
+ join_table = refl.join_table
9
+ foreign_keys_and_classes = [
10
+ [refl.foreign_key.to_s, refl.active_record],
11
+ [refl.association_foreign_key.to_s, refl.class_name.constantize]
12
+ ].sort { |a, b| a.first <=> b.first }
13
+ foreign_keys = foreign_keys_and_classes.map(&:first)
14
+ foreign_key_classes = foreign_keys_and_classes.map(&:last)
15
+ # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
16
+ # figure that anybody who sets THAT up can deal with their own migrations...
17
+ connection = refl.active_record.connection
18
+
19
+ new(join_table, foreign_keys, foreign_key_classes, connection)
20
+ end
21
+ end
22
+
23
+ attr_reader :join_table, :foreign_keys, :foreign_key_classes, :connection
24
+
25
+ def initialize(join_table, foreign_keys, foreign_key_classes, connection)
26
+ @join_table = join_table
27
+ @foreign_keys = foreign_keys
28
+ @foreign_key_classes = foreign_key_classes
29
+ @connection = connection
30
+ end
31
+
32
+ def table_options
33
+ {}
34
+ end
35
+
36
+ def table_name
37
+ join_table
38
+ end
39
+
40
+ def field_specs
41
+ foreign_keys.each_with_index.each_with_object({}) do |(v, position), result|
42
+ result[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :integer, position: position, null: false)
43
+ end
44
+ end
45
+
46
+ def primary_key
47
+ false # no single-column primary key in database
48
+ end
49
+
50
+ def _defined_primary_key
51
+ false # no single-column primary key declared
52
+ end
53
+
54
+ def index_definitions_with_primary_key
55
+ [
56
+ IndexDefinition.new(self, foreign_keys, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME),
57
+ IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
58
+ ]
59
+ end
60
+
61
+ alias_method :index_definitions, :index_definitions_with_primary_key
62
+
63
+ def ignore_indexes
64
+ []
65
+ end
66
+
67
+ def constraint_specs
68
+ [
69
+ ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
70
+ ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
71
+ ]
72
+ end
73
+ end
74
+ end
75
+ end
@@ -19,7 +19,7 @@ module DeclareSchema
19
19
  @table = options.delete(:table_name) || model.table_name
20
20
  @fields = Array.wrap(fields).map(&:to_s)
21
21
  @explicit_name = options[:name] unless options.delete(:allow_equivalent)
22
- @name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
22
+ @name = options.delete(:name) || self.class.index_name(@fields)
23
23
  @unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
24
24
 
25
25
  if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
@@ -60,6 +60,10 @@ module DeclareSchema
60
60
  index_definitions
61
61
  end
62
62
 
63
+ def index_name(columns)
64
+ "on_#{Array(columns).join("_and_")}"
65
+ end
66
+
63
67
  private
64
68
 
65
69
  # This is the old approach which is still needed for MySQL in Rails 4 and SQLite
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.8.0.pre.3"
4
+ VERSION = "0.9.0"
5
5
  end
@@ -6,95 +6,17 @@ require 'active_record/connection_adapters/abstract_adapter'
6
6
  module Generators
7
7
  module DeclareSchema
8
8
  module Migration
9
- HabtmModelShim = Struct.new(:join_table, :foreign_keys, :foreign_key_classes, :connection) do
10
- class << self
11
- def from_reflection(refl)
12
- join_table = refl.join_table
13
- foreign_keys_and_classes = [
14
- [refl.foreign_key.to_s, refl.active_record],
15
- [refl.association_foreign_key.to_s, refl.class_name.constantize]
16
- ].sort { |a, b| a.first <=> b.first }
17
- foreign_keys = foreign_keys_and_classes.map(&:first)
18
- foreign_key_classes = foreign_keys_and_classes.map(&:last)
19
- # this may fail in weird ways if HABTM is running across two DB connections (assuming that's even supported)
20
- # figure that anybody who sets THAT up can deal with their own migrations...
21
- connection = refl.active_record.connection
22
-
23
- new(join_table, foreign_keys, foreign_key_classes, connection)
24
- end
25
- end
26
-
27
- def table_options
28
- {}
29
- end
30
-
31
- def table_name
32
- join_table
33
- end
34
-
35
- def table_exists?
36
- ActiveRecord::Migration.table_exists? table_name
37
- end
38
-
39
- def field_specs
40
- i = 0
41
- foreign_keys.each_with_object({}) do |v, result|
42
- result[v] = ::DeclareSchema::Model::FieldSpec.new(self, v, :integer, position: i, null: false)
43
- i += 1
44
- end
45
- end
46
-
47
- def primary_key
48
- false # no single-column primary key
49
- end
50
-
51
- def index_definitions_with_primary_key
52
- [
53
- ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys, unique: true, name: ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME),
54
- ::DeclareSchema::Model::IndexDefinition.new(self, foreign_keys.last) # not unique by itself; combines with primary key to be unique
55
- ]
56
- end
57
-
58
- alias_method :index_definitions, :index_definitions_with_primary_key
59
-
60
- def ignore_indexes
61
- []
62
- end
63
-
64
- def constraint_specs
65
- [
66
- ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.first, parent_table: foreign_key_classes.first.table_name, constraint_name: "#{join_table}_FK1", dependent: :delete),
67
- ::DeclareSchema::Model::ForeignKeyDefinition.new(self, foreign_keys.last, parent_table: foreign_key_classes.last.table_name, constraint_name: "#{join_table}_FK2", dependent: :delete)
68
- ]
69
- end
70
- end
71
-
72
9
  class Migrator
73
10
  class Error < RuntimeError; end
74
11
 
75
- DEFAULT_CHARSET = "utf8mb4"
76
- DEFAULT_COLLATION = "utf8mb4_bin"
77
-
78
12
  @ignore_models = []
79
13
  @ignore_tables = []
80
14
  @before_generating_migration_callback = nil
81
15
  @active_record_class = ActiveRecord::Base
82
- @default_charset = DEFAULT_CHARSET
83
- @default_collation = DEFAULT_COLLATION
84
16
 
85
17
  class << self
86
- attr_accessor :ignore_models, :ignore_tables, :disable_indexing, :disable_constraints
87
- attr_reader :active_record_class, :default_charset, :default_collation, :before_generating_migration_callback
88
-
89
- def default_charset=(charset)
90
- charset.is_a?(String) or raise ArgumentError, "charset must be a string (got #{charset.inspect})"
91
- @default_charset = charset
92
- end
93
-
94
- def default_collation=(collation)
95
- collation.is_a?(String) or raise ArgumentError, "collation must be a string (got #{collation.inspect})"
96
- @default_collation = collation
97
- end
18
+ attr_accessor :ignore_models, :ignore_tables
19
+ attr_reader :active_record_class, :before_generating_migration_callback
98
20
 
99
21
  def active_record_class
100
22
  @active_record_class.is_a?(Class) or @active_record_class = @active_record_class.to_s.constantize
@@ -121,6 +43,9 @@ module Generators
121
43
  block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
122
44
  @before_generating_migration_callback = block
123
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')
124
49
  end
125
50
 
126
51
  def initialize(ambiguity_resolver = {})
@@ -266,7 +191,7 @@ module Generators
266
191
  end
267
192
  # generate shims for HABTM models
268
193
  habtm_tables.each do |name, refls|
269
- models_by_table_name[name] = HabtmModelShim.from_reflection(refls.first)
194
+ models_by_table_name[name] = ::DeclareSchema::Model::HabtmModelShim.from_reflection(refls.first)
270
195
  end
271
196
  model_table_names = models_by_table_name.keys
272
197
 
@@ -342,18 +267,19 @@ module Generators
342
267
  end
343
268
 
344
269
  #{table_options_definition.alter_table_statement unless ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)}
345
- #{create_indexes(model).join("\n") unless Migrator.disable_indexing}
346
- #{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}
347
272
  EOS
348
273
  end
349
274
 
350
275
  def create_table_options(model, disable_auto_increment)
351
- if model.primary_key.blank? || disable_auto_increment
276
+ primary_key = model._defined_primary_key
277
+ if primary_key.blank? || disable_auto_increment
352
278
  "id: false"
353
- elsif model.primary_key == "id"
279
+ elsif primary_key == "id"
354
280
  "id: :bigint"
355
281
  else
356
- "primary_key: :#{model.primary_key}"
282
+ "primary_key: :#{primary_key}"
357
283
  end
358
284
  end
359
285
 
@@ -362,8 +288,8 @@ module Generators
362
288
  {}
363
289
  else
364
290
  {
365
- charset: model.table_options[:charset] || Migrator.default_charset,
366
- 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
367
293
  }
368
294
  end
369
295
  end
@@ -386,18 +312,18 @@ module Generators
386
312
  new_table_name = model.table_name
387
313
 
388
314
  db_columns = model.connection.columns(current_table_name).index_by(&:name)
389
- key_missing = db_columns[model.primary_key].nil? && model.primary_key.present?
390
- if model.primary_key.present?
391
- 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)
392
318
  end
393
319
 
394
320
  model_column_names = model.field_specs.keys.map(&:to_s)
395
321
  db_column_names = db_columns.keys.map(&:to_s)
396
322
 
397
323
  to_add = model_column_names - db_column_names
398
- 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?
399
325
  to_remove = db_column_names - model_column_names
400
- 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?
401
327
 
402
328
  to_rename = extract_column_renames!(to_add, to_remove, new_table_name)
403
329
 
@@ -477,7 +403,7 @@ module Generators
477
403
  end
478
404
 
479
405
  def change_indexes(model, old_table_name, to_remove)
480
- Migrator.disable_constraints and return [[], []]
406
+ ::DeclareSchema.default_generate_indexing or return [[], []]
481
407
 
482
408
  new_table_name = model.table_name
483
409
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
@@ -518,7 +444,7 @@ module Generators
518
444
 
519
445
  def change_foreign_key_constraints(model, old_table_name)
520
446
  ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
521
- Migrator.disable_indexing and return [[], []]
447
+ ::DeclareSchema.default_generate_foreign_keys or return [[], []]
522
448
 
523
449
  new_table_name = model.table_name
524
450
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
@@ -647,8 +573,8 @@ module Generators
647
573
  # TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
648
574
  def fix_mysql_charset_and_collation(dumped_schema)
649
575
  if !dumped_schema['options: ']
650
- dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
651
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
576
+ dumped_schema.sub!('",', "\", options: \"DEFAULT CHARSET=#{::DeclareSchema.default_charset} "+
577
+ "COLLATE=#{::DeclareSchema.default_collation}\",")
652
578
  end
653
579
  default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
654
580
  default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
@@ -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: {}) }
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