exwiw 0.3.2 → 0.3.4
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 +20 -0
- data/README.md +44 -22
- data/lib/exwiw/adapter/mysql_adapter.rb +19 -4
- data/lib/exwiw/adapter/mysql_client.rb +18 -0
- data/lib/exwiw/belongs_to.rb +13 -0
- data/lib/exwiw/cli.rb +24 -1
- data/lib/exwiw/determine_table_processing_order.rb +24 -0
- data/lib/exwiw/explain_runner.rb +13 -13
- data/lib/exwiw/mongodb_collection_config.rb +31 -6
- data/lib/exwiw/mongodb_field.rb +5 -0
- data/lib/exwiw/runner.rb +36 -17
- data/lib/exwiw/schema_generator.rb +26 -5
- data/lib/exwiw/table_column.rb +5 -0
- data/lib/exwiw/table_config.rb +29 -6
- data/lib/exwiw/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 3b68b4eed496cf67cbac408c7f1c233a881b9778b46f0f5a67d0ff50b9321e03
|
|
4
|
+
data.tar.gz: 521a8cf7d5af0dee4538e407d06d9605d37cb059f5023ee922e11807ff6d1b17
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bdd88c3f13879d23a4ecc7c239f22db2c5c5ddbd3bb06b838ae570d48309ba4f2460a6260d9f848da71717dcf43cae0de3b593e3e6e99f43406d700b39e79a37
|
|
7
|
+
data.tar.gz: ac2e73630034663ef1ef299fd09925843c086776d10e5998d30758e079a4066f388bdb76672867acacbb57024a5a990336c46e5d8f21d42548862125d1004748
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.3.4] - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
|
|
9
|
+
- **Breaking:** the table/collection-level config attribute `skip` is renamed to `ignore`. There is no alias — config files using `"skip": true` must be updated to `"ignore": true` (`exwiw:schema:generate` / `exwiw:mongoid:schema:generate` now emit `ignore`, e.g. for composite-primary-key tables). The library accessors are renamed accordingly (`TableConfig#skip` → `#ignore`, `MongodbCollectionConfig#skip` → `#ignore`).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- `columns` / `fields` and `belongs_tos` entries now accept optional `comment` (a free-form note) and `ignore: true`. An ignored column/field is excluded from the `SELECT` and generated `INSERT`; an ignored `belongs_to` is removed from dependency ordering and query building. These user-owned values are preserved across `exwiw:schema:generate` / `exwiw:mongoid:schema:generate` regenerations (the hand-edited value wins over the auto-generated config), like `replace_with`. The ignored entries are dropped at runtime right after the config is loaded from file, so the JSON on disk keeps them. Applies to both the SQL `TableConfig` and the MongoDB `MongodbCollectionConfig`.
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
|
|
17
|
+
fixed HABTM relationship bug https://github.com/heyinc/exwiw/pull/56
|
|
18
|
+
|
|
19
|
+
## [0.3.3] - 2026-05-31
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- `export` now empties `--output-dir` before writing, so a run never mixes files from a previous export. When running interactively (stdin is a tty) and the dir already has contents, exwiw asks for confirmation before removing them; in non-interactive contexts (CI, pipes) it proceeds without prompting.
|
|
24
|
+
|
|
5
25
|
## [0.3.2] - 2026-05-31
|
|
6
26
|
|
|
7
27
|
### Changed
|
data/README.md
CHANGED
|
@@ -42,22 +42,18 @@ gem install exwiw
|
|
|
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
45
|
For MySQL, exwiw connects through whichever of the `mysql2` or `trilogy` gem is
|
|
57
46
|
available (preferring `mysql2`), so an app on either driver works without any
|
|
58
47
|
extra setup. There is no separate `trilogy` adapter name — pass `--adapter=mysql`
|
|
59
48
|
either way.
|
|
60
49
|
|
|
50
|
+
Set `EXWIW_MYSQL_DRIVER=trilogy` (or `mysql2`) to force a specific driver. This
|
|
51
|
+
is useful when the `mysql2` gem is linked against a `libmysqlclient` that can no
|
|
52
|
+
longer load the server's auth plugin — e.g. a MySQL 9.x client drops the
|
|
53
|
+
`mysql_native_password` plugin and raises `Authentication plugin
|
|
54
|
+
'mysql_native_password' cannot be loaded` on connect. The pure-Ruby `trilogy`
|
|
55
|
+
driver implements that auth handshake itself and sidesteps the issue.
|
|
56
|
+
|
|
61
57
|
## Usage
|
|
62
58
|
|
|
63
59
|
exwiw has two subcommands:
|
|
@@ -101,6 +97,8 @@ exwiw \
|
|
|
101
97
|
|
|
102
98
|
This command will generate sql files in the `dump` directory.
|
|
103
99
|
|
|
100
|
+
The output dir is emptied before each export so it never mixes files from a previous run (defaulting to `dump/` when `--output-dir` is omitted). When run interactively (stdin is a tty) and the dir already contains files, exwiw asks for confirmation before removing them; in non-interactive contexts (CI, pipes) it proceeds without prompting.
|
|
101
|
+
|
|
104
102
|
- `dump/insert-000-schema.sql` — idempotent `CREATE TABLE IF NOT EXISTS ...` for every table in scope. Apply this first to provision an empty database.
|
|
105
103
|
- `dump/insert-{idx}-{table_name}.sql`
|
|
106
104
|
- `dump/delete-{idx}-{table_name}.sql`
|
|
@@ -108,7 +106,7 @@ This command will generate sql files in the `dump` directory.
|
|
|
108
106
|
idx means the order of the dump. bigger idx might depend on smaller idx,
|
|
109
107
|
so you should import the dump in order.
|
|
110
108
|
|
|
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`.
|
|
109
|
+
`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. For `mysql`, set `EXWIW_MYSQLDUMP` to point at a specific `mysqldump` binary when the one on PATH is incompatible with the server (e.g. a MySQL 9.x `mysqldump` cannot load `mysql_native_password` against a server still using that auth plugin — `EXWIW_MYSQLDUMP=/path/to/mysql@8.0/bin/mysqldump`). 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`.
|
|
112
110
|
|
|
113
111
|
you need to delete the records before importing the dump,
|
|
114
112
|
`delete-{idx}-{table_name}.sql` will help you to do that.
|
|
@@ -183,7 +181,7 @@ It is a distinct task and class (`Exwiw::MongoidSchemaGenerator`) from the Activ
|
|
|
183
181
|
|
|
184
182
|
Models in an inheritance hierarchy whose subclasses share the base's collection (Mongoid STI, distinguished by the auto-added `_type` discriminator) collapse into a single config: the generator discovers the subclasses via `descendants` (Mongoid registers only the base class in `Mongoid.models`) and unions every class's `fields` and `belongs_tos` into the collection config, so subclass-only fields and associations are not lost.
|
|
185
183
|
|
|
186
|
-
Regeneration preserves hand-edited `replace_with`, `filter`, `
|
|
184
|
+
Regeneration preserves hand-edited `replace_with`, `filter`, `ignore`, and `bulk_insert_chunk_size` values, like the ActiveRecord generator. Indexes are not written to the config — they are introspected from the live database at dump time (see [MongoDB notes](#mongodb-notes)). Polymorphic `belongs_to` is not yet expanded by this task.
|
|
187
185
|
|
|
188
186
|
### Configuration
|
|
189
187
|
|
|
@@ -258,15 +256,15 @@ A non-zero exit code from the shell hook aborts exwiw.
|
|
|
258
256
|
|
|
259
257
|
Note: Ruby hooks are evaluated via `instance_eval` inside the exwiw process — only pass paths you trust.
|
|
260
258
|
|
|
261
|
-
###
|
|
259
|
+
### Ignore a table
|
|
262
260
|
|
|
263
|
-
Set `"
|
|
261
|
+
Set `"ignore": true` on a table's config JSON to exclude it from data extraction. The table's DDL is still emitted into `insert-000-schema.{sql,js}` so the schema stays consistent, but no `insert-*` / `delete-*` files are generated for it and the table is never queried.
|
|
264
262
|
|
|
265
263
|
```json
|
|
266
264
|
{
|
|
267
265
|
"name": "audit_logs",
|
|
268
266
|
"primary_key": "id",
|
|
269
|
-
"
|
|
267
|
+
"ignore": true,
|
|
270
268
|
"belongs_tos": [],
|
|
271
269
|
"columns": [{ "name": "id" }]
|
|
272
270
|
}
|
|
@@ -274,9 +272,33 @@ Set `"skip": true` on a table's config JSON to exclude it from data extraction.
|
|
|
274
272
|
|
|
275
273
|
Constraints:
|
|
276
274
|
|
|
277
|
-
- If another non-
|
|
278
|
-
- Specifying
|
|
279
|
-
- `
|
|
275
|
+
- If another non-ignored table has a `belongs_to` entry pointing at an ignored table, exwiw raises `ArgumentError` on load. Remove the `belongs_to` entry on the referencing table, or unset `ignore` on the referenced table.
|
|
276
|
+
- Specifying an ignored table as `--target-table` raises `ArgumentError`.
|
|
277
|
+
- `ignore: true` is preserved by `exwiw:schema:generate` regenerations (the receiver value wins over the auto-generated config).
|
|
278
|
+
|
|
279
|
+
### Ignore / annotate a column or `belongs_to`
|
|
280
|
+
|
|
281
|
+
Individual `columns` (SQL) / `fields` (MongoDB) and `belongs_tos` entries accept two optional, **user-owned** keys:
|
|
282
|
+
|
|
283
|
+
- `comment` — a free-form note. Purely informational; exwiw never reads it.
|
|
284
|
+
- `ignore: true` — drops that entry from extraction. An ignored column/field is excluded from the `SELECT` and the generated `INSERT` (the column still exists in the target schema, since the DDL comes from the source database — exwiw just does not copy its data). An ignored `belongs_to` is removed from dependency ordering and query building, so the relation is not traversed.
|
|
285
|
+
|
|
286
|
+
```json
|
|
287
|
+
{
|
|
288
|
+
"name": "users",
|
|
289
|
+
"primary_key": "id",
|
|
290
|
+
"belongs_tos": [
|
|
291
|
+
{ "table_name": "companies", "foreign_key": "company_id" },
|
|
292
|
+
{ "table_name": "audit_logs", "foreign_key": "log_id", "ignore": true, "comment": "huge table, not needed for this export" }
|
|
293
|
+
],
|
|
294
|
+
"columns": [
|
|
295
|
+
{ "name": "id" },
|
|
296
|
+
{ "name": "secret_token", "ignore": true, "comment": "do not copy credentials" }
|
|
297
|
+
]
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
The ignored entries are removed only at runtime, right after the config is loaded from file; the JSON on disk keeps them. Both `comment` and `ignore` are **preserved across `exwiw:schema:generate` / `exwiw:mongoid:schema:generate` regenerations** (the hand-edited value wins over the auto-generated config), just like `replace_with`. This applies to the MongoDB `MongodbCollectionConfig` (`fields` / `belongs_tos`) as well.
|
|
280
302
|
|
|
281
303
|
### Polymorphic `belongs_to`
|
|
282
304
|
|
|
@@ -352,20 +374,20 @@ Constraints:
|
|
|
352
374
|
|
|
353
375
|
### Composite primary keys (unsupported)
|
|
354
376
|
|
|
355
|
-
exwiw does not yet support tables with a composite primary key. When `exwiw:schema:generate` encounters a model whose `primary_key` is an array, it still emits a config entry so the table is not silently dropped, but marks it `
|
|
377
|
+
exwiw does not yet support tables with a composite primary key. When `exwiw:schema:generate` encounters a model whose `primary_key` is an array, it still emits a config entry so the table is not silently dropped, but marks it `ignore: true`, tags it `type: "unsupported_composite_primary_key"`, and records the key columns in a `comment`:
|
|
356
378
|
|
|
357
379
|
```json
|
|
358
380
|
{
|
|
359
381
|
"name": "composite_pk_records",
|
|
360
382
|
"type": "unsupported_composite_primary_key",
|
|
361
|
-
"
|
|
383
|
+
"ignore": true,
|
|
362
384
|
"comment": "exwiw does not support composite primary keys (organization_id, location_id); data extraction is skipped.",
|
|
363
385
|
"belongs_tos": [],
|
|
364
386
|
"columns": [{ "name": "organization_id" }, { "name": "location_id" }, { "name": "name" }]
|
|
365
387
|
}
|
|
366
388
|
```
|
|
367
389
|
|
|
368
|
-
Unlike rails-managed entries, `columns` and `belongs_tos` are retained so the entry is ready to wire up once composite-key support lands. The `type` is purely a marker — `
|
|
390
|
+
Unlike rails-managed entries, `columns` and `belongs_tos` are retained so the entry is ready to wire up once composite-key support lands. The `type` is purely a marker — `ignore: true` is what actually excludes the table from extraction, so removing `ignore` (and supplying a workable `primary_key`) lets you opt the table back in manually.
|
|
369
391
|
|
|
370
392
|
### Bulk insert chunk size
|
|
371
393
|
|
|
@@ -34,8 +34,16 @@ module Exwiw
|
|
|
34
34
|
return
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
# The mysqldump binary is invoked directly (not via the mysql2/trilogy
|
|
38
|
+
# driver), so point EXWIW_MYSQLDUMP at a specific binary when the one on
|
|
39
|
+
# PATH is incompatible with the server — e.g. a MySQL 9.x client whose
|
|
40
|
+
# mysqldump cannot load `mysql_native_password` ("plugin ... cannot be
|
|
41
|
+
# loaded", exit 2) against a server still using that auth plugin.
|
|
42
|
+
mysqldump_bin = ENV['EXWIW_MYSQLDUMP']
|
|
43
|
+
mysqldump_bin = 'mysqldump' if mysqldump_bin.nil? || mysqldump_bin.empty?
|
|
44
|
+
|
|
37
45
|
cmd = [
|
|
38
|
-
|
|
46
|
+
mysqldump_bin,
|
|
39
47
|
"--host=#{@connection_config.host}",
|
|
40
48
|
"--port=#{@connection_config.port}",
|
|
41
49
|
"--user=#{@connection_config.user}",
|
|
@@ -58,11 +66,18 @@ module Exwiw
|
|
|
58
66
|
]
|
|
59
67
|
env = { 'MYSQL_PWD' => @connection_config.password.to_s }
|
|
60
68
|
|
|
61
|
-
@logger.debug(" Running
|
|
62
|
-
stdout, stderr, status =
|
|
69
|
+
@logger.debug(" Running #{mysqldump_bin} for #{table_names.size} table(s)...")
|
|
70
|
+
stdout, stderr, status =
|
|
71
|
+
begin
|
|
72
|
+
Open3.capture3(env, *cmd)
|
|
73
|
+
rescue Errno::ENOENT
|
|
74
|
+
raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \
|
|
75
|
+
"or set EXWIW_MYSQLDUMP to a mysqldump binary."
|
|
76
|
+
end
|
|
63
77
|
unless status.success?
|
|
64
78
|
if stderr.include?('command not found') || stderr.empty?
|
|
65
|
-
raise "Failed to run
|
|
79
|
+
raise "Failed to run `#{mysqldump_bin}`. Ensure the mysql client is installed and on PATH, " \
|
|
80
|
+
"or set EXWIW_MYSQLDUMP to a mysqldump binary. stderr: #{stderr}"
|
|
66
81
|
end
|
|
67
82
|
raise "mysqldump failed (exit #{status.exitstatus}): #{stderr}"
|
|
68
83
|
end
|
|
@@ -18,9 +18,27 @@ module Exwiw
|
|
|
18
18
|
# Immutable value object: a query's column names and its rows.
|
|
19
19
|
Result = Data.define(:fields, :rows)
|
|
20
20
|
|
|
21
|
+
DRIVERS = [:mysql2, :trilogy].freeze
|
|
22
|
+
|
|
21
23
|
# Pick the available driver, preferring mysql2 (exwiw's historical default).
|
|
22
24
|
# require returns false when already loaded, so this is safe to call repeatedly.
|
|
25
|
+
#
|
|
26
|
+
# Set EXWIW_MYSQL_DRIVER=trilogy to force the pure-Ruby trilogy driver. This
|
|
27
|
+
# is useful when the mysql2 gem is linked against a libmysqlclient that can
|
|
28
|
+
# no longer load the server's auth plugin (e.g. MySQL 9.x client dropped the
|
|
29
|
+
# `mysql_native_password` plugin .so, raising "Authentication plugin
|
|
30
|
+
# 'mysql_native_password' cannot be loaded" on connect).
|
|
23
31
|
def self.detect_driver
|
|
32
|
+
forced = ENV['EXWIW_MYSQL_DRIVER']
|
|
33
|
+
if forced && !forced.empty?
|
|
34
|
+
sym = forced.to_sym
|
|
35
|
+
unless DRIVERS.include?(sym)
|
|
36
|
+
raise ArgumentError,
|
|
37
|
+
"EXWIW_MYSQL_DRIVER must be one of #{DRIVERS.join(', ')}, got #{forced.inspect}."
|
|
38
|
+
end
|
|
39
|
+
return sym
|
|
40
|
+
end
|
|
41
|
+
|
|
24
42
|
require 'mysql2'
|
|
25
43
|
:mysql2
|
|
26
44
|
rescue LoadError
|
data/lib/exwiw/belongs_to.rb
CHANGED
|
@@ -12,6 +12,12 @@ module Exwiw
|
|
|
12
12
|
# non-polymorphic belongs_to.
|
|
13
13
|
attribute :foreign_type, optional(String), skip_serializing_if_nil: true
|
|
14
14
|
attribute :type_value, optional(String), skip_serializing_if_nil: true
|
|
15
|
+
# User-owned fields. The schema generators never emit them, but a user can
|
|
16
|
+
# add them by hand and they survive regeneration (see TableConfig#merge /
|
|
17
|
+
# MongodbCollectionConfig#merge). `ignore:true` drops the relation from
|
|
18
|
+
# extraction once the config is loaded (see #reject_ignored_members!).
|
|
19
|
+
attribute :comment, optional(String), skip_serializing_if_nil: true
|
|
20
|
+
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
15
21
|
|
|
16
22
|
def self.from_symbol_keys(hash)
|
|
17
23
|
from(hash.transform_keys(&:to_s))
|
|
@@ -21,6 +27,13 @@ module Exwiw
|
|
|
21
27
|
!foreign_type.nil?
|
|
22
28
|
end
|
|
23
29
|
|
|
30
|
+
# Structural identity used to match a freshly generated belongs_to against a
|
|
31
|
+
# user-maintained one during merge. `comment`/`ignore` are user-owned and so
|
|
32
|
+
# are intentionally excluded.
|
|
33
|
+
def identity
|
|
34
|
+
[table_name, foreign_key, foreign_type, type_value]
|
|
35
|
+
end
|
|
36
|
+
|
|
24
37
|
def to_hash
|
|
25
38
|
super.compact
|
|
26
39
|
end
|
data/lib/exwiw/cli.rb
CHANGED
|
@@ -76,6 +76,7 @@ module Exwiw
|
|
|
76
76
|
|
|
77
77
|
case @subcommand
|
|
78
78
|
when "export"
|
|
79
|
+
confirm_output_dir_clear!
|
|
79
80
|
Runner.new(
|
|
80
81
|
connection_config: connection_config,
|
|
81
82
|
output_dir: @output_dir,
|
|
@@ -270,6 +271,28 @@ module Exwiw
|
|
|
270
271
|
end
|
|
271
272
|
end
|
|
272
273
|
|
|
274
|
+
# The export clears @output_dir before writing (see Runner#clean_output_dir!).
|
|
275
|
+
# That is destructive, so when running interactively (stdin is a tty) ask for
|
|
276
|
+
# confirmation first. In non-interactive contexts (CI, pipes) we proceed
|
|
277
|
+
# without prompting. Only prompt when there is actually something to delete.
|
|
278
|
+
private def confirm_output_dir_clear!
|
|
279
|
+
return unless $stdin.tty?
|
|
280
|
+
return unless Dir.exist?(@output_dir)
|
|
281
|
+
|
|
282
|
+
entries = Dir.each_child(@output_dir).to_a
|
|
283
|
+
return if entries.empty?
|
|
284
|
+
|
|
285
|
+
$stderr.puts "All contents of the output dir will be removed before export:"
|
|
286
|
+
$stderr.puts " #{@output_dir} (#{entries.size} entr#{entries.size == 1 ? 'y' : 'ies'})"
|
|
287
|
+
$stderr.print "Continue? [y/N]: "
|
|
288
|
+
|
|
289
|
+
answer = $stdin.gets&.strip&.downcase
|
|
290
|
+
unless answer == "y" || answer == "yes"
|
|
291
|
+
$stderr.puts "Aborted."
|
|
292
|
+
exit 1
|
|
293
|
+
end
|
|
294
|
+
end
|
|
295
|
+
|
|
273
296
|
private def build_cli_options_hash
|
|
274
297
|
{
|
|
275
298
|
database_host: @database_host,
|
|
@@ -322,7 +345,7 @@ module Exwiw
|
|
|
322
345
|
opts.on("-h", "--host=HOST", "Target database host") { |v| @database_host = v }
|
|
323
346
|
opts.on("-p", "--port=PORT", "Target database port") { |v| @database_port = v }
|
|
324
347
|
opts.on("-u", "--user=USERNAME", "Target database user") { |v| @database_user = v }
|
|
325
|
-
opts.on("-o", "--output-dir=[DUMP_DIR_PATH]", "Output file path. default is dump
|
|
348
|
+
opts.on("-o", "--output-dir=[DUMP_DIR_PATH]", "Output file path. default is dump/. Its contents are emptied before each export (export subcommand only)") do |v|
|
|
326
349
|
v = v.end_with?("/") ? v[0..-2] : v
|
|
327
350
|
@output_dir = File.expand_path(v)
|
|
328
351
|
end
|
|
@@ -24,6 +24,10 @@ module Exwiw
|
|
|
24
24
|
not_resolved_names.empty?
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
if tables_with_no_dependencies.empty?
|
|
28
|
+
raise ArgumentError, build_cycle_error_message(table_by_name, ordered_table_names)
|
|
29
|
+
end
|
|
30
|
+
|
|
27
31
|
tables_with_no_dependencies.each do |table|
|
|
28
32
|
ordered_table_names << table.name
|
|
29
33
|
table_by_name.delete(table.name)
|
|
@@ -38,5 +42,25 @@ module Exwiw
|
|
|
38
42
|
acc << relation.table_name
|
|
39
43
|
end
|
|
40
44
|
end
|
|
45
|
+
|
|
46
|
+
# When no table can be resolved but some remain, the belongs_to graph
|
|
47
|
+
# contains a cycle (e.g. A belongs_to B and B belongs_to A). A topological
|
|
48
|
+
# order cannot exist, so report the offending tables instead of looping
|
|
49
|
+
# forever.
|
|
50
|
+
private_class_method def cycle_diagnostics(table_by_name, ordered_table_names)
|
|
51
|
+
table_by_name.values.map do |table|
|
|
52
|
+
unresolved = (compute_table_dependencies(table) - ordered_table_names - [table.name]).uniq
|
|
53
|
+
" #{table.name} -> #{unresolved.join(', ')}"
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private_class_method def build_cycle_error_message(table_by_name, ordered_table_names)
|
|
58
|
+
"Circular belongs_to dependency detected among tables: " \
|
|
59
|
+
"#{table_by_name.keys.sort.join(', ')}. " \
|
|
60
|
+
"A processing order cannot be determined. " \
|
|
61
|
+
"Remove one of the belongs_to entries forming the cycle.\n" \
|
|
62
|
+
"Unresolved dependencies:\n" \
|
|
63
|
+
"#{cycle_diagnostics(table_by_name, ordered_table_names).join("\n")}"
|
|
64
|
+
end
|
|
41
65
|
end
|
|
42
66
|
end
|
data/lib/exwiw/explain_runner.rb
CHANGED
|
@@ -19,7 +19,7 @@ module Exwiw
|
|
|
19
19
|
def run
|
|
20
20
|
adapter = Adapter.build(@connection_config, @logger)
|
|
21
21
|
configs = load_table_config(adapter.class.table_config_class)
|
|
22
|
-
|
|
22
|
+
validate_ignored(configs)
|
|
23
23
|
|
|
24
24
|
table_by_name = configs.each_with_object({}) { |config, hash| hash[config.name] = config }
|
|
25
25
|
|
|
@@ -32,8 +32,8 @@ module Exwiw
|
|
|
32
32
|
total_size = ordered_table_names.size
|
|
33
33
|
ordered_table_names.each_with_index do |table_name, idx|
|
|
34
34
|
table = table_by_name.fetch(table_name)
|
|
35
|
-
if table.
|
|
36
|
-
@logger.debug("Skipping explain for '#{table_name}' (
|
|
35
|
+
if table.ignore
|
|
36
|
+
@logger.debug("Skipping explain for '#{table_name}' (ignore:true)")
|
|
37
37
|
next
|
|
38
38
|
end
|
|
39
39
|
|
|
@@ -55,30 +55,30 @@ module Exwiw
|
|
|
55
55
|
private def load_table_config(klass)
|
|
56
56
|
Dir[File.join(@config_dir, "*.json")].map do |file|
|
|
57
57
|
json = JSON.parse(File.read(file))
|
|
58
|
-
klass.from(json)
|
|
58
|
+
klass.from(json).reject_ignored_members!
|
|
59
59
|
end
|
|
60
60
|
end
|
|
61
61
|
|
|
62
|
-
private def
|
|
63
|
-
|
|
64
|
-
return if
|
|
62
|
+
private def validate_ignored(configs)
|
|
63
|
+
ignored_names = configs.select { |c| c.ignore }.map(&:name).to_set
|
|
64
|
+
return if ignored_names.empty?
|
|
65
65
|
|
|
66
66
|
configs.each do |config|
|
|
67
|
-
next if config.
|
|
67
|
+
next if config.ignore
|
|
68
68
|
next unless config.respond_to?(:belongs_tos)
|
|
69
69
|
|
|
70
|
-
dangling = config.belongs_tos.select { |rel|
|
|
70
|
+
dangling = config.belongs_tos.select { |rel| ignored_names.include?(rel.table_name) }
|
|
71
71
|
next if dangling.empty?
|
|
72
72
|
|
|
73
73
|
raise ArgumentError,
|
|
74
|
-
"Table '#{config.name}' has belongs_to references to
|
|
74
|
+
"Table '#{config.name}' has belongs_to references to ignored table(s): " \
|
|
75
75
|
"#{dangling.map(&:table_name).join(', ')}. " \
|
|
76
|
-
"Remove the belongs_to entries or unset `
|
|
76
|
+
"Remove the belongs_to entries or unset `ignore` on the referenced table."
|
|
77
77
|
end
|
|
78
78
|
|
|
79
|
-
if @dump_target.table_name &&
|
|
79
|
+
if @dump_target.table_name && ignored_names.include?(@dump_target.table_name)
|
|
80
80
|
raise ArgumentError,
|
|
81
|
-
"--target-table '#{@dump_target.table_name}' is marked
|
|
81
|
+
"--target-table '#{@dump_target.table_name}' is marked ignore:true and cannot be used as a dump target."
|
|
82
82
|
end
|
|
83
83
|
end
|
|
84
84
|
end
|
|
@@ -13,7 +13,7 @@ module Exwiw
|
|
|
13
13
|
attribute :belongs_tos, array(BelongsTo)
|
|
14
14
|
attribute :fields, array(MongodbField)
|
|
15
15
|
attribute :bulk_insert_chunk_size, optional(Integer), skip_serializing_if_nil: true
|
|
16
|
-
attribute :
|
|
16
|
+
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
17
17
|
|
|
18
18
|
# Marks this config as physically embedded inside another collection's
|
|
19
19
|
# documents. When set, this config is not processed as a standalone dump
|
|
@@ -35,12 +35,21 @@ module Exwiw
|
|
|
35
35
|
!embedded_in.nil?
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
+
# Drop the belongs_tos/fields flagged `ignore:true` so they are excluded from
|
|
39
|
+
# extraction. The config files on disk keep these entries; this is applied to
|
|
40
|
+
# the runtime config right after it is loaded (see Runner#load_table_config).
|
|
41
|
+
def reject_ignored_members!
|
|
42
|
+
self.belongs_tos = belongs_tos.reject(&:ignore)
|
|
43
|
+
self.fields = fields.reject(&:ignore)
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
# Merge an auto-generated config (`passed`) into this user-maintained one so
|
|
39
48
|
# that `MongoidSchemaGenerator` regenerations preserve hand-edited values.
|
|
40
49
|
#
|
|
41
50
|
# - structural facts come from the freshly generated config: primary_key,
|
|
42
51
|
# belongs_tos, embedded_in.
|
|
43
|
-
# - user customizations are kept from the receiver: filter,
|
|
52
|
+
# - user customizations are kept from the receiver: filter, ignore,
|
|
44
53
|
# bulk_insert_chunk_size, and each field's `replace_with` masking rule.
|
|
45
54
|
# - generated fields drive the field list (so added/removed fields track the
|
|
46
55
|
# model), but a matching receiver field wins to retain its masking.
|
|
@@ -51,18 +60,34 @@ module Exwiw
|
|
|
51
60
|
merged.name = name
|
|
52
61
|
merged.primary_key = passed.primary_key
|
|
53
62
|
merged.filter = filter
|
|
54
|
-
merged.belongs_tos = passed.belongs_tos
|
|
55
63
|
merged.bulk_insert_chunk_size = bulk_insert_chunk_size
|
|
56
|
-
merged.
|
|
64
|
+
merged.ignore = ignore
|
|
57
65
|
merged.embedded_in = passed.embedded_in
|
|
58
66
|
|
|
67
|
+
# Structural facts of each belongs_to come from the freshly generated
|
|
68
|
+
# config, but the user-owned `comment`/`ignore` carry over when the same
|
|
69
|
+
# relation still exists.
|
|
70
|
+
receiver_belongs_to_by_identity = belongs_tos.each_with_object({}) { |bt, h| h[bt.identity] = bt }
|
|
71
|
+
merged.belongs_tos = passed.belongs_tos.map do |pbt|
|
|
72
|
+
receiver_bt = receiver_belongs_to_by_identity[pbt.identity]
|
|
73
|
+
if receiver_bt
|
|
74
|
+
pbt.comment = receiver_bt.comment if receiver_bt.comment
|
|
75
|
+
pbt.ignore = receiver_bt.ignore unless receiver_bt.ignore.nil?
|
|
76
|
+
end
|
|
77
|
+
pbt
|
|
78
|
+
end
|
|
79
|
+
|
|
59
80
|
# Take each field from the freshly generated config (so structural facts
|
|
60
81
|
# like `mongoid_field_name` track the model) but carry over the user's
|
|
61
|
-
# hand-edited `replace_with`
|
|
82
|
+
# hand-edited `replace_with`/`comment`/`ignore` when the field still exists.
|
|
62
83
|
receiver_field_by_name = fields.each_with_object({}) { |f, h| h[f.name] = f }
|
|
63
84
|
merged.fields = passed.fields.map do |pf|
|
|
64
85
|
receiver = receiver_field_by_name[pf.name]
|
|
65
|
-
|
|
86
|
+
if receiver
|
|
87
|
+
pf.replace_with = receiver.replace_with if receiver.replace_with
|
|
88
|
+
pf.comment = receiver.comment if receiver.comment
|
|
89
|
+
pf.ignore = receiver.ignore unless receiver.ignore.nil?
|
|
90
|
+
end
|
|
66
91
|
pf
|
|
67
92
|
end
|
|
68
93
|
end
|
data/lib/exwiw/mongodb_field.rb
CHANGED
|
@@ -11,6 +11,11 @@ module Exwiw
|
|
|
11
11
|
# masks/projects by `name` (the storage key) — but surfacing the accessor
|
|
12
12
|
# keeps an otherwise cryptic short key understandable in the config.
|
|
13
13
|
attribute :mongoid_field_name, optional(String), skip_serializing_if_nil: true
|
|
14
|
+
# User-owned fields preserved across schema regeneration (see
|
|
15
|
+
# MongodbCollectionConfig#merge). `ignore:true` drops the field from extraction
|
|
16
|
+
# once the config is loaded (see MongodbCollectionConfig#reject_ignored_members!).
|
|
17
|
+
attribute :comment, optional(String), skip_serializing_if_nil: true
|
|
18
|
+
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
14
19
|
|
|
15
20
|
def self.from_symbol_keys(hash)
|
|
16
21
|
from(hash.transform_keys(&:to_s))
|
data/lib/exwiw/runner.rb
CHANGED
|
@@ -30,7 +30,7 @@ module Exwiw
|
|
|
30
30
|
adapter = Adapter.build(@connection_config, @logger)
|
|
31
31
|
configs = load_table_config(adapter.class.table_config_class)
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
validate_ignored(configs)
|
|
34
34
|
validate_rails_managed_target!(configs)
|
|
35
35
|
|
|
36
36
|
table_by_name = configs.each_with_object({}) { |config, hash| hash[config.name] = config }
|
|
@@ -41,9 +41,7 @@ module Exwiw
|
|
|
41
41
|
@logger.info("Determining table processing order...")
|
|
42
42
|
ordered_table_names = DetermineTableProcessingOrder.run(configs.select { |c| adapter.dumpable?(c) })
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
FileUtils.mkdir_p(@output_dir)
|
|
46
|
-
end
|
|
44
|
+
clean_output_dir!
|
|
47
45
|
|
|
48
46
|
ordered_tables = ordered_table_names.map { |n| table_by_name.fetch(n) }
|
|
49
47
|
schema_path = File.join(@output_dir, "insert-000-schema.#{adapter.schema_output_extension}")
|
|
@@ -54,8 +52,8 @@ module Exwiw
|
|
|
54
52
|
ordered_table_names.each_with_index do |table_name, idx|
|
|
55
53
|
table = table_by_name.fetch(table_name)
|
|
56
54
|
|
|
57
|
-
if table.
|
|
58
|
-
@logger.info("Skipping data extraction for '#{table_name}' (
|
|
55
|
+
if table.ignore
|
|
56
|
+
@logger.info("Skipping data extraction for '#{table_name}' (ignore:true)")
|
|
59
57
|
next
|
|
60
58
|
end
|
|
61
59
|
|
|
@@ -123,36 +121,57 @@ module Exwiw
|
|
|
123
121
|
end
|
|
124
122
|
end
|
|
125
123
|
|
|
124
|
+
# Empty the output dir before writing so each export starts from a clean
|
|
125
|
+
# slate and never mixes files from a previous run. Remove the contents
|
|
126
|
+
# (including dotfiles) rather than the dir itself, preserving the dir's own
|
|
127
|
+
# permissions/inode. The CLI is responsible for confirming this with the
|
|
128
|
+
# user when running interactively.
|
|
129
|
+
private def clean_output_dir!
|
|
130
|
+
if Dir.exist?(@output_dir)
|
|
131
|
+
entries = Dir.each_child(@output_dir).to_a
|
|
132
|
+
unless entries.empty?
|
|
133
|
+
@logger.info("Cleaning output dir #{@output_dir} (#{entries.size} entr#{entries.size == 1 ? 'y' : 'ies'})...")
|
|
134
|
+
entries.each { |entry| FileUtils.rm_rf(File.join(@output_dir, entry)) }
|
|
135
|
+
end
|
|
136
|
+
else
|
|
137
|
+
FileUtils.mkdir_p(@output_dir)
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
|
|
126
141
|
private def load_table_config(klass)
|
|
127
142
|
Dir[File.join(@config_dir, "*.json")].map do |file|
|
|
128
143
|
json = JSON.parse(File.read(file))
|
|
129
|
-
|
|
144
|
+
# Drop belongs_tos/columns(fields) flagged ignore:true so they are not
|
|
145
|
+
# considered during extraction. Done here (after loading from file)
|
|
146
|
+
# rather than in `.from` so the schema generators keep the full config
|
|
147
|
+
# and can preserve the ignored entries on regeneration.
|
|
148
|
+
klass.from(json).reject_ignored_members!
|
|
130
149
|
end
|
|
131
150
|
end
|
|
132
151
|
|
|
133
|
-
private def
|
|
134
|
-
|
|
135
|
-
return if
|
|
152
|
+
private def validate_ignored(configs)
|
|
153
|
+
ignored_names = configs.select { |c| c.ignore }.map(&:name).to_set
|
|
154
|
+
return if ignored_names.empty?
|
|
136
155
|
|
|
137
156
|
configs.each do |config|
|
|
138
|
-
next if config.
|
|
157
|
+
next if config.ignore
|
|
139
158
|
next unless config.respond_to?(:belongs_tos)
|
|
140
159
|
|
|
141
|
-
dangling = config.belongs_tos.select { |rel|
|
|
160
|
+
dangling = config.belongs_tos.select { |rel| ignored_names.include?(rel.table_name) }
|
|
142
161
|
next if dangling.empty?
|
|
143
162
|
|
|
144
163
|
raise ArgumentError,
|
|
145
|
-
"Table '#{config.name}' has belongs_to references to
|
|
164
|
+
"Table '#{config.name}' has belongs_to references to ignored table(s): " \
|
|
146
165
|
"#{dangling.map(&:table_name).join(', ')}. " \
|
|
147
|
-
"Remove the belongs_to entries or unset `
|
|
166
|
+
"Remove the belongs_to entries or unset `ignore` on the referenced table."
|
|
148
167
|
end
|
|
149
168
|
|
|
150
|
-
if @dump_target.table_name &&
|
|
169
|
+
if @dump_target.table_name && ignored_names.include?(@dump_target.table_name)
|
|
151
170
|
raise ArgumentError,
|
|
152
|
-
"--target-table '#{@dump_target.table_name}' is marked
|
|
171
|
+
"--target-table '#{@dump_target.table_name}' is marked ignore:true and cannot be used as a dump target."
|
|
153
172
|
end
|
|
154
173
|
|
|
155
|
-
|
|
174
|
+
ignored_names.each { |n| @logger.info("Table '#{n}' is marked ignore:true (schema will be included, data extraction skipped)") }
|
|
156
175
|
end
|
|
157
176
|
|
|
158
177
|
private def validate_rails_managed_target!(configs)
|
|
@@ -80,15 +80,15 @@ module Exwiw
|
|
|
80
80
|
|
|
81
81
|
# Tables with a composite primary key (`representative.primary_key` is an
|
|
82
82
|
# Array) are not supported yet. Emit them with `primary_key` omitted,
|
|
83
|
-
# `
|
|
83
|
+
# `ignore: true`, and a `type` that marks them as unsupported — the `type`
|
|
84
84
|
# acts as a signpost for adding support later. The config file itself is
|
|
85
|
-
# still generated so a user can manually remove `
|
|
86
|
-
# needed.
|
|
85
|
+
# still generated so a user can manually remove `ignore` and wire it up
|
|
86
|
+
# when needed.
|
|
87
87
|
if primary_key.is_a?(Array)
|
|
88
88
|
TableConfig.from_symbol_keys(
|
|
89
89
|
name: table_name,
|
|
90
90
|
type: TableConfig::UNSUPPORTED_COMPOSITE_PRIMARY_KEY,
|
|
91
|
-
|
|
91
|
+
ignore: true,
|
|
92
92
|
comment: "exwiw does not support composite primary keys " \
|
|
93
93
|
"(#{primary_key.join(', ')}); data extraction is skipped.",
|
|
94
94
|
belongs_tos: aggregate_belongs_tos(model_group),
|
|
@@ -147,7 +147,7 @@ module Exwiw
|
|
|
147
147
|
end
|
|
148
148
|
|
|
149
149
|
private def aggregate_belongs_tos(models)
|
|
150
|
-
belongs_to_assocs = models.flat_map { |m| m
|
|
150
|
+
belongs_to_assocs = models.flat_map { |m| belongs_to_associations_for(m) }
|
|
151
151
|
|
|
152
152
|
non_polymorphic = belongs_to_assocs
|
|
153
153
|
.reject(&:polymorphic?)
|
|
@@ -174,6 +174,27 @@ module Exwiw
|
|
|
174
174
|
(non_polymorphic + polymorphic).uniq
|
|
175
175
|
end
|
|
176
176
|
|
|
177
|
+
# `belongs_to` reflections for a model, with the synthetic HABTM left-side
|
|
178
|
+
# association removed.
|
|
179
|
+
#
|
|
180
|
+
# Rails backs every `has_and_belongs_to_many` with an anonymous join model
|
|
181
|
+
# (`HABTM_*`, a concrete `ActiveRecord::Base` descendant whose table is the
|
|
182
|
+
# join table). That join model declares two belongs_tos: the "right side"
|
|
183
|
+
# (named after the association, e.g. `belongs_to :tags` -> `tag_id`), which
|
|
184
|
+
# is correct, and a synthetic `belongs_to :left_side`. The left-side
|
|
185
|
+
# association is built with `anonymous_class:` and no `foreign_key:`, so AR
|
|
186
|
+
# derives its foreign key from the reflection name -> `left_side_id`, a
|
|
187
|
+
# column that does not exist in the join table. Dropping it leaves the join
|
|
188
|
+
# table with only its genuine foreign keys; the right-side reflections of
|
|
189
|
+
# the two HABTM_* models together still supply both (`post_id` + `tag_id`).
|
|
190
|
+
private def belongs_to_associations_for(model)
|
|
191
|
+
assocs = model.reflect_on_all_associations(:belongs_to)
|
|
192
|
+
return assocs unless model.respond_to?(:left_reflection)
|
|
193
|
+
|
|
194
|
+
left = model.left_reflection
|
|
195
|
+
assocs.reject { |assoc| assoc.equal?(left) }
|
|
196
|
+
end
|
|
197
|
+
|
|
177
198
|
# Enumerate the concrete models that can be targets of the polymorphic
|
|
178
199
|
# association `association_name`, by looking them up from every model's
|
|
179
200
|
# `has_many` / `has_one` `as:` option. The order of `concrete_models` depends
|
data/lib/exwiw/table_column.rb
CHANGED
|
@@ -7,6 +7,11 @@ module Exwiw
|
|
|
7
7
|
attribute :name, String
|
|
8
8
|
attribute :replace_with, optional(String), skip_serializing_if_nil: true
|
|
9
9
|
attribute :raw_sql, optional(String), skip_serializing_if_nil: true
|
|
10
|
+
# User-owned fields preserved across schema regeneration (see
|
|
11
|
+
# TableConfig#merge). `ignore:true` drops the column from extraction (SELECT /
|
|
12
|
+
# INSERT) once the config is loaded (see TableConfig#reject_ignored_members!).
|
|
13
|
+
attribute :comment, optional(String), skip_serializing_if_nil: true
|
|
14
|
+
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
10
15
|
|
|
11
16
|
def self.from_symbol_keys(hash)
|
|
12
17
|
from(hash.transform_keys(&:to_s))
|
data/lib/exwiw/table_config.rb
CHANGED
|
@@ -12,7 +12,7 @@ module Exwiw
|
|
|
12
12
|
].freeze
|
|
13
13
|
|
|
14
14
|
# type marking a table with a composite primary key, which exwiw does not
|
|
15
|
-
# support yet. schema:generate attaches it together with
|
|
15
|
+
# support yet. schema:generate attaches it together with ignore:true. Unlike
|
|
16
16
|
# rails-managed tables, columns/belongs_tos are retained so it can serve as a
|
|
17
17
|
# signpost for adding support later.
|
|
18
18
|
UNSUPPORTED_COMPOSITE_PRIMARY_KEY = "unsupported_composite_primary_key"
|
|
@@ -25,7 +25,7 @@ module Exwiw
|
|
|
25
25
|
attribute :belongs_tos, array(BelongsTo), default: []
|
|
26
26
|
attribute :columns, array(TableColumn), default: []
|
|
27
27
|
attribute :bulk_insert_chunk_size, optional(Integer), skip_serializing_if_nil: true
|
|
28
|
-
attribute :
|
|
28
|
+
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
29
29
|
|
|
30
30
|
def self.from(hash)
|
|
31
31
|
config = super
|
|
@@ -54,6 +54,16 @@ module Exwiw
|
|
|
54
54
|
columns.map(&:name)
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Drop the belongs_tos/columns flagged `ignore:true` so they are excluded
|
|
58
|
+
# from extraction (dependency ordering, SELECT projection, INSERT). The
|
|
59
|
+
# config files on disk keep these entries; this is applied to the runtime
|
|
60
|
+
# config right after it is loaded from a file (see Runner#load_table_config).
|
|
61
|
+
def reject_ignored_members!
|
|
62
|
+
self.belongs_tos = belongs_tos.reject(&:ignore)
|
|
63
|
+
self.columns = columns.reject(&:ignore)
|
|
64
|
+
self
|
|
65
|
+
end
|
|
66
|
+
|
|
57
67
|
def belongs_to(table_name)
|
|
58
68
|
belongs_tos.find { |relation| relation.table_name == table_name }
|
|
59
69
|
end
|
|
@@ -111,9 +121,22 @@ module Exwiw
|
|
|
111
121
|
merged_table.type = passed_table.type
|
|
112
122
|
merged_table.comment = comment
|
|
113
123
|
merged_table.filter = filter
|
|
114
|
-
merged_table.belongs_tos = passed_table.belongs_tos
|
|
115
124
|
merged_table.bulk_insert_chunk_size = passed_table.bulk_insert_chunk_size
|
|
116
|
-
merged_table.
|
|
125
|
+
merged_table.ignore = ignore
|
|
126
|
+
|
|
127
|
+
# Structural facts of each belongs_to come from the freshly generated
|
|
128
|
+
# config, but the user-owned `comment`/`ignore` carry over when the same
|
|
129
|
+
# relation still exists.
|
|
130
|
+
receiver_belongs_to_by_identity = belongs_tos.each_with_object({}) { |bt, hash| hash[bt.identity] = bt }
|
|
131
|
+
merged_table.belongs_tos =
|
|
132
|
+
passed_table.belongs_tos.map do |passed_belongs_to|
|
|
133
|
+
receiver_belongs_to = receiver_belongs_to_by_identity[passed_belongs_to.identity]
|
|
134
|
+
if receiver_belongs_to
|
|
135
|
+
passed_belongs_to.comment = receiver_belongs_to.comment if receiver_belongs_to.comment
|
|
136
|
+
passed_belongs_to.ignore = receiver_belongs_to.ignore unless receiver_belongs_to.ignore.nil?
|
|
137
|
+
end
|
|
138
|
+
passed_belongs_to
|
|
139
|
+
end
|
|
117
140
|
|
|
118
141
|
receiver_column_by_name = columns.each_with_object({}) { |column, hash| hash[column.name] = column }
|
|
119
142
|
|
|
@@ -144,9 +167,9 @@ module Exwiw
|
|
|
144
167
|
"Table '#{name}' has type=#{type}; columns must not be defined."
|
|
145
168
|
end
|
|
146
169
|
else
|
|
147
|
-
#
|
|
170
|
+
# An ignore:true table is not extracted, so primary_key is not required
|
|
148
171
|
# (e.g. a composite-primary-key table that exwiw does not support).
|
|
149
|
-
if primary_key.nil? && !
|
|
172
|
+
if primary_key.nil? && !ignore
|
|
150
173
|
raise ArgumentError, "Table '#{name}' requires primary_key."
|
|
151
174
|
end
|
|
152
175
|
end
|
data/lib/exwiw/version.rb
CHANGED