exwiw 1.1.1 → 1.1.3
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 +12 -0
- data/README.md +1 -0
- data/docs/mongodb.md +1 -0
- data/lib/exwiw/adapter/mongodb_adapter.rb +1 -1
- data/lib/exwiw/adapter/mysql_client.rb +3 -1
- data/lib/exwiw/mongodb_collection_config.rb +9 -0
- data/lib/exwiw/query_ast_builder.rb +1 -1
- data/lib/exwiw/reverse_scope.rb +8 -2
- data/lib/exwiw/table_config.rb +5 -0
- 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: 0feed4cda7c72c24738b7ccc03fa5cadc5321b636e48a86e07d8a169c5b7d941
|
|
4
|
+
data.tar.gz: 50241d3f38a64485adcfbc8740e5558e8049c136762068f56e9b505a93251d19
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fad77e8f61f216afc2c9af89f54784e90fc50bfa691b29bd198727be29a6114d208d867e261c89e6b14d3ad27d385c6e981bd133df4803d2cab4c201eb9e090
|
|
7
|
+
data.tar.gz: a874e74e0c5c6fe674511de041baf80b80c7dc6495b7c96a950d5200f393a9e6e6dfa004f0d341245fdf576e4f42c0425079225895e9143f8001612a6d403383
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [1.1.3] - 2026-09-08
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **MySQL (mysql2): `query` no longer reads the field list after draining the result.** mysql2 frees the underlying `MYSQL_RES` as soon as the last row has been fetched, and `Result#fields` dereferences it afterwards, so a buffered query followed by `fields` was a use-after-free. It went unnoticed on the export path, which issues only a handful of such queries, but `exwiw schema check --from-db` / `schema generate --from-db` issue several per table and crashed with a segmentation fault on Ruby 4.0 with MariaDB Connector/C 3.4. The field list is now captured before the rows are read.
|
|
10
|
+
|
|
11
|
+
## [1.1.2] - 2026-09-07
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`reverse_scope.column` matches the arm union against a column other than the primary key.** A table referenced by a shared code rather than by its id (`rate_cards.code <- contracts.rate_code`) could not be reverse-scoped, because the generated clause was always `<table>.<primary_key> IN (…)`. The new optional key names the column on the reverse-scoped table to compare with; unset, the primary key is used as before. Both the SQL adapters and the MongoDB runtime filter honor it, and a `column` that is not declared on the table/collection is rejected on load.
|
|
16
|
+
|
|
5
17
|
## [1.1.1] - 2026-09-07
|
|
6
18
|
|
|
7
19
|
### Fixed
|
data/README.md
CHANGED
|
@@ -882,6 +882,7 @@ Notes:
|
|
|
882
882
|
- **Only scoped referencers belong in `via`.** Each arm's query must come out constrained; an unconstrained referencer (e.g. a `scope_exempt` table, or one with no path to a scope) would project *every* id and union the whole table back — so such an arm is **skipped with a warning** rather than silently widening the dump. An unknown table is likewise skipped with a warning. If no arm survives, the table stays unscopable and (in [scope-column mode](#scope-column-mode)) the run aborts via `validate_scope!`.
|
|
883
883
|
- **Declarations chain.** A referencer that is itself scoped only by its *own* `reverse_scope` counts as scoped: the arm nests that declaration's `UNION` inside its query, so a normalized side table referenced by a document-style hub that is in turn reverse-scoped through a join table resolves end to end (`attachments <- documents <- join rows <- the target`). A cycle of declarations is cut — the repeated table's arm comes out unconstrained and is dropped with the warning above. The chaining is deliberately limited to `reverse_scope` arms: the *automatic* single-referencer detection still treats a child scoped solely by its own declaration as unconstrained, so such a child never rescues (or, by widening the candidate set past one, costs) a parent that relies on the automatic detection — declare `reverse_scope` on the parent if you need that shape. Chains deeper than three declarations are flagged with a warning, since each level re-embeds its referencers' subqueries and the generated SQL grows exponentially with depth.
|
|
884
884
|
- **NULLs are excluded** per arm (`IS NOT NULL`).
|
|
885
|
+
- **`column` picks the key the union is matched against.** By default the arms' projected values are compared with this table's `primary_key`. When the referencers point at another key of the table — say `rate_cards` are looked up by a `code` that `contracts.rate_code` carries, not by `rate_cards.id` — set `"reverse_scope": { "column": "code", "via": [{ "table": "contracts", "column": "rate_code" }] }` and the clause becomes `ON rate_cards.code = …`. The column must be declared in `columns`.
|
|
885
886
|
- **Satellites need no config.** A table that `belongs_to` the reverse-scoped table (e.g. `end_users.id → users.id`, or `identities.user_id → users.id`) tightens to the kept ids automatically through the normal cascade — only the reverse-scoped table itself declares `reverse_scope`. The cascade is **multi-hop**, so a table several `belongs_to` hops below the reverse-scoped table (e.g. `end_user_profiles → end_users → users`) also tightens automatically, with no config of its own.
|
|
886
887
|
- Works in both single-target and scope-column mode. In single-target mode there is no scope-column pre-flight (`validate_scope!`), so a satellite the cascade cannot resolve to a single scopable parent (e.g. it `belongs_to` two scopable hubs) is dumped in full with a warning rather than aborting. Polymorphic foreign keys are not eligible as anchors (the named `column` is always a concrete column).
|
|
887
888
|
- **The MongoDB adapter supports `reverse_scope` too** — same config shape and semantics, but the id set is captured at runtime instead of being emitted as a `UNION` subquery. See [`reverse_scope` on collections](docs/mongodb.md#reverse_scope-on-collections) under MongoDB support.
|
data/docs/mongodb.md
CHANGED
|
@@ -63,6 +63,7 @@ Where the SQL adapters emit a `UNION` subquery, MongoDB has no cross-collection
|
|
|
63
63
|
|
|
64
64
|
- **Processing order**: a reverse-scoped collection is dumped **after** all of its `via` referencers (an arm's own `belongs_to` back to the reverse-scoped collection is inverted rather than kept — the declaration states ids flow referencer → collection). If the arms form a genuine ordering cycle with the `belongs_to` graph, the export aborts with an error naming the cycle members. SQL processing order is unchanged (its INSERT output must stay loadable in foreign-key order).
|
|
65
65
|
- **Arm hygiene mirrors SQL**: an arm whose referencer is unknown, embedded, not dumped, or itself unscoped (no path to the dump target and no `reverse_scope` of its own) is **skipped with a warning** — an unscoped referencer's ids span every scope and would silently widen the dump. Per-arm `null`/absent foreign keys are dropped (the SQL `IS NOT NULL`), an array-valued foreign-key column contributes one id per element, and captured values keep their native BSON types (an `ObjectId` foreign key matches an `ObjectId` `_id` with no coercion).
|
|
66
|
+
- **`reverse_scope.column`** works as in SQL: when set, the captured ids are matched against that field (`{"<column>": {"$in": [...]}}`) instead of `primary_key`. It must be a declared field.
|
|
66
67
|
- **Precedence mirrors SQL**: a collection with its own `belongs_to` path to the dump target is scoped by that path; a `reverse_scope` declared on it is ignored.
|
|
67
68
|
- **Satellites need no config**, as in SQL: a collection that `belongs_to` the reverse-scoped collection tightens to the kept ids automatically through the ordinary captured-parent-id mechanism.
|
|
68
69
|
- **`--parallel-workers` falls back to serial** (with a warning) when any collection declares `reverse_scope` — the parallel schedule does not express the referencers-first ordering constraint yet.
|
|
@@ -637,7 +637,7 @@ module Exwiw
|
|
|
637
637
|
ids = ids.flat_map { |value| value.is_a?(Array) ? value : [value] }
|
|
638
638
|
ids.compact!
|
|
639
639
|
ids.uniq!
|
|
640
|
-
{ config.primary_key => { "$in" => ids } }
|
|
640
|
+
{ config.reverse_scope.key_for(config.primary_key) => { "$in" => ids } }
|
|
641
641
|
end
|
|
642
642
|
|
|
643
643
|
# The set of collection names *genuinely scoped* by the dump target: the
|
|
@@ -110,8 +110,10 @@ module Exwiw
|
|
|
110
110
|
# mysql2 returns nil for a statement with no result set (SET, DDL, ...).
|
|
111
111
|
return Result.new([], []) if res.nil?
|
|
112
112
|
|
|
113
|
+
# mysql2 frees the MYSQL_RES once every row is fetched; #fields after that is a use-after-free.
|
|
114
|
+
fields = res.fields
|
|
113
115
|
rows = res.to_a.map { |row| row.map { |value| self.class.stringify_value(value) } }
|
|
114
|
-
Result.new(
|
|
116
|
+
Result.new(fields, rows)
|
|
115
117
|
when :trilogy
|
|
116
118
|
res = raw.query(sql)
|
|
117
119
|
rows = res.rows.map { |row| row.map { |value| self.class.stringify_value(value) } }
|
|
@@ -68,6 +68,7 @@ module Exwiw
|
|
|
68
68
|
instance.__send__(:validate_belongs_tos!)
|
|
69
69
|
instance.__send__(:validate_fake_data!)
|
|
70
70
|
instance.__send__(:validate_ignored_fields!)
|
|
71
|
+
instance.__send__(:validate_reverse_scope_column!)
|
|
71
72
|
instance
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -222,6 +223,14 @@ module Exwiw
|
|
|
222
223
|
"or use ignore: true on the collection to exclude it entirely)."
|
|
223
224
|
end
|
|
224
225
|
|
|
226
|
+
private def validate_reverse_scope_column!
|
|
227
|
+
column = reverse_scope&.column
|
|
228
|
+
return if column.nil? || ignore || column == primary_key || fields.any? { |f| f.name == column }
|
|
229
|
+
|
|
230
|
+
raise ArgumentError,
|
|
231
|
+
"MongodbCollectionConfig '#{name}': reverse_scope.column '#{column}' is not a declared field."
|
|
232
|
+
end
|
|
233
|
+
|
|
225
234
|
private def validate_embedded!
|
|
226
235
|
return unless embedded?
|
|
227
236
|
|
|
@@ -400,7 +400,7 @@ module Exwiw
|
|
|
400
400
|
return nil if arms.empty?
|
|
401
401
|
|
|
402
402
|
QueryAst::WhereClause.new(
|
|
403
|
-
column_name: table.primary_key,
|
|
403
|
+
column_name: table.reverse_scope.key_for(table.primary_key),
|
|
404
404
|
operator: :in_subquery,
|
|
405
405
|
value: QueryAst::UnionSubquery.new(queries: arms)
|
|
406
406
|
)
|
data/lib/exwiw/reverse_scope.rb
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
module Exwiw
|
|
4
4
|
# One referencer arm of a {ReverseScope}: the referencing table and the column
|
|
5
|
-
# on it that points at the reverse-scoped table's primary key
|
|
5
|
+
# on it that points at the reverse-scoped table's key (its primary key, or the
|
|
6
|
+
# column named by {ReverseScope#column}).
|
|
6
7
|
#
|
|
7
8
|
# `column` is given explicitly so a *non-default* foreign key (e.g.
|
|
8
9
|
# `business_entity_customers.kantan_yoyaku_user_id`, or `organization_admins.id`
|
|
@@ -27,7 +28,7 @@ module Exwiw
|
|
|
27
28
|
# extraction queries should be UNION'd into the id set this table is
|
|
28
29
|
# constrained to:
|
|
29
30
|
#
|
|
30
|
-
# <table>.<pk> IN (
|
|
31
|
+
# <table>.<column or pk> IN (
|
|
31
32
|
# SELECT <ref1>.<col1> FROM <ref1> <ref1 scope> WHERE <col1> IS NOT NULL
|
|
32
33
|
# UNION SELECT <ref2>.<col2> FROM <ref2> <ref2 scope> WHERE <col2> IS NOT NULL
|
|
33
34
|
# UNION ...
|
|
@@ -42,6 +43,11 @@ module Exwiw
|
|
|
42
43
|
class ReverseScope
|
|
43
44
|
include Serdes
|
|
44
45
|
|
|
46
|
+
attribute :column, optional(String), skip_serializing_if_nil: true
|
|
45
47
|
attribute :via, array(ReverseScopeVia), default: []
|
|
48
|
+
|
|
49
|
+
def key_for(primary_key)
|
|
50
|
+
column || primary_key
|
|
51
|
+
end
|
|
46
52
|
end
|
|
47
53
|
end
|
data/lib/exwiw/table_config.rb
CHANGED
|
@@ -245,6 +245,11 @@ module Exwiw
|
|
|
245
245
|
"(got #{batch_scope.size})."
|
|
246
246
|
end
|
|
247
247
|
|
|
248
|
+
if reverse_scope&.column && !ignore && reverse_scope.column != primary_key && columns.none? { |c| c.name == reverse_scope.column }
|
|
249
|
+
raise ArgumentError,
|
|
250
|
+
"Table '#{name}': reverse_scope.column '#{reverse_scope.column}' is not a declared column."
|
|
251
|
+
end
|
|
252
|
+
|
|
248
253
|
columns.each { |column| validate_ruby_side_masking!(column) }
|
|
249
254
|
end
|
|
250
255
|
end
|
data/lib/exwiw/version.rb
CHANGED