lhm 2.1.0 → 2.2.0

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.
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