exwiw 1.1.4 → 1.1.6
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/AGENTS.md +24 -0
- data/CHANGELOG.md +20 -1
- data/README.md +100 -26
- data/lib/exwiw/adapter/mongodb_adapter.rb +2 -2
- data/lib/exwiw/after_insert_hook.rb +17 -7
- data/lib/exwiw/cli.rb +88 -17
- data/lib/exwiw/query_ast.rb +25 -0
- data/lib/exwiw/query_ast_builder.rb +364 -94
- data/lib/exwiw/runner.rb +1 -0
- data/lib/exwiw/table_config.rb +17 -0
- data/lib/exwiw/version.rb +1 -1
- data/lib/exwiw.rb +40 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 345a459c251b837b718e00ed92b011c8975016af2a92149868fcd24744356041
|
|
4
|
+
data.tar.gz: c79d6c2525ae322bab05956102c424eecc6fa40864e0bc7d581912a68d20928e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 881bc0c87eb1121c9670075e059eafd77a3c551f444442575b4545a5c9b78387dd90e2726c0745618b7486e4b1f907d5cb49ce414aefd772c7fd10cd8c80060d
|
|
7
|
+
data.tar.gz: c6a2ee871b7f8ff89e1ddd048c4494c71b7b8776e9d26a1d677b24b11614d1ff4a82c418e5169935514d17404ca18d30626dcfdf105469f6a01e5b0d5664782e
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# AGENTS.md
|
|
2
|
+
|
|
3
|
+
## This repository is public
|
|
4
|
+
|
|
5
|
+
Everything here is world-readable: code, comments, fixtures, commit messages,
|
|
6
|
+
PR and issue text, review comments, and their edit history. Deleting or editing
|
|
7
|
+
text after the fact does not unpublish it.
|
|
8
|
+
|
|
9
|
+
Do not include anything that comes from a private codebase:
|
|
10
|
+
|
|
11
|
+
- table, column, model, or system names from a private application
|
|
12
|
+
- references to private repositories or their PR / issue numbers
|
|
13
|
+
- internal terminology, internal links (chat, wiki, tickets), or any customer or production data
|
|
14
|
+
|
|
15
|
+
Describe motivating cases with invented, generic names, and keep planning and
|
|
16
|
+
tracking in the private repository that needs the change. Re-read the title,
|
|
17
|
+
body, and commit messages against this list before committing or opening a PR.
|
|
18
|
+
|
|
19
|
+
## Development
|
|
20
|
+
|
|
21
|
+
See [README.md](README.md) for usage and [CHANGELOG.md](CHANGELOG.md) for the
|
|
22
|
+
release history. `bundle exec rake` compiles the native extension and runs the
|
|
23
|
+
specs; most scripts under `e2e/` need the databases from `compose.yml`
|
|
24
|
+
(the sqlite ones run without it).
|
data/CHANGELOG.md
CHANGED
|
@@ -2,11 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.1.6] - 2026-09-29
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- **`--target-table` can be omitted in scope-column mode.** Scope-column mode starts from every table that declares a `scope_column`, so the target only had to name one of them and was otherwise unused. `--ids` alone now runs in scope-column mode when any table in the schema declares a `scope_column` (tables marked `ignore: true` do not count), for both `export` and `explain`. When none does, the run stops before any output with an error saying to either name the table the ids belong to with `--target-table` (single-target mode) or declare a `scope_column` (scope-column mode). This check moved from the CLI to the schema pre-flight (`QueryAstBuilder.validate_scope!`), since it needs the schema. Runs that pass `--target-table`, and the deprecated `--scope-column` flag, behave as before. The mongodb adapter has no scope-column mode, so `--ids` there still requires `--target-collection` and is rejected by the CLI without it.
|
|
10
|
+
- **Scope-column mode can filter unrelated groups of tables by different kinds of id in one run.** A table can declare `id_space` next to its `scope_column` to say which ID space the column's values belong to, and `--ids` takes one value list per ID space: `--ids=1,2 --ids=org=<uuid>,<uuid>`. Tables without `id_space`, and `--ids` without a prefix, are the `default` ID space, so existing schemas, commands and configs keep their meaning. A table without a scope column is filtered by the ID space of the table it is scoped through. The config file's `ids:` also accepts a mapping (`ids: { default: [1, 2], org: [...] }`), and a shell after-insert hook receives each space given values as `EXWIW_IDS_<NAME>` (`EXWIW_IDS` stays the `default` space and is always set; Ruby hooks get `ids_for("<name>")`). The run aborts before extracting anything when a table's ID space was given no values, when a given ID space is used by no table, or when a table reaches scoped tables of more than one ID space, which the `belongs_to` walk would otherwise settle silently by picking the nearest. Single `--target-table` mode and the MongoDB adapter use only the `default` space and abort when a named one is given. See [Per-table `scope_column` and ID spaces](README.md#per-table-scope_column-and-id-spaces).
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
|
|
14
|
+
- **`--ids` is read as `<id_space>=<ids>` when the part before its first `=` looks like an ID space name** (a lowercase letter followed by lowercase letters, digits or underscores). An id list whose first id contains such a prefix, like `--ids=key=1`, now names the ID space `key`; pass `--ids=default=key=1` to keep it as an id. Giving `--ids` more than once for the same ID space is now an error instead of the last one silently winning.
|
|
15
|
+
- **`Exwiw::DumpTarget#ids` is a Hash from ID space name to values.** An Array passed to `DumpTarget.new(ids:)` is still accepted as the `default` space's values; read them with `#default_ids`.
|
|
16
|
+
|
|
17
|
+
## [1.1.5] - 2026-09-25
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Single `--target-table` mode extracts every polymorphic arm, not just one.** The multi-arm union was only built in scope-column mode. In single-target mode a table reached through a polymorphic `belongs_to` followed one arm: the arm pointing at the dump target if there was one, otherwise the arm on the BFS's shortest path. Every other type's rows were dropped without a warning. Dumping `shops`, a `reviews` table with `Shop` and `Product` arms came out with the shop's own reviews but none of the reviews of its products. The arms are now resolved as in scope-column mode (an arm pointing at the dump target compares its foreign key with `--ids`, the others join or probe their target's ids) and the table is constrained to their `UNION`. One single-target rule keeps existing dumps from widening: an arm target that is constrained only by the automatic referenced-by detection does not count, because such a parent is extracted just to keep a child's foreign key valid. Dumping `products` therefore still yields only `reviewable_type = 'Product'` reviews. A declared `reverse_scope` does count. Tables that reach the target through a single arm, or through a non-polymorphic `belongs_to`, emit the same SQL as before.
|
|
22
|
+
- **A table whose only route is one polymorphic arm to a table scoped by other means is no longer unscopable.** The parent cascade skipped polymorphic `belongs_to`s outright, so a table like `pictures` with a lone `imageable_type = 'Shop'` arm, pointing at a `shops` table narrowed by `reverse_scope`, was rejected in scope-column mode and dumped in full in single-target mode. The cascade now falls back to the polymorphic arms, pinned by the type column, when no plain `belongs_to` parent is scopable. A table that already had a scopable plain parent is scoped as before. A newly scoped table does, however, count as a constrained child in the automatic referenced-by detection of the tables it points at. A parent that had exactly one constrained child can now have two, so the detection steps aside and the parent falls back to the parent cascade, or is left unscopable when nothing else narrows it. Declare `reverse_scope` on the parent to keep the referencer it used before. When two independent polymorphic associations are both scopable, the table stays unscopable, and single-target mode now warns about the full dump as it does for two plain parents.
|
|
23
|
+
|
|
5
24
|
## [1.1.4] - 2026-09-18
|
|
6
25
|
|
|
7
26
|
### Fixed
|
|
8
27
|
|
|
9
|
-
- **Polymorphic arms are grouped by the type column, so a config that gives each arm its own foreign key no longer loses rows.** The multi-arm walk keyed the group on `(foreign_key, foreign_type)`, which fits what the generators emit for Rails (`<name>_id` + `<name>_type`) but not a hand-written config where one type column selects between several foreign key columns — `
|
|
28
|
+
- **Polymorphic arms are grouped by the type column, so a config that gives each arm its own foreign key no longer loses rows.** The multi-arm walk keyed the group on `(foreign_key, foreign_type)`, which fits what the generators emit for Rails (`<name>_id` + `<name>_type`) but not a hand-written config where one type column selects between several foreign key columns — `owner_type` choosing between `user_id` and `team_id`, say. Such arms were not recognised as siblings, so the table fell back to the single-path behavior: only the arm the BFS happened to settle on was extracted and every other arm's rows were dropped without a warning. Which arm survived depended on path length, so a table could even lose the arm it used to keep once a shorter one appeared. The group is now keyed on `foreign_type` alone and each arm joins on its own foreign key; two *independent* polymorphic associations have distinct type columns and stay in distinct groups, as before. Two consequences for such a table, both already true of the single-foreign-key case: its SQL becomes the `UNION` id-set form instead of a single JOIN, and `batch_scope` on it is now rejected in pre-flight (a batch of one terminus's ids does not constrain the other arms).
|
|
10
29
|
|
|
11
30
|
## [1.1.3] - 2026-09-08
|
|
12
31
|
|
data/README.md
CHANGED
|
@@ -80,7 +80,13 @@ exwiw \
|
|
|
80
80
|
--log-level=info
|
|
81
81
|
```
|
|
82
82
|
|
|
83
|
-
By default `--ids` are matched against the target table's primary key. If the target table declares a per-table `scope_column`, exwiw runs in [scope-column mode](#scope-column-mode) instead — `--ids` are then values of that shared column, and the table is scoped like any other rather than anchored by primary key.
|
|
83
|
+
By default `--ids` are matched against the target table's primary key. If the target table declares a per-table `scope_column`, exwiw runs in [scope-column mode](#scope-column-mode) instead — `--ids` are then values of that shared column, and the table is scoped like any other rather than anchored by primary key. In scope-column mode `--target-table` can be omitted: passing only `--ids` runs in scope-column mode as long as some table in the schema declares a `scope_column`.
|
|
84
|
+
|
|
85
|
+
| `--target-table` | `--ids` | Mode |
|
|
86
|
+
|---|---|---|
|
|
87
|
+
| given | given | Scope-column mode if the table declares a `scope_column`, single-target mode otherwise |
|
|
88
|
+
| omitted | given | Scope-column mode if any table declares a `scope_column`, an error otherwise (SQL adapters only) |
|
|
89
|
+
| omitted | omitted | Every table is dumped in full |
|
|
84
90
|
|
|
85
91
|
When `--target-table` and `--ids` are omitted, exwiw dumps all tables defined in `--schema-dir`:
|
|
86
92
|
|
|
@@ -153,8 +159,8 @@ Only the dump target itself is filtered by `--ids` directly. Every *other* table
|
|
|
153
159
|
How the paths behave and interact:
|
|
154
160
|
|
|
155
161
|
1. **Direct filter.** In the default single-target mode the target is anchored on its primary key (or a custom field via the mongodb-only `--ids-field`). In [scope-column mode](#scope-column-mode) there is no single anchor: every table that declares a `scope_column` is filtered on that column directly.
|
|
156
|
-
2. **`belongs_to` join walk** — the "normal join" path. exwiw BFS-walks `belongs_to` edges to the nearest terminus (the target table, or a directly scoped table in scope-column mode) and compiles the shortest path into `INNER JOIN`s. A [polymorphic `belongs_to`](#polymorphic-belongs_to) hop additionally pins the type column
|
|
157
|
-
3. **Referenced-by** handles a table with no outgoing path that is pointed *at* by a constrained child — `active_storage_blobs`, referenced by `active_storage_attachments.blob_id`, is the canonical case (see [ActiveStorage](#activestorage-has_one_attached--has_many_attached)). It is automatic but deliberately narrow: it requires a single, non-polymorphic referencer. With two or more referencers it steps aside
|
|
162
|
+
2. **`belongs_to` join walk** — the "normal join" path. exwiw BFS-walks `belongs_to` edges to the nearest terminus (the target table, or a directly scoped table in scope-column mode) and compiles the shortest path into `INNER JOIN`s. A [polymorphic `belongs_to`](#polymorphic-belongs_to) hop additionally pins the type column, and is resolved for **every** concrete arm with the arms `UNION`ed (see [Every arm is extracted](#every-arm-is-extracted)).
|
|
163
|
+
3. **Referenced-by** handles a table with no outgoing path that is pointed *at* by a constrained child — `active_storage_blobs`, referenced by `active_storage_attachments.blob_id`, is the canonical case (see [ActiveStorage](#activestorage-has_one_attached--has_many_attached)). It is automatic but deliberately narrow: it requires a single, non-polymorphic referencer. With two or more referencers it steps aside to path 5, then 6, unless you declare `reverse_scope`.
|
|
158
164
|
4. **[`reverse_scope`](#reverse-scope-for-multi-referencer-tables-reverse_scope)** is the declared, multi-referencer form of path 3: the config enumerates which referencers' (already scoped) queries feed the id set. Unscoped arms are skipped with a warning rather than widening the dump.
|
|
159
165
|
5. **Scoped-parent cascade** rescues satellites: a table whose only link is a `belongs_to` toward a hub that is itself scoped (e.g. via referenced-by or `reverse_scope`) is constrained to that parent's in-scope ids. The cascade recurses hop by hop (each level requires a single unambiguous scopable parent) and stops on `belongs_to` cycles.
|
|
160
166
|
6. **Full dump** is the fallback for a genuinely unrelated table — intended for reference/master data. Single-target mode dumps it in full (with a warning when an ambiguous cascade was the reason); scope-column mode refuses to run instead, unless the table is explicitly marked [`scope_exempt: true`](#scope_exempt-intentional-full-dump) (Rails-managed tables are exempt automatically).
|
|
@@ -173,7 +179,8 @@ all. Choosing one table as `--target-table` would leave the others unrelated to
|
|
|
173
179
|
and an unrelated table is dumped in full — a problem if it holds personal data.
|
|
174
180
|
|
|
175
181
|
Scope-column mode handles this shape: instead of anchoring on one table's primary
|
|
176
|
-
key, **every table is filtered by a shared column** whose values are `--ids
|
|
182
|
+
key, **every table is filtered by a shared column** whose values are `--ids` (tables
|
|
183
|
+
keyed by an unrelated kind of id can use their own [ID space](#per-table-scope_column-and-id-spaces)).
|
|
177
184
|
Declare that column per table in the schema config with `scope_column:`:
|
|
178
185
|
|
|
179
186
|
```json
|
|
@@ -185,7 +192,7 @@ Declare that column per table in the schema config with `scope_column:`:
|
|
|
185
192
|
}
|
|
186
193
|
```
|
|
187
194
|
|
|
188
|
-
Then
|
|
195
|
+
Then pass the scope values as `--ids`, without `--target-table`:
|
|
189
196
|
|
|
190
197
|
```bash
|
|
191
198
|
exwiw \
|
|
@@ -193,15 +200,22 @@ exwiw \
|
|
|
193
200
|
--host=localhost --port=5432 --user=reader \
|
|
194
201
|
--database=app_production \
|
|
195
202
|
--schema-dir=exwiw/schema \
|
|
196
|
-
--
|
|
203
|
+
--ids=42,43 \
|
|
197
204
|
--output-dir=dump
|
|
198
205
|
```
|
|
199
206
|
|
|
200
|
-
Because
|
|
207
|
+
Because the schema declares a `scope_column`, exwiw runs in scope-column mode: the
|
|
201
208
|
`--ids` (`42,43`) are **`business_entity_id` values, not shop primary keys**, and
|
|
202
|
-
`shops`
|
|
203
|
-
|
|
204
|
-
`scope_column`
|
|
209
|
+
`shops` is scoped by `business_entity_id IN (42,43)` like every other scoped table.
|
|
210
|
+
Where extraction starts is decided by the schema, so `--target-table` is not
|
|
211
|
+
needed. If no table declares a `scope_column`, `--ids` alone is an error: name the
|
|
212
|
+
table the ids belong to with `--target-table` (single-target mode), or declare a
|
|
213
|
+
`scope_column`. Tables marked `ignore: true` do not count.
|
|
214
|
+
|
|
215
|
+
Naming a scoped table as `--target-table` (`--target-table=shops --ids=42,43`)
|
|
216
|
+
selects the same mode and extracts the same rows; the target is *not* used as a
|
|
217
|
+
primary-key anchor. (A table that declares a `scope_column` therefore can no longer
|
|
218
|
+
be single-extracted by primary key.)
|
|
205
219
|
|
|
206
220
|
Each table is resolved as follows:
|
|
207
221
|
|
|
@@ -236,7 +250,8 @@ Each table is resolved as follows:
|
|
|
236
250
|
> the broader hub cascade, set `ignore: true` on the child's `belongs_to` edge that
|
|
237
251
|
> points at this table.
|
|
238
252
|
|
|
239
|
-
Scope-column mode is SQL-only (mysql / postgresql / sqlite)
|
|
253
|
+
Scope-column mode is SQL-only (mysql / postgresql / sqlite); with the mongodb
|
|
254
|
+
adapter, `--ids` still requires `--target-collection`. It works with `exwiw
|
|
240
255
|
explain` too, which is the recommended way to preview the queries before exporting.
|
|
241
256
|
|
|
242
257
|
#### Cross-database foreign keys
|
|
@@ -266,11 +281,13 @@ opt out of the strict check and be exported in full:
|
|
|
266
281
|
Rails-managed tables (`schema_migrations`, `ar_internal_metadata`) are treated as
|
|
267
282
|
exempt automatically.
|
|
268
283
|
|
|
269
|
-
#### Per-table `scope_column` and
|
|
284
|
+
#### Per-table `scope_column` and ID spaces
|
|
270
285
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
286
|
+
The values of every `scope_column` belong to an **ID space**, and `--ids` gives the
|
|
287
|
+
values of each space. A table that does not declare `id_space` is in the `default`
|
|
288
|
+
space, which a plain `--ids=1,2` fills, so without any `id_space` every scoped table
|
|
289
|
+
is filtered by the same `--ids`. Each table names its own column, so a table that
|
|
290
|
+
stores that same value under a differently named column simply declares that name:
|
|
274
291
|
|
|
275
292
|
```json
|
|
276
293
|
{
|
|
@@ -281,8 +298,42 @@ same value under a differently named column simply declares that name:
|
|
|
281
298
|
}
|
|
282
299
|
```
|
|
283
300
|
|
|
284
|
-
|
|
285
|
-
|
|
301
|
+
When one database holds two groups of tables that no foreign key connects, each
|
|
302
|
+
keyed by a different kind of id, put one group in a named ID space. Here `tenants`
|
|
303
|
+
are identified by integer ids and `organizations` by UUIDs (each object is its own
|
|
304
|
+
schema file):
|
|
305
|
+
|
|
306
|
+
```json
|
|
307
|
+
{ "name": "tenants", "primary_key": "id", "scope_column": "id", "columns": [{ "name": "id" }] }
|
|
308
|
+
{ "name": "organizations", "primary_key": "id", "scope_column": "id", "id_space": "org", "columns": [{ "name": "id" }] }
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
```bash
|
|
312
|
+
exwiw ... --target-table=tenants \
|
|
313
|
+
--ids=1,2 \
|
|
314
|
+
--ids=org=0b6f4c1e-0000-4000-8000-000000000001
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
`--ids=1,2` is the same as `--ids=default=1,2`. The part before the first `=` is
|
|
318
|
+
read as an ID space name only when it is shaped like one (a lowercase letter
|
|
319
|
+
followed by lowercase letters, digits or underscores), so an id that contains `=`
|
|
320
|
+
itself is passed with an explicit `default=`. Each ID space may be given once. A
|
|
321
|
+
table without a scope column is filtered by the ID space of the table it is scoped
|
|
322
|
+
through (a `belongs_to` join, `reverse_scope`, referenced-by or the parent cascade).
|
|
323
|
+
|
|
324
|
+
The run aborts before extracting anything when a table is filtered by an ID space
|
|
325
|
+
that was given no values, when values are given for an ID space no table uses, or
|
|
326
|
+
when a table reaches scoped tables of more than one ID space (otherwise the
|
|
327
|
+
`belongs_to` walk would silently settle on the nearest one).
|
|
328
|
+
|
|
329
|
+
Single `--target-table` mode and the MongoDB adapter use only the `default` space
|
|
330
|
+
and abort when a named one is given. In the config file, `ids:` takes either a list
|
|
331
|
+
(the `default` space) or a mapping from ID space name to values, such as
|
|
332
|
+
`ids: { default: [1, 2], org: [0b6f4c1e-0000-4000-8000-000000000001] }`; as with
|
|
333
|
+
the other keys, it is used only when `--ids` is not passed at all.
|
|
334
|
+
|
|
335
|
+
`scope_exempt`, `scope_column` and `id_space` are user-maintained and preserved
|
|
336
|
+
across `schema:generate` regeneration (the generators never emit them).
|
|
286
337
|
|
|
287
338
|
#### Deprecated: the `--scope-column` flag
|
|
288
339
|
|
|
@@ -290,7 +341,7 @@ Before per-table declarations, scope-column mode was selected with a global
|
|
|
290
341
|
`--scope-column=COLUMN` flag (every table filtered by that one column, `--ids` its
|
|
291
342
|
values, no `--target-table`). The flag still works — SQL-only and mutually
|
|
292
343
|
exclusive with `--target-table` — but is **deprecated** and emits a warning; prefer
|
|
293
|
-
declaring a per-table `scope_column` and
|
|
344
|
+
declaring a per-table `scope_column` and dropping the flag. A per-table
|
|
294
345
|
`scope_column` takes precedence over the flag for any table that sets both.
|
|
295
346
|
|
|
296
347
|
### Config file (`exwiw.yml`)
|
|
@@ -308,6 +359,7 @@ output_format: insert # insert | copy
|
|
|
308
359
|
after_insert_hook: hooks/seed.rb
|
|
309
360
|
log_level: info # debug | info
|
|
310
361
|
# target_table / ids / ids_field / scope_column may also be set here
|
|
362
|
+
# (ids may map ID spaces to values; see "Per-table scope_column and ID spaces")
|
|
311
363
|
# mongodb_query_timeout_ms: 30000 # global query timeout (mongodb only)
|
|
312
364
|
```
|
|
313
365
|
|
|
@@ -637,7 +689,8 @@ psql -d app_dev -f dump/insert-001-shops.sql
|
|
|
637
689
|
|
|
638
690
|
**Ruby hook (`.rb`)**: provides a tiny DSL with these builtins:
|
|
639
691
|
|
|
640
|
-
- `cli_options` — Hash of all parsed CLI options (e.g. `cli_options.fetch(:ids)` returns the `--ids` array)
|
|
692
|
+
- `cli_options` — Hash of all parsed CLI options (e.g. `cli_options.fetch(:ids)` returns the `--ids` array of the `default` ID space)
|
|
693
|
+
- `ids_for(id_space = "default")` — the values the run was scoped by for that ID space (e.g. `ids_for("org")` for `--ids=org=...`)
|
|
641
694
|
- `insert_sql(template)` — appends an ERB-rendered string to a buffer. After the hook finishes, the buffer is concatenated and written to `insert-{N+1}-after_insert.{ext}` where `{N+1}` is one past the last per-table insert file. For the MongoDB adapter the equivalent alias `insert_jsonl(template)` is available; output goes to `insert-{N+1}-after_insert.jsonl`. Multiple `insert_sql` calls in a single hook are joined with `"\n"` into the same file. If no `insert_sql` call is made, no file is created.
|
|
642
695
|
- `insert_jsonl(collection, template)` — **MongoDB adapter only**. SQL statements name their table in-band, but JSONL documents do not — the import convention derives the target collection from the filename — so the two-argument form writes the ERB-rendered extended-JSON lines to the named collection's own `insert-NNN-<collection>.jsonl` file, importable with the same `mongoimport --collection <collection>` convention as the per-collection dump files. Multiple calls targeting the same collection are appended (joined with `"\n"`) into that collection's file; distinct collections get one file each, numbered sequentially after the last per-collection dump file (the collection-less `after_insert` buffer, when also used, keeps `{N+1}` and the collection files follow it). Calling this form with a SQL adapter raises an error.
|
|
643
696
|
|
|
@@ -667,7 +720,8 @@ insert_jsonl 'posts', '{"title":"welcome"}'
|
|
|
667
720
|
|
|
668
721
|
- `EXWIW_OUTPUT_DIR`, `EXWIW_SCHEMA_DIR`
|
|
669
722
|
- `EXWIW_DATABASE_ADAPTER`, `EXWIW_DATABASE_HOST`, `EXWIW_DATABASE_PORT`, `EXWIW_DATABASE_USER`, `EXWIW_DATABASE_NAME`
|
|
670
|
-
- `EXWIW_TARGET_TABLE`, `EXWIW_IDS` (comma-separated), `EXWIW_OUTPUT_FORMAT`
|
|
723
|
+
- `EXWIW_TARGET_TABLE`, `EXWIW_IDS` (comma-separated, the `default` ID space), `EXWIW_OUTPUT_FORMAT`
|
|
724
|
+
- `EXWIW_IDS_<ID_SPACE>` for each ID space given values, with the name uppercased (`--ids=org=...` becomes `EXWIW_IDS_ORG`). `EXWIW_IDS_DEFAULT` is set only when the `default` space is given, while `EXWIW_IDS` is always set
|
|
671
725
|
|
|
672
726
|
A non-zero exit code from the shell hook aborts exwiw.
|
|
673
727
|
|
|
@@ -783,9 +837,9 @@ WHERE reviews.reviewable_id IN (/* products subquery */)
|
|
|
783
837
|
|
|
784
838
|
The same type filter is applied on the join path when the polymorphic table is an intermediate hop rather than the directly-dumped table.
|
|
785
839
|
|
|
786
|
-
#### Every arm is extracted
|
|
840
|
+
#### Every arm is extracted
|
|
787
841
|
|
|
788
|
-
A polymorphic `belongs_to` is several `belongs_to` entries — one per concrete target — that a row selects between via its type column. A single JOIN can only follow **one** of them, so a join table reached through such a hop would come out holding only the rows of that one `type_value`.
|
|
842
|
+
A polymorphic `belongs_to` is several `belongs_to` entries — one per concrete target — that a row selects between via its type column. A single JOIN can only follow **one** of them, so a join table reached through such a hop would come out holding only the rows of that one `type_value`. exwiw therefore resolves **every** arm of the group and constrains the table to the union of the ids the arms keep. In [scope-column mode](#scope-column-mode):
|
|
789
843
|
|
|
790
844
|
```sql
|
|
791
845
|
SELECT comments.* FROM comments
|
|
@@ -811,15 +865,35 @@ Notes:
|
|
|
811
865
|
- **Only arms that reach the scope are included.** An arm whose target has no scope of its own is dropped, never widened — an unscoped arm would pull in every tenant's rows. An arm marked `"ignore": true` is dropped as usual, before any of this.
|
|
812
866
|
- An arm's target does not need a `belongs_to` path to a scoped table: if it is scoped by other means (referenced-by, [`reverse_scope`](#reverse-scope-for-multi-referencer-tables-reverse_scope), or the parent cascade) the arm probes that query's ids instead, still pinned by the type column.
|
|
813
867
|
- An arm whose target is scoped **through this same table** (e.g. `active_storage_blobs`, narrowed by referenced-by from `active_storage_attachments`, appearing as an `ActiveStorage::Blob` arm of those same attachments) is dropped: adopting it would make the two tables scope each other and leave the referenced table short of rows the join table kept — a dangling foreign key on import.
|
|
814
|
-
- **Arms are grouped by the type column (`foreign_type`), not by the foreign key.** Rails keeps every arm's id in one `<name>_id` column, which is what the generators emit, but a hand-written config may give each arm its own foreign key while one type column still selects between them — e.g. `
|
|
815
|
-
-
|
|
816
|
-
-
|
|
868
|
+
- **Arms are grouped by the type column (`foreign_type`), not by the foreign key.** Rails keeps every arm's id in one `<name>_id` column, which is what the generators emit, but a hand-written config may give each arm its own foreign key while one type column still selects between them — e.g. `owner_type` choosing between `user_id` and `team_id`. Those are arms of the same discriminator, so each is resolved and joined on its own key. Two *independent* polymorphic associations on one table have distinct type columns and therefore stay in distinct groups.
|
|
869
|
+
- **The route the walk picks decides whether the arms are unioned.** The route is the shortest one; in single-target mode a `belongs_to` pointing at the dump target itself is taken first. When the route leaves through a polymorphic arm, every arm of that type column is unioned. When it leaves through a non-polymorphic `belongs_to`, the table is joined along that route alone and the polymorphic arms are not unioned in, so a row reachable only through an arm is not extracted. A plain `belongs_to` is not preferred over a shorter polymorphic route. A single arm is emitted as a plain JOIN.
|
|
870
|
+
- A table with no route of its own falls back to the parent cascade, which does prefer plain parents. A scopable plain parent is used when there is exactly one. Only when there is none are the polymorphic `belongs_to`s consulted: the arms of one type column that point at tables scoped by other means (referenced-by, `reverse_scope`, or the cascade) are unioned, each pinned by the type column. Multiple independent polymorphic associations that are all scopable are as ambiguous as multiple plain parents, so the table is left unscopable (dumped in full with a warning in single-target mode). A table scoped this way also counts as a constrained child in the [referenced-by](#how-each-table-is-narrowed--the-six-scoping-paths) detection of the tables it points at, so a parent that had a single constrained referencer may now have two and be narrowed by the next path instead.
|
|
871
|
+
|
|
872
|
+
In single `--target-table` mode the same union is built. An arm that points at the dump target itself compares its foreign key with `--ids`, and the other arms join or probe their way to the target as above. One difference: an arm target that is constrained **only** by the automatic referenced-by detection does not count. Such a parent is extracted just to keep a child's foreign key valid; it does not own the rows that point at it. So dumping `products` still pulls only `reviewable_type = 'Product'` reviews, even though the product's shop is extracted too, while dumping `shops` pulls the shop's own reviews **and** the reviews of its products:
|
|
873
|
+
|
|
874
|
+
```sql
|
|
875
|
+
SELECT reviews.* FROM reviews
|
|
876
|
+
JOIN (
|
|
877
|
+
SELECT DISTINCT exwiw_scope_src_0.id AS exwiw_scope_id
|
|
878
|
+
FROM (
|
|
879
|
+
SELECT reviews.id FROM reviews
|
|
880
|
+
JOIN products ON reviews.reviewable_id = products.id
|
|
881
|
+
AND products.shop_id = 1 AND reviews.reviewable_type = 'Product'
|
|
882
|
+
UNION
|
|
883
|
+
SELECT reviews.id FROM reviews
|
|
884
|
+
WHERE reviews.reviewable_id = 1 AND reviews.reviewable_type = 'Shop'
|
|
885
|
+
) AS exwiw_scope_src_0
|
|
886
|
+
) AS exwiw_scope_ids_0
|
|
887
|
+
ON reviews.id = exwiw_scope_ids_0.exwiw_scope_id
|
|
888
|
+
```
|
|
889
|
+
|
|
890
|
+
A declared [`reverse_scope`](#reverse-scope-for-multi-referencer-tables-reverse_scope) does count, since declaring it states that the referencers own the rows.
|
|
817
891
|
|
|
818
892
|
### ActiveStorage (`has_one_attached` / `has_many_attached`)
|
|
819
893
|
|
|
820
894
|
ActiveStorage is handled automatically — no ActiveStorage-specific configuration is required. The `has_one_attached` / `has_many_attached` macros don't add a column to the owning model; they generate ordinary associations that exwiw already understands:
|
|
821
895
|
|
|
822
|
-
- **`active_storage_attachments`** is the polymorphic join row (`belongs_to :record, polymorphic: true` + `belongs_to :blob`). `exwiw:schema:generate` expands the polymorphic `record` into one `belongs_to` per model that declared `has_*_attached` (found via the generated `has_* ..., as: :record` reflections), exactly like any other [polymorphic `belongs_to`](#polymorphic-belongs_to). So only the attachments whose owner is among the dumped rows are extracted.
|
|
896
|
+
- **`active_storage_attachments`** is the polymorphic join row (`belongs_to :record, polymorphic: true` + `belongs_to :blob`). `exwiw:schema:generate` expands the polymorphic `record` into one `belongs_to` per model that declared `has_*_attached` (found via the generated `has_* ..., as: :record` reflections), exactly like any other [polymorphic `belongs_to`](#polymorphic-belongs_to). So only the attachments whose owner is among the dumped rows are extracted. Every owner type that reaches the scope is extracted (see [Every arm is extracted](#every-arm-is-extracted)); before that, only the single owner type the walk happened to settle on came out.
|
|
823
897
|
- **`active_storage_blobs`** has no `belongs_to` of its own (attachments point *at* it), so it has no path to the dump target. exwiw narrows it via **reverse / "referenced_by" extraction**: a parent table referenced by exactly one constrained, non-polymorphic child is constrained to just the referenced ids instead of dumping every row. The id set is materialized once and joined back (see [Why a JOIN, not `IN (subquery)`](#why-a-join-not-in-subquery)):
|
|
824
898
|
|
|
825
899
|
```sql
|
|
@@ -164,9 +164,9 @@ module Exwiw
|
|
|
164
164
|
# left as Strings rather than guessed at — the caller passes values
|
|
165
165
|
# matching the field's actual type.
|
|
166
166
|
if dump_target.ids_field
|
|
167
|
-
{ dump_target.ids_field => { "$in" => dump_target.
|
|
167
|
+
{ dump_target.ids_field => { "$in" => dump_target.default_ids } }
|
|
168
168
|
else
|
|
169
|
-
{ config.primary_key => { "$in" => coerce_ids(dump_target.
|
|
169
|
+
{ config.primary_key => { "$in" => coerce_ids(dump_target.default_ids) } }
|
|
170
170
|
end
|
|
171
171
|
else
|
|
172
172
|
related_collection_filter(config, config_by_name, dump_target)
|
|
@@ -4,25 +4,26 @@ require 'erb'
|
|
|
4
4
|
|
|
5
5
|
module Exwiw
|
|
6
6
|
class AfterInsertHook
|
|
7
|
-
def self.run(path:, cli_options:, output_dir:, next_idx:, output_extension:, logger:)
|
|
7
|
+
def self.run(path:, cli_options:, dump_target:, output_dir:, next_idx:, output_extension:, logger:)
|
|
8
8
|
ext = File.extname(path)
|
|
9
9
|
|
|
10
10
|
if ext == '.rb'
|
|
11
11
|
run_ruby(
|
|
12
12
|
path: path,
|
|
13
13
|
cli_options: cli_options,
|
|
14
|
+
dump_target: dump_target,
|
|
14
15
|
output_dir: output_dir,
|
|
15
16
|
next_idx: next_idx,
|
|
16
17
|
output_extension: output_extension,
|
|
17
18
|
logger: logger,
|
|
18
19
|
)
|
|
19
20
|
else
|
|
20
|
-
run_shell(path: path, cli_options: cli_options, output_dir: output_dir, logger: logger)
|
|
21
|
+
run_shell(path: path, cli_options: cli_options, dump_target: dump_target, output_dir: output_dir, logger: logger)
|
|
21
22
|
end
|
|
22
23
|
end
|
|
23
24
|
|
|
24
|
-
def self.run_ruby(path:, cli_options:, output_dir:, next_idx:, output_extension:, logger:)
|
|
25
|
-
ctx = Context.new(cli_options, output_extension: output_extension)
|
|
25
|
+
def self.run_ruby(path:, cli_options:, dump_target:, output_dir:, next_idx:, output_extension:, logger:)
|
|
26
|
+
ctx = Context.new(cli_options, dump_target: dump_target, output_extension: output_extension)
|
|
26
27
|
ctx.instance_eval(File.read(path), path)
|
|
27
28
|
|
|
28
29
|
# One output file per buffer, numbered sequentially from next_idx: first
|
|
@@ -53,7 +54,7 @@ module Exwiw
|
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
56
|
-
def self.run_shell(path:, cli_options:, output_dir:, logger:)
|
|
57
|
+
def self.run_shell(path:, cli_options:, dump_target:, output_dir:, logger:)
|
|
57
58
|
env = {
|
|
58
59
|
'EXWIW_OUTPUT_DIR' => output_dir,
|
|
59
60
|
'EXWIW_SCHEMA_DIR' => cli_options[:schema_dir].to_s,
|
|
@@ -64,9 +65,12 @@ module Exwiw
|
|
|
64
65
|
'EXWIW_DATABASE_NAME' => cli_options[:database_name].to_s,
|
|
65
66
|
'EXWIW_TARGET_TABLE' => cli_options[:target_table].to_s,
|
|
66
67
|
'EXWIW_SCOPE_COLUMN' => cli_options[:scope_column].to_s,
|
|
67
|
-
'EXWIW_IDS' =>
|
|
68
|
+
'EXWIW_IDS' => dump_target.default_ids.join(','),
|
|
68
69
|
'EXWIW_OUTPUT_FORMAT' => cli_options[:output_format].to_s,
|
|
69
70
|
}
|
|
71
|
+
dump_target.ids.each do |space, values|
|
|
72
|
+
env["EXWIW_IDS_#{space.upcase}"] = Array(values).join(',')
|
|
73
|
+
end
|
|
70
74
|
logger.info("Running after-insert shell hook: #{path}")
|
|
71
75
|
ok = system(env, path)
|
|
72
76
|
raise "after-insert shell hook failed: #{path}" unless ok
|
|
@@ -79,13 +83,19 @@ module Exwiw
|
|
|
79
83
|
|
|
80
84
|
attr_reader :cli_options, :collected, :collected_by_collection
|
|
81
85
|
|
|
82
|
-
def initialize(cli_options, output_extension: nil)
|
|
86
|
+
def initialize(cli_options, dump_target:, output_extension: nil)
|
|
83
87
|
@cli_options = cli_options
|
|
88
|
+
@dump_target = dump_target
|
|
84
89
|
@output_extension = output_extension
|
|
85
90
|
@collected = []
|
|
86
91
|
@collected_by_collection = {}
|
|
87
92
|
end
|
|
88
93
|
|
|
94
|
+
# The values the run was scoped by for the given ID space (`default` when omitted).
|
|
95
|
+
def ids_for(id_space = DEFAULT_ID_SPACE)
|
|
96
|
+
@dump_target.ids_for(id_space.to_s)
|
|
97
|
+
end
|
|
98
|
+
|
|
89
99
|
def insert_sql(template)
|
|
90
100
|
@collected << render(template)
|
|
91
101
|
end
|
data/lib/exwiw/cli.rb
CHANGED
|
@@ -111,7 +111,8 @@ module Exwiw
|
|
|
111
111
|
@database_name = nil
|
|
112
112
|
@target_table_name = nil
|
|
113
113
|
@target_collection_name = nil
|
|
114
|
-
@
|
|
114
|
+
@ids_by_id_space = {}
|
|
115
|
+
@ids_errors = []
|
|
115
116
|
@ids_field = nil
|
|
116
117
|
@scope_column = nil
|
|
117
118
|
@output_format = nil
|
|
@@ -151,7 +152,7 @@ module Exwiw
|
|
|
151
152
|
|
|
152
153
|
dump_target = DumpTarget.new(
|
|
153
154
|
table_name: @target_table_name,
|
|
154
|
-
ids: @
|
|
155
|
+
ids: @ids_by_id_space,
|
|
155
156
|
ids_field: @ids_field,
|
|
156
157
|
scope_column: @scope_column,
|
|
157
158
|
)
|
|
@@ -297,6 +298,9 @@ module Exwiw
|
|
|
297
298
|
# Fill in any options not given on the CLI from the config file. Done first
|
|
298
299
|
# so a config-provided `adapter` is in place before normalization below.
|
|
299
300
|
# CLI values always win (the merge only fills nil/empty ivars).
|
|
301
|
+
@ids_errors.each { |message| $stderr.puts message }
|
|
302
|
+
exit 1 if @ids_errors.any?
|
|
303
|
+
|
|
300
304
|
apply_config_file!
|
|
301
305
|
|
|
302
306
|
# Default log level once CLI and config have both had their say.
|
|
@@ -316,6 +320,7 @@ module Exwiw
|
|
|
316
320
|
end
|
|
317
321
|
|
|
318
322
|
resolve_target_collection_alias!
|
|
323
|
+
resolve_named_ids!
|
|
319
324
|
resolve_scope_column!
|
|
320
325
|
resolve_ids_field!
|
|
321
326
|
resolve_uri_option!
|
|
@@ -391,18 +396,22 @@ module Exwiw
|
|
|
391
396
|
exit 1
|
|
392
397
|
end
|
|
393
398
|
|
|
394
|
-
if @target_table_name &&
|
|
399
|
+
if @target_table_name && !ids_given?
|
|
395
400
|
$stderr.puts "--ids is required when --target-table is specified"
|
|
396
401
|
exit 1
|
|
397
402
|
end
|
|
398
403
|
|
|
399
|
-
if @scope_column &&
|
|
404
|
+
if @scope_column && !ids_given?
|
|
400
405
|
$stderr.puts "--ids is required when --scope-column is specified"
|
|
401
406
|
exit 1
|
|
402
407
|
end
|
|
403
408
|
|
|
404
|
-
|
|
405
|
-
|
|
409
|
+
# For the SQL adapters, --ids without a target is scope-column mode; whether
|
|
410
|
+
# the schema supports it is checked once it is loaded
|
|
411
|
+
# (QueryAstBuilder.validate_scope!). MongoDB has no scope-column mode.
|
|
412
|
+
if @database_adapter == "mongodb" && !@target_table_name && default_ids.any?
|
|
413
|
+
$stderr.puts "--target-collection (or --target-table) is required when --ids is specified " \
|
|
414
|
+
"with the mongodb adapter"
|
|
406
415
|
exit 1
|
|
407
416
|
end
|
|
408
417
|
|
|
@@ -479,11 +488,23 @@ module Exwiw
|
|
|
479
488
|
@log_level ||= config["log_level"]&.to_sym
|
|
480
489
|
@target_table_name ||= config["target_table"]
|
|
481
490
|
@target_collection_name ||= config["target_collection"]
|
|
482
|
-
|
|
491
|
+
# Like the other keys, `ids` is taken from the config only when the CLI
|
|
492
|
+
# passed no --ids at all; the two are never merged per ID space.
|
|
493
|
+
if !ids_given? && config.key?("ids")
|
|
483
494
|
raw = config["ids"]
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
495
|
+
if raw.is_a?(Hash)
|
|
496
|
+
raw.each do |space, values|
|
|
497
|
+
space = space.to_s
|
|
498
|
+
unless space.match?(ID_SPACE_NAME_PATTERN)
|
|
499
|
+
$stderr.puts "config 'ids' keys must be ID space names (a lowercase letter followed by " \
|
|
500
|
+
"lowercase letters, digits or underscores; got #{space.inspect}) in #{path}"
|
|
501
|
+
exit 1
|
|
502
|
+
end
|
|
503
|
+
@ids_by_id_space[space] = parse_config_ids(values)
|
|
504
|
+
end
|
|
505
|
+
else
|
|
506
|
+
@ids_by_id_space[DEFAULT_ID_SPACE] = parse_config_ids(raw)
|
|
507
|
+
end
|
|
487
508
|
end
|
|
488
509
|
@ids_field ||= config["ids_field"]
|
|
489
510
|
@scope_column ||= config["scope_column"]
|
|
@@ -492,6 +513,56 @@ module Exwiw
|
|
|
492
513
|
@explain_verbosity ||= config["explain_verbosity"]
|
|
493
514
|
end
|
|
494
515
|
|
|
516
|
+
# Accept either a YAML list or a "1,2" string; coerce to strings to match
|
|
517
|
+
# the CLI's `--ids=1,2` -> ["1", "2"] shape.
|
|
518
|
+
private def parse_config_ids(raw)
|
|
519
|
+
(raw.is_a?(String) ? raw.split(",") : Array(raw)).map(&:to_s)
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
# `--ids=<id_space>=1,2` gives the values of a named ID space; without the
|
|
523
|
+
# prefix the values are the default space's. The prefix is recognized only
|
|
524
|
+
# when it is shaped like an ID space name, so an id containing `=` is passed
|
|
525
|
+
# as `--ids=default=<ids>`. Each ID space may be given once: a second --ids
|
|
526
|
+
# for it would otherwise replace the first without notice.
|
|
527
|
+
private def add_ids_option(value)
|
|
528
|
+
prefix, rest = value.to_s.split("=", 2)
|
|
529
|
+
space, list =
|
|
530
|
+
if rest && prefix.match?(ID_SPACE_NAME_PATTERN)
|
|
531
|
+
[prefix, rest]
|
|
532
|
+
else
|
|
533
|
+
[DEFAULT_ID_SPACE, value.to_s]
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
if @ids_by_id_space.key?(space)
|
|
537
|
+
@ids_errors << "--ids was given more than once for the ID space '#{space}'; " \
|
|
538
|
+
"pass all of its ids in one comma-separated --ids."
|
|
539
|
+
end
|
|
540
|
+
@ids_by_id_space[space] = list.split(",")
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
private def ids_given?
|
|
544
|
+
@ids_by_id_space.each_value.any?(&:any?)
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
private def default_ids
|
|
548
|
+
@ids_by_id_space.fetch(DEFAULT_ID_SPACE, [])
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
# Named ID spaces exist only for scope-column mode, which the mongodb
|
|
552
|
+
# adapter does not have. Whether a SQL run is in scope-column mode depends on
|
|
553
|
+
# the schema, so that is checked once it is loaded (QueryAstBuilder.validate_scope!).
|
|
554
|
+
private def resolve_named_ids!
|
|
555
|
+
named = @ids_by_id_space.keys - [DEFAULT_ID_SPACE]
|
|
556
|
+
return if named.empty?
|
|
557
|
+
|
|
558
|
+
if @database_adapter == "mongodb"
|
|
559
|
+
$stderr.puts "--ids for a named ID space (#{named.map { |space| "'#{space}'" }.join(', ')}) " \
|
|
560
|
+
"is not supported by the mongodb adapter, which uses only the default ID space " \
|
|
561
|
+
"(--ids without an `<id_space>=` prefix)."
|
|
562
|
+
exit 1
|
|
563
|
+
end
|
|
564
|
+
end
|
|
565
|
+
|
|
495
566
|
# Strip a trailing slash (like the CLI's dir options) and expand relative to
|
|
496
567
|
# `base` (the config file's directory). Returns nil for a nil value.
|
|
497
568
|
private def expand_dir(value, base)
|
|
@@ -542,8 +613,8 @@ module Exwiw
|
|
|
542
613
|
|
|
543
614
|
# `--scope-column` is **deprecated**: it selected scope-column mode with a
|
|
544
615
|
# single global column for every table. The preferred way is to declare a
|
|
545
|
-
# per-table `scope_column:` in the schema config
|
|
546
|
-
#
|
|
616
|
+
# per-table `scope_column:` in the schema config (`--target-table` is then
|
|
617
|
+
# optional). The flag still works as
|
|
547
618
|
# before — SQL-only and mutually exclusive with `--target-table` — but emits a
|
|
548
619
|
# deprecation warning. Runs after resolve_target_collection_alias! so
|
|
549
620
|
# --target-collection is already folded into @target_table_name.
|
|
@@ -567,7 +638,7 @@ module Exwiw
|
|
|
567
638
|
end
|
|
568
639
|
|
|
569
640
|
$stderr.puts "warning: --scope-column is deprecated; declare a per-table `scope_column:` " \
|
|
570
|
-
"in the schema config and
|
|
641
|
+
"in the schema config and drop the flag instead."
|
|
571
642
|
end
|
|
572
643
|
|
|
573
644
|
# `--uri` supplies a full connection string (e.g. `mongodb+srv://...`) and is
|
|
@@ -780,7 +851,7 @@ module Exwiw
|
|
|
780
851
|
database_adapter: @database_adapter,
|
|
781
852
|
database_name: @database_name,
|
|
782
853
|
target_table: @target_table_name,
|
|
783
|
-
ids:
|
|
854
|
+
ids: default_ids.dup.freeze,
|
|
784
855
|
ids_field: @ids_field,
|
|
785
856
|
scope_column: @scope_column,
|
|
786
857
|
output_format: @output_format,
|
|
@@ -849,11 +920,11 @@ module Exwiw
|
|
|
849
920
|
opts.on("-a", "--adapter=ADAPTER", "Database adapter: mysql, sqlite, postgresql, mongodb (aliases: mysql2, sqlite3)") { |v| @database_adapter = v }
|
|
850
921
|
opts.on("--uri=URI", "Full MongoDB connection URI (mongodb:// or mongodb+srv://). mongodb adapter only; takes precedence over --host/--port/--user. TLS, replicaSet, authSource and credentials are read from the URI.") { |v| @connection_uri = v }
|
|
851
922
|
opts.on("--database=DATABASE", "Target database name") { |v| @database_name = v }
|
|
852
|
-
opts.on("--target-table=[TABLE]", "Target table for extraction. If omitted, dump all tables.") { |v| @target_table_name = v }
|
|
923
|
+
opts.on("--target-table=[TABLE]", "Target table for extraction. If omitted with --ids, run in scope-column mode (SQL adapters; the schema must declare `scope_column:`). If omitted without --ids, dump all tables.") { |v| @target_table_name = v }
|
|
853
924
|
opts.on("--target-collection=[COLLECTION]", "Alias of --target-table for the mongodb adapter.") { |v| @target_collection_name = v }
|
|
854
|
-
opts.on("--ids=[IDS]", "Comma-separated list of identifiers. Required when --target-table is given.") { |v|
|
|
925
|
+
opts.on("--ids=[IDS]", "Comma-separated list of identifiers. Required when --target-table is given. Prefix with `<id_space>=` (e.g. --ids=org=a,b) to give the values of a named ID space; may be repeated, once per ID space. An id containing `=` is passed as --ids=default=<ids>.") { |v| add_ids_option(v) }
|
|
855
926
|
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 }
|
|
856
|
-
opts.on("--scope-column=[COLUMN]", "DEPRECATED. Filter every table by this shared global column (--ids are its values) instead of a single --target-table. SQL adapters only; mutually exclusive with --target-table. Prefer declaring a per-table `scope_column:` in the schema config
|
|
927
|
+
opts.on("--scope-column=[COLUMN]", "DEPRECATED. Filter every table by this shared global column (--ids are its values) instead of a single --target-table. SQL adapters only; mutually exclusive with --target-table. Prefer declaring a per-table `scope_column:` in the schema config.") { |v| @scope_column = v }
|
|
857
928
|
opts.on("--output-format=[FORMAT]", "Output format: insert (default) or copy (PostgreSQL only, export subcommand only)") { |v| @output_format = v }
|
|
858
929
|
opts.on("--insert-only", "DEPRECATED: ignored. exwiw no longer generates delete-*.sql files, so every export is insert-only.") do
|
|
859
930
|
$stderr.puts "warning: --insert-only is obsolete and ignored (exwiw no longer generates delete-*.sql files); remove the flag"
|