declare_schema 0.6.4 → 0.8.0.pre.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DeclareSchema
4
- VERSION = "0.6.4"
4
+ VERSION = "0.8.0.pre.3"
5
5
  end
@@ -96,7 +96,7 @@ module DeclareSchema
96
96
  end
97
97
  end
98
98
  end
99
- rescue ::DeclareSchema::Model::FieldSpec::UnknownSqlTypeError => ex
99
+ rescue ::DeclareSchema::UnknownTypeError => ex
100
100
  say "Invalid field type: #{ex}"
101
101
  end
102
102
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'active_record'
4
+ require 'active_record/connection_adapters/abstract_adapter'
4
5
 
5
6
  module Generators
6
7
  module DeclareSchema
@@ -116,20 +117,6 @@ module Generators
116
117
  ActiveRecord::Base.connection
117
118
  end
118
119
 
119
- def fix_native_types(types)
120
- case connection.class.name
121
- when /mysql/i
122
- types[:integer][:limit] ||= 11
123
- types[:text][:limit] ||= 0xffff
124
- types[:binary][:limit] ||= 0xffff
125
- end
126
- types
127
- end
128
-
129
- def native_types
130
- @native_types ||= fix_native_types(connection.native_database_types)
131
- end
132
-
133
120
  def before_generating_migration(&block)
134
121
  block or raise ArgumentError, 'A block is required when setting the before_generating_migration callback'
135
122
  @before_generating_migration_callback = block
@@ -299,7 +286,7 @@ module Generators
299
286
  "drop_table :#{t}"
300
287
  end * "\n"
301
288
  undo_drops = to_drop.map do |t|
302
- revert_table(t)
289
+ add_table_back(t)
303
290
  end * "\n\n"
304
291
 
305
292
  creates = to_create.map do |t|
@@ -341,11 +328,11 @@ module Generators
341
328
  end
342
329
 
343
330
  def create_table(model)
344
- longest_field_name = model.field_specs.values.map { |f| f.sql_type.to_s.length }.max
331
+ longest_field_name = model.field_specs.values.map { |f| f.type.to_s.length }.max
345
332
  disable_auto_increment = model.respond_to?(:disable_auto_increment) && model.disable_auto_increment
346
333
  table_options_definition = ::DeclareSchema::Model::TableOptionsDefinition.new(model.table_name, table_options_for_model(model))
347
334
  field_definitions = [
348
- disable_auto_increment ? "t.integer :id, limit: 8, auto_increment: false, primary_key: true" : nil,
335
+ ("t.integer :id, limit: 8, auto_increment: false, primary_key: true" if disable_auto_increment),
349
336
  *(model.field_specs.values.sort_by(&:position).map { |f| create_field(f, longest_field_name) })
350
337
  ].compact
351
338
 
@@ -390,9 +377,9 @@ module Generators
390
377
  end
391
378
 
392
379
  def create_field(field_spec, field_name_width)
393
- options = fk_field_options(field_spec.model, field_spec.name).merge(field_spec.sql_options)
394
- args = [field_spec.name.inspect] + format_options(options, field_spec.sql_type)
395
- format("t.%-*s %s", field_name_width, field_spec.sql_type, args.join(', '))
380
+ options = field_spec.sql_options.merge(fk_field_options(field_spec.model, field_spec.name))
381
+ args = [field_spec.name.inspect] + format_options(options.compact)
382
+ format("t.%-*s %s", field_name_width, field_spec.type, args.join(', '))
396
383
  end
397
384
 
398
385
  def change_table(model, current_table_name)
@@ -429,8 +416,8 @@ module Generators
429
416
  adds = to_add.map do |c|
430
417
  args =
431
418
  if (spec = model.field_specs[c])
432
- options = fk_field_options(model, c).merge(spec.sql_options)
433
- [":#{spec.sql_type}", *format_options(options, spec.sql_type)]
419
+ options = spec.sql_options.merge(fk_field_options(model, c))
420
+ [":#{spec.type}", *format_options(options.compact)]
434
421
  else
435
422
  [":integer"]
436
423
  end
@@ -444,34 +431,28 @@ module Generators
444
431
  "remove_column :#{new_table_name}, :#{c}"
445
432
  end
446
433
  undo_removes = to_remove.map do |c|
447
- revert_column(current_table_name, c)
434
+ add_column_back(model, current_table_name, c)
448
435
  end
449
436
 
450
437
  old_names = to_rename.invert
451
438
  changes = []
452
439
  undo_changes = []
453
- to_change.each do |c|
454
- col_name = old_names[c] || c
455
- col = db_columns[col_name]
456
- spec = model.field_specs[c]
457
- if spec.different_to?(current_table_name, col) # TODO: TECH-4814 DRY this up to a diff function that returns the differences. It's different if it has differences. -Colin
458
- change_spec = fk_field_options(model, c)
459
- change_spec[:limit] ||= spec.limit if (spec.sql_type != :text ||
460
- ::DeclareSchema::Model::FieldSpec.mysql_text_limits?) &&
461
- (spec.limit || col.limit)
462
- change_spec[:precision] = spec.precision unless spec.precision.nil?
463
- change_spec[:scale] = spec.scale unless spec.scale.nil?
464
- change_spec[:null] = spec.null unless spec.null && col.null
465
- change_spec[:default] = spec.default unless spec.default.nil? && col.default.nil?
466
- change_spec[:charset] = spec.charset unless spec.charset.nil?
467
- change_spec[:collation] = spec.collation unless spec.collation.nil?
468
-
469
- changes << "change_column :#{new_table_name}, :#{c}, " +
470
- ([":#{spec.sql_type}"] + format_options(change_spec, spec.sql_type, changing: true)).join(", ")
471
- back = change_column_back(current_table_name, col_name)
472
- undo_changes << back unless back.blank?
440
+ to_change.each do |col_name_to_change|
441
+ orig_col_name = old_names[col_name_to_change] || col_name_to_change
442
+ column = db_columns[orig_col_name] or raise "failed to find column info for #{orig_col_name.inspect}"
443
+ spec = model.field_specs[col_name_to_change] or raise "failed to find field spec for #{col_name_to_change.inspect}"
444
+ spec_attrs = spec.schema_attributes(column)
445
+ column_declaration = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
446
+ col_attrs = column_declaration.schema_attributes
447
+ normalized_schema_attrs = spec_attrs.merge(fk_field_options(model, col_name_to_change))
448
+
449
+ if !::DeclareSchema::Model::Column.equivalent_schema_attributes?(normalized_schema_attrs, col_attrs)
450
+ type = normalized_schema_attrs.delete(:type) or raise "no :type found in #{normalized_schema_attrs.inspect}"
451
+ changes << ["change_column #{new_table_name.to_sym.inspect}", col_name_to_change.to_sym.inspect,
452
+ type.to_sym.inspect, *format_options(normalized_schema_attrs)].join(", ")
453
+ undo_changes << change_column_back(model, current_table_name, orig_col_name)
473
454
  end
474
- end.compact
455
+ end
475
456
 
476
457
  index_changes, undo_index_changes = change_indexes(model, current_table_name, to_remove)
477
458
  fk_changes, undo_fk_changes = if ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
@@ -485,26 +466,26 @@ module Generators
485
466
  [[], []]
486
467
  end
487
468
 
488
- [(renames + adds + removes + changes) * "\n",
469
+ [(renames + adds + removes + changes) * "\n",
489
470
  (undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
490
- index_changes * "\n",
491
- undo_index_changes * "\n",
492
- fk_changes * "\n",
493
- undo_fk_changes * "\n",
494
- table_options_changes * "\n",
495
- undo_table_options_changes * "\n"]
471
+ index_changes * "\n",
472
+ undo_index_changes * "\n",
473
+ fk_changes * "\n",
474
+ undo_fk_changes * "\n",
475
+ table_options_changes * "\n",
476
+ undo_table_options_changes * "\n"]
496
477
  end
497
478
 
498
479
  def change_indexes(model, old_table_name, to_remove)
499
- return [[], []] if Migrator.disable_constraints
480
+ Migrator.disable_constraints and return [[], []]
500
481
 
501
482
  new_table_name = model.table_name
502
483
  existing_indexes = ::DeclareSchema::Model::IndexDefinition.for_model(model, old_table_name)
503
484
  model_indexes_with_equivalents = model.index_definitions_with_primary_key
504
485
  model_indexes = model_indexes_with_equivalents.map do |i|
505
486
  if i.explicit_name.nil?
506
- if ex = existing_indexes.find { |e| i != e && e.equivalent?(i) }
507
- i.with_name(ex.name)
487
+ if (existing = existing_indexes.find { |e| i != e && e.equivalent?(i) })
488
+ i.with_name(existing.name)
508
489
  end
509
490
  end || i
510
491
  end
@@ -514,15 +495,13 @@ module Generators
514
495
  end
515
496
  model_has_primary_key = model_indexes.any? { |i| i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME }
516
497
 
517
- add_indexes_init = model_indexes - existing_indexes
518
- drop_indexes_init = existing_indexes - model_indexes
519
498
  undo_add_indexes = []
520
- undo_drop_indexes = []
521
- add_indexes = add_indexes_init.map do |i|
499
+ add_indexes = (model_indexes - existing_indexes).map do |i|
522
500
  undo_add_indexes << drop_index(old_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
523
501
  i.to_add_statement(new_table_name, existing_has_primary_key)
524
502
  end
525
- drop_indexes = drop_indexes_init.map do |i|
503
+ undo_drop_indexes = []
504
+ drop_indexes = (existing_indexes - model_indexes).map do |i|
526
505
  undo_drop_indexes << i.to_add_statement(old_table_name, model_has_primary_key)
527
506
  drop_index(new_table_name, i.name) unless i.name == ::DeclareSchema::Model::IndexDefinition::PRIMARY_KEY_NAME
528
507
  end.compact
@@ -538,24 +517,22 @@ module Generators
538
517
  end
539
518
 
540
519
  def change_foreign_key_constraints(model, old_table_name)
541
- ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise 'SQLite does not support foreign keys'
542
- return [[], []] if Migrator.disable_indexing
520
+ ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/) and raise ArgumentError, 'SQLite does not support foreign keys'
521
+ Migrator.disable_indexing and return [[], []]
543
522
 
544
523
  new_table_name = model.table_name
545
524
  existing_fks = ::DeclareSchema::Model::ForeignKeyDefinition.for_model(model, old_table_name)
546
525
  model_fks = model.constraint_specs
547
- add_fks = model_fks - existing_fks
548
- drop_fks = existing_fks - model_fks
549
- undo_add_fks = []
550
- undo_drop_fks = []
551
526
 
552
- add_fks.map! do |fk|
527
+ undo_add_fks = []
528
+ add_fks = (model_fks - existing_fks).map do |fk|
553
529
  # next if fk.parent.constantize.abstract_class || fk.parent == fk.model.class_name
554
530
  undo_add_fks << remove_foreign_key(old_table_name, fk.constraint_name)
555
531
  fk.to_add_statement
556
- end.compact
532
+ end
557
533
 
558
- drop_fks.map! do |fk|
534
+ undo_drop_fks = []
535
+ drop_fks = (existing_fks - model_fks).map do |fk|
559
536
  undo_drop_fks << fk.to_add_statement
560
537
  remove_foreign_key(new_table_name, fk.constraint_name)
561
538
  end
@@ -567,22 +544,14 @@ module Generators
567
544
  "remove_foreign_key(#{old_table_name.inspect}, name: #{fk_name.to_s.inspect})"
568
545
  end
569
546
 
570
- def format_options(options, type, changing: false)
547
+ def format_options(options)
571
548
  options.map do |k, v|
572
- if !changing && ((k == :limit && type == :decimal) || (k == :null && v == true))
573
- next
574
- end
575
-
576
- if !::DeclareSchema::Model::FieldSpec.mysql_text_limits? && k == :limit && type == :text
577
- next
578
- end
579
-
580
549
  if k.is_a?(Symbol)
581
550
  "#{k}: #{v.inspect}"
582
551
  else
583
552
  "#{k.inspect} => #{v.inspect}"
584
553
  end
585
- end.compact
554
+ end
586
555
  end
587
556
 
588
557
  def fk_field_options(model, field_name)
@@ -620,7 +589,33 @@ module Generators
620
589
  end
621
590
  end
622
591
 
623
- # TODO: TECH-4814 remove all methods from here through end of file
592
+ def with_previous_model_table_name(model, table_name)
593
+ model_table_name, model.table_name = model.table_name, table_name
594
+ yield
595
+ ensure
596
+ model.table_name = model_table_name
597
+ end
598
+
599
+ def add_column_back(model, current_table_name, col_name)
600
+ with_previous_model_table_name(model, current_table_name) do
601
+ column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
602
+ col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
603
+ schema_attributes = col_spec.schema_attributes
604
+ type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
605
+ ["add_column :#{current_table_name}, :#{col_name}, #{type.inspect}", *format_options(schema_attributes)].join(', ')
606
+ end
607
+ end
608
+
609
+ def change_column_back(model, current_table_name, col_name)
610
+ with_previous_model_table_name(model, current_table_name) do
611
+ column = model.columns_hash[col_name] or raise "no columns_hash entry found for #{col_name} in #{model.inspect}"
612
+ col_spec = ::DeclareSchema::Model::Column.new(model, current_table_name, column)
613
+ schema_attributes = col_spec.schema_attributes
614
+ type = schema_attributes.delete(:type) or raise "no :type found in #{schema_attributes.inspect}"
615
+ ["change_column #{current_table_name.to_sym.inspect}", col_name.to_sym.inspect, type.to_sym.inspect, *format_options(schema_attributes)].join(', ')
616
+ end
617
+ end
618
+
624
619
  def default_collation_from_charset(charset)
625
620
  case charset
626
621
  when "utf8"
@@ -630,64 +625,47 @@ module Generators
630
625
  end
631
626
  end
632
627
 
633
- def revert_table(table)
634
- res = StringIO.new
635
- schema_dumper_klass = case Rails::VERSION::MAJOR
636
- when 4
637
- ActiveRecord::SchemaDumper
638
- else
639
- ActiveRecord::ConnectionAdapters::SchemaDumper
640
- end
641
- schema_dumper_klass.send(:new, ActiveRecord::Base.connection).send(:table, table, res)
628
+ SchemaDumper = case Rails::VERSION::MAJOR
629
+ when 4
630
+ ActiveRecord::SchemaDumper
631
+ else
632
+ ActiveRecord::ConnectionAdapters::SchemaDumper
633
+ end
634
+
635
+ def add_table_back(table)
636
+ dumped_schema_stream = StringIO.new
637
+ SchemaDumper.send(:new, ActiveRecord::Base.connection).send(:table, table, dumped_schema_stream)
642
638
 
643
- result = res.string.strip.gsub("\n ", "\n")
639
+ dumped_schema = dumped_schema_stream.string.strip.gsub!("\n ", "\n")
644
640
  if connection.class.name.match?(/mysql/i)
645
- if !result['options: ']
646
- result = result.sub('",', "\", options: \"DEFAULT CHARSET=#{Generators::DeclareSchema::Migration::Migrator.default_charset} "+
647
- "COLLATE=#{Generators::DeclareSchema::Migration::Migrator.default_collation}\",")
648
- end
649
- default_charset = result[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{result.inspect}"
650
- default_collation = result[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
651
- raise "unable to find collation in #{result.inspect} or charset #{default_charset.inspect}"
652
- result = result.split("\n").map do |line|
653
- if line['t.text'] || line['t.string']
654
- if !line['charset: ']
655
- if line['collation: ']
656
- line = line.sub('collation: ', "charset: #{default_charset.inspect}, collation: ")
657
- else
658
- line += ", charset: #{default_charset.inspect}"
659
- end
660
- end
661
- line['collation: '] or line += ", collation: #{default_collation.inspect}"
662
- end
663
- line
664
- end.join("\n")
641
+ fix_mysql_charset_and_collation(dumped_schema)
642
+ else
643
+ dumped_schema
665
644
  end
666
- result
667
645
  end
668
646
 
669
- def column_options_from_reverted_table(table, column)
670
- revert = revert_table(table)
671
- if (md = revert.match(/\s*t\.column\s+"#{column}",\s+(:[a-zA-Z0-9_]+)(?:,\s+(.*?)$)?/m))
672
- # Ugly migration
673
- _, type, options = *md
674
- elsif (md = revert.match(/\s*t\.([a-z_]+)\s+"#{column}"(?:,\s+(.*?)$)?/m))
675
- # Sexy migration
676
- _, string_type, options = *md
677
- type = ":#{string_type}"
647
+ # TODO: rewrite this method to use charset and collation variables rather than manipulating strings. -Colin
648
+ def fix_mysql_charset_and_collation(dumped_schema)
649
+ 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}\",")
678
652
  end
679
- type or raise "unable to find column options for #{table}.#{column} in #{revert.inspect}"
680
- [type, options]
681
- end
682
-
683
- def change_column_back(table, column)
684
- type, options = column_options_from_reverted_table(table, column)
685
- ["change_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
686
- end
687
-
688
- def revert_column(table, column)
689
- type, options = column_options_from_reverted_table(table, column)
690
- ["add_column :#{table}, :#{column}, #{type}", options&.strip].compact.join(', ')
653
+ default_charset = dumped_schema[/CHARSET=(\w+)/, 1] or raise "unable to find charset in #{dumped_schema.inspect}"
654
+ default_collation = dumped_schema[/COLLATE=(\w+)/, 1] || default_collation_from_charset(default_charset) or
655
+ raise "unable to find collation in #{dumped_schema.inspect} or charset #{default_charset.inspect}"
656
+ dumped_schema.split("\n").map do |line|
657
+ if line['t.text'] || line['t.string']
658
+ if !line['charset: ']
659
+ if line['collation: ']
660
+ line.sub!('collation: ', "charset: #{default_charset.inspect}, collation: ")
661
+ else
662
+ line << ", charset: #{default_charset.inspect}"
663
+ end
664
+ end
665
+ line['collation: '] or line << ", collation: #{default_collation.inspect}"
666
+ end
667
+ line
668
+ end.join("\n")
691
669
  end
692
670
  end
693
671
  end
@@ -25,7 +25,7 @@ RSpec.describe DeclareSchema::FieldDeclarationDsl do
25
25
  end
26
26
 
27
27
  it 'stores limits' do
28
- expect(TestModel.field_specs['name'].limit).to eq(127)
28
+ expect(TestModel.field_specs['name'].limit).to eq(127), TestModel.field_specs['name'].inspect
29
29
  end
30
30
 
31
31
  # TODO: fill out remaining tests
@@ -1,69 +1,179 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- RSpec.describe 'DeclareSchema Model FieldSpec' do
3
+ begin
4
+ require 'mysql2'
5
+ rescue LoadError
6
+ end
7
+
8
+ RSpec.describe DeclareSchema::Model::FieldSpec do
9
+ let(:model) { double('model', table_options: {}) }
10
+ let(:col_spec) { double('col_spec', type: :string) }
11
+
4
12
  before do
5
13
  load File.expand_path('prepare_testapp.rb', __dir__)
14
+
15
+ if Rails::VERSION::MAJOR < 5
16
+ allow(col_spec).to receive(:type_cast_from_database, &:itself)
17
+ end
6
18
  end
7
19
 
8
- context 'There are no model columns to change' do
9
- it '#different_to should return false for int8 == int8' do
10
- subject = DeclareSchema::Model::FieldSpec.new(Object, :price, :integer, limit: 8, null: false, position: 0)
20
+ describe '#initialize' do
21
+ it 'normalizes option order' do
22
+ subject = described_class.new(model, :price, :integer, anonymize_using: 'x', null: false, position: 0, limit: 4)
23
+ expect(subject.options.keys).to eq([:limit, :null, :anonymize_using])
24
+ end
11
25
 
12
- case Rails::VERSION::MAJOR
13
- when 4
14
- cast_type = ActiveRecord::Type::Integer.new(limit: 8)
15
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, cast_type, "integer(8)", false)
16
- else
17
- sql_type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: "integer(8)", type: :integer, limit: 8)
18
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, sql_type_metadata, false, "adverts")
19
- end
26
+ it 'raises exception on unknown field type' do
27
+ expect do
28
+ described_class.new(model, :location, :lat_long, position: 0)
29
+ end.to raise_exception(::DeclareSchema::UnknownTypeError, /:lat_long not found in /)
30
+ end
31
+ end
20
32
 
21
- expect(subject.different_to?(subject.name, col)).to eq(false)
33
+ describe '#schema_attributes' do
34
+ describe 'integer 4' do
35
+ it 'returns schema attributes' do
36
+ subject = described_class.new(model, :price, :integer, limit: 4, null: false, position: 0)
37
+ expect(subject.schema_attributes(col_spec)).to eq(type: :integer, limit: 4, null: false)
38
+ end
22
39
  end
23
40
 
24
- it '#different_to should return false for bigint == bigint' do
25
- subject = DeclareSchema::Model::FieldSpec.new(Object, :price, :bigint, null: false, position: 0)
41
+ describe 'integer 8' do
42
+ it 'returns schema attributes' do
43
+ subject = described_class.new(model, :price, :integer, limit: 8, null: true, position: 2)
44
+ expect(subject.schema_attributes(col_spec)).to eq(type: :integer, limit: 8, null: true)
45
+ end
46
+ end
26
47
 
27
- case Rails::VERSION::MAJOR
28
- when 4
29
- cast_type = ActiveRecord::Type::BigInteger.new(limit: 8)
30
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, cast_type, "bigint(20)", false)
31
- else
32
- sql_type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: "bigint(20)", type: :integer, limit: 8)
33
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, sql_type_metadata, false, "adverts")
48
+ describe 'bigint' do
49
+ it 'returns schema attributes' do
50
+ subject = described_class.new(model, :price, :bigint, null: false, position: 2)
51
+ expect(subject.schema_attributes(col_spec)).to eq(type: :integer, limit: 8, null: false)
34
52
  end
53
+ end
35
54
 
36
- expect(subject.different_to?(subject.name, col)).to eq(false)
55
+ describe 'string' do
56
+ it 'returns schema attributes (including charset/collation iff mysql)' do
57
+ subject = described_class.new(model, :title, :string, limit: 100, null: true, charset: 'utf8mb4', position: 0)
58
+ if defined?(Mysql2)
59
+ expect(subject.schema_attributes(col_spec)).to eq(type: :string, limit: 100, null: true, charset: 'utf8mb4', collation: 'utf8mb4_bin')
60
+ else
61
+ expect(subject.schema_attributes(col_spec)).to eq(type: :string, limit: 100, null: true)
62
+ end
63
+ end
37
64
  end
38
65
 
39
- it '#different_to should return false for int8 == bigint' do
40
- subject = DeclareSchema::Model::FieldSpec.new(Object, :price, :integer, limit: 8, null: false, position: 0)
66
+ describe 'text' do
67
+ it 'returns schema attributes (including charset/collation iff mysql)' do
68
+ subject = described_class.new(model, :title, :text, limit: 200, null: true, charset: 'utf8mb4', position: 2)
69
+ if defined?(Mysql2)
70
+ expect(subject.schema_attributes(col_spec)).to eq(type: :text, limit: 255, null: true, charset: 'utf8mb4', collation: 'utf8mb4_bin')
71
+ else
72
+ expect(subject.schema_attributes(col_spec)).to eq(type: :text, null: true)
73
+ end
74
+ end
41
75
 
42
- case Rails::VERSION::MAJOR
43
- when 4
44
- cast_type = ActiveRecord::Type::BigInteger.new(limit: 8)
45
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, cast_type, "bigint(20)", false)
46
- else
47
- sql_type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: "bigint(20)", type: :integer, limit: 8)
48
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, sql_type_metadata, false, "adverts")
76
+ it 'allows a default to be set unless mysql' do
77
+ if defined?(Mysql2)
78
+ expect do
79
+ described_class.new(model, :title, :text, limit: 200, null: true, default: 'none', charset: 'utf8mb4', position: 2)
80
+ end.to raise_exception(DeclareSchema::MysqlTextMayNotHaveDefault)
81
+ else
82
+ subject = described_class.new(model, :title, :text, limit: 200, null: true, default: 'none', charset: 'utf8mb4', position: 2)
83
+ expect(subject.schema_attributes(col_spec)).to eq(type: :text, null: true, default: 'none')
84
+ end
49
85
  end
50
86
 
51
- expect(subject.different_to?(subject.name, col)).to eq(false)
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)
91
+ end
92
+
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)
96
+ end
97
+
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)
101
+ end
102
+ end
103
+
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 } : {} }
107
+
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
112
+
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
117
+ end
118
+ end
119
+ end
120
+
121
+ describe 'datetime' do
122
+ it 'keeps type as "datetime"' do
123
+ subject = described_class.new(model, :created_at, :datetime, null: false, position: 1)
124
+ expect(subject.schema_attributes(col_spec)).to eq(type: :datetime, null: false)
125
+ end
52
126
  end
53
127
 
54
- it '#different_to should return false for bigint == int8' do
55
- subject = DeclareSchema::Model::FieldSpec.new(Object, :price, :bigint, null: false, position: 0)
128
+ describe 'timestamp' do
129
+ it 'normalizes type to "datetime"' do
130
+ subject = described_class.new(model, :created_at, :timestamp, null: true, position: 2)
131
+ expect(subject.schema_attributes(col_spec)).to eq(type: :datetime, null: true)
132
+ end
133
+ end
56
134
 
135
+ describe 'default:' do
136
+ let(:col_spec) { double('col_spec', type: :integer) }
137
+
138
+ it 'typecasts default value' do
139
+ allow(col_spec).to receive(:type_cast_from_database) { |default| Integer(default) }
140
+ subject = described_class.new(model, :price, :integer, limit: 4, default: '42', null: true, position: 2)
141
+ expect(subject.schema_attributes(col_spec)).to eq(type: :integer, limit: 4, default: 42, null: true)
142
+ end
143
+ end
144
+ end
145
+
146
+ describe '#schema_attributes' do
147
+ let(:col_spec) do
57
148
  case Rails::VERSION::MAJOR
58
149
  when 4
59
150
  cast_type = ActiveRecord::Type::Integer.new(limit: 8)
60
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, cast_type, "integer(8)", false)
151
+ ActiveRecord::ConnectionAdapters::Column.new("price", nil, cast_type, "integer(8)", false)
61
152
  else
62
153
  sql_type_metadata = ActiveRecord::ConnectionAdapters::SqlTypeMetadata.new(sql_type: "integer(8)", type: :integer, limit: 8)
63
- col = ActiveRecord::ConnectionAdapters::Column.new("price", nil, sql_type_metadata, false, "adverts")
154
+ ActiveRecord::ConnectionAdapters::Column.new("price", nil, sql_type_metadata, false, "adverts")
64
155
  end
156
+ end
157
+
158
+ it 'returns the attributes except name, position, and non-SQL options' do
159
+ subject = described_class.new(model, :price, :bigint, null: true, default: 0, ruby_default: -> { }, encrypt_using: -> { }, position: 2)
160
+ expect(subject.schema_attributes(col_spec)).to eq(type: :integer, limit: 8, null: true, default: 0)
161
+ end
162
+
163
+ it 'aliases :bigint and :integer limit: 8' do
164
+ int8 = described_class.new(model, :price, :integer, limit: 8, null: false, position: 0)
165
+ bigint = described_class.new(model, :price, :bigint, null: false, position: 0)
166
+
167
+ expected_attributes = { type: :integer, limit: 8, null: false }
168
+ expect(int8.schema_attributes(col_spec)).to eq(expected_attributes)
169
+ expect(bigint.schema_attributes(col_spec)).to eq(expected_attributes)
170
+ end
171
+ end
65
172
 
66
- expect(subject.different_to?(subject.name, col)).to eq(false)
173
+ describe '#sql_options' do
174
+ subject { described_class.new(model, :price, :integer, limit: 4, null: true, default: 0, position: 2, encrypt_using: ->(field) { field }) }
175
+ it 'excludes non-sql options' do
176
+ expect(subject.sql_options).to eq(limit: 4, null: true, default: 0)
67
177
  end
68
178
  end
69
179
  end