declare_schema 0.3.0 → 0.5.0.pre.1
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.
- checksums.yaml +4 -4
- data/.github/dependabot.yml +14 -0
- data/CHANGELOG.md +36 -1
- data/Gemfile +1 -0
- data/Gemfile.lock +16 -14
- data/README.md +66 -0
- data/gemfiles/rails_4.gemfile +1 -0
- data/gemfiles/rails_5.gemfile +1 -0
- data/gemfiles/rails_6.gemfile +1 -0
- data/lib/declare_schema.rb +3 -1
- data/lib/declare_schema/extensions/active_record/fields_declaration.rb +2 -1
- data/lib/declare_schema/model.rb +59 -22
- data/lib/declare_schema/model/field_spec.rb +82 -26
- data/lib/declare_schema/model/foreign_key_definition.rb +73 -0
- data/lib/declare_schema/model/index_definition.rb +138 -0
- data/lib/declare_schema/model/table_options_definition.rb +83 -0
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +105 -52
- data/spec/lib/declare_schema/api_spec.rb +7 -8
- data/spec/lib/declare_schema/generator_spec.rb +51 -10
- data/spec/lib/declare_schema/interactive_primary_key_spec.rb +16 -14
- data/spec/lib/declare_schema/migration_generator_spec.rb +514 -213
- data/spec/lib/declare_schema/model/index_definition_spec.rb +123 -0
- data/spec/lib/declare_schema/model/table_options_definition_spec.rb +84 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +28 -0
- data/spec/spec_helper.rb +4 -0
- data/spec/support/acceptance_spec_helpers.rb +57 -0
- metadata +12 -7
- data/.dependabot/config.yml +0 -10
- data/lib/declare_schema/model/index_spec.rb +0 -175
@@ -0,0 +1,123 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../../../../lib/declare_schema/model/index_definition'
|
4
|
+
|
5
|
+
# Beware: It looks out that Rails' sqlite3 driver has a bug in retrieving indexes.
|
6
|
+
# In sqlite3/schema_statements, it skips over any index that starts with sqlite_:
|
7
|
+
# next if row["name"].starts_with?("sqlite_")
|
8
|
+
# but this will skip over any indexes created to support "unique" column constraints.
|
9
|
+
# This gem provides an explicit name for all indexes so it shouldn't be affected by the bug...
|
10
|
+
# unless you manually create any Sqlite tables with UNIQUE constraints.
|
11
|
+
|
12
|
+
RSpec.describe DeclareSchema::Model::IndexDefinition do
|
13
|
+
before do
|
14
|
+
load File.expand_path('../prepare_testapp.rb', __dir__)
|
15
|
+
|
16
|
+
class IndexDefinitionTestModel < ActiveRecord::Base
|
17
|
+
fields do
|
18
|
+
name :string, limit: 127, index: true
|
19
|
+
|
20
|
+
timestamps
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
class IndexDefinitionCompoundIndexModel < ActiveRecord::Base
|
25
|
+
fields do
|
26
|
+
fk1_id :integer
|
27
|
+
fk2_id :integer
|
28
|
+
|
29
|
+
timestamps
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
let(:model_class) { IndexDefinitionTestModel }
|
35
|
+
|
36
|
+
describe 'instance methods' do
|
37
|
+
let(:model) { model_class.new }
|
38
|
+
subject { declared_class.new(model_class) }
|
39
|
+
|
40
|
+
it 'has index_definitions' do
|
41
|
+
expect(model_class.index_definitions).to be_kind_of(Array)
|
42
|
+
expect(model_class.index_definitions.map(&:name)).to eq(['on_name'])
|
43
|
+
expect([:name, :fields, :unique].map { |attr| model_class.index_definitions[0].send(attr)}).to eq(
|
44
|
+
['on_name', ['name'], false]
|
45
|
+
)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'has index_definitions_with_primary_key' do
|
49
|
+
expect(model_class.index_definitions_with_primary_key).to be_kind_of(Array)
|
50
|
+
result = model_class.index_definitions_with_primary_key.sort_by(&:name)
|
51
|
+
expect(result.map(&:name)).to eq(['PRIMARY', 'on_name'])
|
52
|
+
expect([:name, :fields, :unique].map { |attr| result[0].send(attr)}).to eq(
|
53
|
+
['PRIMARY', ['id'], true]
|
54
|
+
)
|
55
|
+
expect([:name, :fields, :unique].map { |attr| result[1].send(attr)}).to eq(
|
56
|
+
['on_name', ['name'], false]
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe 'class << self' do
|
62
|
+
context 'with a migrated database' do
|
63
|
+
before do
|
64
|
+
ActiveRecord::Base.connection.execute <<~EOS
|
65
|
+
CREATE TABLE index_definition_test_models (
|
66
|
+
id INTEGER NOT NULL PRIMARY KEY,
|
67
|
+
name TEXT NOT NULL
|
68
|
+
)
|
69
|
+
EOS
|
70
|
+
ActiveRecord::Base.connection.execute <<~EOS
|
71
|
+
CREATE UNIQUE INDEX index_definition_test_models_on_name ON index_definition_test_models(name)
|
72
|
+
EOS
|
73
|
+
ActiveRecord::Base.connection.execute <<~EOS
|
74
|
+
CREATE TABLE index_definition_compound_index_models (
|
75
|
+
fk1_id INTEGER NULL,
|
76
|
+
fk2_id INTEGER NULL,
|
77
|
+
PRIMARY KEY (fk1_id, fk2_id)
|
78
|
+
)
|
79
|
+
EOS
|
80
|
+
ActiveRecord::Base.connection.schema_cache.clear!
|
81
|
+
end
|
82
|
+
|
83
|
+
describe 'for_model' do
|
84
|
+
subject { described_class.for_model(model_class) }
|
85
|
+
|
86
|
+
context 'with single-column PK' do
|
87
|
+
it 'returns the indexes for the model' do
|
88
|
+
expect(subject.size).to eq(2), subject.inspect
|
89
|
+
expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
|
90
|
+
['index_definition_test_models_on_name', ['name'], true]
|
91
|
+
)
|
92
|
+
expect([:name, :columns, :unique].map { |attr| subject[1].send(attr) }).to eq(
|
93
|
+
['PRIMARY', ['id'], true]
|
94
|
+
)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
context 'with compound-column PK' do
|
99
|
+
let(:model_class) { IndexDefinitionCompoundIndexModel }
|
100
|
+
|
101
|
+
it 'returns the indexes for the model' do
|
102
|
+
# Simulate MySQL for Rails 4 work-around
|
103
|
+
if Rails::VERSION::MAJOR < 5
|
104
|
+
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")
|
106
|
+
expect(connection_stub).to receive(:indexes).
|
107
|
+
with('index_definition_compound_index_models').
|
108
|
+
and_return([DeclareSchema::Model::IndexDefinition.new(model_class, ['fk1_id', 'fk2_id'], name: 'PRIMARY')])
|
109
|
+
|
110
|
+
expect(model_class.connection).to receive(:dup).and_return(connection_stub)
|
111
|
+
end
|
112
|
+
|
113
|
+
expect(subject.size).to eq(1), subject.inspect
|
114
|
+
expect([:name, :columns, :unique].map { |attr| subject[0].send(attr) }).to eq(
|
115
|
+
['PRIMARY', ['fk1_id', 'fk2_id'], true]
|
116
|
+
)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
# TODO: fill out remaining tests
|
123
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_record/connection_adapters/mysql2_adapter'
|
4
|
+
require_relative '../../../../lib/declare_schema/model/table_options_definition'
|
5
|
+
|
6
|
+
RSpec.describe DeclareSchema::Model::TableOptionsDefinition do
|
7
|
+
before do
|
8
|
+
load File.expand_path('../prepare_testapp.rb', __dir__)
|
9
|
+
|
10
|
+
class TableOptionsDefinitionTestModel < ActiveRecord::Base
|
11
|
+
fields do
|
12
|
+
name :string, limit: 127, index: true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:model_class) { TableOptionsDefinitionTestModel }
|
18
|
+
|
19
|
+
context 'instance methods' do
|
20
|
+
let(:table_options) { { charset: "utf8", collation: "utf8_general"} }
|
21
|
+
let(:model) { described_class.new('table_options_definition_test_models', table_options) }
|
22
|
+
|
23
|
+
describe '#to_key' do
|
24
|
+
subject { model.to_key }
|
25
|
+
it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"]) }
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '#settings' do
|
29
|
+
subject { model.settings }
|
30
|
+
it { should eq("CHARACTER SET utf8 COLLATE utf8_general") }
|
31
|
+
end
|
32
|
+
|
33
|
+
describe '#hash' do
|
34
|
+
subject { model.hash }
|
35
|
+
it { should eq(["table_options_definition_test_models", "{:charset=>\"utf8\", :collation=>\"utf8_general\"}"].hash) }
|
36
|
+
end
|
37
|
+
|
38
|
+
describe '#to_s' do
|
39
|
+
subject { model.to_s }
|
40
|
+
it { should eq("CHARACTER SET utf8 COLLATE utf8_general") }
|
41
|
+
end
|
42
|
+
|
43
|
+
describe '#alter_table_statement' do
|
44
|
+
subject { model.alter_table_statement }
|
45
|
+
it { should eq('execute "ALTER TABLE \"table_options_definition_test_models\" CHARACTER SET utf8 COLLATE utf8_general;"') }
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
context 'class << self' do
|
51
|
+
describe '#for_model' do
|
52
|
+
context 'when using a SQLite connection' do
|
53
|
+
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
|
59
|
+
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
|
77
|
+
end
|
78
|
+
|
79
|
+
subject { described_class.for_model(model_class) }
|
80
|
+
it { should eq(described_class.new(model_class.table_name, { charset: "utf8", collation: "utf8_general" })) }
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -43,6 +43,34 @@ module Generators
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
+
describe '#default_charset' do
|
47
|
+
subject { described_class.default_charset }
|
48
|
+
|
49
|
+
context 'when not explicitly set' do
|
50
|
+
it { should eq(:utf8mb4) }
|
51
|
+
end
|
52
|
+
|
53
|
+
context 'when explicitly set' do
|
54
|
+
before { described_class.default_charset = :utf8 }
|
55
|
+
after { described_class.default_charset = described_class::DEFAULT_CHARSET }
|
56
|
+
it { should eq(:utf8) }
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe '#default_collation' do
|
61
|
+
subject { described_class.default_collation }
|
62
|
+
|
63
|
+
context 'when not explicitly set' do
|
64
|
+
it { should eq(:utf8mb4_general) }
|
65
|
+
end
|
66
|
+
|
67
|
+
context 'when explicitly set' do
|
68
|
+
before { described_class.default_collation = :utf8mb4_general_ci }
|
69
|
+
after { described_class.default_collation = described_class::DEFAULT_COLLATION }
|
70
|
+
it { should eq(:utf8mb4_general_ci) }
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
46
74
|
describe 'load_rails_models' do
|
47
75
|
before do
|
48
76
|
expect(Rails.application).to receive(:eager_load!)
|
data/spec/spec_helper.rb
CHANGED
@@ -4,7 +4,11 @@ require "bundler/setup"
|
|
4
4
|
require "declare_schema"
|
5
5
|
require "climate_control"
|
6
6
|
|
7
|
+
require_relative "./support/acceptance_spec_helpers"
|
8
|
+
|
7
9
|
RSpec.configure do |config|
|
10
|
+
config.include AcceptanceSpecHelpers
|
11
|
+
|
8
12
|
# Enable flags like --only-failures and --next-failure
|
9
13
|
config.example_status_persistence_file_path = ".rspec_status"
|
10
14
|
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module AcceptanceSpecHelpers
|
4
|
+
def generate_model(model_name, *fields)
|
5
|
+
Rails::Generators.invoke('declare_schema:model', [model_name, *fields])
|
6
|
+
end
|
7
|
+
|
8
|
+
def generate_migrations(*flags)
|
9
|
+
Rails::Generators.invoke('declare_schema:migration', flags)
|
10
|
+
end
|
11
|
+
|
12
|
+
def expect_model_definition_to_eq(model, expectation)
|
13
|
+
expect_file_to_eq("#{TESTAPP_PATH}/app/models/#{model}.rb", expectation)
|
14
|
+
end
|
15
|
+
|
16
|
+
def expect_test_definition_to_eq(model, expectation)
|
17
|
+
expect_file_to_eq("#{TESTAPP_PATH}/test/models/#{model}_test.rb", expectation)
|
18
|
+
end
|
19
|
+
|
20
|
+
def expect_test_fixture_to_eq(model, expectation)
|
21
|
+
expect_file_to_eq("#{TESTAPP_PATH}/test/fixtures/#{model}.yml", expectation)
|
22
|
+
end
|
23
|
+
|
24
|
+
def expect_file_to_eq(file_path, expectation)
|
25
|
+
expect(File.exist?(file_path)).to be_truthy
|
26
|
+
expect(File.read(file_path)).to eq(expectation)
|
27
|
+
end
|
28
|
+
|
29
|
+
def clean_up_model(model)
|
30
|
+
system("rm -rf #{TESTAPP_PATH}/app/models/#{model}.rb #{TESTAPP_PATH}/test/models/#{model}.rb #{TESTAPP_PATH}/test/fixtures/#{model}.rb")
|
31
|
+
end
|
32
|
+
|
33
|
+
def load_models
|
34
|
+
Rails.application.config.autoload_paths += ["#{TESTAPP_PATH}/app/models"]
|
35
|
+
$LOAD_PATH << "#{TESTAPP_PATH}/app/models"
|
36
|
+
end
|
37
|
+
|
38
|
+
def migrate_up(expected_value)
|
39
|
+
MigrationUpEquals.new(expected_value)
|
40
|
+
end
|
41
|
+
|
42
|
+
def migrate_down(expected_value)
|
43
|
+
MigrationDownEquals.new(expected_value)
|
44
|
+
end
|
45
|
+
|
46
|
+
class MigrationUpEquals < RSpec::Matchers::BuiltIn::Eq
|
47
|
+
def matches?(subject)
|
48
|
+
super(subject[0])
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
class MigrationDownEquals < RSpec::Matchers::BuiltIn::Eq
|
53
|
+
def matches?(subject)
|
54
|
+
super(subject[1])
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: declare_schema
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.5.0.pre.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Invoca Development adapted from hobo_fields by Tom Locke
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-
|
11
|
+
date: 2020-12-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -32,7 +32,7 @@ executables:
|
|
32
32
|
extensions: []
|
33
33
|
extra_rdoc_files: []
|
34
34
|
files:
|
35
|
-
- ".dependabot
|
35
|
+
- ".github/dependabot.yml"
|
36
36
|
- ".github/workflows/gem_release.yml"
|
37
37
|
- ".gitignore"
|
38
38
|
- ".rspec"
|
@@ -59,7 +59,9 @@ files:
|
|
59
59
|
- lib/declare_schema/field_declaration_dsl.rb
|
60
60
|
- lib/declare_schema/model.rb
|
61
61
|
- lib/declare_schema/model/field_spec.rb
|
62
|
-
- lib/declare_schema/model/
|
62
|
+
- lib/declare_schema/model/foreign_key_definition.rb
|
63
|
+
- lib/declare_schema/model/index_definition.rb
|
64
|
+
- lib/declare_schema/model/table_options_definition.rb
|
63
65
|
- lib/declare_schema/railtie.rb
|
64
66
|
- lib/declare_schema/version.rb
|
65
67
|
- lib/generators/declare_schema/migration/USAGE
|
@@ -76,15 +78,18 @@ files:
|
|
76
78
|
- spec/lib/declare_schema/generator_spec.rb
|
77
79
|
- spec/lib/declare_schema/interactive_primary_key_spec.rb
|
78
80
|
- spec/lib/declare_schema/migration_generator_spec.rb
|
81
|
+
- spec/lib/declare_schema/model/index_definition_spec.rb
|
82
|
+
- spec/lib/declare_schema/model/table_options_definition_spec.rb
|
79
83
|
- spec/lib/declare_schema/prepare_testapp.rb
|
80
84
|
- spec/lib/generators/declare_schema/migration/migrator_spec.rb
|
81
85
|
- spec/spec_helper.rb
|
86
|
+
- spec/support/acceptance_spec_helpers.rb
|
82
87
|
- test_responses.txt
|
83
88
|
homepage: https://github.com/Invoca/declare_schema
|
84
89
|
licenses: []
|
85
90
|
metadata:
|
86
91
|
allowed_push_host: https://rubygems.org
|
87
|
-
post_install_message:
|
92
|
+
post_install_message:
|
88
93
|
rdoc_options: []
|
89
94
|
require_paths:
|
90
95
|
- lib
|
@@ -100,7 +105,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
100
105
|
version: 1.3.6
|
101
106
|
requirements: []
|
102
107
|
rubygems_version: 3.0.3
|
103
|
-
signing_key:
|
108
|
+
signing_key:
|
104
109
|
specification_version: 4
|
105
110
|
summary: Database migration generator for Rails
|
106
111
|
test_files: []
|
data/.dependabot/config.yml
DELETED
@@ -1,175 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module DeclareSchema
|
4
|
-
module Model
|
5
|
-
class IndexSpec
|
6
|
-
include Comparable
|
7
|
-
|
8
|
-
attr_reader :table, :fields, :explicit_name, :name, :unique, :where
|
9
|
-
|
10
|
-
class IndexNameTooLongError < RuntimeError; end
|
11
|
-
|
12
|
-
PRIMARY_KEY_NAME = "PRIMARY_KEY"
|
13
|
-
MYSQL_INDEX_NAME_MAX_LENGTH = 64
|
14
|
-
|
15
|
-
def initialize(model, fields, options = {})
|
16
|
-
@model = model
|
17
|
-
@table = options.delete(:table_name) || model.table_name
|
18
|
-
@fields = Array.wrap(fields).map(&:to_s)
|
19
|
-
@explicit_name = options[:name] unless options.delete(:allow_equivalent)
|
20
|
-
@name = options.delete(:name) || model.connection.index_name(table, column: @fields).gsub(/index.*_on_/, 'on_')
|
21
|
-
@unique = options.delete(:unique) || name == PRIMARY_KEY_NAME || false
|
22
|
-
|
23
|
-
if @name.length > MYSQL_INDEX_NAME_MAX_LENGTH
|
24
|
-
raise IndexNameTooLongError, "Index '#{@name}' exceeds MySQL limit of #{MYSQL_INDEX_NAME_MAX_LENGTH} characters. Give it a shorter name."
|
25
|
-
end
|
26
|
-
|
27
|
-
if (where = options[:where])
|
28
|
-
@where = where.start_with?('(') ? where : "(#{where})"
|
29
|
-
end
|
30
|
-
end
|
31
|
-
|
32
|
-
class << self
|
33
|
-
# extract IndexSpecs from an existing table
|
34
|
-
def for_model(model, old_table_name = nil)
|
35
|
-
t = old_table_name || model.table_name
|
36
|
-
connection = model.connection.dup
|
37
|
-
# TODO: Change below to use prepend
|
38
|
-
class << connection # defeat Rails code that skips the primary keys by changing their name to PRIMARY_KEY_NAME
|
39
|
-
def each_hash(result)
|
40
|
-
super do |hash|
|
41
|
-
if hash[:Key_name] == "PRIMARY"
|
42
|
-
hash[:Key_name] = PRIMARY_KEY_NAME
|
43
|
-
end
|
44
|
-
yield hash
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
connection.indexes(t).map do |i|
|
49
|
-
new(model, i.columns, name: i.name, unique: i.unique, where: i.where, table_name: old_table_name) unless model.ignore_indexes.include?(i.name)
|
50
|
-
end.compact
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def primary_key?
|
55
|
-
name == PRIMARY_KEY_NAME
|
56
|
-
end
|
57
|
-
|
58
|
-
def to_add_statement(new_table_name, existing_primary_key = nil)
|
59
|
-
if primary_key? && !ActiveRecord::Base.connection.class.name.match?(/SQLite3Adapter/)
|
60
|
-
to_add_primary_key_statement(new_table_name, existing_primary_key)
|
61
|
-
else
|
62
|
-
r = +"add_index #{new_table_name.to_sym.inspect}, #{fields.map(&:to_sym).inspect}"
|
63
|
-
r += ", unique: true" if unique
|
64
|
-
r += ", where: '#{where}'" if where.present?
|
65
|
-
r += ", name: '#{name}'"
|
66
|
-
r
|
67
|
-
end
|
68
|
-
end
|
69
|
-
|
70
|
-
def to_add_primary_key_statement(new_table_name, existing_primary_key)
|
71
|
-
drop = "DROP PRIMARY KEY, " if existing_primary_key
|
72
|
-
statement = "ALTER TABLE #{new_table_name} #{drop}ADD PRIMARY KEY (#{fields.join(', ')})"
|
73
|
-
"execute #{statement.inspect}"
|
74
|
-
end
|
75
|
-
|
76
|
-
def to_key
|
77
|
-
@key ||= [table, fields, name, unique, where].map(&:to_s)
|
78
|
-
end
|
79
|
-
|
80
|
-
def settings
|
81
|
-
@settings ||= [table, fields, unique].map(&:to_s)
|
82
|
-
end
|
83
|
-
|
84
|
-
def hash
|
85
|
-
to_key.hash
|
86
|
-
end
|
87
|
-
|
88
|
-
def <=>(rhs)
|
89
|
-
to_key <=> rhs.to_key
|
90
|
-
end
|
91
|
-
|
92
|
-
def equivalent?(rhs)
|
93
|
-
settings == rhs.settings
|
94
|
-
end
|
95
|
-
|
96
|
-
def with_name(new_name)
|
97
|
-
self.class.new(@model, @fields, table_name: @table_name, index_name: @index_name, unique: @unique, name: new_name)
|
98
|
-
end
|
99
|
-
|
100
|
-
alias eql? ==
|
101
|
-
end
|
102
|
-
|
103
|
-
class ForeignKeySpec
|
104
|
-
include Comparable
|
105
|
-
|
106
|
-
attr_reader :constraint_name, :model, :foreign_key, :options, :on_delete_cascade
|
107
|
-
|
108
|
-
def initialize(model, foreign_key, options = {})
|
109
|
-
@model = model
|
110
|
-
@foreign_key = foreign_key.presence
|
111
|
-
@options = options
|
112
|
-
|
113
|
-
@child_table = model.table_name # unless a table rename, which would happen when a class is renamed??
|
114
|
-
@parent_table_name = options[:parent_table]
|
115
|
-
@foreign_key_name = options[:foreign_key] || self.foreign_key
|
116
|
-
@index_name = options[:index_name] || model.connection.index_name(model.table_name, column: foreign_key)
|
117
|
-
@constraint_name = options[:constraint_name] || @index_name || ''
|
118
|
-
@on_delete_cascade = options[:dependent] == :delete
|
119
|
-
|
120
|
-
# Empty constraint lets mysql generate the name
|
121
|
-
end
|
122
|
-
|
123
|
-
class << self
|
124
|
-
def for_model(model, old_table_name)
|
125
|
-
show_create_table = model.connection.select_rows("show create table #{model.connection.quote_table_name(old_table_name)}").first.last
|
126
|
-
constraints = show_create_table.split("\n").map { |line| line.strip if line['CONSTRAINT'] }.compact
|
127
|
-
|
128
|
-
constraints.map do |fkc|
|
129
|
-
options = {}
|
130
|
-
name, foreign_key, parent_table = fkc.match(/CONSTRAINT `([^`]*)` FOREIGN KEY \(`([^`]*)`\) REFERENCES `([^`]*)`/).captures
|
131
|
-
options[:constraint_name] = name
|
132
|
-
options[:parent_table] = parent_table
|
133
|
-
options[:foreign_key] = foreign_key
|
134
|
-
options[:dependent] = :delete if fkc['ON DELETE CASCADE']
|
135
|
-
|
136
|
-
new(model, foreign_key, options)
|
137
|
-
end
|
138
|
-
end
|
139
|
-
end
|
140
|
-
|
141
|
-
def parent_table_name
|
142
|
-
@parent_table_name ||=
|
143
|
-
options[:class_name]&.is_a?(Class) &&
|
144
|
-
options[:class_name].respond_to?(:table_name) &&
|
145
|
-
options[:class_name]&.table_name
|
146
|
-
@parent_table_name ||=
|
147
|
-
options[:class_name]&.constantize &&
|
148
|
-
options[:class_name].constantize.respond_to?(:table_name) &&
|
149
|
-
options[:class_name].constantize.table_name ||
|
150
|
-
foreign_key.gsub(/_id/, '').camelize.constantize.table_name
|
151
|
-
end
|
152
|
-
|
153
|
-
attr_writer :parent_table_name
|
154
|
-
|
155
|
-
def to_add_statement(_ = true)
|
156
|
-
statement = "ALTER TABLE #{@child_table} ADD CONSTRAINT #{@constraint_name} FOREIGN KEY #{@index_name}(#{@foreign_key_name}) REFERENCES #{parent_table_name}(id) #{'ON DELETE CASCADE' if on_delete_cascade}"
|
157
|
-
"execute #{statement.inspect}"
|
158
|
-
end
|
159
|
-
|
160
|
-
def to_key
|
161
|
-
@key ||= [@child_table, parent_table_name, @foreign_key_name, @on_delete_cascade].map(&:to_s)
|
162
|
-
end
|
163
|
-
|
164
|
-
def hash
|
165
|
-
to_key.hash
|
166
|
-
end
|
167
|
-
|
168
|
-
def <=>(rhs)
|
169
|
-
to_key <=> rhs.to_key
|
170
|
-
end
|
171
|
-
|
172
|
-
alias eql? ==
|
173
|
-
end
|
174
|
-
end
|
175
|
-
end
|