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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +21 -2
- data/Gemfile.lock +54 -52
- data/lib/declare_schema/field_declaration_dsl.rb +3 -4
- data/lib/declare_schema/model.rb +1 -2
- data/lib/declare_schema/model/column.rb +158 -0
- data/lib/declare_schema/model/field_spec.rb +59 -144
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +109 -131
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
- data/spec/lib/declare_schema/field_spec_spec.rb +148 -38
- data/spec/lib/declare_schema/migration_generator_spec.rb +45 -42
- data/spec/lib/declare_schema/model/column_spec.rb +127 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +2 -11
- metadata +4 -2
@@ -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
|
-
|
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.
|
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
|
-
|
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)
|
394
|
-
args = [field_spec.name.inspect] + format_options(options
|
395
|
-
format("t.%-*s %s", field_name_width, field_spec.
|
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)
|
433
|
-
[":#{spec.
|
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
|
-
|
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 |
|
454
|
-
|
455
|
-
|
456
|
-
spec
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
|
463
|
-
|
464
|
-
|
465
|
-
|
466
|
-
|
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
|
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)
|
469
|
+
[(renames + adds + removes + changes) * "\n",
|
489
470
|
(undo_renames + undo_adds + undo_removes + undo_changes) * "\n",
|
490
|
-
index_changes
|
491
|
-
undo_index_changes
|
492
|
-
fk_changes
|
493
|
-
undo_fk_changes
|
494
|
-
table_options_changes
|
495
|
-
undo_table_options_changes
|
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 [[], []]
|
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
|
507
|
-
i.with_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
|
-
|
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
|
-
|
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 [[], []]
|
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
|
-
|
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
|
532
|
+
end
|
557
533
|
|
558
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
|
640
|
-
|
641
|
-
|
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
|
-
|
639
|
+
dumped_schema = dumped_schema_stream.string.strip.gsub!("\n ", "\n")
|
644
640
|
if connection.class.name.match?(/mysql/i)
|
645
|
-
|
646
|
-
|
647
|
-
|
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
|
-
|
670
|
-
|
671
|
-
if
|
672
|
-
|
673
|
-
|
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
|
-
|
680
|
-
[
|
681
|
-
|
682
|
-
|
683
|
-
|
684
|
-
|
685
|
-
|
686
|
-
|
687
|
-
|
688
|
-
|
689
|
-
|
690
|
-
|
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
|
-
|
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
|
-
|
9
|
-
it '
|
10
|
-
subject =
|
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
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
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
|
-
|
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
|
-
|
40
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
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
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|