exwiw 0.9.23 → 0.9.24

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: 9cbe5a94be08902db6990812f48eb07b3aa5cae7a507acb0f8c3cf1f90edc175
4
- data.tar.gz: 19b92d8feee9e874135fc0c9f833d290aacdb1be35c6bcd3da72c7c212352327
3
+ metadata.gz: 73740836024ec336feb7138c5a94e396df206de932de00737404d556fc403e0a
4
+ data.tar.gz: de53ac3f5e4c93f6761524041261c2becb65baa9bd396e528ca0ffa9b82b518e
5
5
  SHA512:
6
- metadata.gz: 75e631242a39ba72bd16ed7d28435f935c9d591d89888632f5fd835b2b727d6f497a976b91bbcc69bdbc8c85fbfb68d2265488d3b07b19baa37001a7d832d173
7
- data.tar.gz: abcfacf75eff9c74324fae77915f10b86ab7258f5251bf5fa7adbe73a3af318fd06ffc4e791e7abc00439b1007901d43e80865ff0394ada7eb8deba040707d1d
6
+ metadata.gz: ac457715cf1eaf521840903e2b2fbb6c5f503230d0abfb7cc24510067c2aa68c8b841d370d8eeed3e0c0ee009a105bea87cfe455a21c50b5bd462feb48c14f22
7
+ data.tar.gz: 945c4c46077e9ee6678f6fbc6e72b13186e393c4f06035dd1e9d63f10efa3c1fe6adb67a05f470e1330a48d5d8f34d008c67cb8795ba7dbda8294178f13082a0
data/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [0.9.24] - 2026-08-17
6
+
7
+ ### Fixed
8
+
9
+ - **MongoDB: `ignore: true` is now honored on embedded configs instead of silently exporting the subdocuments raw.** An ignored embedded config keeps its path out of the parent's projection, so the whole subdocument is absent from the dump ([docs/mongodb.md](docs/mongodb.md#excluding-an-embedded-path-ignore-true)); an ignored *field* of an embedded config is deleted from each subdocument before masking runs, as on a top-level collection. That deletion also fixes a leak there: an ignored field a child `references` is fetched as a propagation key and used to be exported. A primary key marked `ignore: true` — previously a silent no-op — now raises `ArgumentError` on load: dropping `_id` would make a restore assign fresh ids and break every reference pointing at the documents.
10
+
5
11
  ## [0.9.23] - 2026-08-13
6
12
 
7
13
  ### Fixed
data/README.md CHANGED
@@ -683,6 +683,7 @@ Constraints:
683
683
  - 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.
684
684
  - Specifying an ignored table as `--target-table` raises `ArgumentError`.
685
685
  - `ignore: true` is preserved by `exwiw:schema:generate` regenerations (the receiver value wins over the auto-generated config).
686
+ - On a MongoDB [embedded config](docs/mongodb.md#excluding-an-embedded-path-ignore-true), which is masked through its parent rather than dumped on its own, `ignore: true` excludes the embedded path from the parent's projection — the subdocument is left out of the dump entirely.
686
687
 
687
688
  ### Ignore / annotate a column or `belongs_to`
688
689
 
@@ -706,6 +707,8 @@ Individual `columns` (SQL) / `fields` (MongoDB) and `belongs_tos` entries accept
706
707
  }
707
708
  ```
708
709
 
710
+ On a MongoDB [embedded config](docs/mongodb.md#embedded-documents) an ignored field cannot be left out of the query — the subdocuments arrive as part of whatever the parent collection fetched — so it is removed from each subdocument during masking instead. The result is the same: the field is absent from the dump. A MongoDB config marking its primary key `ignore: true` is rejected on load — the dump has to keep the identifier, or a restore would assign fresh ids and break every reference pointing at the documents.
711
+
709
712
  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.
710
713
 
711
714
  ### `needs_mask_decision`
data/docs/mongodb.md CHANGED
@@ -38,6 +38,7 @@ exwiw can export a MongoDB database with `--adapter=mongodb`. This document coll
38
38
 
39
39
  - `replace_with_fake_data` is supported on a field ([full reference](../README.md#replace_with_fake_data)) — its `seed` names a field of the same collection (bare or `collection.`-qualified) or the primary key (`_id`), and it derives the same fake value as the SQL adapters for the same seed. It is applied document-side after `replace_with` (so a fake seed reads the already-masked value, matching the SQL adapters where `replace_with` runs in the database first), works inside embedded subdocuments, and is exclusive with `replace_with` on the same field. Add `gem "faker"` to use it (except a config using only `ja` person types). `raw_sql` and `map` are **not** supported (the `MongodbField` schema does not declare them; such keys are rejected on load — see [Unknown keys are rejected](../README.md#unknown-keys-are-rejected)); use `replace_with` for template masking.
40
40
  - The MongoDB adapter does not support the collection-level `filter` field (it raises `NotImplementedError` if set, since the SQL-string filter cannot be applied to MongoDB).
41
+ - A field marked [`ignore: true`](../README.md#ignore--annotate-a-column-or-belongs_to) is absent from the dump on an embedded config too, though it gets there differently: a top-level collection's ignored field is left out of the projection (unless a child references it — then it is fetched as a propagation key and dropped during masking too), while an embedded one arrives inside the parent's document and is deleted from the subdocument during masking. It is deleted before the collection's own `replace_with` / `replace_with_fake_data` run, so — as on a top-level collection — nothing masking the same subdocument can read it. The primary key cannot be marked `ignore: true` (that raises `ArgumentError` on load): dropping it would leave the dumped documents with no identifier, so a restore would assign fresh ids and break every reference pointing at them.
41
42
 
42
43
  ## `reverse_scope` on collections
43
44
 
@@ -107,6 +108,30 @@ At runtime:
107
108
  - Cross-collection references from inside an embedded subdocument (`belongs_tos` on an embedded config) are not supported and raise `ArgumentError` on load.
108
109
  - Specifying an embedded config as `--target-table` raises `NotImplementedError`; pass the top-level collection name instead.
109
110
 
111
+ ### Excluding an embedded path (`ignore: true`)
112
+
113
+ A parent collection fetches an embedded path only because some config claims it, so marking the embedded config [`ignore: true`](../README.md#ignore-a-table) leaves the path out of the parent's projection: the **whole subdocument is absent from the dump**, and any embedded chain hanging off it goes with it.
114
+
115
+ ```jsonc
116
+ // posts is embedded in users.posts, but its content is not wanted in the dump
117
+ {
118
+ "name": "posts",
119
+ "primary_key": "_id",
120
+ "ignore": true,
121
+ "comment": "subdocument is environment-specific; drop it rather than mask it",
122
+ "embedded_in": { "collection_name": "users", "path": "posts" },
123
+ "belongs_tos": [],
124
+ "fields": [{ "name": "_id" }, { "name": "title" }]
125
+ }
126
+ ```
127
+
128
+ This is the only way to keep an embedded path out of the dump — MongoDB does not allow mixing exclusions into the inclusion projection exwiw builds — and it is how to drop a subdocument that must not survive into the restored database at all, rather than be masked into a fake value. Two consequences worth knowing:
129
+
130
+ - Dropping is not masking: a field that must keep a plausible value belongs in `replace_with` / `replace_with_fake_data`, not here. An absent path also means an application reading it sees "not set", which is a different state from "set to a masked value".
131
+ - The path is only excluded when *no* config asks for it. If the parent collection also declares the path in its own `fields`, that declaration still pulls the raw subdocument into the dump — now with no masking applied to it, since the ignored config's rules no longer run. Remove the parent's field entry (or mark it `ignore: true`) as well.
132
+
133
+ Like every other `ignore`, it is preserved across `exwiw:mongoid:schema:generate` regenerations, so the exclusion survives a config refresh.
134
+
110
135
  ## `exwiw explain` verbosity
111
136
 
112
137
  The mongodb explain runs the server's [explain command](https://www.mongodb.com/docs/manual/reference/command/explain/) at a configurable verbosity. The default, **`queryPlanner`, only plans the query and does not execute it**, so it is safe to point at a production source. Set it with the `EXWIW_MONGODB_EXPLAIN_VERBOSITY` environment variable or the `explain_verbosity:` config key (the env var wins):
@@ -383,9 +383,18 @@ module Exwiw
383
383
  "collection-level `filter` is not supported by MongodbAdapter (collection: #{config.name})"
384
384
  end
385
385
 
386
+ # Index the embedded configs by the collection they are embedded in, so
387
+ # the parent's projection can pull their paths in and its mask plan can
388
+ # descend into them.
389
+ #
390
+ # An `ignore: true` child is left out, which keeps its path out of the
391
+ # parent's inclusion projection and so out of the dump — the only way to
392
+ # exclude an embedded path (see docs/mongodb.md). Its own embedded
393
+ # children need no handling: they are never reached.
386
394
  private def index_embedded_children(config_by_name)
387
395
  config_by_name.each_value.with_object({}) do |child, acc|
388
396
  next unless child.embedded?
397
+ next if child.ignore
389
398
 
390
399
  (acc[child.embedded_in.collection_name] ||= []) << child
391
400
  end
@@ -706,9 +715,10 @@ module Exwiw
706
715
  # document of that collection. `masked_fields` is `[field_name,
707
716
  # template_segments]` for each field carrying a `replace_with`;
708
717
  # `faked_fields` is `[field_name, deriver, seed_field]` for each field
709
- # carrying a `replace_with_fake_data`; `embedded` is one EmbeddedMask per
710
- # embedded child.
711
- MaskPlan = Struct.new(:masked_fields, :faked_fields, :embedded)
718
+ # carrying a `replace_with_fake_data`; `dropped_fields` is the name of each
719
+ # field marked `ignore:true`; `embedded` is one EmbeddedMask per embedded
720
+ # child.
721
+ MaskPlan = Struct.new(:masked_fields, :faked_fields, :dropped_fields, :embedded)
712
722
 
713
723
  # A pre-resolved embedded-child mask: the parent path split once into
714
724
  # `prefix` (the containers to descend into) and `last` (the field holding
@@ -741,11 +751,15 @@ module Exwiw
741
751
  acc << [field.name, mask]
742
752
  end
743
753
  faked_fields = build_faked_fields(config)
754
+ # Mostly for an embedded config, but a top-level one fetches an ignored
755
+ # field too when it is a propagation key (a child's `references`), and
756
+ # this is what keeps it out of the dump.
757
+ dropped_fields = config.ignored_field_names
744
758
  embedded = embedded_children_of(config).map do |child|
745
759
  *prefix, last = child.embedded_in.path.split(".")
746
760
  EmbeddedMask.new(prefix, last, build_mask_plan(child))
747
761
  end
748
- MaskPlan.new(masked_fields, faked_fields, embedded)
762
+ MaskPlan.new(masked_fields, faked_fields, dropped_fields, embedded)
749
763
  end
750
764
 
751
765
  # Compile each `replace_with_fake_data` field into `[field_name, deriver,
@@ -775,13 +789,19 @@ module Exwiw
775
789
  end
776
790
  end
777
791
 
778
- # Apply a precompiled MaskPlan to a document in place: render each
779
- # `replace_with` field, then each `replace_with_fake_data` field, then
780
- # descend into each embedded child (recursing into its own plan). Fake
781
- # fields are applied after replace_with so a fake seed reads the already-
782
- # masked value — matching the SQL adapters, where replace_with runs in the
783
- # database before the Ruby-side fake transform sees the row.
792
+ # Apply a precompiled MaskPlan to a document in place: drop each
793
+ # `ignore:true` field, then render each `replace_with` field, then each
794
+ # `replace_with_fake_data` field, then descend into each embedded child
795
+ # (recursing into its own plan). Fake fields are applied after replace_with
796
+ # so a fake seed reads the already-masked value — matching the SQL
797
+ # adapters, where replace_with runs in the database before the Ruby-side
798
+ # fake transform sees the row.
799
+ #
800
+ # Dropping comes first so an ignored field is invisible to the masking that
801
+ # follows, as on a top-level collection — normally not fetched, and dropped
802
+ # here when the projection pulled it in as a propagation key.
784
803
  private def apply_mask_plan!(doc, plan)
804
+ plan.dropped_fields.each { |name| doc.delete(name) }
785
805
  plan.masked_fields.each do |name, mask|
786
806
  # Preserve a NULL / absent source value instead of clobbering it into a
787
807
  # masked literal. `doc[name].nil?` is true for both an explicit nil and
@@ -67,6 +67,7 @@ module Exwiw
67
67
  instance.__send__(:validate_embedded!)
68
68
  instance.__send__(:validate_belongs_tos!)
69
69
  instance.__send__(:validate_fake_data!)
70
+ instance.__send__(:validate_ignored_fields!)
70
71
  instance
71
72
  end
72
73
 
@@ -78,11 +79,20 @@ module Exwiw
78
79
  !embedded_in.nil?
79
80
  end
80
81
 
82
+ # The names of the fields flagged `ignore:true`, kept answerable after
83
+ # #reject_ignored_members! has dropped the entries: an embedded config's
84
+ # subdocuments arrive inside the parent's document, so masking is the only
85
+ # place they can be removed (see MongodbAdapter#build_mask_plan).
86
+ def ignored_field_names
87
+ @ignored_field_names ||= fields.select(&:ignore).map(&:name)
88
+ end
89
+
81
90
  # Drop the belongs_tos/fields flagged `ignore:true` so they are excluded from
82
91
  # extraction. The config files on disk keep these entries; this is applied to
83
92
  # the runtime config right after it is loaded (see Runner#load_table_config).
84
93
  def reject_ignored_members!
85
94
  self.belongs_tos = belongs_tos.reject(&:ignore)
95
+ ignored_field_names # memoize before the entries are dropped
86
96
  self.fields = fields.reject(&:ignore)
87
97
  self
88
98
  end
@@ -197,6 +207,21 @@ module Exwiw
197
207
  end
198
208
  end
199
209
 
210
+ # A primary key marked `ignore: true` is rejected on load. Honoring it
211
+ # would delete the identifier from every dumped document — restore assigns
212
+ # fresh ids, breaking every reference pointing at them, and a `{_id}` mask
213
+ # template renders empty on every row — and silently not honoring it (the
214
+ # historical behavior, since the projection always fetches the key) would
215
+ # leave the config lying about what the dump contains.
216
+ private def validate_ignored_fields!
217
+ return unless fields.any? { |f| f.ignore && f.name == primary_key }
218
+
219
+ raise ArgumentError,
220
+ "MongodbCollectionConfig '#{name}': the primary key '#{primary_key}' must not be " \
221
+ "marked ignore: true (the dump has to keep the identifier; remove the ignore, " \
222
+ "or use ignore: true on the collection to exclude it entirely)."
223
+ end
224
+
200
225
  private def validate_embedded!
201
226
  return unless embedded?
202
227
 
data/lib/exwiw/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Exwiw
4
- VERSION = "0.9.23"
4
+ VERSION = "0.9.24"
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: 0.9.23
4
+ version: 0.9.24
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shia