Skip to content

Commit

Permalink
Add support for tables with multiple outbound foreign key constraints
Browse files Browse the repository at this point in the history
  • Loading branch information
Romain Pomier authored and pomier committed Oct 27, 2015
1 parent 8d6e8fd commit 491d563
Show file tree
Hide file tree
Showing 8 changed files with 232 additions and 9 deletions.
4 changes: 1 addition & 3 deletions lib/lhm/migrator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -207,9 +207,7 @@ def execute
end

def destination_create
original = %{CREATE TABLE `#{ @origin.name }`}
replacement = %{CREATE TABLE `#{ @origin.destination_name }`}
stmt = @origin.ddl.gsub(original, replacement)
stmt = @origin.destination_ddl
@connection.execute(tagged(stmt))
end

Expand Down
91 changes: 88 additions & 3 deletions lib/lhm/table.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@

module Lhm
class Table
attr_reader :name, :columns, :indices, :pk, :ddl
attr_reader :schema, :name, :columns, :indices, :constraints, :pk, :ddl

def initialize(name, pk = 'id', ddl = nil)
def initialize(name, schema = 'default', pk = 'id', ddl = nil)
@name = name
@schema = schema
@columns = {}
@indices = {}
@constraints = {}
@pk = pk
@ddl = ddl
end
Expand All @@ -28,6 +30,32 @@ def self.parse(table_name, connection)
Parser.new(table_name, connection).parse
end

def destination_ddl
original = %r{CREATE TABLE ("|`)#{ name }\1}
repl = '\1'
replacement = %Q{CREATE TABLE #{ repl }#{ destination_name }#{ repl }}

dest = ddl
dest.gsub!(original, replacement)

foreign_keys = constraints.select { |col, c| !c[:referenced_column].nil? }

foreign_keys.keys.each_with_index do |key, i|
original = foreign_keys[key][:name]
replacement = replacement_constraint(original)
dest.gsub!(original, replacement)
end

dest
end

@@schema_constraints = {}

def self.schema_constraints(schema, value = nil)
@@schema_constraints[schema] = value if value
@@schema_constraints[schema]
end

class Parser
include SqlHelper

Expand All @@ -47,7 +75,7 @@ def ddl
def parse
schema = read_information_schema

Table.new(@table_name, extract_primary_key(schema), ddl).tap do |table|
Table.new(@table_name, @schema_name, extract_primary_key(schema), ddl).tap do |table|
schema.each do |defn|
column_name = struct_key(defn, 'COLUMN_NAME')
column_type = struct_key(defn, 'COLUMN_TYPE')
Expand All @@ -64,6 +92,15 @@ def parse
extract_indices(read_indices).each do |idx, columns|
table.indices[idx] = columns
end

constraints = {}
extract_constraints(read_constraints(nil)).each do |data|
if data[:schema] == @schema_name && data[:table] == @table_name
table.constraints[data[:column]] = data
end
constraints[data[:name]] = data
end
Table.schema_constraints(@schema_name, constraints)
end
end

Expand Down Expand Up @@ -98,6 +135,47 @@ def extract_indices(indices)
end
end

def read_constraints(table = @table_name)
query = %Q{
select *
from information_schema.key_column_usage
where table_schema = '#{ @schema_name }'
and referenced_column_name is not null
}
query += %Q{
and table_name = '#{ @table_name }'
} if table

@connection.select_all(query)
end

def extract_constraints(constraints)
columns = %w{
CONSTRAINT_NAME
TABLE_SCHEMA
TABLE_NAME
COLUMN_NAME
ORDINAL_POSITION
POSITION_IN_UNIQUE_CONSTRAINT
REFERENCED_TABLE_SCHEMA
REFERENCED_TABLE_NAME
REFERENCED_COLUMN_NAME
}

constraints.map do |row|
result = {}
columns.each do |c|
sym = c.dup
# The order of these substitutions is important
sym.gsub!(/CONSTRAINT_/, '')
sym.gsub!(/_NAME/, '')
sym.gsub!(/TABLE_/, '')
result[sym.downcase.to_sym] = row[struct_key(row, c)]
end
result
end
end

def extract_primary_key(schema)
cols = schema.select do |defn|
column_key = struct_key(defn, 'COLUMN_KEY')
Expand All @@ -112,5 +190,12 @@ def extract_primary_key(schema)
keys.length == 1 ? keys.first : keys
end
end

private

def replacement_constraint(name)
(name =~ /_lhmn$/).nil? ? "#{name}_lhmn" : name.gsub(/_lhmn$/, '')
end

end
end
8 changes: 8 additions & 0 deletions spec/fixtures/fk_example.ddl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE `fk_example` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`master_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_example_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `fk_example_ibfk_2` FOREIGN KEY (`master_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
8 changes: 8 additions & 0 deletions spec/fixtures/fk_example_second_pass.ddl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
CREATE TABLE `fk_example_second_pass` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
`master_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_example_ibfk_1_lhmn` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
CONSTRAINT `fk_example_ibfk_2_lhmn` FOREIGN KEY (`master_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
89 changes: 89 additions & 0 deletions spec/integration/foreign_keys_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright (c) 2011 - 2013, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
# Schmidt

require File.expand_path(File.dirname(__FILE__)) + '/integration_helper'

require 'lhm'

describe Lhm do
include IntegrationHelper

before(:each) do
connect_master!
Lhm.cleanup(true)
%w(fk_example fk_example_second_pass).each do |table|
execute "drop table if exists #{table}"
end
table_create(:users)
end

describe 'the simplest case' do
before(:each) do
table_create(:fk_example)
end

after(:each) do
execute 'drop table if exists fk_example'
Lhm.cleanup(true)
end

it 'should handle tables with foreign keys by appending the suffix' do
Lhm.change_table(:fk_example) do |t|
t.add_column(:new_column, "INT(12) DEFAULT '0'")
end

slave do
actual = table_read(:fk_example).constraints['user_id']
expected = {
name: 'fk_example_ibfk_1_lhmn',
referenced_table: 'users',
referenced_column: 'id',
}
hash_slice(actual, expected.keys).must_equal(expected)

actual = table_read(:fk_example).constraints['master_id']
expected = {
name: 'fk_example_ibfk_2_lhmn',
referenced_table: 'users',
referenced_column: 'id',
}
hash_slice(actual, expected.keys).must_equal(expected)
end
end
end

describe 'manage a new migration by removing the suffix' do
before(:each) do
table_create(:fk_example_second_pass)
end

after(:each) do
execute 'drop table if exists fk_example_second_pass'
Lhm.cleanup(true)
end

it 'should be able to create this table' do
Lhm.change_table(:fk_example_second_pass) do |t|
t.add_column(:new_column, "INT(12) DEFAULT '0'")
end

slave do
actual = table_read(:fk_example_second_pass).constraints['user_id']
expected = {
name: 'fk_example_ibfk_1',
referenced_table: 'users',
referenced_column: 'id',
}
hash_slice(actual, expected.keys).must_equal(expected)

actual = table_read(:fk_example_second_pass).constraints['master_id']
expected = {
name: 'fk_example_ibfk_2',
referenced_table: 'users',
referenced_column: 'id',
}
hash_slice(actual, expected.keys).must_equal(expected)
end
end
end
end
5 changes: 5 additions & 0 deletions spec/integration/integration_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,11 @@ def table_exists?(table)
connection.table_exists?(table.name)
end

def hash_slice(hash, keys)
return hash.slice(*keys) if hash.respond_to?(:slice)
keys.each { |k| [k, hash[k]] }.to_h
end

#
# Database Helpers
#
Expand Down
17 changes: 17 additions & 0 deletions spec/integration/table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,23 @@
indices['index_users_on_reference'].
must_equal(['reference'])
end

it 'should parse constraints' do
begin
@table = table_create(:fk_example)
@table.constraints.keys.must_equal %w(user_id master_id)

expected = {
name: 'fk_example_ibfk_1',
referenced_table: 'users',
referenced_column: 'id'
}

hash_slice(@table.constraints['user_id'], expected.keys).must_equal expected
ensure
execute 'drop table if exists fk_example'
end
end
end
end
end
19 changes: 16 additions & 3 deletions spec/unit/table_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,38 @@
end
end

describe 'ddl' do
it 'should build the destination table' do
table = 'users'
schema = 'default'

@table = Lhm::Table.new(table, schema, 'id', %Q{CREATE TABLE `#{table}` (random_constraint)})
@table.constraints['user_id'] = {:name => 'random_constraint', :referenced_column => true}
Lhm::Table.schema_constraints(schema, {'random_constraint_lhmn' => true})

@table.destination_ddl.must_equal %Q{CREATE TABLE `#{@table.destination_name}` (random_constraint_lhmn)}
end
end

describe 'constraints' do
def set_columns(table, columns)
table.instance_variable_set('@columns', columns)
end

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

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

it 'should not be satisfied if id is not numeric' do
@table = Lhm::Table.new('table', 'id')
@table = Lhm::Table.new('table', 'default', 'id')
set_columns(@table, { 'id' => { :type => 'varchar(255)' } })
@table.satisfies_id_column_requirement?.must_equal false
end
Expand Down

0 comments on commit 491d563

Please sign in to comment.