lhm 1.0.0.rc7 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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