diff --git a/lib/lhm/connection.rb b/lib/lhm/connection.rb index b9b607df..ad3d8e49 100644 --- a/lib/lhm/connection.rb +++ b/lib/lhm/connection.rb @@ -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) @@ -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) diff --git a/lib/lhm/table.rb b/lib/lhm/table.rb index 1f7ed377..abc2bd38 100644 --- a/lib/lhm/table.rb +++ b/lib/lhm/table.rb @@ -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 @@ -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 @@ -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") @@ -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 @@ -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") @@ -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 diff --git a/spec/fixtures/fk_example.ddl b/spec/fixtures/fk_example.ddl new file mode 100644 index 00000000..01880a27 --- /dev/null +++ b/spec/fixtures/fk_example.ddl @@ -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 diff --git a/spec/fixtures/fk_example_non_sequential.ddl b/spec/fixtures/fk_example_non_sequential.ddl new file mode 100644 index 00000000..b61db2a0 --- /dev/null +++ b/spec/fixtures/fk_example_non_sequential.ddl @@ -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 diff --git a/spec/integration/foreign_keys_spec.rb b/spec/integration/foreign_keys_spec.rb new file mode 100644 index 00000000..c5d43070 --- /dev/null +++ b/spec/integration/foreign_keys_spec.rb @@ -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 diff --git a/spec/integration/integration_helper.rb b/spec/integration/integration_helper.rb index d489e3fc..a88c6e8b 100644 --- a/spec/integration/integration_helper.rb +++ b/spec/integration/integration_helper.rb @@ -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 # diff --git a/spec/integration/table_spec.rb b/spec/integration/table_spec.rb index 58905f88..ea1167ae 100644 --- a/spec/integration/table_spec.rb +++ b/spec/integration/table_spec.rb @@ -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 diff --git a/spec/unit/table_spec.rb b/spec/unit/table_spec.rb index 285a4bc2..3974be61 100644 --- a/spec/unit/table_spec.rb +++ b/spec/unit/table_spec.rb @@ -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