declare_schema 0.6.0 → 0.7.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.
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