exwiw 0.3.0 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +17 -1
- data/README.md +24 -8
- data/lib/exwiw/adapter/{mysql2_adapter.rb → mysql_adapter.rb} +14 -22
- data/lib/exwiw/adapter/mysql_client.rb +115 -0
- data/lib/exwiw/adapter/postgresql_adapter.rb +8 -6
- data/lib/exwiw/adapter/{sqlite3_adapter.rb → sqlite_adapter.rb} +10 -8
- data/lib/exwiw/adapter.rb +36 -6
- data/lib/exwiw/belongs_to.rb +4 -3
- data/lib/exwiw/cli.rb +25 -18
- data/lib/exwiw/mongoid_schema_generator.rb +6 -4
- data/lib/exwiw/query_ast.rb +5 -5
- data/lib/exwiw/query_ast_builder.rb +14 -13
- data/lib/exwiw/schema_generator.rb +24 -21
- data/lib/exwiw/table_config.rb +6 -5
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +3 -2
- metadata +4 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 83bb3c7245d9174e5d16e85e6de2a530b58ca445b570d692c5198682fc43b9ef
|
|
4
|
+
data.tar.gz: cee17f919dab949c82942eae146a1d6182909ab6ef595e8cfc7d4c3f744d1ede
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d655b0e5d55759932d7ab6646cb9ed9d0b699e6c96e6061863091eded7914c736b74c558e8a96d132ea94ef668376ae372a9eacf23f978d52540bf31c38c7c18
|
|
7
|
+
data.tar.gz: 5e068572f70d2b1869e5dffceba9f681d0b153c0947da3fd7b35d746e7f33bbc6c2d5fa25d23c9dfa0f190e14d3304ec6f2940aa19168749aa05b303c572ea10
|
data/CHANGELOG.md
CHANGED
|
@@ -2,13 +2,29 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.2] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- Adapter names are now driver-agnostic: `--adapter=mysql` and `--adapter=sqlite` replace the driver-flavored `mysql2` / `sqlite3` as the canonical names (`postgresql` and `mongodb` are unchanged). The CLI stays backward compatible — `mysql2` and `sqlite3` are accepted as aliases and folded onto the canonical name, and the matching is case-insensitive so a Rails app's `connection.adapter_name` (e.g. `Mysql2`, `SQLite`) is absorbed too. There is intentionally no `trilogy` adapter *name*: the MySQL adapter now connects through whichever of the `mysql2` or `trilogy` gem is installed (preferring `mysql2`), so an app on either driver works by passing `--adapter=mysql`. trilogy's typed result values are normalized back to the same raw strings `mysql2`'s `cast: false` returns, so the generated SQL is identical regardless of driver. **Breaking** only for code using the library classes directly: `Exwiw::Adapter::Mysql2Adapter` → `Exwiw::Adapter::MysqlAdapter` and `Exwiw::Adapter::Sqlite3Adapter` → `Exwiw::Adapter::SqliteAdapter`. The `EXWIW_DATABASE_ADAPTER` value passed to `--after-insert-hook` is now the canonical name (`mysql` / `sqlite`).
|
|
10
|
+
|
|
11
|
+
## [0.3.1] - 2026-05-31
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- **Breaking:** the `dump` subcommand is renamed to `export` to match the gem name (Export What I Want). Invoke `exwiw export ...` (or omit the subcommand, which now defaults to `export`) instead of `exwiw dump ...`. There is no `dump` alias.
|
|
16
|
+
|
|
5
17
|
## [0.3.0] - 2026-05-31
|
|
6
18
|
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- New `--ids-column=COLUMN` CLI option matches `--ids` against an arbitrary column on the target table instead of its primary key (e.g. `--target-table=users --ids=alice@example.com --ids-column=email`). This is the SQL-adapter (mysql2/postgresql/sqlite3) counterpart of the mongodb `--ids-field`; the two are mutually exclusive and each is rejected by the other adapter family (`--ids-field` is mongodb-only, `--ids-column` is sql-only), mirroring the existing `--target-table` / `--target-collection` split. Related tables are still extracted correctly: rather than propagating `--ids` directly onto foreign keys (which would be wrong when filtering on a non-primary-key column), each foreign key is resolved through the target via a subquery (`WHERE fk IN (SELECT pk FROM target WHERE COLUMN IN (...))`), so only the target table's filter column changes and direct / indirect / polymorphic relations all extract correctly. Note: if `COLUMN` is itself masked, re-running `delete-*` against an already-imported (masked) dump won't match, so prefer a stable natural key. ([#47](https://github.com/heyinc/exwiw/pull/47))
|
|
22
|
+
|
|
7
23
|
## [0.2.9] - 2026-05-31
|
|
8
24
|
|
|
9
25
|
### Added
|
|
10
26
|
|
|
11
|
-
- New `--ids-field=FIELD` CLI option matches `--ids` against an arbitrary field on the target collection instead of its primary key (e.g. `--target-collection=users --ids=a@example.com --ids-field=email`). Only the target collection's filter changes — downstream foreign-key propagation still keys off the primary key. Unlike the primary-key path, the supplied ids are **not** type-coerced (a custom field's stored type is unknown, so values are passed through as-is).
|
|
27
|
+
- New `--ids-field=FIELD` CLI option matches `--ids` against an arbitrary field on the target collection instead of its primary key (e.g. `--target-collection=users --ids=a@example.com --ids-field=email`). Only the target collection's filter changes — downstream foreign-key propagation still keys off the primary key. Unlike the primary-key path, the supplied ids are **not** type-coerced (a custom field's stored type is unknown, so values are passed through as-is). This flag is **mongodb-only**.
|
|
12
28
|
- New `--target-collection=COLLECTION` CLI option, a mongodb-only alias of `--target-table`. Specifying both, or using `--target-collection` with a non-mongodb adapter, is rejected at validation time.
|
|
13
29
|
- New rake task `exwiw:schema:generate_mongoid` (backed by `Exwiw::MongoidSchemaGenerator`) generates `MongodbCollectionConfig` files by introspecting Mongoid document models — a separate task/class from the ActiveRecord `schema:generate` because the ORMs expose different metadata. It derives the collection name, the `_id` primary key, `fields` (including referenced `belongs_to` foreign keys), `belongs_tos` from referenced `belongs_to` associations, and `embedded_in` from `embedded_in` / `embeds_many` / `embeds_one` associations (each embedded config names its immediate parent collection and `store_as` document key; nested embedding is emitted as a chain — `comments` embedded_in `posts`, `posts` embedded_in `users` — so the adapter can recurse through both array and Hash subdocuments). Regeneration preserves hand-edited `replace_with` / `filter` / `skip` / `bulk_insert_chunk_size`. Polymorphic `belongs_to` is not yet expanded. Models in an inheritance hierarchy whose subclasses share the base's collection (Mongoid STI, `_type` discriminator) collapse into a single config: subclasses are discovered via `descendants` (Mongoid registers only the base in `Mongoid.models`) and every class's `fields` / `belongs_tos` are unioned, so subclass-only fields and associations are preserved. A referenced `belongs_to` declared on an *embedded* document (e.g. `Comment embedded_in :post, belongs_to :author`) is dropped from the embedded config's `belongs_tos` (cross-collection refs from inside embedded subdocuments are unsupported and rejected on load), while its foreign-key column is still kept as an ordinary field. A `has_and_belongs_to_many` association is likewise dropped from `belongs_tos` (its foreign keys are stored as an array field such as `tag_ids`, which exwiw cannot follow as a single-valued foreign key), while that `*_ids` array column is kept as an ordinary field. A *polymorphic* `embedded_in` (`embedded_in :addressable, polymorphic: true`) has no single embedding parent collection and cannot be expressed as an `embedded_in` config, so the generator raises a clear, actionable error rather than crashing on the unresolvable parent class. A *self-referential / cyclic* embedding (Mongoid's `recursively_embeds_many` / `recursively_embeds_one`) makes a collection both top-level and embedded inside documents of its own type; since exwiw represents a collection as either top-level or embedded (not both), the generator likewise raises a clear error rather than emit an `embedded_in` config that would silently make the collection undumpable. The `created_at` / `updated_at` columns added by `include Mongoid::Timestamps` are tracked as ordinary fields, and their BSON `ObjectId` / `Date` values (the shape a live `find` returns) serialize as MongoDB Extended JSON (`$oid` / `$date`) through the dump path — now covered end-to-end against the generated configs. An aliased field (`field :ctry, as: :country`) is emitted by its **stored** document key (`ctry`), never the Ruby accessor (`country`), so masking and projection target the key that actually appears in the document; the accessor is additionally surfaced as `mongoid_field_name` on that field so the otherwise cryptic short key stays understandable (association aliases such as `shop => shop_id` and the built-in `id => _id` are not field renames and are not annotated).
|
|
14
30
|
|
data/README.md
CHANGED
|
@@ -37,25 +37,41 @@ gem install exwiw
|
|
|
37
37
|
|
|
38
38
|
## Supported Databases
|
|
39
39
|
|
|
40
|
-
-
|
|
40
|
+
- mysql
|
|
41
41
|
- postgresql
|
|
42
|
-
-
|
|
42
|
+
- sqlite
|
|
43
43
|
- mongodb (experimental, see [MongoDB notes](#mongodb-notes))
|
|
44
44
|
|
|
45
|
+
Adapter names are driver-agnostic — they name the database, not the Ruby driver
|
|
46
|
+
or Rails ActiveRecord adapter. For backward compatibility and to absorb the Rails
|
|
47
|
+
adapter spelling, the following aliases are also accepted (case-insensitive):
|
|
48
|
+
|
|
49
|
+
| `--adapter` value | Aliases accepted |
|
|
50
|
+
| --- | --- |
|
|
51
|
+
| `mysql` | `mysql2` |
|
|
52
|
+
| `sqlite` | `sqlite3` |
|
|
53
|
+
|
|
54
|
+
So `--adapter=mysql2` and `--adapter=mysql` both select the same MySQL adapter.
|
|
55
|
+
|
|
56
|
+
For MySQL, exwiw connects through whichever of the `mysql2` or `trilogy` gem is
|
|
57
|
+
available (preferring `mysql2`), so an app on either driver works without any
|
|
58
|
+
extra setup. There is no separate `trilogy` adapter name — pass `--adapter=mysql`
|
|
59
|
+
either way.
|
|
60
|
+
|
|
45
61
|
## Usage
|
|
46
62
|
|
|
47
63
|
exwiw has two subcommands:
|
|
48
64
|
|
|
49
|
-
- `
|
|
50
|
-
- `explain` — print the compiled SQL and its `EXPLAIN` output for each query that `
|
|
65
|
+
- `export` (default) — generate INSERT/COPY SQL files. If the subcommand is omitted, `export` is assumed.
|
|
66
|
+
- `explain` — print the compiled SQL and its `EXPLAIN` output for each query that `export` would run, without executing the SELECTs.
|
|
51
67
|
|
|
52
|
-
### `exwiw
|
|
68
|
+
### `exwiw export`
|
|
53
69
|
|
|
54
70
|
```bash
|
|
55
71
|
# dump & masking all records from database to dump.sql based on schema.json
|
|
56
72
|
# pass database password as an environment variable 'DATABASE_PASSWORD'
|
|
57
73
|
exwiw \
|
|
58
|
-
--adapter=
|
|
74
|
+
--adapter=mysql \
|
|
59
75
|
--host=localhost \
|
|
60
76
|
--port=3306 \
|
|
61
77
|
--user=reader \
|
|
@@ -92,7 +108,7 @@ This command will generate sql files in the `dump` directory.
|
|
|
92
108
|
idx means the order of the dump. bigger idx might depend on smaller idx,
|
|
93
109
|
so you should import the dump in order.
|
|
94
110
|
|
|
95
|
-
`insert-000-schema.sql` is generated by shelling out to the database client tools (`mysqldump` for `
|
|
111
|
+
`insert-000-schema.sql` is generated by shelling out to the database client tools (`mysqldump` for `mysql`, `pg_dump` for `postgresql`, and the sqlite3 driver for `sqlite`), so the corresponding client must be available on PATH when running exwiw. The output is post-processed to make it idempotent: `CREATE TABLE IF NOT EXISTS`, `CREATE INDEX IF NOT EXISTS` (where the engine supports it), and PostgreSQL's `ALTER TABLE ... ADD CONSTRAINT` statements are wrapped in `DO $$ ... EXCEPTION WHEN duplicate_object`.
|
|
96
112
|
|
|
97
113
|
you need to delete the records before importing the dump,
|
|
98
114
|
`delete-{idx}-{table_name}.sql` will help you to do that.
|
|
@@ -101,7 +117,7 @@ idx meaning is the same as insert sql.
|
|
|
101
117
|
|
|
102
118
|
### `exwiw explain`
|
|
103
119
|
|
|
104
|
-
Print the compiled SQL and its `EXPLAIN` output (estimate-only; `EXPLAIN QUERY PLAN` on SQLite) for each query that `
|
|
120
|
+
Print the compiled SQL and its `EXPLAIN` output (estimate-only; `EXPLAIN QUERY PLAN` on SQLite) for each query that `export` would run, to stdout. No SELECT is executed. Supported for `mysql`, `postgresql`, and `sqlite`. The `mongodb` adapter is not yet supported.
|
|
105
121
|
|
|
106
122
|
```bash
|
|
107
123
|
# preview the queries exwiw would run, without executing the SELECTs
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module Adapter
|
|
5
|
-
class
|
|
5
|
+
class MysqlAdapter < Base
|
|
6
6
|
def build_query(table, dump_target, table_by_name)
|
|
7
7
|
Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
|
|
8
8
|
end
|
|
@@ -11,17 +11,17 @@ module Exwiw
|
|
|
11
11
|
sql = commented_sql(query_ast)
|
|
12
12
|
|
|
13
13
|
@logger.debug(" Executing SQL: \n#{sql}")
|
|
14
|
-
connection.query(sql
|
|
14
|
+
connection.query(sql).rows
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def explain(query_ast)
|
|
18
18
|
sql = commented_sql(query_ast)
|
|
19
19
|
|
|
20
20
|
@logger.debug(" Executing EXPLAIN: \n#{sql}")
|
|
21
|
-
|
|
22
|
-
rows.each_with_index.flat_map do |row, i|
|
|
21
|
+
result = connection.query("EXPLAIN #{sql}")
|
|
22
|
+
result.rows.each_with_index.flat_map do |row, i|
|
|
23
23
|
["*************************** #{i + 1}. row ***************************"] +
|
|
24
|
-
row.map { |k, v| "#{k}: #{v}" }
|
|
24
|
+
result.fields.zip(row).map { |k, v| "#{k}: #{v}" }
|
|
25
25
|
end.join("\n")
|
|
26
26
|
end
|
|
27
27
|
|
|
@@ -138,10 +138,11 @@ module Exwiw
|
|
|
138
138
|
subquery_sql = compile_ast(subquery_ast)
|
|
139
139
|
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
140
140
|
|
|
141
|
-
# first_join.base_where_clauses
|
|
142
|
-
# (from_table_name)
|
|
143
|
-
#
|
|
144
|
-
#
|
|
141
|
+
# first_join.base_where_clauses holds conditions on the outer
|
|
142
|
+
# delete-target table (from_table_name), such as a polymorphic type
|
|
143
|
+
# column. They are not part of the subquery, so add them to the outer
|
|
144
|
+
# WHERE. This prevents deleting rows that belong to a different
|
|
145
|
+
# polymorphic type.
|
|
145
146
|
first_join.base_where_clauses.each do |where|
|
|
146
147
|
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
147
148
|
|
|
@@ -171,8 +172,9 @@ module Exwiw
|
|
|
171
172
|
sql += " AND #{compiled_where_condition}"
|
|
172
173
|
end
|
|
173
174
|
|
|
174
|
-
# base_where_clauses
|
|
175
|
-
#
|
|
175
|
+
# base_where_clauses is compiled against the joined-from table
|
|
176
|
+
# (base_table_name), e.g. the type-column filter on a polymorphic
|
|
177
|
+
# source table.
|
|
176
178
|
join.base_where_clauses.each do |where|
|
|
177
179
|
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
178
180
|
sql += " AND #{compiled_where_condition}"
|
|
@@ -255,17 +257,7 @@ module Exwiw
|
|
|
255
257
|
end
|
|
256
258
|
|
|
257
259
|
private def connection
|
|
258
|
-
@connection ||=
|
|
259
|
-
begin
|
|
260
|
-
require 'mysql2'
|
|
261
|
-
Mysql2::Client.new(
|
|
262
|
-
host: @connection_config.host,
|
|
263
|
-
port: @connection_config.port,
|
|
264
|
-
username: @connection_config.user,
|
|
265
|
-
password: @connection_config.password,
|
|
266
|
-
database: @connection_config.database_name
|
|
267
|
-
)
|
|
268
|
-
end
|
|
260
|
+
@connection ||= MysqlClient.new(@connection_config)
|
|
269
261
|
end
|
|
270
262
|
end
|
|
271
263
|
end
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
|
|
5
|
+
module Exwiw
|
|
6
|
+
module Adapter
|
|
7
|
+
# Thin wrapper over the MySQL driver so MysqlAdapter does not care whether the
|
|
8
|
+
# host app ships the `mysql2` gem or the `trilogy` gem. exwiw only runs simple
|
|
9
|
+
# SELECT / EXPLAIN queries, so both drivers are normalized to the same shape:
|
|
10
|
+
# rows as arrays of String|nil plus the column names.
|
|
11
|
+
#
|
|
12
|
+
# Values are normalized to strings to match mysql2's `cast: false` mode, where
|
|
13
|
+
# every column comes back as a raw string and is quoted uniformly downstream
|
|
14
|
+
# (see MysqlAdapter#escape_value). mysql2 already returns strings in that mode;
|
|
15
|
+
# trilogy always casts to Ruby types (Integer / BigDecimal / Time / Date / ...),
|
|
16
|
+
# so its values are stringified back into the same literal form here.
|
|
17
|
+
class MysqlClient
|
|
18
|
+
# Immutable value object: a query's column names and its rows.
|
|
19
|
+
Result = Data.define(:fields, :rows)
|
|
20
|
+
|
|
21
|
+
# Pick the available driver, preferring mysql2 (exwiw's historical default).
|
|
22
|
+
# require returns false when already loaded, so this is safe to call repeatedly.
|
|
23
|
+
def self.detect_driver
|
|
24
|
+
require 'mysql2'
|
|
25
|
+
:mysql2
|
|
26
|
+
rescue LoadError
|
|
27
|
+
begin
|
|
28
|
+
require 'trilogy'
|
|
29
|
+
:trilogy
|
|
30
|
+
rescue LoadError
|
|
31
|
+
raise LoadError,
|
|
32
|
+
"exwiw needs the 'mysql2' or 'trilogy' gem to connect to MySQL. " \
|
|
33
|
+
"Add `gem \"mysql2\"` (or `gem \"trilogy\"`) to your Gemfile."
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Render a driver-returned value as the raw string mysql2's `cast: false`
|
|
38
|
+
# would have produced, so trilogy's typed values quote identically.
|
|
39
|
+
def self.stringify_value(value)
|
|
40
|
+
case value
|
|
41
|
+
when nil then nil
|
|
42
|
+
when String then value
|
|
43
|
+
when Time
|
|
44
|
+
# Emit fractional seconds only when present. A Time can't tell us the
|
|
45
|
+
# column's declared precision, so a zero fraction on a DATETIME(6)
|
|
46
|
+
# column comes out as "...:00" here whereas mysql2's `cast: false`
|
|
47
|
+
# echoes the raw "...:00.000000"; both re-insert to the same instant.
|
|
48
|
+
value.nsec.zero? ? value.strftime('%Y-%m-%d %H:%M:%S') : value.strftime('%Y-%m-%d %H:%M:%S.%6N')
|
|
49
|
+
when Date then value.strftime('%Y-%m-%d')
|
|
50
|
+
when true then '1'
|
|
51
|
+
when false then '0'
|
|
52
|
+
else
|
|
53
|
+
if defined?(BigDecimal) && value.is_a?(BigDecimal)
|
|
54
|
+
value.to_s('F')
|
|
55
|
+
else
|
|
56
|
+
value.to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
attr_reader :driver
|
|
62
|
+
|
|
63
|
+
# `driver:` is mainly a test seam to force a specific driver; in normal use
|
|
64
|
+
# it is auto-detected.
|
|
65
|
+
def initialize(connection_config, driver: nil)
|
|
66
|
+
@connection_config = connection_config
|
|
67
|
+
@driver = (driver || self.class.detect_driver).to_sym
|
|
68
|
+
ensure_driver_loaded!
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# @param sql [String]
|
|
72
|
+
# @return [Result] fields (Array<String>) and rows (Array<Array<String|nil>>)
|
|
73
|
+
def query(sql)
|
|
74
|
+
case @driver
|
|
75
|
+
when :mysql2
|
|
76
|
+
res = raw.query(sql, cast: false, as: :array)
|
|
77
|
+
Result.new(res.fields, res.to_a)
|
|
78
|
+
when :trilogy
|
|
79
|
+
res = raw.query(sql)
|
|
80
|
+
rows = res.rows.map { |row| row.map { |value| self.class.stringify_value(value) } }
|
|
81
|
+
Result.new(res.fields, rows)
|
|
82
|
+
else
|
|
83
|
+
raise "Unsupported MySQL driver: #{@driver.inspect}"
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private def ensure_driver_loaded!
|
|
88
|
+
case @driver
|
|
89
|
+
when :mysql2 then require 'mysql2'
|
|
90
|
+
when :trilogy then require 'trilogy'
|
|
91
|
+
else raise "Unsupported MySQL driver: #{@driver.inspect}"
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
private def raw
|
|
96
|
+
@raw ||= build_raw
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
private def build_raw
|
|
100
|
+
options = {
|
|
101
|
+
host: @connection_config.host,
|
|
102
|
+
port: @connection_config.port&.to_i,
|
|
103
|
+
username: @connection_config.user,
|
|
104
|
+
password: @connection_config.password,
|
|
105
|
+
database: @connection_config.database_name,
|
|
106
|
+
}.compact
|
|
107
|
+
|
|
108
|
+
case @driver
|
|
109
|
+
when :mysql2 then Mysql2::Client.new(options)
|
|
110
|
+
when :trilogy then Trilogy.new(options)
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
@@ -180,10 +180,11 @@ module Exwiw
|
|
|
180
180
|
subquery_sql = compile_ast(subquery_ast)
|
|
181
181
|
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
182
182
|
|
|
183
|
-
# first_join.base_where_clauses
|
|
184
|
-
# (from_table_name)
|
|
185
|
-
#
|
|
186
|
-
#
|
|
183
|
+
# first_join.base_where_clauses holds conditions on the outer
|
|
184
|
+
# delete-target table (from_table_name), such as a polymorphic type
|
|
185
|
+
# column. They are not part of the subquery, so add them to the outer
|
|
186
|
+
# WHERE. This prevents deleting rows that belong to a different
|
|
187
|
+
# polymorphic type.
|
|
187
188
|
first_join.base_where_clauses.each do |where|
|
|
188
189
|
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
189
190
|
|
|
@@ -213,8 +214,9 @@ module Exwiw
|
|
|
213
214
|
sql += " AND #{compiled_where_condition}"
|
|
214
215
|
end
|
|
215
216
|
|
|
216
|
-
# base_where_clauses
|
|
217
|
-
#
|
|
217
|
+
# base_where_clauses is compiled against the joined-from table
|
|
218
|
+
# (base_table_name), e.g. the type-column filter on a polymorphic
|
|
219
|
+
# source table.
|
|
218
220
|
join.base_where_clauses.each do |where|
|
|
219
221
|
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
220
222
|
sql += " AND #{compiled_where_condition}"
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module Adapter
|
|
5
|
-
class
|
|
5
|
+
class SqliteAdapter < Base
|
|
6
6
|
def build_query(table, dump_target, table_by_name)
|
|
7
7
|
Exwiw::QueryAstBuilder.run(table.name, table_by_name, dump_target, @logger)
|
|
8
8
|
end
|
|
@@ -53,7 +53,7 @@ module Exwiw
|
|
|
53
53
|
end
|
|
54
54
|
|
|
55
55
|
File.open(output_path, 'w') do |file|
|
|
56
|
-
file.puts("-- Auto-generated by exwiw. Idempotent CREATE statements for
|
|
56
|
+
file.puts("-- Auto-generated by exwiw. Idempotent CREATE statements for SQLite.")
|
|
57
57
|
file.puts(statements.join("\n"))
|
|
58
58
|
end
|
|
59
59
|
@logger.info(" Wrote #{statements.size} schema statement(s) to #{output_path}.")
|
|
@@ -125,10 +125,11 @@ module Exwiw
|
|
|
125
125
|
subquery_sql = compile_ast(subquery_ast)
|
|
126
126
|
sql += "\nWHERE #{select_query_ast.from_table_name}.#{foreign_key} IN (#{subquery_sql})"
|
|
127
127
|
|
|
128
|
-
# first_join.base_where_clauses
|
|
129
|
-
# (from_table_name)
|
|
130
|
-
#
|
|
131
|
-
#
|
|
128
|
+
# first_join.base_where_clauses holds conditions on the outer
|
|
129
|
+
# delete-target table (from_table_name), such as a polymorphic type
|
|
130
|
+
# column. They are not part of the subquery, so add them to the outer
|
|
131
|
+
# WHERE. This prevents deleting rows that belong to a different
|
|
132
|
+
# polymorphic type.
|
|
132
133
|
first_join.base_where_clauses.each do |where|
|
|
133
134
|
next unless where.is_a?(Exwiw::QueryAst::WhereClause)
|
|
134
135
|
|
|
@@ -158,8 +159,9 @@ module Exwiw
|
|
|
158
159
|
sql += " AND #{compiled_where_condition}"
|
|
159
160
|
end
|
|
160
161
|
|
|
161
|
-
# base_where_clauses
|
|
162
|
-
#
|
|
162
|
+
# base_where_clauses is compiled against the joined-from table
|
|
163
|
+
# (base_table_name), e.g. the type-column filter on a polymorphic
|
|
164
|
+
# source table.
|
|
163
165
|
join.base_where_clauses.each do |where|
|
|
164
166
|
compiled_where_condition = compile_where_condition(where, join.base_table_name)
|
|
165
167
|
sql += " AND #{compiled_where_condition}"
|
data/lib/exwiw/adapter.rb
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module Adapter
|
|
5
|
+
# The adapter names exwiw uses internally. Deliberately driver-agnostic
|
|
6
|
+
# (e.g. `mysql`, not `mysql2`) so they describe the database, not the Ruby
|
|
7
|
+
# driver or Rails ActiveRecord adapter that happens to talk to it.
|
|
8
|
+
CANONICAL_ADAPTERS = %w[mysql sqlite postgresql mongodb].freeze
|
|
9
|
+
|
|
10
|
+
# Maps the older driver-flavored spellings onto exwiw's canonical adapter
|
|
11
|
+
# name, so the CLI stays backward compatible (`--adapter=mysql2` still works)
|
|
12
|
+
# and a Rails app's `connection.adapter_name` (e.g. "Mysql2", "SQLite") is
|
|
13
|
+
# absorbed — lookup is case-insensitive (see .normalize_name).
|
|
14
|
+
#
|
|
15
|
+
# NOTE: this only normalizes the *name*; exwiw always connects with the
|
|
16
|
+
# `mysql2` gem (see MysqlAdapter#connection). It is deliberately not aliased
|
|
17
|
+
# from `trilogy`: a source app using the trilogy driver still needs the
|
|
18
|
+
# `mysql2` gem available for exwiw to connect, so accepting `--adapter=trilogy`
|
|
19
|
+
# would falsely imply trilogy support. Use `--adapter=mysql` in that case.
|
|
20
|
+
ADAPTER_ALIASES = {
|
|
21
|
+
"mysql2" => "mysql",
|
|
22
|
+
"sqlite3" => "sqlite",
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
# Normalize an adapter name to its canonical form. Unknown names are passed
|
|
26
|
+
# through (downcased) so the caller can reject them with a clear message.
|
|
27
|
+
# Returns nil for nil input.
|
|
28
|
+
def self.normalize_name(name)
|
|
29
|
+
return nil if name.nil?
|
|
30
|
+
|
|
31
|
+
key = name.to_s.downcase
|
|
32
|
+
ADAPTER_ALIASES.fetch(key, key)
|
|
33
|
+
end
|
|
34
|
+
|
|
5
35
|
class Base
|
|
6
36
|
attr_reader :connection_config
|
|
7
37
|
|
|
@@ -129,17 +159,17 @@ module Exwiw
|
|
|
129
159
|
end
|
|
130
160
|
|
|
131
161
|
def self.build(connection_config, logger)
|
|
132
|
-
case connection_config.adapter
|
|
133
|
-
when '
|
|
134
|
-
Adapter::
|
|
135
|
-
when '
|
|
136
|
-
Adapter::
|
|
162
|
+
case normalize_name(connection_config.adapter)
|
|
163
|
+
when 'sqlite'
|
|
164
|
+
Adapter::SqliteAdapter.new(connection_config, logger)
|
|
165
|
+
when 'mysql'
|
|
166
|
+
Adapter::MysqlAdapter.new(connection_config, logger)
|
|
137
167
|
when 'postgresql'
|
|
138
168
|
Adapter::PostgresqlAdapter.new(connection_config, logger)
|
|
139
169
|
when 'mongodb'
|
|
140
170
|
Adapter::MongodbAdapter.new(connection_config, logger)
|
|
141
171
|
else
|
|
142
|
-
raise
|
|
172
|
+
raise "Unsupported adapter: #{connection_config.adapter.inspect}"
|
|
143
173
|
end
|
|
144
174
|
end
|
|
145
175
|
end
|
data/lib/exwiw/belongs_to.rb
CHANGED
|
@@ -6,9 +6,10 @@ module Exwiw
|
|
|
6
6
|
|
|
7
7
|
attribute :foreign_key, String
|
|
8
8
|
attribute :table_name, String
|
|
9
|
-
# polymorphic
|
|
10
|
-
# (
|
|
11
|
-
#
|
|
9
|
+
# Set only for a polymorphic association. `foreign_type` is the name of the
|
|
10
|
+
# column storing the type (e.g. `reviewable_type`), and `type_value` is the
|
|
11
|
+
# value held in that column (e.g. `"Product"`). Both are nil for a
|
|
12
|
+
# non-polymorphic belongs_to.
|
|
12
13
|
attribute :foreign_type, optional(String), skip_serializing_if_nil: true
|
|
13
14
|
attribute :type_value, optional(String), skip_serializing_if_nil: true
|
|
14
15
|
|
data/lib/exwiw/cli.rb
CHANGED
|
@@ -10,7 +10,7 @@ require 'exwiw'
|
|
|
10
10
|
|
|
11
11
|
module Exwiw
|
|
12
12
|
class CLI
|
|
13
|
-
KNOWN_SUBCOMMANDS = %w[
|
|
13
|
+
KNOWN_SUBCOMMANDS = %w[export explain].freeze
|
|
14
14
|
|
|
15
15
|
def self.start(argv)
|
|
16
16
|
new(argv).run
|
|
@@ -23,7 +23,7 @@ module Exwiw
|
|
|
23
23
|
if !@argv.empty? && !@argv.first.start_with?("-") && KNOWN_SUBCOMMANDS.include?(@argv.first)
|
|
24
24
|
@argv.shift
|
|
25
25
|
else
|
|
26
|
-
"
|
|
26
|
+
"export"
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
@help = @argv.empty?
|
|
@@ -75,7 +75,7 @@ module Exwiw
|
|
|
75
75
|
logger = build_logger
|
|
76
76
|
|
|
77
77
|
case @subcommand
|
|
78
|
-
when "
|
|
78
|
+
when "export"
|
|
79
79
|
Runner.new(
|
|
80
80
|
connection_config: connection_config,
|
|
81
81
|
output_dir: @output_dir,
|
|
@@ -99,6 +99,19 @@ module Exwiw
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
private def validate_options!
|
|
102
|
+
# Fold driver/Rails adapter spellings (mysql2, sqlite3) into exwiw's
|
|
103
|
+
# canonical names up front, so every check below — and the
|
|
104
|
+
# EXWIW_DATABASE_ADAPTER passed to hooks — sees the canonical name.
|
|
105
|
+
@database_adapter = Adapter.normalize_name(@database_adapter)
|
|
106
|
+
|
|
107
|
+
# Reject an unknown adapter up front, before checking adapter-specific
|
|
108
|
+
# options like host/port/password, so the error points at the real problem.
|
|
109
|
+
unless Adapter::CANONICAL_ADAPTERS.include?(@database_adapter)
|
|
110
|
+
$stderr.puts "Invalid adapter. Available options are: #{Adapter::CANONICAL_ADAPTERS.join(', ')} " \
|
|
111
|
+
"(aliases also accepted: #{Adapter::ADAPTER_ALIASES.keys.join(', ')})"
|
|
112
|
+
exit 1
|
|
113
|
+
end
|
|
114
|
+
|
|
102
115
|
resolve_target_collection_alias!
|
|
103
116
|
resolve_ids_column_alias!
|
|
104
117
|
|
|
@@ -106,7 +119,7 @@ module Exwiw
|
|
|
106
119
|
validate_explain_only!
|
|
107
120
|
end
|
|
108
121
|
|
|
109
|
-
if @database_adapter != "
|
|
122
|
+
if @database_adapter != "sqlite"
|
|
110
123
|
required_options = {
|
|
111
124
|
"Target database host" => @database_host,
|
|
112
125
|
"Target database port" => @database_port,
|
|
@@ -126,13 +139,7 @@ module Exwiw
|
|
|
126
139
|
end
|
|
127
140
|
end
|
|
128
141
|
|
|
129
|
-
|
|
130
|
-
unless valid_adapters.include?(@database_adapter)
|
|
131
|
-
$stderr.puts "Invalid adapter. Available options are: #{valid_adapters.join(', ')}"
|
|
132
|
-
exit 1
|
|
133
|
-
end
|
|
134
|
-
|
|
135
|
-
if @subcommand == "dump"
|
|
142
|
+
if @subcommand == "export"
|
|
136
143
|
@output_dir ||= "dump"
|
|
137
144
|
@output_format ||= "insert"
|
|
138
145
|
@insert_only = @insert_only ? true : false
|
|
@@ -227,7 +234,7 @@ module Exwiw
|
|
|
227
234
|
end
|
|
228
235
|
|
|
229
236
|
if @ids_column
|
|
230
|
-
sql_adapters = ["
|
|
237
|
+
sql_adapters = ["mysql", "postgresql", "sqlite"]
|
|
231
238
|
unless sql_adapters.include?(@database_adapter)
|
|
232
239
|
$stderr.puts "--ids-column is only supported by the sql adapters (use --ids-field)"
|
|
233
240
|
exit 1
|
|
@@ -306,7 +313,7 @@ module Exwiw
|
|
|
306
313
|
Usage: exwiw [SUBCOMMAND] [options]
|
|
307
314
|
|
|
308
315
|
Subcommands:
|
|
309
|
-
|
|
316
|
+
export Generate INSERT/COPY SQL files (default when omitted).
|
|
310
317
|
explain Print EXPLAIN output for each extraction query to stdout.
|
|
311
318
|
(not yet supported for the mongodb adapter)
|
|
312
319
|
BANNER
|
|
@@ -315,7 +322,7 @@ module Exwiw
|
|
|
315
322
|
opts.on("-h", "--host=HOST", "Target database host") { |v| @database_host = v }
|
|
316
323
|
opts.on("-p", "--port=PORT", "Target database port") { |v| @database_port = v }
|
|
317
324
|
opts.on("-u", "--user=USERNAME", "Target database user") { |v| @database_user = v }
|
|
318
|
-
opts.on("-o", "--output-dir=[DUMP_DIR_PATH]", "Output file path. default is dump/ (
|
|
325
|
+
opts.on("-o", "--output-dir=[DUMP_DIR_PATH]", "Output file path. default is dump/ (export subcommand only)") do |v|
|
|
319
326
|
v = v.end_with?("/") ? v[0..-2] : v
|
|
320
327
|
@output_dir = File.expand_path(v)
|
|
321
328
|
end
|
|
@@ -323,16 +330,16 @@ module Exwiw
|
|
|
323
330
|
v = v.end_with?("/") ? v[0..-2] : v
|
|
324
331
|
@config_dir = File.expand_path(v)
|
|
325
332
|
end
|
|
326
|
-
opts.on("-a", "--adapter=ADAPTER", "Database adapter") { |v| @database_adapter = v }
|
|
333
|
+
opts.on("-a", "--adapter=ADAPTER", "Database adapter: mysql, sqlite, postgresql, mongodb (aliases: mysql2, sqlite3)") { |v| @database_adapter = v }
|
|
327
334
|
opts.on("--database=DATABASE", "Target database name") { |v| @database_name = v }
|
|
328
335
|
opts.on("--target-table=[TABLE]", "Target table for extraction. If omitted, dump all tables.") { |v| @target_table_name = v }
|
|
329
336
|
opts.on("--target-collection=[COLLECTION]", "Alias of --target-table for the mongodb adapter.") { |v| @target_collection_name = v }
|
|
330
337
|
opts.on("--ids=[IDS]", "Comma-separated list of identifiers. Required when --target-table is given.") { |v| @ids = v.split(',') }
|
|
331
338
|
opts.on("--ids-field=[FIELD]", "Field on the target collection that --ids is matched against. Defaults to the primary key. (mongodb adapter only)") { |v| @ids_field = v }
|
|
332
339
|
opts.on("--ids-column=[COLUMN]", "Column on the target table that --ids is matched against. Defaults to the primary key. (sql adapters only)") { |v| @ids_column = v }
|
|
333
|
-
opts.on("--output-format=[FORMAT]", "Output format: insert (default) or copy (PostgreSQL only,
|
|
334
|
-
opts.on("--insert-only", "Do not generate DELETE SQL files (
|
|
335
|
-
opts.on("--after-insert-hook=PATH", "Path to a .rb or .sh post-processing hook executed after all insert/delete files are written (
|
|
340
|
+
opts.on("--output-format=[FORMAT]", "Output format: insert (default) or copy (PostgreSQL only, export subcommand only)") { |v| @output_format = v }
|
|
341
|
+
opts.on("--insert-only", "Do not generate DELETE SQL files (export subcommand only)") { @insert_only = true }
|
|
342
|
+
opts.on("--after-insert-hook=PATH", "Path to a .rb or .sh post-processing hook executed after all insert/delete files are written (export subcommand only)") do |v|
|
|
336
343
|
@after_insert_hook_path = File.expand_path(v)
|
|
337
344
|
end
|
|
338
345
|
opts.on("--log-level=LEVEL", "Log level (debug, info). default is info") { |v| @log_level = v.to_sym }
|
|
@@ -159,11 +159,13 @@ module Exwiw
|
|
|
159
159
|
end
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
-
# polymorphic belongs_to (`belongs_to :reviewable, polymorphic: true`)
|
|
163
|
-
#
|
|
164
|
-
#
|
|
162
|
+
# A polymorphic belongs_to (`belongs_to :reviewable, polymorphic: true`)
|
|
163
|
+
# has no single target collection, so it is not supported yet. Exclude it
|
|
164
|
+
# here to avoid emitting an incorrect FK (leaving room to expand it later,
|
|
165
|
+
# like the ActiveRecord version does).
|
|
165
166
|
#
|
|
166
|
-
#
|
|
167
|
+
# In an inheritance hierarchy the base class and its subclasses carry the
|
|
168
|
+
# same belongs_to twice, so uniq them.
|
|
167
169
|
belongs_to_assocs
|
|
168
170
|
.reject(&:polymorphic?)
|
|
169
171
|
.map { |assoc| { table_name: assoc.klass.collection_name.to_s, foreign_key: assoc.foreign_key } }
|
data/lib/exwiw/query_ast.rb
CHANGED
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
module QueryAst
|
|
5
5
|
class JoinClause
|
|
6
|
-
# `where_clauses`
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
#
|
|
6
|
+
# `where_clauses` is compiled against this join's join_table_name (the
|
|
7
|
+
# joined-to table). `base_where_clauses`, on the other hand, is compiled
|
|
8
|
+
# against base_table_name (the joined-from table). The latter is used for
|
|
9
|
+
# the case where the source table polymorphically belongs_to the joined-to
|
|
10
|
+
# table and the type column (foreign_type) lives on the source table.
|
|
11
11
|
attr_reader :base_table_name, :foreign_key, :join_table_name, :primary_key, :where_clauses, :base_where_clauses
|
|
12
12
|
|
|
13
13
|
def initialize(base_table_name:, foreign_key:, join_table_name:, primary_key:, where_clauses: [], base_where_clauses: [])
|
|
@@ -58,12 +58,12 @@ module Exwiw
|
|
|
58
58
|
base_where_clauses: []
|
|
59
59
|
)
|
|
60
60
|
|
|
61
|
-
#
|
|
62
|
-
#
|
|
63
|
-
# (foreign_type)
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
61
|
+
# When this hop itself is a polymorphic belongs_to (e.g. comments
|
|
62
|
+
# polymorphically belongs_to posts as commentable), the type column
|
|
63
|
+
# (foreign_type) lives on the source table (from_table = base_table_name).
|
|
64
|
+
# The foreign key alone is not enough — a value like reviewable_id=1 can
|
|
65
|
+
# collide with rows of another model — so add the type condition to
|
|
66
|
+
# base_where_clauses to narrow down the source table.
|
|
67
67
|
if relation.polymorphic?
|
|
68
68
|
join_clause.base_where_clauses.push QueryAst::WhereClause.new(
|
|
69
69
|
column_name: relation.foreign_type,
|
|
@@ -75,10 +75,11 @@ module Exwiw
|
|
|
75
75
|
if relation_to_dump_target
|
|
76
76
|
join_clause.where_clauses.push dump_target_fk_clause(relation_to_dump_target.foreign_key)
|
|
77
77
|
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# (= join_table_name)
|
|
81
|
-
#
|
|
78
|
+
# When the intermediate table polymorphically belongs_to the dump
|
|
79
|
+
# target, also add the type column (foreign_type) to the join
|
|
80
|
+
# condition. The type column lives on to_table (= join_table_name), so
|
|
81
|
+
# it rides on the existing mechanism where a JoinClause's where_clauses
|
|
82
|
+
# are compiled against join_table_name.
|
|
82
83
|
if relation_to_dump_target.polymorphic?
|
|
83
84
|
join_clause.where_clauses.push QueryAst::WhereClause.new(
|
|
84
85
|
column_name: relation_to_dump_target.foreign_type,
|
|
@@ -121,9 +122,9 @@ module Exwiw
|
|
|
121
122
|
|
|
122
123
|
clauses.push dump_target_fk_clause(belongs_to.foreign_key)
|
|
123
124
|
|
|
124
|
-
# polymorphic belongs_to
|
|
125
|
-
# (
|
|
126
|
-
#
|
|
125
|
+
# For a polymorphic belongs_to the foreign key alone cannot distinguish the
|
|
126
|
+
# type (e.g. reviewable_id=1 could be a Product or another model), so add a
|
|
127
|
+
# condition filtering the type column (foreign_type) by type_value.
|
|
127
128
|
if belongs_to.polymorphic?
|
|
128
129
|
clauses.push Exwiw::QueryAst::WhereClause.new(
|
|
129
130
|
column_name: belongs_to.foreign_type,
|
|
@@ -78,11 +78,12 @@ module Exwiw
|
|
|
78
78
|
representative = model_group.first
|
|
79
79
|
primary_key = representative.primary_key
|
|
80
80
|
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
81
|
+
# Tables with a composite primary key (`representative.primary_key` is an
|
|
82
|
+
# Array) are not supported yet. Emit them with `primary_key` omitted,
|
|
83
|
+
# `skip: true`, and a `type` that marks them as unsupported — the `type`
|
|
84
|
+
# acts as a signpost for adding support later. The config file itself is
|
|
85
|
+
# still generated so a user can manually remove `skip` and wire it up when
|
|
86
|
+
# needed.
|
|
86
87
|
if primary_key.is_a?(Array)
|
|
87
88
|
TableConfig.from_symbol_keys(
|
|
88
89
|
name: table_name,
|
|
@@ -110,12 +111,13 @@ module Exwiw
|
|
|
110
111
|
@models.reject(&:abstract_class?).select(&:table_exists?)
|
|
111
112
|
end
|
|
112
113
|
|
|
113
|
-
# rails-managed
|
|
114
|
-
#
|
|
115
|
-
# multi-DB
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
# (
|
|
114
|
+
# The rails-managed tables (`schema_migrations` / `ar_internal_metadata`)
|
|
115
|
+
# have no model class, so they cannot be picked up from
|
|
116
|
+
# `ActiveRecord::Base.descendants`. In a multi-DB setup each connection has
|
|
117
|
+
# its own migration history table, so we take the target connection and only
|
|
118
|
+
# emit an entry when the table actually exists on that connection. The table
|
|
119
|
+
# name itself (including any prefix/suffix) comes from the global settings
|
|
120
|
+
# (`ActiveRecord::Base.schema_migrations_table_name`, etc.).
|
|
119
121
|
private def build_rails_managed_tables(conn)
|
|
120
122
|
result = []
|
|
121
123
|
|
|
@@ -151,11 +153,11 @@ module Exwiw
|
|
|
151
153
|
.reject(&:polymorphic?)
|
|
152
154
|
.map { |assoc| { table_name: assoc.table_name, foreign_key: assoc.foreign_key } }
|
|
153
155
|
|
|
154
|
-
# polymorphic
|
|
155
|
-
#
|
|
156
|
-
# `has_many/has_one ..., as: <association_name
|
|
157
|
-
#
|
|
158
|
-
# (`
|
|
156
|
+
# A polymorphic belongs_to (`belongs_to :reviewable, polymorphic: true`)
|
|
157
|
+
# has no single target table. The candidate tables are found by looking up
|
|
158
|
+
# the other models that declare `has_many/has_one ..., as: <association_name>`.
|
|
159
|
+
# For each candidate table, expand one belongs_to entry carrying the type
|
|
160
|
+
# column (`foreign_type`) and the stored type value (`type_value`).
|
|
159
161
|
polymorphic = belongs_to_assocs
|
|
160
162
|
.select(&:polymorphic?)
|
|
161
163
|
.flat_map do |assoc|
|
|
@@ -172,11 +174,12 @@ module Exwiw
|
|
|
172
174
|
(non_polymorphic + polymorphic).uniq
|
|
173
175
|
end
|
|
174
176
|
|
|
175
|
-
#
|
|
176
|
-
# `
|
|
177
|
-
# `
|
|
178
|
-
# Ruby
|
|
179
|
-
#
|
|
177
|
+
# Enumerate the concrete models that can be targets of the polymorphic
|
|
178
|
+
# association `association_name`, by looking them up from every model's
|
|
179
|
+
# `has_many` / `has_one` `as:` option. The order of `concrete_models` depends
|
|
180
|
+
# on `ActiveRecord::Base.descendants`, which can vary by Ruby version, so sort
|
|
181
|
+
# by `table_name` to return a deterministic order and keep the generated
|
|
182
|
+
# belongs_to ordering stable.
|
|
180
183
|
private def polymorphic_target_models(association_name)
|
|
181
184
|
concrete_models.select do |model|
|
|
182
185
|
(model.reflect_on_all_associations(:has_many) +
|
data/lib/exwiw/table_config.rb
CHANGED
|
@@ -11,9 +11,10 @@ module Exwiw
|
|
|
11
11
|
RAILS_MANAGED_INTERNAL_METADATA,
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
|
-
# exwiw
|
|
15
|
-
# schema:generate
|
|
16
|
-
#
|
|
14
|
+
# type marking a table with a composite primary key, which exwiw does not
|
|
15
|
+
# support yet. schema:generate attaches it together with skip:true. Unlike
|
|
16
|
+
# rails-managed tables, columns/belongs_tos are retained so it can serve as a
|
|
17
|
+
# signpost for adding support later.
|
|
17
18
|
UNSUPPORTED_COMPOSITE_PRIMARY_KEY = "unsupported_composite_primary_key"
|
|
18
19
|
|
|
19
20
|
attribute :name, String
|
|
@@ -143,8 +144,8 @@ module Exwiw
|
|
|
143
144
|
"Table '#{name}' has type=#{type}; columns must not be defined."
|
|
144
145
|
end
|
|
145
146
|
else
|
|
146
|
-
# skip:true
|
|
147
|
-
# (
|
|
147
|
+
# A skip:true table is not extracted, so primary_key is not required
|
|
148
|
+
# (e.g. a composite-primary-key table that exwiw does not support).
|
|
148
149
|
if primary_key.nil? && !skip
|
|
149
150
|
raise ArgumentError, "Table '#{name}' requires primary_key."
|
|
150
151
|
end
|
data/lib/exwiw/version.rb
CHANGED
data/lib/exwiw.rb
CHANGED
|
@@ -13,8 +13,9 @@ require_relative "exwiw/mongodb_field"
|
|
|
13
13
|
require_relative "exwiw/mongodb_collection_config"
|
|
14
14
|
require_relative "exwiw/ddl_postprocessor"
|
|
15
15
|
require_relative "exwiw/adapter"
|
|
16
|
-
require_relative "exwiw/adapter/
|
|
17
|
-
require_relative "exwiw/adapter/
|
|
16
|
+
require_relative "exwiw/adapter/sqlite_adapter"
|
|
17
|
+
require_relative "exwiw/adapter/mysql_client"
|
|
18
|
+
require_relative "exwiw/adapter/mysql_adapter"
|
|
18
19
|
require_relative "exwiw/adapter/postgresql_adapter"
|
|
19
20
|
require_relative "exwiw/adapter/mongodb_adapter"
|
|
20
21
|
require_relative "exwiw/determine_table_processing_order"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: exwiw
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shia
|
|
@@ -45,9 +45,10 @@ files:
|
|
|
45
45
|
- lib/exwiw.rb
|
|
46
46
|
- lib/exwiw/adapter.rb
|
|
47
47
|
- lib/exwiw/adapter/mongodb_adapter.rb
|
|
48
|
-
- lib/exwiw/adapter/
|
|
48
|
+
- lib/exwiw/adapter/mysql_adapter.rb
|
|
49
|
+
- lib/exwiw/adapter/mysql_client.rb
|
|
49
50
|
- lib/exwiw/adapter/postgresql_adapter.rb
|
|
50
|
-
- lib/exwiw/adapter/
|
|
51
|
+
- lib/exwiw/adapter/sqlite_adapter.rb
|
|
51
52
|
- lib/exwiw/after_insert_hook.rb
|
|
52
53
|
- lib/exwiw/belongs_to.rb
|
|
53
54
|
- lib/exwiw/cli.rb
|