exwiw 0.3.3 → 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 +18 -0
- data/README.md +36 -10
- data/lib/exwiw/belongs_to.rb +13 -0
- data/lib/exwiw/cli.rb +24 -1
- 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,8 +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
|
+
|
|
5
19
|
## [0.3.3] - 2026-05-31
|
|
6
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
|
+
|
|
7
25
|
## [0.3.2] - 2026-05-31
|
|
8
26
|
|
|
9
27
|
### Changed
|
data/README.md
CHANGED
|
@@ -97,6 +97,8 @@ exwiw \
|
|
|
97
97
|
|
|
98
98
|
This command will generate sql files in the `dump` directory.
|
|
99
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
|
+
|
|
100
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.
|
|
101
103
|
- `dump/insert-{idx}-{table_name}.sql`
|
|
102
104
|
- `dump/delete-{idx}-{table_name}.sql`
|
|
@@ -179,7 +181,7 @@ It is a distinct task and class (`Exwiw::MongoidSchemaGenerator`) from the Activ
|
|
|
179
181
|
|
|
180
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.
|
|
181
183
|
|
|
182
|
-
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.
|
|
183
185
|
|
|
184
186
|
### Configuration
|
|
185
187
|
|
|
@@ -254,15 +256,15 @@ A non-zero exit code from the shell hook aborts exwiw.
|
|
|
254
256
|
|
|
255
257
|
Note: Ruby hooks are evaluated via `instance_eval` inside the exwiw process — only pass paths you trust.
|
|
256
258
|
|
|
257
|
-
###
|
|
259
|
+
### Ignore a table
|
|
258
260
|
|
|
259
|
-
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.
|
|
260
262
|
|
|
261
263
|
```json
|
|
262
264
|
{
|
|
263
265
|
"name": "audit_logs",
|
|
264
266
|
"primary_key": "id",
|
|
265
|
-
"
|
|
267
|
+
"ignore": true,
|
|
266
268
|
"belongs_tos": [],
|
|
267
269
|
"columns": [{ "name": "id" }]
|
|
268
270
|
}
|
|
@@ -270,9 +272,33 @@ Set `"skip": true` on a table's config JSON to exclude it from data extraction.
|
|
|
270
272
|
|
|
271
273
|
Constraints:
|
|
272
274
|
|
|
273
|
-
- If another non-
|
|
274
|
-
- Specifying
|
|
275
|
-
- `
|
|
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.
|
|
276
302
|
|
|
277
303
|
### Polymorphic `belongs_to`
|
|
278
304
|
|
|
@@ -348,20 +374,20 @@ Constraints:
|
|
|
348
374
|
|
|
349
375
|
### Composite primary keys (unsupported)
|
|
350
376
|
|
|
351
|
-
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`:
|
|
352
378
|
|
|
353
379
|
```json
|
|
354
380
|
{
|
|
355
381
|
"name": "composite_pk_records",
|
|
356
382
|
"type": "unsupported_composite_primary_key",
|
|
357
|
-
"
|
|
383
|
+
"ignore": true,
|
|
358
384
|
"comment": "exwiw does not support composite primary keys (organization_id, location_id); data extraction is skipped.",
|
|
359
385
|
"belongs_tos": [],
|
|
360
386
|
"columns": [{ "name": "organization_id" }, { "name": "location_id" }, { "name": "name" }]
|
|
361
387
|
}
|
|
362
388
|
```
|
|
363
389
|
|
|
364
|
-
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.
|
|
365
391
|
|
|
366
392
|
### Bulk insert chunk size
|
|
367
393
|
|
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
|
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