convergence 1.0.5 → 1.1.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +55 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +52 -0
  5. data/CONTRIBUTING.md +80 -0
  6. data/Gemfile.lock +24 -9
  7. data/README.md +131 -1
  8. data/Rakefile +48 -5
  9. data/convergence.gemspec +3 -1
  10. data/lib/convergence/cli.rb +10 -2
  11. data/lib/convergence/command/apply.rb +38 -9
  12. data/lib/convergence/command/dryrun.rb +5 -5
  13. data/lib/convergence/command/export.rb +19 -1
  14. data/lib/convergence/command.rb +6 -0
  15. data/lib/convergence/config.rb +1 -1
  16. data/lib/convergence/database_connector/postgres_connector.rb +25 -0
  17. data/lib/convergence/database_connector/sqlite_connector.rb +22 -0
  18. data/lib/convergence/database_connector.rb +6 -0
  19. data/lib/convergence/default_parameter/mysql_default_parameter.rb +1 -1
  20. data/lib/convergence/default_parameter/postgres_default_parameter.rb +57 -0
  21. data/lib/convergence/default_parameter/sqlite_default_parameter.rb +57 -0
  22. data/lib/convergence/default_parameter.rb +6 -0
  23. data/lib/convergence/diff.rb +9 -2
  24. data/lib/convergence/dsl.rb +18 -3
  25. data/lib/convergence/dumper/mysql_schema_dumper.rb +37 -9
  26. data/lib/convergence/dumper/postgres_schema_dumper.rb +193 -0
  27. data/lib/convergence/dumper/sqlite_schema_dumper.rb +132 -0
  28. data/lib/convergence/dumper.rb +113 -0
  29. data/lib/convergence/sql_generator/mysql_generator.rb +18 -3
  30. data/lib/convergence/sql_generator/postgres_generator.rb +256 -0
  31. data/lib/convergence/sql_generator/sqlite_generator.rb +178 -0
  32. data/lib/convergence/version.rb +1 -1
  33. data/spec/config/spec_database.yml +15 -2
  34. data/spec/convergence/config_spec.rb +75 -0
  35. data/spec/convergence/diff_spec.rb +125 -45
  36. data/spec/convergence/dsl_spec.rb +26 -0
  37. data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +25 -2
  38. data/spec/convergence/dumper/postgres_schema_dumper_spec.rb +103 -0
  39. data/spec/convergence/dumper/sqlite_schema_dumper_spec.rb +85 -0
  40. data/spec/convergence/dumper_spec.rb +110 -16
  41. data/spec/convergence/foreign_key_spec.rb +33 -0
  42. data/spec/convergence/index_spec.rb +52 -0
  43. data/spec/convergence/pretty_diff_spec.rb +85 -0
  44. data/spec/convergence/table_spec.rb +23 -0
  45. data/spec/fixtures/add_table_with_enum_set.schema +5 -0
  46. data/spec/fixtures/change_table_comment_to_paper.schema +2 -0
  47. data/spec/fixtures/execute_raw_sql.schema +28 -0
  48. data/spec/fixtures/postgres/add_columns_to_paper.schema +29 -0
  49. data/spec/fixtures/postgres/add_table.schema +32 -0
  50. data/spec/fixtures/postgres/change_comment_columns_to_paper.schema +28 -0
  51. data/spec/fixtures/postgres/change_table_comment_to_paper.schema +28 -0
  52. data/spec/fixtures/postgres/drop_foreign_key.schema +25 -0
  53. data/spec/fixtures/postgres/drop_table.schema +19 -0
  54. data/spec/fixtures/postgres/remove_columns_to_paper.schema +27 -0
  55. data/spec/fixtures/postgres_test_db.sql +41 -0
  56. data/spec/fixtures/sqlite/add_columns_to_paper.schema +29 -0
  57. data/spec/fixtures/sqlite/add_table.schema +32 -0
  58. data/spec/fixtures/sqlite/change_columns_to_paper.schema +28 -0
  59. data/spec/fixtures/sqlite/drop_foreign_key.schema +25 -0
  60. data/spec/fixtures/sqlite/drop_table.schema +19 -0
  61. data/spec/fixtures/sqlite/remove_columns_to_paper.schema +27 -0
  62. data/spec/fixtures/sqlite_test_db.sql +31 -0
  63. data/spec/fixtures/test_db.sql +10 -0
  64. data/spec/integrations/command_diff.rb +35 -0
  65. data/spec/integrations/command_dryrun.rb +37 -2
  66. data/spec/integrations/command_export.rb +28 -0
  67. data/spec/postgres_integrations/command_dryrun.rb +79 -0
  68. data/spec/spec_helper.rb +13 -0
  69. data/spec/sqlite_integrations/command_dryrun.rb +70 -0
  70. metadata +97 -9
  71. data/.travis.yml +0 -9
@@ -0,0 +1,256 @@
1
+ require 'convergence/sql_generator'
2
+
3
+ class SQLGenerator::PostgresGenerator < SQLGenerator
4
+ # Convergence's DSL type names follow MySQL terminology (kept for cross-adapter DSL
5
+ # compatibility). Map them to their closest native PostgreSQL type.
6
+ TYPE_MAPPING = {
7
+ tinyint: 'smallint',
8
+ smallint: 'smallint',
9
+ mediumint: 'integer',
10
+ int: 'integer',
11
+ bigint: 'bigint',
12
+ float: 'real',
13
+ double: 'double precision',
14
+ decimal: 'decimal',
15
+ char: 'char',
16
+ varchar: 'varchar',
17
+ tinyblob: 'bytea',
18
+ blob: 'bytea',
19
+ mediumblob: 'bytea',
20
+ longblob: 'bytea',
21
+ tinytext: 'text',
22
+ text: 'text',
23
+ mediumtext: 'text',
24
+ longtext: 'text',
25
+ date: 'date',
26
+ time: 'time',
27
+ datetime: 'timestamp',
28
+ timestamp: 'timestamp',
29
+ year: 'integer',
30
+ json: 'jsonb'
31
+ }.freeze
32
+ TYPES_WITH_LIMIT = [:char, :varchar].freeze
33
+ UNSUPPORTED_TYPES = [:enum, :set].freeze
34
+
35
+ attr_reader :original_table
36
+
37
+ def generate(to_table, delta, original_table, safe_migration: false)
38
+ @original_table = original_table
39
+ sqls = []
40
+ sqls << change_table_sql(to_table, delta)
41
+ sqls << (safe_migration ? [] : drop_table_sqls(delta))
42
+ sqls << create_table_sqls(delta)
43
+ sqls.reject!(&:empty?)
44
+ sqls.join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ # FIXME: multiple pk change not supported yet
50
+ def change_table_sql(to_table, delta)
51
+ change_table = delta[:change_table]
52
+ results = []
53
+ change_table.each do |table_name, table_delta|
54
+ unless table_delta[:remove_foreign_key].empty?
55
+ results << alter_remove_foreign_keys_sql(table_name, table_delta[:remove_foreign_key].keys)
56
+ end
57
+ table_delta[:remove_index].each do |index_name, _index|
58
+ results << alter_remove_index_sql(index_name)
59
+ end
60
+ unless table_delta[:remove_column].empty?
61
+ results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
62
+ end
63
+ unless table_delta[:add_column].empty?
64
+ results << alter_add_columns_sql(table_name, table_delta[:add_column].values)
65
+ end
66
+ table_delta[:change_column].each do |column_name, column|
67
+ results << alter_change_column_sql(table_name, column_name, column, to_table)
68
+ end
69
+ table_delta[:add_index].each do |_index_name, index|
70
+ results << alter_add_index_sql(table_name, index)
71
+ end
72
+ unless table_delta[:add_foreign_key].empty?
73
+ results << alter_add_foreign_keys_sql(table_name, table_delta[:add_foreign_key].values)
74
+ end
75
+ unless table_delta[:change_table_option].empty?
76
+ results << alter_change_table_sql(table_name, table_delta[:change_table_option])
77
+ end
78
+ end
79
+ results << '' unless results.empty?
80
+ results
81
+ end
82
+
83
+ def alter_add_columns_sql(table_name, columns)
84
+ add_column_sqls = columns.map { |column| %(ALTER TABLE "#{table_name}" ADD COLUMN #{create_column_sql(column, output_primary_key: true)};) }
85
+ comment_sqls = columns.map { |column| comment_on_column_sql(table_name, column) }.compact
86
+ (add_column_sqls + comment_sqls).join("\n")
87
+ end
88
+
89
+ def alter_remove_columns_sql(table_name, columns)
90
+ columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
91
+ end
92
+
93
+ def alter_change_column_sql(table_name, column_name, change_column_option, to_table)
94
+ column = to_table[table_name].columns[column_name]
95
+ column.options.merge!(after: change_column_option[:after]) unless change_column_option[:after].nil?
96
+ sqls = []
97
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" TYPE #{postgres_column_type(column)} USING "#{column_name}"::#{postgres_column_type(column)};)
98
+ if column.options[:null]
99
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" DROP NOT NULL;)
100
+ else
101
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" SET NOT NULL;)
102
+ end
103
+ if column.options[:default]
104
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" SET DEFAULT #{quote_default_expression(column.options[:default])};)
105
+ else
106
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" DROP DEFAULT;)
107
+ end
108
+ comment_sql = comment_on_column_sql(table_name, column)
109
+ sqls << comment_sql unless comment_sql.nil?
110
+ sqls.join("\n")
111
+ end
112
+
113
+ def alter_change_table_sql(table_name, change_table_option)
114
+ return '' unless change_table_option.key?(:comment)
115
+ %(COMMENT ON TABLE "#{table_name}" IS '#{escape_quote(change_table_option[:comment])}';)
116
+ end
117
+
118
+ def alter_remove_index_sql(index_name)
119
+ %(DROP INDEX "#{index_name}";)
120
+ end
121
+
122
+ def alter_add_index_sql(table_name, index)
123
+ sql = 'CREATE'
124
+ sql += ' UNIQUE' if index.options[:unique]
125
+ sql += %( INDEX "#{index.index_name}" ON "#{table_name}" (#{quoted_index_columns(index).join(',')});)
126
+ sql
127
+ end
128
+
129
+ def alter_remove_foreign_keys_sql(table_name, index_names)
130
+ index_names.map { |index_name| %(ALTER TABLE "#{table_name}" DROP CONSTRAINT "#{index_name}";) }.join("\n")
131
+ end
132
+
133
+ def alter_add_foreign_keys_sql(table_name, foreign_keys)
134
+ foreign_keys.map { |foreign_key| %(ALTER TABLE "#{table_name}" ADD #{foreign_key_constraint_sql(foreign_key)};) }.join("\n")
135
+ end
136
+
137
+ def foreign_key_constraint_sql(foreign_key)
138
+ sql = %(CONSTRAINT "#{foreign_key.key_name}" FOREIGN KEY )
139
+ sql += "(#{[foreign_key.from_columns].flatten.map { |v| %("#{v}") }.join(',')}) "
140
+ sql += %(REFERENCES "#{foreign_key.to_table}" )
141
+ sql += "(#{[foreign_key.to_columns].flatten.map { |v| %("#{v}") }.join(',')})"
142
+ sql
143
+ end
144
+
145
+ def create_table_sqls(delta)
146
+ delta[:add_table].map do |table_name, table|
147
+ column_sql = (create_table_column_sql(table) << create_table_index_sql(table))
148
+ .flatten
149
+ .reject(&:empty?)
150
+ .join(",\n ")
151
+ sql = <<-SQL
152
+ CREATE TABLE "#{table_name}" (
153
+ #{column_sql}
154
+ );
155
+ SQL
156
+ sql = sql.strip
157
+ index_sqls = table.indexes.values.map { |index| alter_add_index_sql(table_name, index) }
158
+ comment_sqls = create_table_comment_sqls(table_name, table)
159
+ ([sql] + index_sqls + comment_sqls).join("\n")
160
+ end
161
+ end
162
+
163
+ def create_table_comment_sqls(table_name, table)
164
+ sqls = []
165
+ if table.table_options[:comment] && !table.table_options[:comment].to_s.empty?
166
+ sqls << %(COMMENT ON TABLE "#{table_name}" IS '#{escape_quote(table.table_options[:comment])}';)
167
+ end
168
+ table.columns.values.each do |column|
169
+ comment_sql = comment_on_column_sql(table_name, column)
170
+ sqls << comment_sql unless comment_sql.nil?
171
+ end
172
+ sqls
173
+ end
174
+
175
+ def comment_on_column_sql(table_name, column)
176
+ return nil if column.options[:comment].nil? || column.options[:comment].to_s.empty?
177
+ %(COMMENT ON COLUMN "#{table_name}"."#{column.column_name}" IS '#{escape_quote(column.options[:comment])}';)
178
+ end
179
+
180
+ def drop_table_sqls(delta)
181
+ delta[:remove_table].map do |table_name, _|
182
+ %(DROP TABLE "#{table_name}";)
183
+ end
184
+ end
185
+
186
+ def create_table_column_sql(table)
187
+ table.columns.values.map do |column|
188
+ create_column_sql(column)
189
+ end
190
+ end
191
+
192
+ def create_column_sql(column, output_primary_key: false, output_identity: true)
193
+ fail NotImplementedError.new("#{column.type} is not supported on PostgreSQL adapter yet") if UNSUPPORTED_TYPES.include?(column.type)
194
+ sql = %("#{column.column_name}")
195
+ sql += " #{postgres_column_type(column)}"
196
+ if column.options[:null]
197
+ sql += ' DEFAULT NULL' unless column.options[:default]
198
+ else
199
+ sql += ' NOT NULL'
200
+ end
201
+ if column.options[:primary_key] && output_primary_key
202
+ sql += ' PRIMARY KEY'
203
+ end
204
+ if column.options[:default]
205
+ sql += " DEFAULT #{quote_default_expression(column.options[:default])}"
206
+ end
207
+ if auto_increment?(column) && output_identity
208
+ sql += ' GENERATED BY DEFAULT AS IDENTITY'
209
+ end
210
+ sql
211
+ end
212
+
213
+ def postgres_column_type(column)
214
+ return 'boolean' if column.type == :tinyint && column.options[:limit].to_s == '1'
215
+ type = TYPE_MAPPING[column.type] || column.type.to_s
216
+ if TYPES_WITH_LIMIT.include?(column.type) && column.options[:limit]
217
+ "#{type}(#{column.options[:limit]})"
218
+ elsif column.type == :decimal && column.options[:precision] && column.options[:scale]
219
+ "#{type}(#{column.options[:precision]}, #{column.options[:scale]})"
220
+ else
221
+ type
222
+ end
223
+ end
224
+
225
+ def auto_increment?(column)
226
+ extra = column.options[:extra]
227
+ !extra.nil? && extra.to_s.upcase.include?('AUTO_INCREMENT')
228
+ end
229
+
230
+ def create_table_index_sql(table)
231
+ pkeys = table.columns.select { |_k, v| v.options[:primary_key] }
232
+ foreign_keys = table.foreign_keys.values
233
+ results = []
234
+ unless pkeys.empty?
235
+ results << %(PRIMARY KEY (#{pkeys.keys.map { |v| %("#{v}") }.join(',')}))
236
+ end
237
+ results << foreign_keys.map { |fk| foreign_key_constraint_sql(fk) }
238
+ results
239
+ end
240
+
241
+ def quoted_index_columns(index)
242
+ [index.index_columns].flatten.map { |v| %("#{v}") }
243
+ end
244
+
245
+ def escape_quote(value)
246
+ value.to_s.gsub("'", "''")
247
+ end
248
+
249
+ def quote_default_expression(value)
250
+ if value.is_a?(Proc)
251
+ value.call
252
+ else
253
+ %('#{escape_quote(value)}')
254
+ end
255
+ end
256
+ end
@@ -0,0 +1,178 @@
1
+ require 'convergence/sql_generator'
2
+
3
+ # SQLite's ALTER TABLE support is intentionally limited: it can add/drop columns
4
+ # and create/drop indexes, but it cannot change a column's type/null/default,
5
+ # nor add/remove a foreign key or primary key on an existing table. Doing any of
6
+ # those requires SQLite's official "rebuild the table" procedure (create a new
7
+ # table, copy the data over, drop the old one, rename), which this adapter does
8
+ # not implement yet -- it raises NotImplementedError instead of generating SQL
9
+ # that SQLite would reject.
10
+ class SQLGenerator::SqliteGenerator < SQLGenerator
11
+ UNSUPPORTED_TYPES = [:enum, :set].freeze
12
+
13
+ attr_reader :original_table
14
+
15
+ def generate(to_table, delta, original_table, safe_migration: false)
16
+ @original_table = original_table
17
+ sqls = []
18
+ sqls << change_table_sql(to_table, delta)
19
+ sqls << (safe_migration ? [] : drop_table_sqls(delta))
20
+ sqls << create_table_sqls(delta)
21
+ sqls.reject!(&:empty?)
22
+ sqls.join("\n")
23
+ end
24
+
25
+ private
26
+
27
+ def change_table_sql(to_table, delta)
28
+ change_table = delta[:change_table]
29
+ results = []
30
+ change_table.each do |table_name, table_delta|
31
+ unless table_delta[:remove_foreign_key].empty?
32
+ fail unsupported_alter_error(table_name, 'removing a foreign key')
33
+ end
34
+ unless table_delta[:add_foreign_key].empty?
35
+ fail unsupported_alter_error(table_name, 'adding a foreign key')
36
+ end
37
+ unless table_delta[:change_column].empty?
38
+ columns = table_delta[:change_column].keys.join(', ')
39
+ fail unsupported_alter_error(table_name, "changing column(s) #{columns} (type/null/default/primary key)")
40
+ end
41
+ table_delta[:remove_index].each do |index_name, _index|
42
+ results << alter_remove_index_sql(index_name)
43
+ end
44
+ unless table_delta[:remove_column].empty?
45
+ results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
46
+ end
47
+ unless table_delta[:add_column].empty?
48
+ results << alter_add_columns_sql(table_name, table_delta[:add_column].values)
49
+ end
50
+ table_delta[:add_index].each do |_index_name, index|
51
+ results << alter_add_index_sql(table_name, index)
52
+ end
53
+ # SQLite has no table-level options comparable to MySQL's engine/comment/
54
+ # charset, so any detected change_table_option diff is silently ignored.
55
+ end
56
+ results << '' unless results.empty?
57
+ results
58
+ end
59
+
60
+ def unsupported_alter_error(table_name, action)
61
+ NotImplementedError.new(
62
+ "SQLite adapter does not support #{action} on an existing table (`#{table_name}`) via ALTER TABLE. " \
63
+ 'This requires rebuilding the table (create new, copy data, drop old, rename), which is not supported yet.'
64
+ )
65
+ end
66
+
67
+ def alter_add_columns_sql(table_name, columns)
68
+ columns.map { |column| %(ALTER TABLE "#{table_name}" ADD COLUMN #{create_column_sql(column)};) }.join("\n")
69
+ end
70
+
71
+ def alter_remove_columns_sql(table_name, columns)
72
+ columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
73
+ end
74
+
75
+ def alter_remove_index_sql(index_name)
76
+ %(DROP INDEX "#{index_name}";)
77
+ end
78
+
79
+ def alter_add_index_sql(table_name, index)
80
+ sql = 'CREATE'
81
+ sql += ' UNIQUE' if index.options[:unique]
82
+ sql += %( INDEX "#{index.index_name}" ON "#{table_name}" (#{quoted_index_columns(index).join(',')});)
83
+ sql
84
+ end
85
+
86
+ def create_table_sqls(delta)
87
+ delta[:add_table].map do |table_name, table|
88
+ column_sql = (create_table_column_sql(table) << create_table_constraint_sql(table))
89
+ .flatten
90
+ .reject(&:empty?)
91
+ .join(",\n ")
92
+ sql = <<-SQL
93
+ CREATE TABLE "#{table_name}" (
94
+ #{column_sql}
95
+ );
96
+ SQL
97
+ index_sqls = table.indexes.values.map { |index| alter_add_index_sql(table_name, index) }
98
+ ([sql.strip] + index_sqls).join("\n")
99
+ end
100
+ end
101
+
102
+ def drop_table_sqls(delta)
103
+ delta[:remove_table].map do |table_name, _|
104
+ %(DROP TABLE "#{table_name}";)
105
+ end
106
+ end
107
+
108
+ def create_table_column_sql(table)
109
+ table.columns.values.map do |column|
110
+ create_column_sql(column)
111
+ end
112
+ end
113
+
114
+ def create_column_sql(column)
115
+ fail NotImplementedError.new("#{column.type} is not supported on the SQLite adapter yet") if UNSUPPORTED_TYPES.include?(column.type)
116
+ return %("#{column.column_name}" INTEGER PRIMARY KEY AUTOINCREMENT) if auto_increment?(column)
117
+ sql = %("#{column.column_name}")
118
+ sql += " #{sqlite_column_type(column)}"
119
+ if column.options[:primary_key]
120
+ sql += ' PRIMARY KEY'
121
+ end
122
+ sql += ' NOT NULL' unless column.options[:null]
123
+ if column.options[:default]
124
+ sql += " DEFAULT #{quote_default_expression(column.options[:default])}"
125
+ end
126
+ sql
127
+ end
128
+
129
+ def sqlite_column_type(column)
130
+ type = column.type.to_s
131
+ if column.type == :decimal && column.options[:precision] && column.options[:scale]
132
+ "#{type}(#{column.options[:precision]}, #{column.options[:scale]})"
133
+ elsif column.options[:limit] && !column.options[:limit].to_s.empty?
134
+ "#{type}(#{column.options[:limit]})"
135
+ else
136
+ type
137
+ end
138
+ end
139
+
140
+ def auto_increment?(column)
141
+ extra = column.options[:extra]
142
+ !extra.nil? && extra.to_s.upcase.include?('AUTO_INCREMENT')
143
+ end
144
+
145
+ # A single-column INTEGER PRIMARY KEY AUTOINCREMENT already declares itself as
146
+ # the primary key inline, so it must not also appear in a table-level
147
+ # PRIMARY KEY (...) clause.
148
+ def create_table_constraint_sql(table)
149
+ pkeys = table.columns.reject { |_k, v| auto_increment?(v) }.select { |_k, v| v.options[:primary_key] }
150
+ foreign_keys = table.foreign_keys.values
151
+ results = []
152
+ unless pkeys.empty?
153
+ results << %(PRIMARY KEY (#{pkeys.keys.map { |v| %("#{v}") }.join(',')}))
154
+ end
155
+ results << foreign_keys.map { |fk| foreign_key_constraint_sql(fk) }
156
+ results
157
+ end
158
+
159
+ def foreign_key_constraint_sql(foreign_key)
160
+ sql = %(CONSTRAINT "#{foreign_key.key_name}" FOREIGN KEY )
161
+ sql += "(#{[foreign_key.from_columns].flatten.map { |v| %("#{v}") }.join(',')}) "
162
+ sql += %(REFERENCES "#{foreign_key.to_table}" )
163
+ sql += "(#{[foreign_key.to_columns].flatten.map { |v| %("#{v}") }.join(',')})"
164
+ sql
165
+ end
166
+
167
+ def quoted_index_columns(index)
168
+ [index.index_columns].flatten.map { |v| %("#{v}") }
169
+ end
170
+
171
+ def quote_default_expression(value)
172
+ if value.is_a?(Proc)
173
+ value.call
174
+ else
175
+ %('#{value.to_s.gsub("'", "''")}')
176
+ end
177
+ end
178
+ end
@@ -1,3 +1,3 @@
1
1
  module Convergence
2
- VERSION = '1.0.5'
2
+ VERSION = '1.1.0'
3
3
  end
@@ -1,6 +1,19 @@
1
1
  mysql:
2
2
  adapter: mysql2
3
3
  database: convergence_test_db
4
- host: localhost
5
- port: 3389
4
+ host: 127.0.0.1
5
+ port: 3306
6
6
  username: root
7
+ password: root
8
+
9
+ postgresql:
10
+ adapter: postgresql
11
+ database: convergence_test_db
12
+ host: 127.0.0.1
13
+ port: 5432
14
+ username: postgres
15
+ password: postgres
16
+
17
+ sqlite3:
18
+ adapter: sqlite3
19
+ database: sqlite_test_db.sqlite3
@@ -0,0 +1,75 @@
1
+ require 'spec_helper'
2
+ require 'convergence/config'
3
+ require 'tempfile'
4
+
5
+ describe Convergence::Config do
6
+ describe '#initialize' do
7
+ it 'sets the known attributes' do
8
+ config = Convergence::Config.new(
9
+ adapter: 'mysql2',
10
+ database: 'my_db',
11
+ host: '127.0.0.1',
12
+ port: 3306,
13
+ username: 'root',
14
+ password: 'secret'
15
+ )
16
+ expect(config.adapter).to eq('mysql2')
17
+ expect(config.database).to eq('my_db')
18
+ expect(config.host).to eq('127.0.0.1')
19
+ expect(config.port).to eq(3306)
20
+ expect(config.username).to eq('root')
21
+ expect(config.password).to eq('secret')
22
+ end
23
+
24
+ it 'ignores unknown attributes instead of raising' do
25
+ expect { Convergence::Config.new(adapter: 'mysql2', unknown_key: 'value') }.not_to raise_error
26
+ end
27
+
28
+ it 'ignores nil-valued attributes' do
29
+ config = Convergence::Config.new(adapter: 'mysql2', database: nil)
30
+ expect(config.database).to be_nil
31
+ end
32
+
33
+ context 'when the adapter is mysql/mysql2' do
34
+ it 'builds a nested MySQL SSL config object' do
35
+ config = Convergence::Config.new(adapter: 'mysql2', sslca: '/path/to/ca.pem', sslverify: true)
36
+ expect(config.mysql).to be_a(Convergence::Config::MySQL)
37
+ expect(config.mysql.ssl_options).to eq(sslca: '/path/to/ca.pem', sslverify: true)
38
+ end
39
+
40
+ it 'omits unset SSL options from ssl_options' do
41
+ config = Convergence::Config.new(adapter: 'mysql2')
42
+ expect(config.mysql.ssl_options).to eq({})
43
+ end
44
+ end
45
+
46
+ context 'when the adapter is not mysql/mysql2' do
47
+ it 'does not build a MySQL SSL config object' do
48
+ config = Convergence::Config.new(adapter: 'postgresql')
49
+ expect(config.mysql).to be_nil
50
+ end
51
+ end
52
+ end
53
+
54
+ describe '.load' do
55
+ it 'parses attributes from a YAML file' do
56
+ Tempfile.create(['database', '.yml']) do |file|
57
+ file.write("adapter: mysql2\ndatabase: my_db\nhost: 127.0.0.1\n")
58
+ file.flush
59
+ config = Convergence::Config.load(file.path)
60
+ expect(config.adapter).to eq('mysql2')
61
+ expect(config.database).to eq('my_db')
62
+ expect(config.host).to eq('127.0.0.1')
63
+ end
64
+ end
65
+
66
+ it 'evaluates ERB before parsing the YAML' do
67
+ Tempfile.create(['database', '.yml']) do |file|
68
+ file.write("adapter: mysql2\ndatabase: <%= 'interpolated_' + 'db' %>\n")
69
+ file.flush
70
+ config = Convergence::Config.load(file.path)
71
+ expect(config.database).to eq('interpolated_db')
72
+ end
73
+ end
74
+ end
75
+ end