Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for multiple outbound foreign keys constraints #77

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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