lhm 2.1.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +5 -13
  2. data/.rubocop.yml +256 -0
  3. data/.travis.yml +2 -3
  4. data/CHANGELOG.md +21 -0
  5. data/README.md +30 -3
  6. data/Rakefile +2 -2
  7. data/bin/lhm-config.sh +7 -0
  8. data/bin/lhm-kill-queue +13 -15
  9. data/bin/lhm-spec-clobber.sh +1 -1
  10. data/bin/lhm-spec-grants.sh +2 -2
  11. data/bin/lhm-spec-setup-cluster.sh +1 -1
  12. data/gemfiles/ar-2.3_mysql.gemfile +1 -0
  13. data/lhm.gemspec +6 -6
  14. data/lib/lhm.rb +20 -8
  15. data/lib/lhm/atomic_switcher.rb +2 -1
  16. data/lib/lhm/chunker.rb +19 -19
  17. data/lib/lhm/command.rb +1 -1
  18. data/lib/lhm/connection.rb +12 -0
  19. data/lib/lhm/entangler.rb +5 -5
  20. data/lib/lhm/intersection.rb +29 -16
  21. data/lib/lhm/invoker.rb +3 -3
  22. data/lib/lhm/locked_switcher.rb +6 -6
  23. data/lib/lhm/migration.rb +5 -4
  24. data/lib/lhm/migrator.rb +39 -10
  25. data/lib/lhm/printer.rb +4 -6
  26. data/lib/lhm/sql_helper.rb +4 -4
  27. data/lib/lhm/table.rb +12 -12
  28. data/lib/lhm/version.rb +1 -1
  29. data/spec/fixtures/users.ddl +1 -1
  30. data/spec/integration/atomic_switcher_spec.rb +7 -7
  31. data/spec/integration/chunker_spec.rb +2 -2
  32. data/spec/integration/cleanup_spec.rb +34 -10
  33. data/spec/integration/entangler_spec.rb +11 -11
  34. data/spec/integration/integration_helper.rb +10 -3
  35. data/spec/integration/lhm_spec.rb +96 -46
  36. data/spec/integration/locked_switcher_spec.rb +7 -7
  37. data/spec/integration/table_spec.rb +15 -15
  38. data/spec/test_helper.rb +4 -4
  39. data/spec/unit/atomic_switcher_spec.rb +6 -6
  40. data/spec/unit/chunker_spec.rb +22 -9
  41. data/spec/unit/entangler_spec.rb +19 -19
  42. data/spec/unit/intersection_spec.rb +27 -15
  43. data/spec/unit/lhm_spec.rb +6 -6
  44. data/spec/unit/locked_switcher_spec.rb +14 -14
  45. data/spec/unit/migration_spec.rb +6 -6
  46. data/spec/unit/migrator_spec.rb +53 -41
  47. data/spec/unit/printer_spec.rb +7 -7
  48. data/spec/unit/sql_helper_spec.rb +10 -10
  49. data/spec/unit/table_spec.rb +11 -11
  50. data/spec/unit/throttler_spec.rb +12 -12
  51. metadata +12 -10
@@ -3,7 +3,7 @@
3
3
  set -e
4
4
  set -u
5
5
 
6
- source ~/.lhm
6
+ source `dirname $0`/lhm-config.sh
7
7
 
8
8
  lhmkill() {
9
9
  echo killing lhm-cluster
@@ -1,6 +1,6 @@
1
1
  #!/bin/sh
2
2
 
3
- source ~/.lhm
3
+ source `dirname $0`/lhm-config.sh
4
4
 
5
5
  master() { "$mysqldir"/bin/mysql --protocol=TCP -P $master_port -uroot; }
6
6
  slave() { "$mysqldir"/bin/mysql --protocol=TCP -P $slave_port -uroot; }
@@ -12,7 +12,7 @@ echo "grant replication slave on *.* to 'slave'@'localhost'" | master
12
12
 
13
13
  # set up slave
14
14
 
15
- echo "change master to master_user = 'slave', master_password = 'slave', master_port = 3306, master_host = 'localhost'" | slave
15
+ echo "change master to master_user = 'slave', master_password = 'slave', master_port = $master_port, master_host = 'localhost'" | slave
16
16
  echo "start slave" | slave
17
17
  echo "show slave status \G" | slave
18
18
 
@@ -7,7 +7,7 @@
7
7
  set -e
8
8
  set -u
9
9
 
10
- source ~/.lhm
10
+ source `dirname $0`/lhm-config.sh
11
11
 
12
12
  #
13
13
  # Main
@@ -2,4 +2,5 @@ source "https://rubygems.org"
2
2
 
3
3
  gem "mysql", "~> 2.8.1"
4
4
  gem "activerecord", "~> 2.3.14"
5
+ gem "iconv", "~> 1.0.4"
5
6
  gemspec :path=>"../"
@@ -6,19 +6,19 @@ $:.unshift(lib) unless $:.include?(lib)
6
6
  require 'lhm/version'
7
7
 
8
8
  Gem::Specification.new do |s|
9
- s.name = "lhm"
9
+ s.name = 'lhm'
10
10
  s.version = Lhm::VERSION
11
11
  s.platform = Gem::Platform::RUBY
12
- s.authors = ["SoundCloud", "Rany Keddo", "Tobias Bielohlawek", "Tobias Schmidt"]
12
+ s.authors = ['SoundCloud', 'Rany Keddo', 'Tobias Bielohlawek', 'Tobias Schmidt']
13
13
  s.email = %q{rany@soundcloud.com, tobi@soundcloud.com, ts@soundcloud.com}
14
14
  s.summary = %q{online schema changer for mysql}
15
15
  s.description = %q{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.}
16
16
  s.homepage = %q{http://github.com/soundcloud/lhm}
17
17
  s.files = `git ls-files`.split("\n")
18
18
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
- s.require_paths = ["lib"]
20
- s.executables = ["lhm-kill-queue"]
19
+ s.require_paths = ['lib']
20
+ s.executables = ['lhm-kill-queue']
21
21
 
22
- s.add_development_dependency "minitest", "~> 5.0.8"
23
- s.add_development_dependency "rake"
22
+ s.add_development_dependency 'minitest', '~> 5.0.8'
23
+ s.add_development_dependency 'rake'
24
24
  end
data/lib/lhm.rb CHANGED
@@ -51,9 +51,22 @@ module Lhm
51
51
  true
52
52
  end
53
53
 
54
- def cleanup(run = false)
55
- lhm_tables = connection.select_values("show tables").select { |name| name =~ /^lhm(a|n)_/ }
56
- lhm_triggers = connection.select_values("show triggers").collect do |trigger|
54
+ # Cleanup tables and triggers
55
+ #
56
+ # @param [Boolean] run execute now or just display information
57
+ # @param [Hash] options Optional options to alter cleanup behaviour
58
+ # @option options [Time] :until
59
+ # Filter to only remove tables up to specified time (defaults to: nil)
60
+ def cleanup(run = false, options = {})
61
+ lhm_tables = connection.select_values('show tables').select { |name| name =~ /^lhm(a|n)_/ }
62
+ if options[:until]
63
+ lhm_tables.select! { |table|
64
+ table_date_time = Time.strptime(table, 'lhma_%Y_%m_%d_%H_%M_%S')
65
+ table_date_time <= options[:until]
66
+ }
67
+ end
68
+
69
+ lhm_triggers = connection.select_values('show triggers').collect do |trigger|
57
70
  trigger.respond_to?(:trigger) ? trigger.trigger : trigger
58
71
  end.select { |name| name =~ /^lhmt/ }
59
72
 
@@ -66,12 +79,12 @@ module Lhm
66
79
  end
67
80
  true
68
81
  elsif lhm_tables.empty? && lhm_triggers.empty?
69
- puts "Everything is clean. Nothing to do."
82
+ puts 'Everything is clean. Nothing to do.'
70
83
  true
71
84
  else
72
- puts "Existing LHM backup tables: #{lhm_tables.join(", ")}."
73
- puts "Existing LHM triggers: #{lhm_triggers.join(", ")}."
74
- puts "Run Lhm.cleanup(true) to drop them all."
85
+ puts "Existing LHM backup tables: #{lhm_tables.join(', ')}."
86
+ puts "Existing LHM triggers: #{lhm_triggers.join(', ')}."
87
+ puts 'Run Lhm.cleanup(true) to drop them all.'
75
88
  false
76
89
  end
77
90
  end
@@ -107,5 +120,4 @@ module Lhm
107
120
  def connection
108
121
  Connection.new(adapter)
109
122
  end
110
-
111
123
  end
@@ -29,7 +29,7 @@ module Lhm
29
29
 
30
30
  def atomic_switch
31
31
  [
32
- "rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " +
32
+ "rename table `#{ @origin.name }` to `#{ @migration.archive_name }`, " \
33
33
  "`#{ @destination.name }` to `#{ @origin.name }`"
34
34
  ]
35
35
  end
@@ -42,6 +42,7 @@ module Lhm
42
42
  end
43
43
 
44
44
  private
45
+
45
46
  def execute
46
47
  @connection.sql(statements)
47
48
  end
@@ -25,7 +25,7 @@ module Lhm
25
25
  def execute
26
26
  return unless @start && @limit
27
27
  @next_to_insert = @start
28
- until @next_to_insert >= @limit
28
+ while @next_to_insert < @limit || (@next_to_insert == 1 && @start == 1)
29
29
  stride = @throttler.stride
30
30
  affected_rows = @connection.update(copy(bottom, top(stride)))
31
31
 
@@ -50,8 +50,8 @@ module Lhm
50
50
  end
51
51
 
52
52
  def copy(lowest, highest)
53
- "insert ignore into `#{ destination_name }` (#{ columns }) " +
54
- "select #{ select_columns } from `#{ origin_name }` " +
53
+ "insert ignore into `#{ destination_name }` (#{ destination_columns }) " \
54
+ "select #{ origin_columns } from `#{ origin_name }` " \
55
55
  "#{ conditions } `#{ origin_name }`.`id` between #{ lowest } and #{ highest }"
56
56
  end
57
57
 
@@ -65,22 +65,22 @@ module Lhm
65
65
  limit ? limit.to_i : nil
66
66
  end
67
67
 
68
- #XXX this is extremely brittle and doesn't work when filter contains more
69
- #than one SQL clause, e.g. "where ... group by foo". Before making any
70
- #more changes here, please consider either:
68
+ # XXX this is extremely brittle and doesn't work when filter contains more
69
+ # than one SQL clause, e.g. "where ... group by foo". Before making any
70
+ # more changes here, please consider either:
71
71
  #
72
- #1. Letting users only specify part of defined clauses (i.e. don't allow
73
- #`filter` on Migrator to accept both WHERE and INNER JOIN
74
- #2. Changing query building so that it uses structured data rather than
75
- #strings until the last possible moment.
72
+ # 1. Letting users only specify part of defined clauses (i.e. don't allow
73
+ # `filter` on Migrator to accept both WHERE and INNER JOIN
74
+ # 2. Changing query building so that it uses structured data rather than
75
+ # strings until the last possible moment.
76
76
  def conditions
77
77
  if @migration.conditions
78
78
  @migration.conditions.
79
- sub(/\)\Z/, "").
80
- #put any where conditions in parens
81
- sub(/where\s(\w.*)\Z/, "where (\\1)") + " and"
79
+ sub(/\)\Z/, '').
80
+ # put any where conditions in parens
81
+ sub(/where\s(\w.*)\Z/, 'where (\\1)') + ' and'
82
82
  else
83
- "where"
83
+ 'where'
84
84
  end
85
85
  end
86
86
 
@@ -92,17 +92,17 @@ module Lhm
92
92
  @migration.origin.name
93
93
  end
94
94
 
95
- def columns
96
- @columns ||= @migration.intersection.joined
95
+ def origin_columns
96
+ @origin_columns ||= @migration.intersection.origin.typed(origin_name)
97
97
  end
98
98
 
99
- def select_columns
100
- @select_columns ||= @migration.intersection.typed("`#{origin_name}`")
99
+ def destination_columns
100
+ @destination_columns ||= @migration.intersection.destination.joined
101
101
  end
102
102
 
103
103
  def validate
104
104
  if @start && @limit && @start > @limit
105
- error("impossible chunk options (limit must be greater than start)")
105
+ error('impossible chunk options (limit must be greater than start)')
106
106
  end
107
107
  end
108
108
  end
@@ -10,7 +10,7 @@ module Lhm
10
10
  Lhm.logger.info "Starting run of class=#{self.class}"
11
11
  validate
12
12
 
13
- if(block_given?)
13
+ if block_given?
14
14
  before
15
15
  block.call(self)
16
16
  after
@@ -77,6 +77,14 @@ module Lhm
77
77
  and table_name = '#{ table_name }'
78
78
  })
79
79
  end
80
+
81
+ def quote_value(value)
82
+ quoter.quote_value(value)
83
+ end
84
+
85
+ def quoter
86
+ @quoter ||= Object.new.tap { |o| o.extend(DataObjects::Quoting) }
87
+ end
80
88
  end
81
89
 
82
90
  class ActiveRecordConnection
@@ -138,6 +146,10 @@ module Lhm
138
146
  def table_exists?(table_name)
139
147
  @adapter.table_exists?(table_name)
140
148
  end
149
+
150
+ def quote_value(value)
151
+ @adapter.quote(value)
152
+ end
141
153
  end
142
154
  end
143
155
  end
@@ -14,7 +14,7 @@ module Lhm
14
14
  # Creates entanglement between two tables. All creates, updates and deletes
15
15
  # to origin will be repeated on the destination table.
16
16
  def initialize(migration, connection = nil)
17
- @common = migration.intersection
17
+ @intersection = migration.intersection
18
18
  @origin = migration.origin
19
19
  @destination = migration.destination
20
20
  @connection = connection
@@ -40,8 +40,8 @@ module Lhm
40
40
  strip %Q{
41
41
  create trigger `#{ trigger(:ins) }`
42
42
  after insert on `#{ @origin.name }` for each row
43
- replace into `#{ @destination.name }` (#{ @common.joined }) #{ SqlHelper.annotation }
44
- values (#{ @common.typed("NEW") })
43
+ replace into `#{ @destination.name }` (#{ @intersection.destination.joined }) #{ SqlHelper.annotation }
44
+ values (#{ @intersection.origin.typed('NEW') })
45
45
  }
46
46
  end
47
47
 
@@ -49,8 +49,8 @@ module Lhm
49
49
  strip %Q{
50
50
  create trigger `#{ trigger(:upd) }`
51
51
  after update on `#{ @origin.name }` for each row
52
- replace into `#{ @destination.name }` (#{ @common.joined }) #{ SqlHelper.annotation }
53
- values (#{ @common.typed("NEW") })
52
+ replace into `#{ @destination.name }` (#{ @intersection.destination.joined }) #{ SqlHelper.annotation }
53
+ values (#{ @intersection.origin.typed('NEW') })
54
54
  }
55
55
  end
56
56
 
@@ -4,35 +4,48 @@
4
4
  module Lhm
5
5
  # Determine and format columns common to origin and destination.
6
6
  class Intersection
7
- def initialize(origin, destination)
7
+ def initialize(origin, destination, renames = {})
8
8
  @origin = origin
9
9
  @destination = destination
10
+ @renames = renames
10
11
  end
11
12
 
12
- def common
13
- (@origin.columns.keys & @destination.columns.keys).sort
13
+ def origin
14
+ (common + @renames.keys).extend(Joiners)
14
15
  end
15
16
 
16
- def escaped
17
- common.map { |name| tick(name) }
17
+ def destination
18
+ (common + @renames.values).extend(Joiners)
18
19
  end
19
20
 
20
- def joined
21
- escaped.join(", ")
22
- end
21
+ private
23
22
 
24
- def typed(type)
25
- common.map { |name| qualified(name, type) }.join(", ")
23
+ def common
24
+ (@origin.columns.keys & @destination.columns.keys).sort
26
25
  end
27
26
 
28
- private
27
+ module Joiners
28
+ def escaped
29
+ self.map { |name| tick(name) }
30
+ end
29
31
 
30
- def qualified(name, type)
31
- "#{ type }.`#{ name }`"
32
- end
32
+ def joined
33
+ escaped.join(', ')
34
+ end
35
+
36
+ def typed(type)
37
+ self.map { |name| qualified(name, type) }.join(', ')
38
+ end
39
+
40
+ private
41
+
42
+ def qualified(name, type)
43
+ "`#{ type }`.`#{ name }`"
44
+ end
33
45
 
34
- def tick(name)
35
- "`#{ name }`"
46
+ def tick(name)
47
+ "`#{ name }`"
48
+ end
36
49
  end
37
50
  end
38
51
  end
@@ -47,8 +47,8 @@ module Lhm
47
47
  options[:atomic_switch] = true
48
48
  else
49
49
  raise Error.new(
50
- "Using mysql #{version_string}. You must explicitly set " +
51
- "options[:atomic_switch] (re SqlHelper#supports_atomic_switch?)")
50
+ "Using mysql #{version_string}. You must explicitly set " \
51
+ 'options[:atomic_switch] (re SqlHelper#supports_atomic_switch?)')
52
52
  end
53
53
  end
54
54
 
@@ -56,7 +56,7 @@ module Lhm
56
56
  options[:throttler] = Throttler::Factory.create_throttler(*options[:throttler])
57
57
  elsif options[:throttle] || options[:stride]
58
58
  # we still support the throttle and stride as a Fixnum input
59
- warn "throttle option will no longer accept a Fixnum in the next versions."
59
+ warn 'throttle option will no longer accept a Fixnum in the next versions.'
60
60
  options[:throttler] = Throttler::LegacyTime.new(options[:throttle], options[:stride])
61
61
  else
62
62
  options[:throttler] = Lhm.throttler
@@ -38,17 +38,17 @@ module Lhm
38
38
  "lock table `#{ @origin.name }` write, `#{ @destination.name }` write",
39
39
  "alter table `#{ @origin.name }` rename `#{ @migration.archive_name }`",
40
40
  "alter table `#{ @destination.name }` rename `#{ @origin.name }`",
41
- "commit",
42
- "unlock tables"
41
+ 'commit',
42
+ 'unlock tables'
43
43
  ]
44
44
  end
45
45
 
46
46
  def uncommitted(&block)
47
47
  [
48
- "set @lhm_auto_commit = @@session.autocommit",
49
- "set session autocommit = 0",
48
+ 'set @lhm_auto_commit = @@session.autocommit',
49
+ 'set session autocommit = 0',
50
50
  yield,
51
- "set session autocommit = @lhm_auto_commit"
51
+ 'set session autocommit = @lhm_auto_commit'
52
52
  ].flatten
53
53
  end
54
54
 
@@ -62,7 +62,7 @@ module Lhm
62
62
  private
63
63
 
64
64
  def revert
65
- @connection.sql("unlock tables")
65
+ @connection.sql('unlock tables')
66
66
  end
67
67
 
68
68
  def execute
@@ -5,13 +5,14 @@ require 'lhm/intersection'
5
5
 
6
6
  module Lhm
7
7
  class Migration
8
- attr_reader :origin, :destination, :conditions
8
+ attr_reader :origin, :destination, :conditions, :renames
9
9
 
10
- def initialize(origin, destination, conditions = nil, time = Time.now)
10
+ def initialize(origin, destination, conditions = nil, renames = {}, time = Time.now)
11
11
  @origin = origin
12
12
  @destination = destination
13
13
  @conditions = conditions
14
14
  @start = time
15
+ @renames = renames
15
16
  end
16
17
 
17
18
  def archive_name
@@ -19,11 +20,11 @@ module Lhm
19
20
  end
20
21
 
21
22
  def intersection
22
- Intersection.new(@origin, @destination)
23
+ Intersection.new(@origin, @destination, @renames)
23
24
  end
24
25
 
25
26
  def startstamp
26
- @start.strftime "%Y_%m_%d_%H_%M_%S_#{ "%03d" % (@start.usec / 1000) }"
27
+ @start.strftime "%Y_%m_%d_%H_%M_%S_#{ '%03d' % (@start.usec / 1000) }"
27
28
  end
28
29
  end
29
30
  end
@@ -13,13 +13,14 @@ module Lhm
13
13
  include Command
14
14
  include SqlHelper
15
15
 
16
- attr_reader :name, :statements, :connection, :conditions
16
+ attr_reader :name, :statements, :connection, :conditions, :renames
17
17
 
18
18
  def initialize(table, connection = nil)
19
19
  @connection = connection
20
20
  @origin = table
21
21
  @name = table.destination_name
22
22
  @statements = []
23
+ @renames = {}
23
24
  end
24
25
 
25
26
  # Alter a table with a custom statement
@@ -52,7 +53,7 @@ module Lhm
52
53
  # @param [String] name Name of the column to add
53
54
  # @param [String] definition Valid SQL column definition
54
55
  def add_column(name, definition)
55
- ddl("alter table `%s` add column `%s` %s" % [@name, name, definition])
56
+ ddl('alter table `%s` add column `%s` %s' % [@name, name, definition])
56
57
  end
57
58
 
58
59
  # Change an existing column to a new definition
@@ -66,7 +67,28 @@ module Lhm
66
67
  # @param [String] name Name of the column to change
67
68
  # @param [String] definition Valid SQL column definition
68
69
  def change_column(name, definition)
69
- ddl("alter table `%s` modify column `%s` %s" % [@name, name, definition])
70
+ ddl('alter table `%s` modify column `%s` %s' % [@name, name, definition])
71
+ end
72
+
73
+ # Rename an existing column.
74
+ #
75
+ # @example
76
+ #
77
+ # Lhm.change_table(:users) do |m|
78
+ # m.rename_column(:login, :username)
79
+ # end
80
+ #
81
+ # @param [String] old Name of the column to change
82
+ # @param [String] nu New name to use for the column
83
+ def rename_column(old, nu)
84
+ col = @origin.columns[old.to_s]
85
+
86
+ definition = col[:type]
87
+ definition += ' NOT NULL' unless col[:is_nullable]
88
+ definition += " DEFAULT #{@connection.quote_value(col[:column_default])}" if col[:column_default]
89
+
90
+ ddl('alter table `%s` change column `%s` `%s` %s' % [@name, old, nu, definition])
91
+ @renames[old.to_s] = nu.to_s
70
92
  end
71
93
 
72
94
  # Remove a column from a table
@@ -79,7 +101,7 @@ module Lhm
79
101
  #
80
102
  # @param [String] name Name of the column to delete
81
103
  def remove_column(name)
82
- ddl("alter table `%s` drop `%s`" % [@name, name])
104
+ ddl('alter table `%s` drop `%s`' % [@name, name])
83
105
  end
84
106
 
85
107
  # Add an index to a table
@@ -136,10 +158,10 @@ module Lhm
136
158
  # Optional name of the index to be removed
137
159
  def remove_index(columns, index_name = nil)
138
160
  columns = [columns].flatten.map(&:to_sym)
139
- from_origin = @origin.indices.find {|name, cols| cols.map(&:to_sym) == columns}
161
+ from_origin = @origin.indices.find { |name, cols| cols.map(&:to_sym) == columns }
140
162
  index_name ||= from_origin[0] unless from_origin.nil?
141
163
  index_name ||= idx_name(@origin.name, columns)
142
- ddl("drop index `%s` on `%s`" % [index_name, @name])
164
+ ddl('drop index `%s` on `%s`' % [index_name, @name])
143
165
  end
144
166
 
145
167
  # Filter the data that is copied into the new table by the provided SQL.
@@ -166,7 +188,7 @@ module Lhm
166
188
  end
167
189
 
168
190
  unless @origin.satisfies_primary_key?
169
- error("origin does not satisfy primary key requirements")
191
+ error('origin does not satisfy primary key requirements')
170
192
  end
171
193
 
172
194
  dest = @origin.destination_name
@@ -179,7 +201,7 @@ module Lhm
179
201
  def execute
180
202
  destination_create
181
203
  @connection.sql(@statements)
182
- Migration.new(@origin, destination_read, conditions)
204
+ Migration.new(@origin, destination_read, conditions, renames)
183
205
  end
184
206
 
185
207
  def destination_create
@@ -191,10 +213,17 @@ module Lhm
191
213
  end
192
214
 
193
215
  def index_ddl(cols, unique = nil, index_name = nil)
194
- type = unique ? "unique index" : "index"
216
+ assert_valid_idx_name(index_name)
217
+ type = unique ? 'unique index' : 'index'
195
218
  index_name ||= idx_name(@origin.name, cols)
196
219
  parts = [type, index_name, @name, idx_spec(cols)]
197
- "create %s `%s` on `%s` (%s)" % parts
220
+ 'create %s `%s` on `%s` (%s)' % parts
221
+ end
222
+
223
+ def assert_valid_idx_name(index_name)
224
+ if index_name && !(index_name.is_a?(String) || index_name.is_a?(Symbol))
225
+ raise ArgumentError, 'index_name must be a string or symbol'
226
+ end
198
227
  end
199
228
  end
200
229
  end