declare_schema 0.8.0.pre.3 → 0.9.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.
@@ -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