Skip to content

Commit 491d563

Browse files
Romain Pomierpomier
authored andcommitted
Add support for tables with multiple outbound foreign key constraints
1 parent 8d6e8fd commit 491d563

8 files changed

Lines changed: 232 additions & 9 deletions

File tree

lib/lhm/migrator.rb

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,7 @@ def execute
207207
end
208208

209209
def destination_create
210-
original = %{CREATE TABLE `#{ @origin.name }`}
211-
replacement = %{CREATE TABLE `#{ @origin.destination_name }`}
212-
stmt = @origin.ddl.gsub(original, replacement)
210+
stmt = @origin.destination_ddl
213211
@connection.execute(tagged(stmt))
214212
end
215213

lib/lhm/table.rb

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@
55

66
module Lhm
77
class Table
8-
attr_reader :name, :columns, :indices, :pk, :ddl
8+
attr_reader :schema, :name, :columns, :indices, :constraints, :pk, :ddl
99

10-
def initialize(name, pk = 'id', ddl = nil)
10+
def initialize(name, schema = 'default', pk = 'id', ddl = nil)
1111
@name = name
12+
@schema = schema
1213
@columns = {}
1314
@indices = {}
15+
@constraints = {}
1416
@pk = pk
1517
@ddl = ddl
1618
end
@@ -28,6 +30,32 @@ def self.parse(table_name, connection)
2830
Parser.new(table_name, connection).parse
2931
end
3032

33+
def destination_ddl
34+
original = %r{CREATE TABLE ("|`)#{ name }\1}
35+
repl = '\1'
36+
replacement = %Q{CREATE TABLE #{ repl }#{ destination_name }#{ repl }}
37+
38+
dest = ddl
39+
dest.gsub!(original, replacement)
40+
41+
foreign_keys = constraints.select { |col, c| !c[:referenced_column].nil? }
42+
43+
foreign_keys.keys.each_with_index do |key, i|
44+
original = foreign_keys[key][:name]
45+
replacement = replacement_constraint(original)
46+
dest.gsub!(original, replacement)
47+
end
48+
49+
dest
50+
end
51+
52+
@@schema_constraints = {}
53+
54+
def self.schema_constraints(schema, value = nil)
55+
@@schema_constraints[schema] = value if value
56+
@@schema_constraints[schema]
57+
end
58+
3159
class Parser
3260
include SqlHelper
3361

@@ -47,7 +75,7 @@ def ddl
4775
def parse
4876
schema = read_information_schema
4977

50-
Table.new(@table_name, extract_primary_key(schema), ddl).tap do |table|
78+
Table.new(@table_name, @schema_name, extract_primary_key(schema), ddl).tap do |table|
5179
schema.each do |defn|
5280
column_name = struct_key(defn, 'COLUMN_NAME')
5381
column_type = struct_key(defn, 'COLUMN_TYPE')
@@ -64,6 +92,15 @@ def parse
6492
extract_indices(read_indices).each do |idx, columns|
6593
table.indices[idx] = columns
6694
end
95+
96+
constraints = {}
97+
extract_constraints(read_constraints(nil)).each do |data|
98+
if data[:schema] == @schema_name && data[:table] == @table_name
99+
table.constraints[data[:column]] = data
100+
end
101+
constraints[data[:name]] = data
102+
end
103+
Table.schema_constraints(@schema_name, constraints)
67104
end
68105
end
69106

@@ -98,6 +135,47 @@ def extract_indices(indices)
98135
end
99136
end
100137

138+
def read_constraints(table = @table_name)
139+
query = %Q{
140+
select *
141+
from information_schema.key_column_usage
142+
where table_schema = '#{ @schema_name }'
143+
and referenced_column_name is not null
144+
}
145+
query += %Q{
146+
and table_name = '#{ @table_name }'
147+
} if table
148+
149+
@connection.select_all(query)
150+
end
151+
152+
def extract_constraints(constraints)
153+
columns = %w{
154+
CONSTRAINT_NAME
155+
TABLE_SCHEMA
156+
TABLE_NAME
157+
COLUMN_NAME
158+
ORDINAL_POSITION
159+
POSITION_IN_UNIQUE_CONSTRAINT
160+
REFERENCED_TABLE_SCHEMA
161+
REFERENCED_TABLE_NAME
162+
REFERENCED_COLUMN_NAME
163+
}
164+
165+
constraints.map do |row|
166+
result = {}
167+
columns.each do |c|
168+
sym = c.dup
169+
# The order of these substitutions is important
170+
sym.gsub!(/CONSTRAINT_/, '')
171+
sym.gsub!(/_NAME/, '')
172+
sym.gsub!(/TABLE_/, '')
173+
result[sym.downcase.to_sym] = row[struct_key(row, c)]
174+
end
175+
result
176+
end
177+
end
178+
101179
def extract_primary_key(schema)
102180
cols = schema.select do |defn|
103181
column_key = struct_key(defn, 'COLUMN_KEY')
@@ -112,5 +190,12 @@ def extract_primary_key(schema)
112190
keys.length == 1 ? keys.first : keys
113191
end
114192
end
193+
194+
private
195+
196+
def replacement_constraint(name)
197+
(name =~ /_lhmn$/).nil? ? "#{name}_lhmn" : name.gsub(/_lhmn$/, '')
198+
end
199+
115200
end
116201
end

spec/fixtures/fk_example.ddl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE `fk_example` (
2+
`id` int(11) NOT NULL AUTO_INCREMENT,
3+
`user_id` int(11) NOT NULL,
4+
`master_id` int(11) NOT NULL,
5+
PRIMARY KEY (`id`),
6+
CONSTRAINT `fk_example_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
7+
CONSTRAINT `fk_example_ibfk_2` FOREIGN KEY (`master_id`) REFERENCES `users` (`id`)
8+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
CREATE TABLE `fk_example_second_pass` (
2+
`id` int(11) NOT NULL AUTO_INCREMENT,
3+
`user_id` int(11) NOT NULL,
4+
`master_id` int(11) NOT NULL,
5+
PRIMARY KEY (`id`),
6+
CONSTRAINT `fk_example_ibfk_1_lhmn` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
7+
CONSTRAINT `fk_example_ibfk_2_lhmn` FOREIGN KEY (`master_id`) REFERENCES `users` (`id`)
8+
) ENGINE=InnoDB DEFAULT CHARSET=utf8
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2+
# Schmidt
3+
4+
require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'
5+
6+
require 'lhm'
7+
8+
describe Lhm do
9+
include IntegrationHelper
10+
11+
before(:each) do
12+
connect_master!
13+
Lhm.cleanup(true)
14+
%w(fk_example fk_example_second_pass).each do |table|
15+
execute "drop table if exists #{table}"
16+
end
17+
table_create(:users)
18+
end
19+
20+
describe 'the simplest case' do
21+
before(:each) do
22+
table_create(:fk_example)
23+
end
24+
25+
after(:each) do
26+
execute 'drop table if exists fk_example'
27+
Lhm.cleanup(true)
28+
end
29+
30+
it 'should handle tables with foreign keys by appending the suffix' do
31+
Lhm.change_table(:fk_example) do |t|
32+
t.add_column(:new_column, "INT(12) DEFAULT '0'")
33+
end
34+
35+
slave do
36+
actual = table_read(:fk_example).constraints['user_id']
37+
expected = {
38+
name: 'fk_example_ibfk_1_lhmn',
39+
referenced_table: 'users',
40+
referenced_column: 'id',
41+
}
42+
hash_slice(actual, expected.keys).must_equal(expected)
43+
44+
actual = table_read(:fk_example).constraints['master_id']
45+
expected = {
46+
name: 'fk_example_ibfk_2_lhmn',
47+
referenced_table: 'users',
48+
referenced_column: 'id',
49+
}
50+
hash_slice(actual, expected.keys).must_equal(expected)
51+
end
52+
end
53+
end
54+
55+
describe 'manage a new migration by removing the suffix' do
56+
before(:each) do
57+
table_create(:fk_example_second_pass)
58+
end
59+
60+
after(:each) do
61+
execute 'drop table if exists fk_example_second_pass'
62+
Lhm.cleanup(true)
63+
end
64+
65+
it 'should be able to create this table' do
66+
Lhm.change_table(:fk_example_second_pass) do |t|
67+
t.add_column(:new_column, "INT(12) DEFAULT '0'")
68+
end
69+
70+
slave do
71+
actual = table_read(:fk_example_second_pass).constraints['user_id']
72+
expected = {
73+
name: 'fk_example_ibfk_1',
74+
referenced_table: 'users',
75+
referenced_column: 'id',
76+
}
77+
hash_slice(actual, expected.keys).must_equal(expected)
78+
79+
actual = table_read(:fk_example_second_pass).constraints['master_id']
80+
expected = {
81+
name: 'fk_example_ibfk_2',
82+
referenced_table: 'users',
83+
referenced_column: 'id',
84+
}
85+
hash_slice(actual, expected.keys).must_equal(expected)
86+
end
87+
end
88+
end
89+
end

spec/integration/integration_helper.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ def table_exists?(table)
125125
connection.table_exists?(table.name)
126126
end
127127

128+
def hash_slice(hash, keys)
129+
return hash.slice(*keys) if hash.respond_to?(:slice)
130+
keys.each { |k| [k, hash[k]] }.to_h
131+
end
132+
128133
#
129134
# Database Helpers
130135
#

spec/integration/table_spec.rb

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,23 @@
8686
indices['index_users_on_reference'].
8787
must_equal(['reference'])
8888
end
89+
90+
it 'should parse constraints' do
91+
begin
92+
@table = table_create(:fk_example)
93+
@table.constraints.keys.must_equal %w(user_id master_id)
94+
95+
expected = {
96+
name: 'fk_example_ibfk_1',
97+
referenced_table: 'users',
98+
referenced_column: 'id'
99+
}
100+
101+
hash_slice(@table.constraints['user_id'], expected.keys).must_equal expected
102+
ensure
103+
execute 'drop table if exists fk_example'
104+
end
105+
end
89106
end
90107
end
91108
end

spec/unit/table_spec.rb

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,25 +15,38 @@
1515
end
1616
end
1717

18+
describe 'ddl' do
19+
it 'should build the destination table' do
20+
table = 'users'
21+
schema = 'default'
22+
23+
@table = Lhm::Table.new(table, schema, 'id', %Q{CREATE TABLE `#{table}` (random_constraint)})
24+
@table.constraints['user_id'] = {:name => 'random_constraint', :referenced_column => true}
25+
Lhm::Table.schema_constraints(schema, {'random_constraint_lhmn' => true})
26+
27+
@table.destination_ddl.must_equal %Q{CREATE TABLE `#{@table.destination_name}` (random_constraint_lhmn)}
28+
end
29+
end
30+
1831
describe 'constraints' do
1932
def set_columns(table, columns)
2033
table.instance_variable_set('@columns', columns)
2134
end
2235

2336
it 'should be satisfied with a single column primary key called id' do
24-
@table = Lhm::Table.new('table', 'id')
37+
@table = Lhm::Table.new('table', 'default', 'id')
2538
set_columns(@table, { 'id' => { :type => 'int(1)' } })
2639
@table.satisfies_id_column_requirement?.must_equal true
2740
end
2841

2942
it 'should be satisfied with a primary key not called id, as long as there is still an id' do
30-
@table = Lhm::Table.new('table', 'uuid')
43+
@table = Lhm::Table.new('table', 'default', 'uuid')
3144
set_columns(@table, { 'id' => { :type => 'int(1)' } })
3245
@table.satisfies_id_column_requirement?.must_equal true
3346
end
3447

3548
it 'should not be satisfied if id is not numeric' do
36-
@table = Lhm::Table.new('table', 'id')
49+
@table = Lhm::Table.new('table', 'default', 'id')
3750
set_columns(@table, { 'id' => { :type => 'varchar(255)' } })
3851
@table.satisfies_id_column_requirement?.must_equal false
3952
end

0 commit comments

Comments
 (0)