lhm 1.0.0.rc7 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,22 @@
1
+ # 1.0.2 (Febuary 17, 2012)
2
+
3
+ * closes https://github.com/soundcloud/large-hadron-migrator/issues/11
4
+ this critical bug could cause data loss. table parser was replaced with
5
+ an implementation that reads directly from information_schema.
6
+
7
+ # 1.0.1 (Febuary 09, 2012)
8
+
9
+ * released to rubygems
10
+
11
+ # 1.0.0 (Febuary 09, 2012)
12
+
13
+ * added change_column
14
+ * final 1.0 release
15
+
16
+ # 1.0.0.rc8 (Febuary 09, 2012)
17
+
18
+ * removed spec binaries from gem bins
19
+
1
20
  # 1.0.0.rc7 (January 31, 2012)
2
21
 
3
22
  * added SqlHelper.annotation into the middle of trigger statements. this
data/README.md CHANGED
@@ -1,5 +1,7 @@
1
1
  # Large Hadron Migrator [![Build Status](https://secure.travis-ci.org/soundcloud/large-hadron-migrator.png)](http://travis-ci.org/soundcloud/large-hadron-migrator)
2
2
 
3
+ Update: There is currently [An issue](https://github.com/soundcloud/large-hadron-migrator/issues/11) with the migration. Fix coming up.
4
+
3
5
  Rails style database migrations are a useful way to evolve your data schema in
4
6
  an agile manner. Most Rails projects start like this, and at first, making
5
7
  changes is fast and easy.
@@ -21,7 +21,7 @@ echo removing $basedir
21
21
  rm -rf "$basedir"
22
22
 
23
23
  echo setting up cluster
24
- lhm-spec-setup-cluster
24
+ bin/lhm-spec-setup-cluster.sh
25
25
 
26
26
  echo staring instances
27
27
  "$mysqldir"/bin/mysqld --defaults-file="$basedir/master/my.cnf" 2>&1 >$basedir/master/lhm.log &
@@ -29,7 +29,7 @@ echo staring instances
29
29
  sleep 5
30
30
 
31
31
  echo running grants
32
- lhm-spec-grants
32
+ bin/lhm-spec-grants.sh
33
33
 
34
34
  trap lhmkill SIGTERM SIGINT
35
35
 
@@ -17,12 +17,7 @@ Gem::Specification.new do |s|
17
17
  s.files = `git ls-files`.split("\n")
18
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
19
  s.require_paths = ["lib"]
20
- s.executables = [
21
- "lhm-spec-clobber",
22
- "lhm-spec-grants",
23
- "lhm-spec-setup-cluster",
24
- "lhm-kill-queue"
25
- ]
20
+ s.executables = ["lhm-kill-queue"]
26
21
 
27
22
  # this should be a real dependency, but we're using a different gem in our code
28
23
  s.add_development_dependency "mysql", "~> 2.8.1"
@@ -55,6 +55,21 @@ module Lhm
55
55
  ddl("alter table `%s` add column `%s` %s" % [@name, name, definition])
56
56
  end
57
57
 
58
+ # Change an existing column to a new definition
59
+ #
60
+ # @example
61
+ #
62
+ # Lhm.change_table(:users) do |m|
63
+ # m.change_column(:comment, "VARCHAR(12) DEFAULT '0' NOT NULL")
64
+ # end
65
+ #
66
+ # @param [String] name Name of the column to change
67
+ # @param [String] definition Valid SQL column definition
68
+ def change_column(name, definition)
69
+ remove_column(name)
70
+ add_column(name, definition)
71
+ end
72
+
58
73
  # Remove a column from a table
59
74
  #
60
75
  # @example
@@ -1,6 +1,8 @@
1
1
  # Copyright (c) 2011, SoundCloud Ltd., Rany Keddo, Tobias Bielohlawek, Tobias
2
2
  # Schmidt
3
3
 
4
+ require 'lhm/sql_helper'
5
+
4
6
  module Lhm
5
7
  class Table
6
8
  attr_reader :name, :columns, :indices, :pk, :ddl
@@ -22,58 +24,71 @@ module Lhm
22
24
  end
23
25
 
24
26
  def self.parse(table_name, connection)
25
- sql = "show create table `#{ table_name }`"
26
- ddl = connection.execute(sql).fetch_row.last
27
-
28
- Parser.new(ddl).parse
27
+ Parser.new(table_name, connection).parse
29
28
  end
30
29
 
31
30
  class Parser
32
- def initialize(ddl)
33
- @ddl = ddl
34
- end
31
+ include SqlHelper
35
32
 
36
- def lines
37
- @ddl.lines.to_a.map(&:strip).reject(&:empty?)
33
+ def initialize(table_name, connection)
34
+ @table_name = table_name.to_s
35
+ @schema_name = connection.current_database
36
+ @connection = connection
38
37
  end
39
38
 
40
- def create_definitions
41
- lines[1..-2]
39
+ def ddl
40
+ sql = "show create table `#{ @table_name }`"
41
+ @connection.execute(sql).fetch_row.last
42
42
  end
43
43
 
44
44
  def parse
45
- _, name = *lines.first.match("`([^ ]*)`")
46
- pk_line = create_definitions.grep(primary).first
47
-
48
- if pk_line
49
- _, pk = *pk_line.match(primary)
50
- table = Table.new(name, pk, @ddl)
51
-
52
- create_definitions.each do |definition|
53
- case definition
54
- when index
55
- table.indices[$1] = { :metadata => $2 }
56
- when column
57
- table.columns[$1] = { :type => $2, :metadata => $3 }
58
- end
45
+ schema = read_information_schema
46
+
47
+ Table.new(@table_name, extract_primary_key(schema), ddl).tap do |table|
48
+ schema.each do |defn|
49
+ table.columns[defn["COLUMN_NAME"]] = {
50
+ :type => defn["COLUMN_TYPE"],
51
+ :is_nullable => defn["IS_NULLABLE"],
52
+ :column_default => defn["COLUMN_DEFAULT"]
53
+ }
59
54
  end
60
55
 
61
- table
56
+ extract_indices(read_indices).each do |idx, columns|
57
+ table.indices[idx] = columns
58
+ end
62
59
  end
63
60
  end
64
61
 
65
62
  private
66
63
 
67
- def primary
68
- /^PRIMARY KEY (?:USING (?:HASH|[BR]TREE) )?\(`([^ ]*)`\),?$/
64
+ def read_information_schema
65
+ @connection.select_all %Q{
66
+ select *
67
+ from information_schema.columns
68
+ where table_name = "#{ @table_name }"
69
+ and table_schema = "#{ @schema_name }"
70
+ }
69
71
  end
70
72
 
71
- def index
72
- /^(?:UNIQUE )?(?:INDEX|KEY) `([^ ]*)` (.*?),?$/
73
+ def read_indices
74
+ @connection.select_all %Q{
75
+ show indexes from `#{ @schema_name }`.`#{ @table_name }`
76
+ where key_name != "PRIMARY"
77
+ }
78
+ end
79
+
80
+ def extract_indices(indices)
81
+ indices.map { |row| [row["Key_name"], row["Column_name"]] }.
82
+ inject(Hash.new { |h, k| h[k] = []}) do |memo, (idx, column)|
83
+ memo[idx] << column
84
+ memo
85
+ end
73
86
  end
74
87
 
75
- def column
76
- /^`([^ ]*)` ([^ ]*) (.*?),?$/
88
+ def extract_primary_key(schema)
89
+ cols = schema.select { |defn| defn["COLUMN_KEY"] == "PRI" }
90
+ keys = cols.map { |defn| defn["COLUMN_NAME"] }
91
+ keys.length == 1 ? keys.first : keys
77
92
  end
78
93
  end
79
94
  end
@@ -2,5 +2,5 @@
2
2
  # Schmidt
3
3
 
4
4
  module Lhm
5
- VERSION = "1.0.0.rc7"
5
+ VERSION = "1.0.2"
6
6
  end
@@ -16,7 +16,7 @@ directory master and slave databases will get installed into.
16
16
  # setup
17
17
 
18
18
  You can set the integration specs up to run against a master slave setup by
19
- running the included `lhm-spec-clobber` script. this deletes the configured
19
+ running the included `bin/lhm-spec-clobber.sh` script. this deletes the configured
20
20
  lhm master slave setup and reinstalls and configures a master slave setup.
21
21
 
22
22
  Follow the manual instructions if you want more control over this process.
@@ -25,7 +25,7 @@ Follow the manual instructions if you want more control over this process.
25
25
 
26
26
  ## set up instances
27
27
 
28
- lhm-spec-setup-cluster
28
+ bin/lhm-spec-setup-cluster.sh
29
29
 
30
30
  ## start instances
31
31
 
@@ -35,7 +35,7 @@ Follow the manual instructions if you want more control over this process.
35
35
 
36
36
  ## run the grants
37
37
 
38
- lhm-spec-grants
38
+ bin/lhm-spec-grants.sh
39
39
 
40
40
  ## run specs
41
41
 
@@ -5,6 +5,7 @@ CREATE TABLE `users` (
5
5
  `group` varchar(255) DEFAULT NULL,
6
6
  `created_at` datetime DEFAULT NULL,
7
7
  `comment` varchar(20) DEFAULT NULL,
8
+ `description` text,
8
9
  PRIMARY KEY (`id`),
9
10
  UNIQUE KEY `index_users_on_reference` (`reference`),
10
11
  KEY `index_users_on_username_and_created_at` (`username`,`created_at`)
@@ -102,8 +102,12 @@ module IntegrationHelper
102
102
  def key?(table_name, cols, type = :non_unique)
103
103
  non_unique = type == :non_unique ? 1 : 0
104
104
  key_name = Lhm::SqlHelper.idx_name(table_name, cols)
105
- query = "show indexes in #{ table_name } where key_name = '#{ key_name }' and non_unique = #{ non_unique }"
106
- !!select_value(query)
105
+
106
+ !!select_value(%Q<
107
+ show indexes in #{ table_name }
108
+ where key_name = '#{ key_name }'
109
+ and non_unique = #{ non_unique }
110
+ >)
107
111
  end
108
112
 
109
113
  #
@@ -23,7 +23,8 @@ describe Lhm do
23
23
  slave do
24
24
  table_read(:users).columns["logins"].must_equal({
25
25
  :type => "int(12)",
26
- :metadata => "DEFAULT '0'"
26
+ :is_nullable => "YES",
27
+ :column_default => '0'
27
28
  })
28
29
  end
29
30
  end
@@ -98,7 +99,22 @@ describe Lhm do
98
99
  slave do
99
100
  table_read(:users).columns["flag"].must_equal({
100
101
  :type => "tinyint(1)",
101
- :metadata => "DEFAULT NULL"
102
+ :is_nullable => "YES",
103
+ :column_default => nil
104
+ })
105
+ end
106
+ end
107
+
108
+ it "should change a column" do
109
+ Lhm.change_table(:users) do |t|
110
+ t.change_column(:comment, "varchar(20) DEFAULT 'none' NOT NULL")
111
+ end
112
+
113
+ slave do
114
+ table_read(:users).columns["comment"].must_equal({
115
+ :type => "varchar(20)",
116
+ :is_nullable => "NO",
117
+ :column_default => "none"
102
118
  })
103
119
  end
104
120
  end
@@ -0,0 +1,48 @@
1
+ # Copyright (c) 2011, 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
+ require 'lhm/table'
8
+
9
+ describe Lhm::Table do
10
+ include IntegrationHelper
11
+
12
+ describe Lhm::Table::Parser do
13
+ describe "create table parsing" do
14
+ before(:each) do
15
+ connect_master!
16
+ @table = table_create(:users)
17
+ end
18
+
19
+ it "should parse table name in show create table" do
20
+ @table.name.must_equal("users")
21
+ end
22
+
23
+ it "should parse primary key" do
24
+ @table.pk.must_equal("id")
25
+ end
26
+
27
+ it "should parse column type in show create table" do
28
+ @table.columns["username"][:type].must_equal("varchar(255)")
29
+ end
30
+
31
+ it "should parse column metadata" do
32
+ @table.columns["username"][:column_default].must_equal nil
33
+ end
34
+
35
+ it "should parse indices" do
36
+ @table.
37
+ indices["index_users_on_username_and_created_at"].
38
+ must_equal(["username", "created_at"])
39
+ end
40
+
41
+ it "should parse index" do
42
+ @table.
43
+ indices["index_users_on_reference"].
44
+ must_equal(["reference"])
45
+ end
46
+ end
47
+ end
48
+ end
@@ -64,6 +64,15 @@ describe Lhm::Migrator do
64
64
  "alter table `lhmn_alt` drop `logins`"
65
65
  ])
66
66
  end
67
+
68
+ it "should change a column" do
69
+ @creator.change_column("logins", "INT(255)")
70
+
71
+ @creator.statements.must_equal([
72
+ "alter table `lhmn_alt` drop `logins`",
73
+ "alter table `lhmn_alt` add column `logins` INT(255)"
74
+ ])
75
+ end
67
76
  end
68
77
 
69
78
  describe "direct changes" do
@@ -9,11 +9,8 @@ describe Lhm::Table do
9
9
  include UnitHelper
10
10
 
11
11
  describe "names" do
12
- before(:each) do
13
- @table = Lhm::Table::Parser.new(fixture("users.ddl")).parse
14
- end
15
-
16
12
  it "should name destination" do
13
+ @table = Lhm::Table.new("users")
17
14
  @table.destination_name.must_equal "lhmn_users"
18
15
  end
19
16
  end
@@ -34,40 +31,4 @@ describe Lhm::Table do
34
31
  @table.satisfies_primary_key?.must_equal false
35
32
  end
36
33
  end
37
-
38
- describe Lhm::Table::Parser do
39
- describe "create table parsing" do
40
- before(:each) do
41
- @table = Lhm::Table::Parser.new(fixture("users.ddl")).parse
42
- end
43
-
44
- it "should parse table name in show create table" do
45
- @table.name.must_equal("users")
46
- end
47
-
48
- it "should parse primary key" do
49
- @table.pk.must_equal("id")
50
- end
51
-
52
- it "should parse column type in show create table" do
53
- @table.columns["username"][:type].must_equal("varchar(255)")
54
- end
55
-
56
- it "should parse column metadata" do
57
- @table.columns["username"][:metadata].must_equal("DEFAULT NULL")
58
- end
59
-
60
- it "should parse indices in show create table" do
61
- @table.
62
- indices["index_users_on_username_and_created_at"][:metadata].
63
- must_equal("(`username`,`created_at`)")
64
- end
65
-
66
- it "should parse indices in show create table" do
67
- @table.
68
- indices["index_users_on_reference"][:metadata].
69
- must_equal("(`reference`)")
70
- end
71
- end
72
- end
73
34
  end
metadata CHANGED
@@ -1,13 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lhm
3
3
  version: !ruby/object:Gem::Version
4
- prerelease: true
5
- segments:
6
- - 1
7
- - 0
8
- - 0
9
- - rc7
10
- version: 1.0.0.rc7
4
+ prerelease:
5
+ version: 1.0.2
11
6
  platform: ruby
12
7
  authors:
13
8
  - SoundCloud
@@ -18,8 +13,7 @@ autorequire:
18
13
  bindir: bin
19
14
  cert_chain: []
20
15
 
21
- date: 2012-01-31 00:00:00 +01:00
22
- default_executable:
16
+ date: 2012-02-17 00:00:00 Z
23
17
  dependencies:
24
18
  - !ruby/object:Gem::Dependency
25
19
  name: mysql
@@ -29,10 +23,6 @@ dependencies:
29
23
  requirements:
30
24
  - - ~>
31
25
  - !ruby/object:Gem::Version
32
- segments:
33
- - 2
34
- - 8
35
- - 1
36
26
  version: 2.8.1
37
27
  type: :development
38
28
  version_requirements: *id001
@@ -44,10 +34,6 @@ dependencies:
44
34
  requirements:
45
35
  - - "="
46
36
  - !ruby/object:Gem::Version
47
- segments:
48
- - 2
49
- - 10
50
- - 0
51
37
  version: 2.10.0
52
38
  type: :development
53
39
  version_requirements: *id002
@@ -59,8 +45,6 @@ dependencies:
59
45
  requirements:
60
46
  - - ">="
61
47
  - !ruby/object:Gem::Version
62
- segments:
63
- - 0
64
48
  version: "0"
65
49
  type: :development
66
50
  version_requirements: *id003
@@ -72,17 +56,12 @@ dependencies:
72
56
  requirements:
73
57
  - - ">="
74
58
  - !ruby/object:Gem::Version
75
- segments:
76
- - 0
77
59
  version: "0"
78
60
  type: :runtime
79
61
  version_requirements: *id004
80
62
  description: Migrate large tables without downtime by copying to a temporary table in chunks. The old table is not dropped. Instead, it is moved to timestamp_table_name for verification.
81
63
  email: rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com
82
64
  executables:
83
- - lhm-spec-clobber
84
- - lhm-spec-grants
85
- - lhm-spec-setup-cluster
86
65
  - lhm-kill-queue
87
66
  extensions: []
88
67
 
@@ -96,11 +75,8 @@ files:
96
75
  - README.md
97
76
  - Rakefile
98
77
  - bin/lhm-kill-queue
99
- - bin/lhm-spec-clobber
100
78
  - bin/lhm-spec-clobber.sh
101
- - bin/lhm-spec-grants
102
79
  - bin/lhm-spec-grants.sh
103
- - bin/lhm-spec-setup-cluster
104
80
  - bin/lhm-spec-setup-cluster.sh
105
81
  - gemfiles/ar-2.3.gemfile
106
82
  - gemfiles/ar-3.1.gemfile
@@ -128,6 +104,7 @@ files:
128
104
  - spec/integration/integration_helper.rb
129
105
  - spec/integration/lhm_spec.rb
130
106
  - spec/integration/locked_switcher_spec.rb
107
+ - spec/integration/table_spec.rb
131
108
  - spec/unit/chunker_spec.rb
132
109
  - spec/unit/entangler_spec.rb
133
110
  - spec/unit/intersection_spec.rb
@@ -137,7 +114,6 @@ files:
137
114
  - spec/unit/sql_helper_spec.rb
138
115
  - spec/unit/table_spec.rb
139
116
  - spec/unit/unit_helper.rb
140
- has_rdoc: true
141
117
  homepage: http://github.com/soundcloud/large-hadron-migrator
142
118
  licenses: []
143
119
 
@@ -151,23 +127,17 @@ required_ruby_version: !ruby/object:Gem::Requirement
151
127
  requirements:
152
128
  - - ">="
153
129
  - !ruby/object:Gem::Version
154
- segments:
155
- - 0
156
130
  version: "0"
157
131
  required_rubygems_version: !ruby/object:Gem::Requirement
158
132
  none: false
159
133
  requirements:
160
- - - ">"
134
+ - - ">="
161
135
  - !ruby/object:Gem::Version
162
- segments:
163
- - 1
164
- - 3
165
- - 1
166
- version: 1.3.1
136
+ version: "0"
167
137
  requirements: []
168
138
 
169
139
  rubyforge_project:
170
- rubygems_version: 1.3.7
140
+ rubygems_version: 1.8.15
171
141
  signing_key:
172
142
  specification_version: 3
173
143
  summary: online schema changer for mysql
@@ -182,6 +152,7 @@ test_files:
182
152
  - spec/integration/integration_helper.rb
183
153
  - spec/integration/lhm_spec.rb
184
154
  - spec/integration/locked_switcher_spec.rb
155
+ - spec/integration/table_spec.rb
185
156
  - spec/unit/chunker_spec.rb
186
157
  - spec/unit/entangler_spec.rb
187
158
  - spec/unit/intersection_spec.rb
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'open3'
4
-
5
- bindir = File.dirname(File.expand_path(__FILE__))
6
- script = File.join(bindir, File.basename($0) + ".sh 2>&1")
7
-
8
- Open3.popen3(script) do |input, output|
9
- output.each { |line| print(line) }
10
- end
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'open3'
4
-
5
- bindir = File.dirname(File.expand_path(__FILE__))
6
- script = File.join(bindir, File.basename($0) + ".sh 2>&1")
7
-
8
- Open3.popen3(script) do |input, output|
9
- output.each { |line| print(line) }
10
- end
@@ -1,10 +0,0 @@
1
- #!/usr/bin/env ruby
2
-
3
- require 'open3'
4
-
5
- bindir = File.dirname(File.expand_path(__FILE__))
6
- script = File.join(bindir, File.basename($0) + ".sh 2>&1")
7
-
8
- Open3.popen3(script) do |input, output|
9
- output.each { |line| print(line) }
10
- end