exwiw 1.1.1 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: be2a5e77df07b1f463bb898d1ae06fd98bf1744d5e9cd82951bb37ba080ed379
4
- data.tar.gz: dfb735937b5afb901a314fc72349e6093df0796b166e1d9b4a7212de0a2654e7
3
+ metadata.gz: 0014e16bc315ba2aee359cdf8aeebc71edb91da1ae2a6dc1792d4da55f81593f
4
+ data.tar.gz: bec818638046dc447be1624043639240743bda93bd25607fc3977475cfc4c68c
5
5
  SHA512:
6
- metadata.gz: 9736dc30f83eb07744acc42413f2b2f697c19267bcbd40a355166e78e8fae7fdd42c66f057d5583297dc21f7c9ac6a2dd84814f1cfd819208dadad97e5b3afda
7
- data.tar.gz: bf9797812840921845668d489015f05d017418e298075bde89fdc98c1d32f322520f8194a6b4f989a47fc65a5d0955ffd941fc51c20270745a391a18361e26b7
6
+ metadata.gz: 773b59703dd74b61cb4da1665f73492fe6c35b906134d26cb406ce174e5919859a5e4c6418a660dfb8f3972a75fc908da4bac77606c39474c11e015edc180638
7
+ data.tar.gz: 366f12162f6ddace018a027efbaa9a2563023dc51d4e26da20cebab8d988671419d4a3055f13dbe7b2d384030279443865b4e239d0d19fd903dba28ea4b8632a
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.1.2] - 2026-09-07
6
+
7
+ ### Added
8
+
9
+ - **`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.
10
+
5
11
  ## [1.1.1] - 2026-09-07
6
12
 
7
13
  ### 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
@@ -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
  )
@@ -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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exwiw
4
- VERSION = "1.1.1"
4
+ VERSION = "1.1.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: exwiw
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia