convergence 1.0.6 → 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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -4
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +46 -0
  5. data/CONTRIBUTING.md +80 -0
  6. data/Gemfile.lock +23 -8
  7. data/README.md +131 -1
  8. data/Rakefile +45 -2
  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/database_connector/postgres_connector.rb +25 -0
  16. data/lib/convergence/database_connector/sqlite_connector.rb +22 -0
  17. data/lib/convergence/database_connector.rb +6 -0
  18. data/lib/convergence/default_parameter/mysql_default_parameter.rb +1 -1
  19. data/lib/convergence/default_parameter/postgres_default_parameter.rb +57 -0
  20. data/lib/convergence/default_parameter/sqlite_default_parameter.rb +57 -0
  21. data/lib/convergence/default_parameter.rb +6 -0
  22. data/lib/convergence/diff.rb +9 -2
  23. data/lib/convergence/dsl.rb +18 -3
  24. data/lib/convergence/dumper/mysql_schema_dumper.rb +37 -9
  25. data/lib/convergence/dumper/postgres_schema_dumper.rb +193 -0
  26. data/lib/convergence/dumper/sqlite_schema_dumper.rb +132 -0
  27. data/lib/convergence/dumper.rb +113 -0
  28. data/lib/convergence/sql_generator/mysql_generator.rb +18 -3
  29. data/lib/convergence/sql_generator/postgres_generator.rb +256 -0
  30. data/lib/convergence/sql_generator/sqlite_generator.rb +178 -0
  31. data/lib/convergence/version.rb +1 -1
  32. data/spec/config/spec_database.yml +12 -0
  33. data/spec/convergence/config_spec.rb +75 -0
  34. data/spec/convergence/diff_spec.rb +125 -45
  35. data/spec/convergence/dsl_spec.rb +26 -0
  36. data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +25 -2
  37. data/spec/convergence/dumper/postgres_schema_dumper_spec.rb +103 -0
  38. data/spec/convergence/dumper/sqlite_schema_dumper_spec.rb +85 -0
  39. data/spec/convergence/dumper_spec.rb +110 -16
  40. data/spec/convergence/foreign_key_spec.rb +33 -0
  41. data/spec/convergence/index_spec.rb +52 -0
  42. data/spec/convergence/pretty_diff_spec.rb +85 -0
  43. data/spec/convergence/table_spec.rb +23 -0
  44. data/spec/fixtures/add_table_with_enum_set.schema +5 -0
  45. data/spec/fixtures/change_table_comment_to_paper.schema +2 -0
  46. data/spec/fixtures/execute_raw_sql.schema +28 -0
  47. data/spec/fixtures/postgres/add_columns_to_paper.schema +29 -0
  48. data/spec/fixtures/postgres/add_table.schema +32 -0
  49. data/spec/fixtures/postgres/change_comment_columns_to_paper.schema +28 -0
  50. data/spec/fixtures/postgres/change_table_comment_to_paper.schema +28 -0
  51. data/spec/fixtures/postgres/drop_foreign_key.schema +25 -0
  52. data/spec/fixtures/postgres/drop_table.schema +19 -0
  53. data/spec/fixtures/postgres/remove_columns_to_paper.schema +27 -0
  54. data/spec/fixtures/postgres_test_db.sql +41 -0
  55. data/spec/fixtures/sqlite/add_columns_to_paper.schema +29 -0
  56. data/spec/fixtures/sqlite/add_table.schema +32 -0
  57. data/spec/fixtures/sqlite/change_columns_to_paper.schema +28 -0
  58. data/spec/fixtures/sqlite/drop_foreign_key.schema +25 -0
  59. data/spec/fixtures/sqlite/drop_table.schema +19 -0
  60. data/spec/fixtures/sqlite/remove_columns_to_paper.schema +27 -0
  61. data/spec/fixtures/sqlite_test_db.sql +31 -0
  62. data/spec/fixtures/test_db.sql +10 -0
  63. data/spec/integrations/command_diff.rb +35 -0
  64. data/spec/integrations/command_dryrun.rb +37 -2
  65. data/spec/integrations/command_export.rb +28 -0
  66. data/spec/postgres_integrations/command_dryrun.rb +79 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/sqlite_integrations/command_dryrun.rb +70 -0
  69. metadata +96 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c5b9f06840ed6ef0686f00bfb8d677fc0ad0e389398ae30a5e20b86ca2cdf15
4
- data.tar.gz: 5f9f77898685d66e68d2c110ba53e4709fd45fffba9f5a13f77c8aa30b4605a0
3
+ metadata.gz: 75c8ae12a9ee346dd376ff2dfe6f03451a96aa4dc58acf414f9c0447c3e79bab
4
+ data.tar.gz: 57e2096d724239594acd415001e801f89f22bba706e3382ccca46dd3309cd5b9
5
5
  SHA512:
6
- metadata.gz: 587acb6716a2358a4babb9468473c65fcd0f61f5f81ead163aefe615bc81b547b0e487d9dbc558600a922d5c5bb4356c2ae7e153d837bcb1b865c1258c23b109
7
- data.tar.gz: 7c87a0849716fce53dea625c05532357bc9c92076f7658cdad1c29c0ee44a7eb48a1d3c32233c519e36d1f592e1d9c13c4cecde7ee586e16f307e6c9fda54d6e
6
+ metadata.gz: ca8171ecd7a63c898401fb316be440a1cee97b4cd44d52bda5ac449c608dd54432574aa5ce6e2222fe0d65acac0e89d4ec092eecd6e83cb6a3e9874ffb382cbc
7
+ data.tar.gz: 3b7ca04775d9e8800a69f6b70034d9d906b8ba679a575603b8d7d5283db2dd031215d50c42e4d16993f2e686c38ba4bb04ffa4d1028eac2f896f52df05c99f31
@@ -4,21 +4,52 @@ on: [push, pull_request]
4
4
 
5
5
  jobs:
6
6
  test:
7
+ name: Test with MySQL ${{ matrix.mysql }} / PostgreSQL 16, Ruby ${{ matrix.ruby }}
7
8
  runs-on: ubuntu-latest
8
9
  strategy:
10
+ fail-fast: false
9
11
  matrix:
10
- ruby: ['2.4', '2.5', '2.6', '2.7', '3.0', '3.1']
12
+ mysql: ['5.7', '8.0']
13
+ ruby: ['3.1', '3.2', '3.3', '3.4']
14
+ services:
15
+ mysql:
16
+ image: mysql:${{ matrix.mysql }}
17
+ env:
18
+ MYSQL_ROOT_PASSWORD: root
19
+ ports:
20
+ - 3306:3306
21
+ options: >-
22
+ --health-cmd "mysqladmin ping"
23
+ --health-interval 10s
24
+ --health-timeout 5s
25
+ --health-retries 5
26
+ postgres:
27
+ image: postgres:16
28
+ env:
29
+ POSTGRES_PASSWORD: postgres
30
+ ports:
31
+ - 5432:5432
32
+ options: >-
33
+ --health-cmd pg_isready
34
+ --health-interval 10s
35
+ --health-timeout 5s
36
+ --health-retries 5
11
37
  steps:
12
- - uses: actions/checkout@v2
38
+ - uses: actions/checkout@v4
39
+ - name: Install PostgreSQL and SQLite client libraries
40
+ run: sudo apt-get update && sudo apt-get install -y libpq-dev libsqlite3-dev sqlite3
13
41
  - name: Set up Ruby
14
42
  uses: ruby/setup-ruby@v1
15
43
  with:
16
44
  ruby-version: ${{ matrix.ruby }}
45
+ bundler-cache: true
17
46
  - name: Install dependencies
18
47
  run: bundle install
19
- - name: Start MySQL
20
- run: sudo systemctl start mysql.service
21
48
  - name: Prepare database
22
49
  run: bundle exec rake db:convergence:prepare
50
+ - name: Prepare PostgreSQL database
51
+ run: bundle exec rake db:convergence:postgres:prepare
52
+ - name: Prepare SQLite database
53
+ run: bundle exec rake db:convergence:sqlite:prepare
23
54
  - name: Run tests
24
55
  run: bundle exec rake spec
data/.gitignore CHANGED
@@ -2,6 +2,7 @@ tmp/
2
2
  tags
3
3
  database.yml
4
4
  pkg/
5
+ spec/fixtures/sqlite_test_db.sqlite3
5
6
 
6
7
  .bundle
7
8
  .ruby-version
data/CHANGELOG.md CHANGED
@@ -1,3 +1,49 @@
1
+ ## Convergence 1.1.0 (August 25, 2026) ##
2
+
3
+ * Add PostgreSQL support (PR: #93)
4
+
5
+ *nishio-dens*
6
+
7
+ * Add SQLite support (PR: #95)
8
+
9
+ *nishio-dens*
10
+
11
+ * Support ENUM/SET column types (PR: #92)
12
+
13
+ *nishio-dens*
14
+
15
+ * Add `execute()` DSL method to run raw SQL (PR: #94)
16
+
17
+ *nishio-dens*
18
+
19
+ * Add `--dump-rails-migration` option to export DB schema as a Rails migration file (PR: #96)
20
+
21
+ *nishio-dens*
22
+
23
+ * Add `--safe-migration` option to skip DROP TABLE queries (PR: #91)
24
+
25
+ *nishio-dens*
26
+
27
+ * Add `--ignore-auto-increment` option (PR: #90)
28
+
29
+ *nishio-dens*
30
+
31
+ * Fix MySQL 8.0 test failure and modernize CI Ruby matrix (PR: #89)
32
+
33
+ *nishio-dens*
34
+
35
+ * Bump diffy to >= 3.4.2, fixing CVE-2022-33127 (PR: #100)
36
+
37
+ *nishio-dens*
38
+
39
+ * Add specs for previously untested classes/commands (PR: #97)
40
+
41
+ *nishio-dens*
42
+
43
+ * Add CONTRIBUTING.md (PR: #98)
44
+
45
+ *nishio-dens*
46
+
1
47
  ## Convergence 1.0.6 (April 28, 2022) ##
2
48
 
3
49
  * Fixes YAML.safe_load arguments (PR: #84)
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,80 @@
1
+ # Contributing to Convergence
2
+
3
+ Thanks for considering a contribution! This document covers everything you need to get a development
4
+ environment running, test your change, and open a pull request.
5
+
6
+ ## Development setup
7
+
8
+ Convergence supports MySQL, PostgreSQL, and SQLite, so the test suite exercises all three. You don't need
9
+ every adapter running to work on a change that's scoped to one of them, but `rake spec` runs the whole suite
10
+ regardless of which adapter you touched.
11
+
12
+ 1. Install Ruby (see `required_ruby_version` in `convergence.gemspec` for the minimum version) and Bundler.
13
+ 2. Install dependencies:
14
+
15
+ ```
16
+ $ bundle install
17
+ ```
18
+
19
+ `sqlite3`, `mysql2`, and `pg` are native extensions; if a gem fails to build, make sure the corresponding
20
+ client library/headers are installed for your OS (e.g. `libmysqlclient-dev`, `libpq-dev`, `libsqlite3-dev`
21
+ on Debian/Ubuntu, or the Homebrew equivalents on macOS).
22
+ 3. Start MySQL and PostgreSQL (locally, in Docker, however you prefer) and set connection details for all
23
+ three adapters in `spec/config/spec_database.yml` if the defaults don't match your setup.
24
+ 4. Prepare the test databases:
25
+
26
+ ```
27
+ $ bundle exec rake db:convergence:prepare # MySQL
28
+ $ bundle exec rake db:convergence:postgres:prepare # PostgreSQL
29
+ $ bundle exec rake db:convergence:sqlite:prepare # SQLite (no server needed, just a file)
30
+ ```
31
+
32
+ ## Running the tests
33
+
34
+ ```
35
+ $ bundle exec rspec
36
+ ```
37
+
38
+ or, equivalently:
39
+
40
+ ```
41
+ $ bundle exec rake spec
42
+ ```
43
+
44
+ If you change the schema of the shared test fixtures (`spec/fixtures/test_db.sql`,
45
+ `spec/fixtures/postgres_test_db.sql`, `spec/fixtures/sqlite_test_db.sql`), rebuild the affected database with
46
+ the corresponding `overhaul` task (e.g. `rake db:convergence:overhaul`) before rerunning the suite.
47
+
48
+ ## Code style
49
+
50
+ The project uses RuboCop (see `.rubocop.yml` for the enabled/disabled cops); there's no dedicated Rake task
51
+ wired up, so run it directly if you want to check your changes:
52
+
53
+ ```
54
+ $ bundle exec rubocop
55
+ ```
56
+
57
+ ## Adding support for a new adapter capability
58
+
59
+ Convergence's DSL uses MySQL terminology for column types so that schema files stay close to portable across
60
+ adapters. If you're adding a feature to an existing adapter (or a new adapter entirely), the adapter-specific
61
+ logic lives in four places per adapter:
62
+
63
+ - `lib/convergence/database_connector/*_connector.rb` — opens the DB connection.
64
+ - `lib/convergence/dumper/*_schema_dumper.rb` — reads the current schema from the database.
65
+ - `lib/convergence/sql_generator/*_generator.rb` — turns a diff into `CREATE`/`ALTER`/`DROP` SQL.
66
+ - `lib/convergence/default_parameter/*_default_parameter.rb` — fills in / strips adapter-specific defaults
67
+ (e.g. MySQL's default charset) so diffing doesn't produce spurious noise.
68
+
69
+ All four are dispatched by adapter name in `lib/convergence/database_connector.rb`, `lib/convergence/command.rb`,
70
+ `lib/convergence/command/apply.rb`, and `lib/convergence/default_parameter.rb` respectively — a new adapter
71
+ needs a `when` branch added to each. Add a parallel fixture database/schema files under `spec/fixtures/` and a
72
+ matching test suite (see the existing `spec/postgres_integrations/` or `spec/sqlite_integrations/` for the
73
+ pattern) so the new adapter is covered end-to-end.
74
+
75
+ ## Opening a pull request
76
+
77
+ - Keep pull requests focused on a single change; it makes review (and `CHANGELOG.md`/release notes) easier.
78
+ - Add or update specs for any behavior change.
79
+ - Update `README.md` if you're adding a user-facing option or adapter capability.
80
+ - Reference the issue you're addressing (e.g. `Closes #123`) in the pull request description.
data/Gemfile.lock CHANGED
@@ -1,19 +1,22 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- convergence (1.0.6)
4
+ convergence (1.1.0)
5
5
  diff-lcs
6
- diffy
6
+ diffy (>= 3.4.2)
7
7
  mysql2
8
+ pg
9
+ sqlite3
8
10
  thor
9
11
 
10
12
  GEM
11
13
  remote: https://rubygems.org/
12
14
  specs:
13
15
  ast (2.4.1)
16
+ bigdecimal (4.1.2)
14
17
  coderay (1.1.3)
15
18
  diff-lcs (1.4.4)
16
- diffy (3.4.0)
19
+ diffy (3.4.4)
17
20
  ffi (1.14.2)
18
21
  formatador (0.2.5)
19
22
  guard (2.16.2)
@@ -30,12 +33,15 @@ GEM
30
33
  guard (~> 2.1)
31
34
  guard-compat (~> 1.1)
32
35
  rspec (>= 2.99.0, < 4.0)
36
+ io-console (0.9.2)
33
37
  listen (3.3.3)
34
38
  rb-fsevent (~> 0.10, >= 0.10.3)
35
39
  rb-inotify (~> 0.9, >= 0.9.10)
36
40
  lumberjack (1.2.8)
37
- method_source (1.0.0)
38
- mysql2 (0.5.3)
41
+ method_source (1.1.0)
42
+ mini_portile2 (2.8.9)
43
+ mysql2 (0.5.7)
44
+ bigdecimal
39
45
  nenv (0.3.0)
40
46
  notiffany (0.1.3)
41
47
  nenv (~> 0.1)
@@ -43,15 +49,20 @@ GEM
43
49
  parallel (1.20.1)
44
50
  parser (3.0.0.0)
45
51
  ast (~> 2.4.1)
46
- pry (0.13.1)
52
+ pg (1.6.3)
53
+ pg (1.6.3-x86_64-linux)
54
+ pry (0.16.0)
47
55
  coderay (~> 1.1)
48
56
  method_source (~> 1.0)
57
+ reline (>= 0.6.0)
49
58
  rainbow (3.0.0)
50
- rake (13.0.3)
59
+ rake (13.4.2)
51
60
  rb-fsevent (0.10.4)
52
61
  rb-inotify (0.10.1)
53
62
  ffi (~> 1.0)
54
63
  regexp_parser (2.0.3)
64
+ reline (0.7.0)
65
+ io-console (~> 0.5)
55
66
  rexml (3.2.5)
56
67
  rspec (3.10.0)
57
68
  rspec-core (~> 3.10.0)
@@ -79,11 +90,15 @@ GEM
79
90
  parser (>= 2.7.1.5)
80
91
  ruby-progressbar (1.11.0)
81
92
  shellany (0.0.1)
93
+ sqlite3 (2.8.1)
94
+ mini_portile2 (~> 2.8.0)
95
+ sqlite3 (2.8.1-x86_64-linux-gnu)
82
96
  thor (1.0.1)
83
97
  unicode-display_width (1.7.0)
84
98
 
85
99
  PLATFORMS
86
100
  ruby
101
+ x86_64-linux
87
102
 
88
103
  DEPENDENCIES
89
104
  convergence!
@@ -95,4 +110,4 @@ DEPENDENCIES
95
110
  rubocop
96
111
 
97
112
  BUNDLED WITH
98
- 2.2.32
113
+ 4.0.19
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Convergence
2
2
 
3
3
  Convergence is a pure-Ruby database schema migration tool.
4
- Currently, This tool is support only MySQL.
4
+ Currently, This tool supports MySQL, PostgreSQL, and SQLite.
5
5
 
6
6
  It defines DB Schema using Convergence DSL(like Rails DSL).
7
7
  For more information about Convergence DSL, See below ['Detail About Convergence DSL'](#detail-about-convergence-dsl)
@@ -133,8 +133,10 @@ Create Table: CREATE TABLE `test_tables` (
133
133
  ```
134
134
  Commands:
135
135
  convergence apply FILE -c, --config=CONFIG # execute sql to your database
136
+ # [--dry-run], [--rollback-dry-run], [--safe-migration], [--ignore-auto-increment]
136
137
  convergence diff FILE1 FILE2 # print diff of DSLs
137
138
  convergence export -c, --config=CONFIG # export db schema to dsl
139
+ # [--dump-rails-migration], [--filename=FILENAME]
138
140
  convergence help [COMMAND] # Describe available commands or one specific command
139
141
  convergence version # print the version
140
142
  ```
@@ -152,6 +154,49 @@ username: root
152
154
  password:
153
155
  ```
154
156
 
157
+ To use PostgreSQL instead, set `adapter: postgresql` (`postgres`/`pg` also work):
158
+
159
+ ```
160
+ $ cat database.yml
161
+ adapter: postgresql
162
+ database: convergence_test
163
+ host: 127.0.0.1
164
+ username: postgres
165
+ password:
166
+ ```
167
+
168
+ #### PostgreSQL notes
169
+
170
+ The Convergence DSL uses MySQL terminology for column types (`tinyint`, `mediumint`, `datetime`, ...) so the
171
+ same schema files stay close to portable across adapters; the PostgreSQL adapter maps them to their closest
172
+ native PostgreSQL type (e.g. `mediumint`/`int` → `integer`, `datetime` → `timestamp`, `json` → `jsonb`,
173
+ `extra: 'auto_increment'` → `GENERATED BY DEFAULT AS IDENTITY`, requiring PostgreSQL 10+).
174
+
175
+ A few MySQL-specific concepts have no PostgreSQL equivalent and are ignored on that adapter: `engine`,
176
+ `row_format`, `default_charset`/`character_set`, and `collate`. `enum`/`set` column types are not supported yet
177
+ on PostgreSQL.
178
+
179
+ To use SQLite instead, set `adapter: sqlite3` (`sqlite` also works) and point `database` at a file path:
180
+
181
+ ```
182
+ $ cat database.yml
183
+ adapter: sqlite3
184
+ database: /path/to/development.sqlite3
185
+ ```
186
+
187
+ #### SQLite notes
188
+
189
+ SQLite's `ALTER TABLE` support is intentionally limited by SQLite itself: it can only add/drop columns and
190
+ create/drop indexes on an existing table. Changing a column's type/null/default, and adding or removing a
191
+ foreign key or primary key on an existing table, all require SQLite's own "rebuild the table" procedure
192
+ (create a new table, copy the data over, drop the old one, rename it), which this adapter does not implement
193
+ yet -- it raises `NotImplementedError` instead of generating SQL that SQLite would reject. Creating a brand new
194
+ table with any of the above (including foreign keys) works fine.
195
+
196
+ Like PostgreSQL, MySQL-only table options (`engine`, `row_format`, `default_charset`/`character_set`, `collate`,
197
+ table/column `comment`) have no SQLite equivalent and are ignored. `extra: 'auto_increment'` maps to SQLite's
198
+ `INTEGER PRIMARY KEY AUTOINCREMENT`.
199
+
155
200
  #### Use SSL connection
156
201
 
157
202
  If you would like to use SSL connection, you can specify SSL options in database.yml
@@ -216,6 +261,43 @@ create_table "paper_authors", collate: "utf8_general_ci", comment: "Paper Author
216
261
  end
217
262
  ```
218
263
 
264
+ ### Export as a Rails migration file
265
+
266
+ If you're migrating to (or working alongside) a Rails app, pass `--dump-rails-migration` to export the schema as an
267
+ ActiveRecord migration instead of a Convergence DSL file. `--filename` sets the base filename/class name (Rails'
268
+ usual `snake_case` filename / `CamelCase` class name convention applies).
269
+
270
+ ```
271
+ $ convergence export -c database.yml --dump-rails-migration --filename create_initial_tables
272
+ ```
273
+
274
+ ```ruby
275
+ # Filename: 20240101000000_create_initial_tables.rb
276
+ class CreateInitialTables < ActiveRecord::Migration[7.0]
277
+ def change
278
+ create_table :authors do |t|
279
+ t.string :name, limit: 110
280
+ t.timestamps
281
+
282
+ t.index :created_at, name: "index_authors_on_created_at"
283
+ end
284
+ end
285
+ end
286
+ ```
287
+
288
+ The output is written to stdout with a `# Filename: ...` comment on top (including a timestamp prefix in the usual
289
+ Rails migration filename format) — redirect it to that file yourself, e.g.
290
+ `convergence export ... > db/migrate/$(date +%Y%m%d%H%M%S)_create_initial_tables.rb`.
291
+
292
+ A few notes on the conversion:
293
+ * An `id` column that matches Rails' implicit auto-incrementing primary key (`int`/`bigint`, `primary_key: true`,
294
+ `extra: 'auto_increment'`) is omitted, since `create_table` adds it automatically.
295
+ * `created_at`/`updated_at` columns with matching `datetime`/`null` options are collapsed into `t.timestamps`.
296
+ * Column types use Rails' type names (e.g. `varchar` → `string`, `datetime`/`timestamp` → `datetime`); `enum`/`set`
297
+ have no ActiveRecord equivalent and fall back to `string`.
298
+ * MySQL-only concepts with no `create_table` DSL equivalent (`character_set`, `collate`, `extra`, `after`) are
299
+ dropped from column definitions.
300
+
219
301
  ### Dry run
220
302
 
221
303
  ```
@@ -235,6 +317,29 @@ $ convergence apply example.schema -c database.yml --rollback-dry-run
235
317
  $ convergence apply example.schema -c database.yml
236
318
  ```
237
319
 
320
+ ### Ignore AUTO_INCREMENT changes
321
+
322
+ If you dump your schema from a database with a high AUTO_INCREMENT value (e.g. a dev/staging box you've been testing on)
323
+ and apply it to another database, convergence will normally generate a query that bumps AUTO_INCREMENT to match, which
324
+ can jump/skip a large range of ids. Pass `--ignore-auto-increment` to skip generating AUTO_INCREMENT change queries entirely.
325
+
326
+ ```
327
+ $ convergence apply example.schema -c database.yml --ignore-auto-increment
328
+ ```
329
+
330
+ This also applies to `--dry-run` and `--rollback-dry-run`.
331
+
332
+ ### Safe migration
333
+
334
+ If you want to prevent convergence from ever dropping a table (e.g. when the DSL file removed a table that still
335
+ has important data), pass `--safe-migration`. It skips generating `DROP TABLE` queries; every other change is still applied.
336
+
337
+ ```
338
+ $ convergence apply example.schema -c database.yml --safe-migration
339
+ ```
340
+
341
+ This also applies to `--dry-run` and `--rollback-dry-run`.
342
+
238
343
  ### Include Other Schema files
239
344
 
240
345
  ```
@@ -242,6 +347,20 @@ include 'first_schema.schema'
242
347
  include 'other_file.schema'
243
348
  ```
244
349
 
350
+ ### Execute raw SQL
351
+
352
+ For things the DSL has no dedicated syntax for (views, triggers, stored procedures, data backfills, grants, ...),
353
+ use `execute` to run an arbitrary SQL statement. Unlike `create_table`, statements passed to `execute` are **not**
354
+ diffed against the current schema: they run every time the schema file is applied, so make sure the SQL itself is
355
+ idempotent (e.g. `CREATE OR REPLACE VIEW ...`, `CREATE TABLE IF NOT EXISTS ...`).
356
+
357
+ ```
358
+ execute "CREATE OR REPLACE VIEW active_users AS SELECT * FROM users WHERE active = 1"
359
+ ```
360
+
361
+ `execute` statements always run after the generated schema changes, and are skipped by `--rollback-dry-run` (there's
362
+ no way to know how to reverse an arbitrary SQL statement).
363
+
245
364
 
246
365
  ## Detail About Convergence DSL
247
366
 
@@ -273,6 +392,8 @@ Convergence is currently support column types below.
273
392
  - timestamp
274
393
  - year
275
394
  - json
395
+ - enum
396
+ - set
276
397
 
277
398
  ```
278
399
  create_table "tests", comment: 'Column type example' do |t|
@@ -282,6 +403,8 @@ create_table "tests", comment: 'Column type example' do |t|
282
403
  t.varchar 'test_string', null: true, default: 'hello', limit: 300
283
404
  t.text 'text_col'
284
405
  t.datetime 'created_at'
406
+ t.enum 'status', values: %w(active inactive pending), default: 'active'
407
+ t.set 'flags', values: %w(a b c)
285
408
  end
286
409
  ```
287
410
 
@@ -344,9 +467,16 @@ end
344
467
 
345
468
  ```
346
469
  $ bundle exec rake db:convergence:prepare
470
+ $ bundle exec rake db:convergence:postgres:prepare
471
+ $ bundle exec rake db:convergence:sqlite:prepare
347
472
  $ bundle exec rspec
348
473
  ```
349
474
 
475
+ ## Contributing
476
+
477
+ Bug reports and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for how to set up a
478
+ development environment, run the test suite, and what to include in a pull request.
479
+
350
480
  ## Copyright
351
481
 
352
482
  Copyright © 2014-2018 S.nishio. See LICENSE.txt for further details.
data/Rakefile CHANGED
@@ -5,8 +5,10 @@ require 'yaml'
5
5
  RSpec::Core::RakeTask.new('spec')
6
6
  task default: :spec
7
7
 
8
- mysql_settings = YAML.load_file("#{File.dirname(__FILE__)}/spec/config/spec_database.yml")['mysql']
9
- mysql_settings = Hash[mysql_settings.map { |k, v| [k.to_sym, v] }]
8
+ spec_database_settings = YAML.load_file("#{File.dirname(__FILE__)}/spec/config/spec_database.yml")
9
+ mysql_settings = Hash[spec_database_settings['mysql'].map { |k, v| [k.to_sym, v] }]
10
+ postgres_settings = Hash[spec_database_settings['postgresql'].map { |k, v| [k.to_sym, v] }]
11
+ sqlite_db_path = File.expand_path("#{File.dirname(__FILE__)}/spec/fixtures/#{spec_database_settings['sqlite3']['database']}")
10
12
 
11
13
  namespace :db do
12
14
  namespace :convergence do
@@ -30,5 +32,46 @@ namespace :db do
30
32
  desc 'Prepare the test databases'
31
33
  task prepare: [:build_databases, :create_tables]
32
34
  task overhaul: [:drop_databases, :build_databases, :create_tables]
35
+
36
+ namespace :postgres do
37
+ desc 'Build the PostgreSQL database for tests'
38
+ task :build_databases do
39
+ ENV['PGPASSWORD'] = postgres_settings[:password].to_s
40
+ system("psql -U #{postgres_settings[:username]} -h #{postgres_settings[:host]} -p #{postgres_settings[:port]} -d postgres -c 'create database #{postgres_settings[:database]};'")
41
+ end
42
+
43
+ task :drop_databases do
44
+ ENV['PGPASSWORD'] = postgres_settings[:password].to_s
45
+ system("psql -U #{postgres_settings[:username]} -h #{postgres_settings[:host]} -p #{postgres_settings[:port]} -d postgres -c 'drop database if exists #{postgres_settings[:database]};'")
46
+ end
47
+
48
+ desc 'Create tables on the PostgreSQL test database'
49
+ task :create_tables do
50
+ ENV['PGPASSWORD'] = postgres_settings[:password].to_s
51
+ query_path = "#{File.dirname(__FILE__)}/spec/fixtures/postgres_test_db.sql"
52
+ system("psql -U #{postgres_settings[:username]} -h #{postgres_settings[:host]} -p #{postgres_settings[:port]} -d #{postgres_settings[:database]} -f #{query_path}")
53
+ end
54
+
55
+ desc 'Prepare the PostgreSQL test database'
56
+ task prepare: [:build_databases, :create_tables]
57
+ task overhaul: [:drop_databases, :build_databases, :create_tables]
58
+ end
59
+
60
+ namespace :sqlite do
61
+ desc 'Remove the SQLite test database file'
62
+ task :drop_databases do
63
+ File.delete(sqlite_db_path) if File.exist?(sqlite_db_path)
64
+ end
65
+
66
+ desc 'Create tables on the SQLite test database'
67
+ task :create_tables do
68
+ query_path = "#{File.dirname(__FILE__)}/spec/fixtures/sqlite_test_db.sql"
69
+ system("sqlite3 #{sqlite_db_path} < #{query_path}")
70
+ end
71
+
72
+ desc 'Prepare the SQLite test database'
73
+ task prepare: [:drop_databases, :create_tables]
74
+ task overhaul: [:drop_databases, :create_tables]
75
+ end
33
76
  end
34
77
  end
data/convergence.gemspec CHANGED
@@ -19,8 +19,10 @@ Gem::Specification.new do |spec|
19
19
  spec.require_paths = ['lib']
20
20
 
21
21
  spec.add_dependency 'mysql2'
22
+ spec.add_dependency 'pg'
23
+ spec.add_dependency 'sqlite3'
22
24
  spec.add_dependency 'diff-lcs'
23
- spec.add_dependency 'diffy'
25
+ spec.add_dependency 'diffy', '>= 3.4.2'
24
26
  spec.add_dependency 'thor'
25
27
 
26
28
  spec.required_ruby_version = ">= 2.4.0"
@@ -11,13 +11,19 @@ class Convergence::CLI < Thor
11
11
  method_option :config, aliases: '-c', type: :string, required: true, desc: 'Database Yaml Setting'
12
12
  method_option :dry_run, type: :boolean
13
13
  method_option :rollback_dry_run, type: :boolean
14
+ method_option :ignore_auto_increment, type: :boolean, desc: 'Do not generate AUTO_INCREMENT change queries'
15
+ method_option :safe_migration, type: :boolean, desc: 'Do not generate DROP TABLE queries'
14
16
 
15
17
  def self.exit_on_failure?
16
18
  true
17
19
  end
18
20
 
19
21
  def apply(file)
20
- opts = { input: file }
22
+ opts = {
23
+ input: file,
24
+ ignore_auto_increment: options[:ignore_auto_increment],
25
+ safe_migration: options[:safe_migration]
26
+ }
21
27
  if options[:dry_run]
22
28
  require 'convergence/command/dryrun'
23
29
  Convergence::Command::Dryrun.new(opts, config: config).execute
@@ -39,9 +45,11 @@ class Convergence::CLI < Thor
39
45
 
40
46
  desc 'export', 'export db schema to dsl'
41
47
  method_option :config, aliases: '-c', type: :string, required: true, desc: 'Database Yaml Setting'
48
+ method_option :dump_rails_migration, type: :boolean, desc: 'Output an ActiveRecord migration file instead of a Convergence DSL file'
49
+ method_option :filename, type: :string, desc: 'Base filename (without timestamp/extension) used for --dump-rails-migration'
42
50
  def export
43
51
  require 'convergence/command/export'
44
- opts = {}
52
+ opts = { dump_rails_migration: options[:dump_rails_migration], filename: options[:filename] }
45
53
  Convergence::Command::Export.new(opts, config: config).execute
46
54
  end
47
55
 
@@ -8,40 +8,69 @@ require 'convergence/diff'
8
8
  class Convergence::Command::Apply < Convergence::Command
9
9
  def execute
10
10
  current_dir_path = Pathname.new(@opts[:input]).realpath.dirname
11
- input_tables = Convergence::DSL.parse(File.open(@opts[:input]).read, current_dir_path)
11
+ dsl = Convergence::DSL.parse_dsl(File.open(@opts[:input]).read, current_dir_path)
12
12
  current_tables = dumper.dump
13
- execute_sql(input_tables, current_tables)
13
+ execute_sql(dsl.tables, current_tables, dsl.raw_sqls)
14
14
  end
15
15
 
16
- def generate_sql(input_tables, current_tables)
16
+ def generate_sql(input_tables, current_tables, raw_sqls = [])
17
17
  current_tables_with_full_option =
18
18
  Convergence::DefaultParameter.append_database_default_parameter(current_tables, database_adapter)
19
19
  input_tables_with_full_option =
20
20
  Convergence::DefaultParameter.append_database_default_parameter(input_tables, database_adapter)
21
- delta = Convergence::Diff.new.diff(current_tables_with_full_option, input_tables_with_full_option)
22
- sql_generator.generate(input_tables_with_full_option, delta, current_tables_with_full_option)
21
+ delta = Convergence::Diff
22
+ .new(ignore_auto_increment: @opts[:ignore_auto_increment])
23
+ .diff(current_tables_with_full_option, input_tables_with_full_option)
24
+ sql = sql_generator.generate(
25
+ input_tables_with_full_option,
26
+ delta,
27
+ current_tables_with_full_option,
28
+ safe_migration: @opts[:safe_migration]
29
+ )
30
+ append_raw_sqls(sql, raw_sqls)
23
31
  end
24
32
 
25
33
  private
26
34
 
35
+ def append_raw_sqls(sql, raw_sqls)
36
+ return sql if raw_sqls.empty?
37
+ raw_sql_block = raw_sqls.map { |q| q.strip.end_with?(';') ? q.strip : "#{q.strip};" }.join("\n")
38
+ [sql, raw_sql_block].reject(&:empty?).join("\n")
39
+ end
40
+
27
41
  def sql_generator
28
42
  @sql_generator ||= case database_adapter
29
43
  when 'mysql', 'mysql2'
30
44
  require 'convergence/sql_generator/mysql_generator'
31
45
  SQLGenerator::MysqlGenerator.new
46
+ when 'postgresql', 'postgres', 'pg'
47
+ require 'convergence/sql_generator/postgres_generator'
48
+ SQLGenerator::PostgresGenerator.new
49
+ when 'sqlite3', 'sqlite'
50
+ require 'convergence/sql_generator/sqlite_generator'
51
+ SQLGenerator::SqliteGenerator.new
32
52
  else
33
53
  fail NotImplementedError.new('unknown database adapter')
34
54
  end
35
55
  end
36
56
 
37
- def execute_sql(input_tables, current_tables)
38
- sql = generate_sql(input_tables, current_tables)
39
- unless sql.strip.empty?
40
- sql = <<-SQL
57
+ def wrap_with_constraint_pragma(sql)
58
+ case database_adapter
59
+ when 'mysql', 'mysql2'
60
+ <<-SQL
41
61
  SET FOREIGN_KEY_CHECKS=0;
42
62
  #{sql}
43
63
  SET FOREIGN_KEY_CHECKS=1;
44
64
  SQL
65
+ else
66
+ sql
67
+ end
68
+ end
69
+
70
+ def execute_sql(input_tables, current_tables, raw_sqls = [])
71
+ sql = generate_sql(input_tables, current_tables, raw_sqls)
72
+ unless sql.strip.empty?
73
+ sql = wrap_with_constraint_pragma(sql)
45
74
  end
46
75
  sql.split(';').each do |q2|
47
76
  q = q2.strip