Skip to content
Closed
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
10 changes: 2 additions & 8 deletions lib/lhm/connection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -59,10 +59,7 @@ def select_value(sql)
end

def destination_create(origin)
original = %{CREATE TABLE "#{ origin.name }"}
replacement = %{CREATE TABLE "#{ origin.destination_name }"}

sql(origin.ddl.gsub(original, replacement))
sql(origin.destination_ddl)
end

def execute(sql)
Expand Down Expand Up @@ -125,10 +122,7 @@ def select_value(sql)
end

def destination_create(origin)
original = %{CREATE TABLE `#{ origin.name }`}
replacement = %{CREATE TABLE `#{ origin.destination_name }`}

sql(origin.ddl.gsub(original, replacement))
sql(origin.destination_ddl)
end

def execute(sql)
Expand Down
106 changes: 103 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 @@ -27,6 +29,33 @@ def self.parse(table_name, connection)
Parser.new(table_name, connection).parse
end

def destination_ddl
original = %r{CREATE TABLE ("|`)#{ name }\1}
# Strange substitutions are happening when I put this in the string directly
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 @@ -43,7 +72,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 @@ -59,6 +88,18 @@ 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 @@ -93,6 +134,48 @@ 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 @@ -107,5 +190,22 @@ def extract_primary_key(schema)
keys.length == 1 ? keys.first : keys
end
end

private

def replacement_constraint(name)
existing = Table.schema_constraints(@schema)

seq = 1
name = name.dup

begin
name.sub!(/(_\d+)?$/, "_#{seq}")
seq += 1
end while existing.has_key?(name)

return name
end

end
end
6 changes: 6 additions & 0 deletions spec/fixtures/fk_example.ddl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE `fk_example` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_example_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
6 changes: 6 additions & 0 deletions spec/fixtures/fk_example_non_sequential.ddl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE `fk_example_non_sequential` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`user_id` int(11) NOT NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk_example_ibfk_2` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
73 changes: 73 additions & 0 deletions spec/integration/foreign_keys_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# 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) { connect_master! }

before(:each) do
# Be absolutely sure none of these exist yet
Lhm.cleanup(true)
%w{fk_example fk_example_non_sequential}.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
# Clean it up since it could cause trouble
execute 'drop table if exists fk_example'
Lhm.cleanup(true)
end
it 'should handle tables with foriegn keys' 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_2',
referenced_table: 'users',
referenced_column: 'id'
}
hash_slice(actual, expected.keys).must_equal(expected)
end
end
end

describe 'the foreign key sequence number is not 1' do
before(:each) do
table_create(:fk_example_non_sequential)
end

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

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


end
10 changes: 10 additions & 0 deletions spec/integration/integration_helper.rb
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,16 @@ def table_exists?(table)
connection.table_exists?(table.name)
end


def hash_slice(hash, keys)
if hash.respond_to?(:slice)
hash.slice(*keys)
else
check = {}
keys.each {|k| check[k] = hash[k]}
check
end
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 @@ -43,6 +43,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}

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,19 +15,32 @@
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_1' => true})

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

describe "constraints" do
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")
@table.satisfies_primary_key?.must_equal true
end

it "should not be satisfied with a primary key unless called id" do
@table = Lhm::Table.new("table", "uuid")
@table = Lhm::Table.new("table", "default", "uuid")
@table.satisfies_primary_key?.must_equal false
end

it "should not be satisfied with multicolumn primary key" do
@table = Lhm::Table.new("table", ["id", "secondary"])
@table = Lhm::Table.new("table", "default", ["id", "secondary"])
@table.satisfies_primary_key?.must_equal false
end
end
Expand Down