declare_schema 0.6.0 → 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/declare_schema_build.yml +21 -5
  3. data/Appraisals +21 -4
  4. data/CHANGELOG.md +40 -0
  5. data/Gemfile +1 -2
  6. data/Gemfile.lock +7 -9
  7. data/README.md +3 -3
  8. data/Rakefile +17 -4
  9. data/bin/declare_schema +1 -1
  10. data/declare_schema.gemspec +1 -1
  11. data/gemfiles/rails_4_mysql.gemfile +22 -0
  12. data/gemfiles/{rails_4.gemfile → rails_4_sqlite.gemfile} +1 -2
  13. data/gemfiles/rails_5_mysql.gemfile +22 -0
  14. data/gemfiles/{rails_5.gemfile → rails_5_sqlite.gemfile} +1 -2
  15. data/gemfiles/rails_6_mysql.gemfile +22 -0
  16. data/gemfiles/{rails_6.gemfile → rails_6_sqlite.gemfile} +2 -3
  17. data/lib/declare_schema/command.rb +10 -3
  18. data/lib/declare_schema/model/column.rb +168 -0
  19. data/lib/declare_schema/model/field_spec.rb +59 -143
  20. data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
  21. data/lib/declare_schema/model/table_options_definition.rb +8 -6
  22. data/lib/declare_schema/version.rb +1 -1
  23. data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
  24. data/lib/generators/declare_schema/migration/migrator.rb +142 -116
  25. data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
  26. data/spec/lib/declare_schema/field_spec_spec.rb +135 -38
  27. data/spec/lib/declare_schema/generator_spec.rb +4 -2
  28. data/spec/lib/declare_schema/interactive_primary_key_spec.rb +8 -2
  29. data/spec/lib/declare_schema/migration_generator_spec.rb +277 -171
  30. data/spec/lib/declare_schema/model/column_spec.rb +141 -0
  31. data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
  32. data/spec/lib/declare_schema/model/index_definition_spec.rb +4 -5
  33. data/spec/lib/declare_schema/model/table_options_definition_spec.rb +19 -29
  34. data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +12 -26
  35. data/spec/support/acceptance_spec_helpers.rb +3 -3
  36. metadata +15 -9
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails'
4
+
5
+ begin
6
+ require 'mysql2'
7
+ rescue LoadError
8
+ end
9
+
10
+ require_relative '../../../../lib/declare_schema/model/column'
11
+
12
+ RSpec.describe DeclareSchema::Model::Column do
13
+ before do
14
+ load File.expand_path('../prepare_testapp.rb', __dir__)
15
+ end
16
+
17
+ describe 'class methods' do
18
+ describe '.native_type?' do
19
+ if Rails::VERSION::MAJOR >= 5
20
+ let(:native_types) { [:string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean, :json] }
21
+ else
22
+ let(:native_types) { [:string, :text, :integer, :float, :decimal, :datetime, :time, :date, :binary, :boolean] }
23
+ end
24
+
25
+ it 'is falsey for :primary_key' do
26
+ expect(described_class.native_type?(:primary_key)).to be_falsey
27
+ end
28
+
29
+ it 'is truthy for native types' do
30
+ native_types.each do |type|
31
+ expect(described_class.native_type?(type)).to be_truthy, type.inspect
32
+ end
33
+ end
34
+
35
+ it 'is falsey for other types' do
36
+ [:email, :url].each do |type|
37
+ expect(described_class.native_type?(type)).to be_falsey
38
+ end
39
+ end
40
+ end
41
+
42
+ describe '.native_types' do
43
+ subject { described_class.native_types }
44
+
45
+ it 'returns the native type for :primary_key' do
46
+ expect(subject[:primary_key]).to match(/auto_increment PRIMARY KEY|PRIMARY KEY AUTOINCREMENT NOT NULL/)
47
+ end
48
+
49
+ it 'returns the native type for :string' do
50
+ expect(subject.dig(:string, :name)).to eq('varchar')
51
+ end
52
+
53
+ it 'returns the native type for :integer' do
54
+ expect(subject.dig(:integer, :name)).to match(/int/)
55
+ end
56
+
57
+ it 'returns the native type for :datetime' do
58
+ expect(subject.dig(:datetime, :name)).to eq('datetime')
59
+ end
60
+ end
61
+
62
+ describe '.sql_type' do
63
+ it 'returns the sql type for :string' do
64
+ expect(described_class.sql_type(:string)).to eq(:string)
65
+ end
66
+
67
+ it 'returns the sql type for :integer' do
68
+ expect(described_class.sql_type(:integer)).to match(:integer)
69
+ end
70
+
71
+ it 'returns the sql type for :datetime' do
72
+ expect(described_class.sql_type(:datetime)).to eq(:datetime)
73
+ end
74
+
75
+ it 'raises UnknownSqlType' do
76
+ expect do
77
+ described_class.sql_type(:email)
78
+ end.to raise_exception(::DeclareSchema::UnknownSqlTypeError, /:email for type :email/)
79
+ end
80
+ end
81
+
82
+ describe '.deserialize_default_value' do
83
+ require 'rails'
84
+
85
+ if ::Rails::VERSION::MAJOR >= 5
86
+ it 'deserializes :boolean' do
87
+ expect(described_class.deserialize_default_value(nil, :boolean, 'true')).to eq(true)
88
+ expect(described_class.deserialize_default_value(nil, :boolean, 'false')).to eq(false)
89
+ end
90
+
91
+ it 'deserializes :integer' do
92
+ expect(described_class.deserialize_default_value(nil, :integer, '12')).to eq(12)
93
+ end
94
+
95
+ it 'deserializes :json' do
96
+ expect(described_class.deserialize_default_value(nil, :json, '{}')).to eq({})
97
+ end
98
+ end
99
+ end
100
+ end
101
+
102
+ describe 'instance methods' do
103
+ before do
104
+ class ColumnTestModel < ActiveRecord::Base
105
+ fields do
106
+ title :string, limit: 127, null: false
107
+ count :integer, null: false
108
+ end
109
+ end
110
+ end
111
+ let(:model) { ColumnTestModel }
112
+ let(:current_table_name) { model.table_name }
113
+ let(:column) { double("ActiveRecord Column",
114
+ name: 'count',
115
+ type: :integer,
116
+ limit: nil,
117
+ precision: nil,
118
+ scale: nil,
119
+ type_cast_from_database: nil,
120
+ null: false,
121
+ default: nil,
122
+ sql_type_metadata: {}) }
123
+ subject { described_class.new(model, current_table_name, column) }
124
+
125
+ describe '#sql_type' do
126
+ it 'returns sql type' do
127
+ expect(subject.sql_type).to match(/int/)
128
+ end
129
+ end
130
+
131
+ describe '#schema_attributes' do
132
+ it 'returns a hash with relevant key/values' do
133
+ if defined?(Mysql2)
134
+ expect(subject.schema_attributes).to eq(type: :integer, null: false, limit: 4)
135
+ else
136
+ expect(subject.schema_attributes).to eq(type: :integer, null: false)
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../../../lib/declare_schema/model/foreign_key_definition'
4
+
5
+ RSpec.describe DeclareSchema::Model::ForeignKeyDefinition do
6
+ before do
7
+ load File.expand_path('../prepare_testapp.rb', __dir__)
8
+
9
+ class Network < ActiveRecord::Base
10
+ fields do
11
+ name :string, limit: 127, index: true
12
+
13
+ timestamps
14
+ end
15
+ end
16
+ end
17
+
18
+ let(:model_class) { Network }
19
+
20
+ describe 'instance methods' do
21
+ let(:connection) { instance_double(ActiveRecord::Base.connection.class) }
22
+ let(:model) { instance_double('Model', table_name: 'models', connection: connection) }
23
+ let(:foreign_key) { :network_id }
24
+ let(:options) { {} }
25
+ subject { described_class.new(model, foreign_key, options)}
26
+
27
+ before do
28
+ allow(connection).to receive(:index_name).with('models', column: 'network_id') { 'on_network_id' }
29
+ end
30
+
31
+ describe '#initialize' do
32
+ it 'normalizes symbols to strings' do
33
+ expect(subject.foreign_key).to eq('network_id')
34
+ expect(subject.parent_table_name).to eq('networks')
35
+ end
36
+
37
+ context 'when most options passed' do
38
+ let(:options) { { parent_table: :networks, foreign_key: :the_network_id, index_name: :index_on_network_id } }
39
+
40
+ it 'normalizes symbols to strings' do
41
+ expect(subject.foreign_key).to eq('network_id')
42
+ expect(subject.foreign_key_name).to eq('the_network_id')
43
+ expect(subject.parent_table_name).to eq('networks')
44
+ expect(subject.foreign_key).to eq('network_id')
45
+ expect(subject.constraint_name).to eq('index_on_network_id')
46
+ expect(subject.on_delete_cascade).to be_falsey
47
+ end
48
+ end
49
+
50
+ context 'when all options passed' do
51
+ let(:foreign_key) { nil }
52
+ let(:options) { { parent_table: :networks, foreign_key: :the_network_id, index_name: :index_on_network_id,
53
+ constraint_name: :constraint_1, dependent: :delete } }
54
+
55
+ it 'normalizes symbols to strings' do
56
+ expect(subject.foreign_key).to be_nil
57
+ expect(subject.foreign_key_name).to eq('the_network_id')
58
+ expect(subject.parent_table_name).to eq('networks')
59
+ expect(subject.constraint_name).to eq('constraint_1')
60
+ expect(subject.on_delete_cascade).to be_truthy
61
+ end
62
+ end
63
+ end
64
+
65
+ describe '#to_add_statement' do
66
+ it 'returns add_foreign_key command' do
67
+ expect(subject.to_add_statement).to eq('add_foreign_key("models", "networks", column: "network_id", name: "on_network_id")')
68
+ end
69
+ end
70
+ end
71
+
72
+ describe 'class << self' do
73
+ let(:connection) { instance_double(ActiveRecord::Base.connection.class) }
74
+ let(:model) { instance_double('Model', table_name: 'models', connection: connection) }
75
+ let(:old_table_name) { 'networks' }
76
+ before do
77
+ allow(connection).to receive(:quote_table_name).with('networks') { 'networks' }
78
+ allow(connection).to receive(:select_rows) { [['CONSTRAINT `constraint` FOREIGN KEY (`network_id`) REFERENCES `networks` (`id`)']] }
79
+ allow(connection).to receive(:index_name).with('models', column: 'network_id') { }
80
+ end
81
+
82
+ describe '.for_model' do
83
+ subject { described_class.for_model(model, old_table_name) }
84
+
85
+ it 'returns new object' do
86
+ expect(subject.size).to eq(1), subject.inspect
87
+ expect(subject.first).to be_kind_of(described_class)
88
+ expect(subject.first.foreign_key).to eq('network_id')
89
+ end
90
+ end
91
+ end
92
+ # TODO: fill out remaining tests
93
+ end
@@ -64,7 +64,7 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
64
64
  ActiveRecord::Base.connection.execute <<~EOS
65
65
  CREATE TABLE index_definition_test_models (
66
66
  id INTEGER NOT NULL PRIMARY KEY,
67
- name TEXT NOT NULL
67
+ name #{if defined?(Sqlite3) then 'TEXT' else 'VARCHAR(255)' end} NOT NULL
68
68
  )
69
69
  EOS
70
70
  ActiveRecord::Base.connection.execute <<~EOS
@@ -72,8 +72,8 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
72
72
  EOS
73
73
  ActiveRecord::Base.connection.execute <<~EOS
74
74
  CREATE TABLE index_definition_compound_index_models (
75
- fk1_id INTEGER NULL,
76
- fk2_id INTEGER NULL,
75
+ fk1_id INTEGER NOT NULL,
76
+ fk2_id INTEGER NOT NULL,
77
77
  PRIMARY KEY (fk1_id, fk2_id)
78
78
  )
79
79
  EOS
@@ -99,10 +99,9 @@ RSpec.describe DeclareSchema::Model::IndexDefinition do
99
99
  let(:model_class) { IndexDefinitionCompoundIndexModel }
100
100
 
101
101
  it 'returns the indexes for the model' do
102
- # Simulate MySQL for Rails 4 work-around
103
102
  if Rails::VERSION::MAJOR < 5
104
103
  expect(model_class.connection).to receive(:primary_key).with('index_definition_compound_index_models').and_return(nil)
105
- connection_stub = instance_double(ActiveRecord::ConnectionAdapters::SQLite3Adapter, "connection")
104
+ connection_stub = instance_double(ActiveRecord::Base.connection.class, "connection")
106
105
  expect(connection_stub).to receive(:indexes).
107
106
  with('index_definition_compound_index_models').
108
107
  and_return([DeclareSchema::Model::IndexDefinition.new(model_class, ['fk1_id', 'fk2_id'], name: 'PRIMARY')])
@@ -1,6 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'active_record/connection_adapters/mysql2_adapter'
3
+ begin
4
+ require 'mysql2'
5
+ require 'active_record/connection_adapters/mysql2_adapter'
6
+ rescue LoadError
7
+ end
4
8
  require_relative '../../../../lib/declare_schema/model/table_options_definition'
5
9
 
6
10
  RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
@@ -22,7 +26,7 @@ RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
22
26
 
23
27
  describe '#to_key' do
24
28
  subject { model.to_key }
25
- it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"]) }
29
+ it { should eq(['table_options_definition_test_models', '{:charset=>"utf8", :collation=>"utf8_general"}']) }
26
30
  end
27
31
 
28
32
  describe '#settings' do
@@ -32,7 +36,7 @@ RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
32
36
 
33
37
  describe '#hash' do
34
38
  subject { model.hash }
35
- it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"].hash) }
39
+ it { should eq(['table_options_definition_test_models', '{:charset=>"utf8", :collation=>"utf8_general"}'].hash) }
36
40
  end
37
41
 
38
42
  describe '#to_s' do
@@ -42,42 +46,28 @@ RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
42
46
 
43
47
  describe '#alter_table_statement' do
44
48
  subject { model.alter_table_statement }
45
- it { should eq('execute "ALTER TABLE \"table_options_definition_test_models\" CHARACTER SET utf8 COLLATE utf8_general;"') }
49
+ it { should match(/execute "ALTER TABLE .*table_options_definition_test_models.* CHARACTER SET utf8 COLLATE utf8_general"/) }
46
50
  end
47
51
  end
48
52
 
49
53
 
50
54
  context 'class << self' do
51
55
  describe '#for_model' do
52
- context 'when using a SQLite connection' do
56
+ context 'when database migrated' do
57
+ let(:options) do
58
+ if defined?(Mysql2)
59
+ { charset: "utf8mb4", collation: "utf8mb4_bin" }
60
+ else
61
+ { }
62
+ end
63
+ end
53
64
  subject { described_class.for_model(model_class) }
54
- it { should eq(described_class.new(model_class.table_name, {})) }
55
- end
56
- # TODO: Convert these tests to run against a MySQL database so that we can
57
- # perform them without mocking out so much
58
- context 'when using a MySQL connection' do
65
+
59
66
  before do
60
- double(ActiveRecord::ConnectionAdapters::Mysql2Adapter).tap do |stub_connection|
61
- expect(stub_connection).to receive(:class).and_return(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
62
- expect(stub_connection).to receive(:current_database).and_return('test_database')
63
- expect(stub_connection).to receive(:quote_string).with('test_database').and_return('test_database')
64
- expect(stub_connection).to receive(:quote_string).with(model_class.table_name).and_return(model_class.table_name)
65
- expect(stub_connection).to(
66
- receive(:select_one).with(<<~EOS)
67
- SELECT CCSA.character_set_name, CCSA.collation_name
68
- FROM information_schema.`TABLES` T, information_schema.`COLLATION_CHARACTER_SET_APPLICABILITY` CCSA
69
- WHERE CCSA.collation_name = T.table_collation AND
70
- T.table_schema = 'test_database' AND
71
- T.table_name = '#{model_class.table_name}';
72
- EOS
73
- .and_return({ "character_set_name" => "utf8", "collation_name" => "utf8_general" })
74
- )
75
- allow(model_class).to receive(:connection).and_return(stub_connection)
76
- end
67
+ generate_migrations '-n', '-m'
77
68
  end
78
69
 
79
- subject { described_class.for_model(model_class) }
80
- it { should eq(described_class.new(model_class.table_name, { charset: "utf8", collation: "utf8_general" })) }
70
+ it { should eq(described_class.new(model_class.table_name, options)) }
81
71
  end
82
72
  end
83
73
  end
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ begin
4
+ require 'mysql2'
5
+ rescue LoadError
6
+ end
3
7
  require 'rails'
4
8
  require 'rails/generators'
5
9
 
@@ -14,26 +18,8 @@ module Generators
14
18
  subject { described_class.new }
15
19
 
16
20
  describe 'format_options' do
17
- let(:mysql_longtext_limit) { 0xffff_ffff }
18
-
19
- context 'MySQL' do
20
- before do
21
- expect(::DeclareSchema::Model::FieldSpec).to receive(:mysql_text_limits?).and_return(true)
22
- end
23
-
24
- it 'returns text limits' do
25
- expect(subject.format_options({ limit: mysql_longtext_limit }, :text)).to eq(["limit: #{mysql_longtext_limit}"])
26
- end
27
- end
28
-
29
- context 'non-MySQL' do
30
- before do
31
- expect(::DeclareSchema::Model::FieldSpec).to receive(:mysql_text_limits?).and_return(false)
32
- end
33
-
34
- it 'returns text limits' do
35
- expect(subject.format_options({ limit: mysql_longtext_limit }, :text)).to eq([])
36
- end
21
+ it 'returns an array of option .inspect strings, with symbols using the modern : hash notation' do
22
+ expect(subject.format_options({ limit: 4, 'key' => 'value "quoted"' })).to eq(["limit: 4", '"key" => "value \"quoted\""'])
37
23
  end
38
24
  end
39
25
 
@@ -47,13 +33,13 @@ module Generators
47
33
  subject { described_class.default_charset }
48
34
 
49
35
  context 'when not explicitly set' do
50
- it { should eq(:utf8mb4) }
36
+ it { should eq("utf8mb4") }
51
37
  end
52
38
 
53
39
  context 'when explicitly set' do
54
- before { described_class.default_charset = :utf8 }
40
+ before { described_class.default_charset = "utf8" }
55
41
  after { described_class.default_charset = described_class::DEFAULT_CHARSET }
56
- it { should eq(:utf8) }
42
+ it { should eq("utf8") }
57
43
  end
58
44
  end
59
45
 
@@ -61,13 +47,13 @@ module Generators
61
47
  subject { described_class.default_collation }
62
48
 
63
49
  context 'when not explicitly set' do
64
- it { should eq(:utf8mb4_general) }
50
+ it { should eq("utf8mb4_bin") }
65
51
  end
66
52
 
67
53
  context 'when explicitly set' do
68
- before { described_class.default_collation = :utf8mb4_general_ci }
54
+ before { described_class.default_collation = "utf8mb4_general_ci" }
69
55
  after { described_class.default_collation = described_class::DEFAULT_COLLATION }
70
- it { should eq(:utf8mb4_general_ci) }
56
+ it { should eq("utf8mb4_general_ci") }
71
57
  end
72
58
  end
73
59
 
@@ -23,7 +23,7 @@ module AcceptanceSpecHelpers
23
23
 
24
24
  def expect_file_to_eq(file_path, expectation)
25
25
  expect(File.exist?(file_path)).to be_truthy
26
- expect(File.read(file_path)).to eq(expectation)
26
+ expect(File.read(file_path).gsub(/require '([^']*)'/, 'require "\1"')).to eq(expectation)
27
27
  end
28
28
 
29
29
  def clean_up_model(model)
@@ -45,13 +45,13 @@ module AcceptanceSpecHelpers
45
45
 
46
46
  class MigrationUpEquals < RSpec::Matchers::BuiltIn::Eq
47
47
  def matches?(subject)
48
- super(subject[0])
48
+ super(subject[0].gsub(/, +([a-z_]+:)/i, ', \1')) # normalize multiple spaces to one
49
49
  end
50
50
  end
51
51
 
52
52
  class MigrationDownEquals < RSpec::Matchers::BuiltIn::Eq
53
53
  def matches?(subject)
54
- super(subject[1])
54
+ super(subject[1].gsub(/, +([a-z_]+:)/i, ', \1')) # normalize multiple spaces to one
55
55
  end
56
56
  end
57
57
  end