exwiw 0.9.22 → 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 +4 -4
- data/CHANGELOG.md +12 -0
- data/README.md +3 -0
- data/docs/mongodb.md +33 -1
- data/lib/exwiw/adapter/mongodb_adapter.rb +30 -10
- data/lib/exwiw/mongodb_collection_config.rb +25 -0
- data/lib/exwiw/mongoid_schema_generator.rb +100 -33
- 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: 73740836024ec336feb7138c5a94e396df206de932de00737404d556fc403e0a
|
|
4
|
+
data.tar.gz: de53ac3f5e4c93f6761524041261c2becb65baa9bd396e528ca0ffa9b82b518e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ac457715cf1eaf521840903e2b2fbb6c5f503230d0abfb7cc24510067c2aa68c8b841d370d8eeed3e0c0ee009a105bea87cfe455a21c50b5bd462feb48c14f22
|
|
7
|
+
data.tar.gz: 945c4c46077e9ee6678f6fbc6e72b13186e393c4f06035dd1e9d63f10efa3c1fe6adb67a05f470e1330a48d5d8f34d008c67cb8795ba7dbda8294178f13082a0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
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
|
+
|
|
11
|
+
## [0.9.23] - 2026-08-13
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- **A collection stored into by both a top-level and an embedded model is now generated as the top-level collection it is, instead of silently ceasing to be dumped.** `MongoidSchemaGenerator` treated a collection group as embedded as soon as *any* model in it was — and an embedded class's collection name derives from its class name alone, so it collides with any top-level model that stores into a collection of that name, with nothing declaring the two to be related and neither side wrong. The emitted config then carried an `embedded_in` and an empty `belongs_tos`, with three consequences: `MongodbAdapter#dumpable?` (`!embedded?`) silently never dumped the collection's root documents; the top-level models' `belongs_tos` — the extraction paths that reach everything scoped below them — disappeared from the config; and a hand-maintained top-level config could not survive regeneration either, because `MongodbCollectionConfig#merge` always takes the freshly generated `embedded_in`, so `schema:check_mongoid` reported that file as changed on every run with no way to resolve it. Such a collection is genuinely top-level — it has root documents that must be dumped — so it is now built from its **root** models alone (their fields, their `belongs_tos`, and safe mode's structural/unique sets), with no `embedded_in`, and the collision is named on stderr instead: the embedded documents of the same name are **not** covered by that config and are masked, as before, through a config written by hand with an `embedded_in` and a name of its own (generation and `tidy_mongoid` both leave `embedded_in` configs alone, so such a file is stable). The collision is deliberately *not* recorded as a generated `comment` on the config, since a generated comment wins the merge and would overwrite — on every run — the note a user wrote about this very situation. Only a *root* model counts as evidence of a root collection, which is the second shape this distinguishes: an **embedded family under a plain base class** (`Address` holding the shared fields and declaring no `embedded_in`, `BillingAddress < Address` declaring one and inheriting the collection name) is not mixed at all. `embedded?` answers "does this class declare an `embedded_in`", not "does this collection have root documents", and a shared base stores nothing at the root — its fields describe subdocuments. Such a family keeps generating exactly the embedded config it always did (the same field union, in the same order, with the embedding derived from a class that actually declares one), because flipping it to a top-level config would delete the config masking those subdocuments: the same worst-case failure in the opposite direction. A non-embedded model whose embedded descendants are in the group is therefore always read as their base class, conservatively so even if the application also stored root documents under it — that keeps the behavior (and the limitation) the generator always had rather than trading a silent masking regression for a silent dumping one. Collections stored into only by top-level models, or only by embedded ones, are unaffected, including the unrepresentable embedding shapes that abort or, under `EXWIW_SKIP_UNSUPPORTED=1`, emit an `ignore: true` config. Related: a model declared `store_in collection: nil` — how an application says a document class is never persisted and only wants Mongoid's casting — is now skipped entirely instead of contributing to a config for the nameless group every such class collapses into, which describes nothing and, as a top-level config, would instruct the dump to read a collection with no name. They are dropped where the generator selects the models it describes, so they leave `tidy_mongoid`'s live-collection set as well.
|
|
16
|
+
|
|
5
17
|
## [0.9.22] - 2026-08-13
|
|
6
18
|
|
|
7
19
|
### Added
|
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):
|
|
@@ -138,7 +163,7 @@ For MongoDB applications backed by [Mongoid](https://www.mongodb.com/docs/mongoi
|
|
|
138
163
|
bundle exec rake exwiw:schema:generate_mongoid
|
|
139
164
|
```
|
|
140
165
|
|
|
141
|
-
It is a distinct task and class (`Exwiw::MongoidSchemaGenerator`) from the ActiveRecord generator because the two ORMs expose entirely different metadata. From each model it derives:
|
|
166
|
+
It is a distinct task and class (`Exwiw::MongoidSchemaGenerator`) from the ActiveRecord generator because the two ORMs expose entirely different metadata. Every application document model is described, except one declared `store_in collection: nil` — the way an application says a document class is never persisted and only wants Mongoid's casting: it has no collection name to describe, so it is skipped (and does not count as a live collection when tidying). From each remaining model it derives:
|
|
142
167
|
|
|
143
168
|
- the collection name and the `_id` primary key,
|
|
144
169
|
- `fields` from the declared Mongoid fields (referenced `belongs_to` foreign keys such as `shop_id`, and the `created_at` / `updated_at` columns added by `Mongoid::Timestamps`, are ordinary fields — their BSON `ObjectId` / `Date` values serialize as MongoDB Extended JSON at dump time). For an aliased field (`field :ctry, as: :country`), the generator emits the **stored** document key (`ctry`), never the Ruby accessor (`country`), so masking and projection target the key that actually appears in the document, and additionally records the accessor as `mongoid_field_name` on that field so the short key stays understandable (association aliases such as `shop => shop_id` and the built-in `id => _id` are not field renames and are not annotated),
|
|
@@ -147,6 +172,13 @@ It is a distinct task and class (`Exwiw::MongoidSchemaGenerator`) from the Activ
|
|
|
147
172
|
|
|
148
173
|
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.
|
|
149
174
|
|
|
175
|
+
A collection name can also be shared by embedded and non-embedded models, in two shapes the generator tells apart. `embedded?` only answers "does this class declare an `embedded_in`", which is not the same question as "does this collection have root documents":
|
|
176
|
+
|
|
177
|
+
- **An embedded family under a plain base class** — `Address` holding the shared fields and declaring no `embedded_in`, with `BillingAddress < Address` declaring one and inheriting the collection name. Nothing is stored at the root under the base; it is part of the embedded family, and its fields describe subdocuments. The collection stays **embedded**, exactly as before: one `embedded_in` config unioning the base's fields with its subclasses', derived from a class that actually declares the embedding. (A non-embedded model whose embedded descendants are in the same group is always read as their base — conservatively so, even if the application also stored root documents under it, since the alternative would silently delete the config masking those subdocuments.)
|
|
178
|
+
- **A genuine collision** — an unrelated top-level model stores into a collection whose name an embedded class derives from its own class name. Such a collection is genuinely **top-level**: it has root documents that must be dumped, so its config is built from the root models only (their fields and `belongs_tos`) with no `embedded_in`. An embedded base in the same group does not contribute its fields. Representing the group as embedded would make `dumpable?` skip the collection and silently drop those documents from the export.
|
|
179
|
+
|
|
180
|
+
The collision is reported on stderr, because the embedded documents of the same name are **not** covered by that config: to mask them, add a config by hand with an `embedded_in` and a `name` / file name of your own choosing (generation and `tidy_mongoid` both leave `embedded_in` configs alone, so a hand-written one is stable). This is deliberately not recorded as a `comment` on the generated config — a generated comment takes precedence when merging, so it would overwrite a note you wrote about this very situation on every run.
|
|
181
|
+
|
|
150
182
|
Regeneration preserves hand-edited `replace_with`, `filter`, `ignore`, `bulk_insert_chunk_size`, and `query_timeout_ms` values, like the ActiveRecord generator. Indexes are not written to the config — they are introspected from the live database at dump time (see [Output](#output)). Polymorphic `belongs_to` is not yet expanded by this task.
|
|
151
183
|
|
|
152
184
|
### Safe mode, `tidy_mongoid` and `check_mongoid`
|
|
@@ -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`; `
|
|
710
|
-
# embedded
|
|
711
|
-
|
|
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:
|
|
779
|
-
# `
|
|
780
|
-
# descend into each embedded child
|
|
781
|
-
#
|
|
782
|
-
# masked value — matching the SQL
|
|
783
|
-
# database before the Ruby-side
|
|
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
|
|
|
@@ -275,50 +275,108 @@ module Exwiw
|
|
|
275
275
|
return existing if existing&.ignore
|
|
276
276
|
|
|
277
277
|
ordered = models.sort_by { |model| [model.fields.size, model.name] }
|
|
278
|
+
embedded_models, non_embedded_models = ordered.partition(&:embedded?)
|
|
279
|
+
|
|
280
|
+
return top_level_collection(collection_name, ordered, existing) if embedded_models.empty?
|
|
281
|
+
|
|
282
|
+
# `embedded?` means "declares an `embedded_in`", not "has root documents":
|
|
283
|
+
# a plain base class of embedded documents declares none but stores nothing
|
|
284
|
+
# at the root either. Reading it as a root model would flip the family to a
|
|
285
|
+
# top-level config and delete the `embedded_in` that masks its subdocuments,
|
|
286
|
+
# so a non-embedded model with embedded descendants in the group counts as
|
|
287
|
+
# their base — even if root documents were also stored under it (keeps the
|
|
288
|
+
# generator's historical behavior rather than risking a masking regression).
|
|
289
|
+
root_models = non_embedded_models.reject do |model|
|
|
290
|
+
embedded_models.any? { |embedded| embedded < model }
|
|
291
|
+
end
|
|
278
292
|
|
|
279
|
-
|
|
293
|
+
# A root model left after that is a genuine name collision (an embedded
|
|
294
|
+
# class derives its collection name from the class name alone). The
|
|
295
|
+
# collection has root documents that must keep being dumped, so generate it
|
|
296
|
+
# as top-level from the root models only — emitting `embedded_in` here would
|
|
297
|
+
# make `MongodbAdapter#dumpable?` skip it silently, and #merge would force
|
|
298
|
+
# that shape back onto a hand-maintained config on every run. The embedded
|
|
299
|
+
# namesakes are masked by a hand-written `embedded_in` config under a
|
|
300
|
+
# synthetic name (which generation and tidy leave alone); the warning says
|
|
301
|
+
# so instead of a generated `comment`, which would overwrite the user's own
|
|
302
|
+
# note via #merge.
|
|
303
|
+
if root_models.any?
|
|
304
|
+
warn_mixed_embedding(collection_name, embedded_models)
|
|
305
|
+
return top_level_collection(collection_name, root_models, existing)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Purely embedded (possibly under a plain base, whose fields keep being
|
|
309
|
+
# unioned in as before).
|
|
310
|
+
embedded_collection(collection_name, ordered)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
# The config for a collection dumped in its own right: its models' fields and
|
|
314
|
+
# the belongs_tos its extraction follows.
|
|
315
|
+
private def top_level_collection(collection_name, models, existing)
|
|
316
|
+
MongodbCollectionConfig.from_symbol_keys(
|
|
280
317
|
name: collection_name,
|
|
281
318
|
primary_key: PRIMARY_KEY,
|
|
282
|
-
|
|
283
|
-
|
|
319
|
+
belongs_tos: aggregate_belongs_tos(models, existing),
|
|
320
|
+
fields: aggregate_fields(models),
|
|
321
|
+
)
|
|
322
|
+
end
|
|
284
323
|
|
|
285
|
-
|
|
324
|
+
# The config for a collection that exists only inside another's documents.
|
|
325
|
+
# `models` is the whole group, which may include the embedded documents' plain
|
|
326
|
+
# base class — its fields belong in the union, but only a class that declares
|
|
327
|
+
# an `embedded_in` can say where the collection lives, hence the `find`.
|
|
328
|
+
private def embedded_collection(collection_name, models)
|
|
329
|
+
attrs = {
|
|
330
|
+
name: collection_name,
|
|
331
|
+
primary_key: PRIMARY_KEY,
|
|
332
|
+
fields: aggregate_fields(models),
|
|
286
333
|
# Cross-collection references from inside an embedded array are not
|
|
287
334
|
# supported (MongodbCollectionConfig rejects them), so embedded configs
|
|
288
335
|
# always carry an empty belongs_tos and instead declare where they live.
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
attrs[:belongs_tos] = aggregate_belongs_tos(ordered, existing)
|
|
336
|
+
belongs_tos: [],
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
begin
|
|
340
|
+
attrs[:embedded_in] = embedded_in_for(models.find(&:embedded?))
|
|
341
|
+
rescue => e
|
|
342
|
+
# Known-unrepresentable shapes arrive as UnsupportedEmbedding (with a
|
|
343
|
+
# concise reason). Without skip_unsupported, re-raise so the historical
|
|
344
|
+
# fail-loud behavior is preserved. The broad rescue is a deliberate
|
|
345
|
+
# safety net for skip_unsupported (a best-effort bootstrapping mode):
|
|
346
|
+
# any other error while deriving the embedding is turned into an
|
|
347
|
+
# `ignore: true` config too, so a single odd model never aborts the run.
|
|
348
|
+
raise e unless @skip_unsupported
|
|
349
|
+
|
|
350
|
+
reason =
|
|
351
|
+
if e.is_a?(UnsupportedEmbedding)
|
|
352
|
+
e.reason
|
|
353
|
+
else
|
|
354
|
+
"raised #{e.class} while deriving embedded_in (#{e.message.lines.first&.strip})"
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
# Emit the collection as a top-level config marked `ignore: true` so it
|
|
358
|
+
# is NOT (wrongly) dumped as its own collection, and record why. The
|
|
359
|
+
# user can hand-write its embedded_in config later to dump/mask it.
|
|
360
|
+
warn("exwiw: skip_unsupported: '#{collection_name}' #{reason}; emitting ignore:true (define embedded_in by hand to dump/mask it).")
|
|
361
|
+
attrs[:ignore] = true
|
|
362
|
+
attrs[:comment] = "exwiw could not derive embedded_in (#{reason}); marked ignore:true. Define this collection's embedded_in config by hand to dump/mask it."
|
|
317
363
|
end
|
|
318
364
|
|
|
319
365
|
MongodbCollectionConfig.from_symbol_keys(attrs)
|
|
320
366
|
end
|
|
321
367
|
|
|
368
|
+
# Names the collision on stderr, once per affected collection.
|
|
369
|
+
private def warn_mixed_embedding(collection_name, embedded_models)
|
|
370
|
+
warn(
|
|
371
|
+
"exwiw: collection '#{collection_name}' is stored into by both top-level and embedded models " \
|
|
372
|
+
"(embedded: #{embedded_models.map(&:name).sort.join(', ')}); generating it as a TOP-LEVEL collection " \
|
|
373
|
+
"from its non-embedded model(s) only, so its root documents keep being dumped. The embedded " \
|
|
374
|
+
"documents of the same name are NOT covered by this config: to mask them, add a config by hand " \
|
|
375
|
+
"with an `embedded_in` and a distinct `name`/file name of your choosing (generation and tidy leave " \
|
|
376
|
+
"embedded configs alone)."
|
|
377
|
+
)
|
|
378
|
+
end
|
|
379
|
+
|
|
322
380
|
# Mongoid registers only the base class of an inheritance hierarchy in
|
|
323
381
|
# `Mongoid.models`; subclasses that store into the base's collection
|
|
324
382
|
# (STI-style, distinguished by the auto-added `_type` discriminator) are
|
|
@@ -339,11 +397,20 @@ module Exwiw
|
|
|
339
397
|
concrete(@models)
|
|
340
398
|
end
|
|
341
399
|
|
|
400
|
+
# The single place a model is judged fit to describe (used by generate! and
|
|
401
|
+
# tidy! alike): Mongoid's internal helper classes are dropped, and so is any
|
|
402
|
+
# class with an empty `collection_name` — `store_in collection: nil` is the
|
|
403
|
+
# never-persisted-model idiom, and a config for the nameless group such
|
|
404
|
+
# classes collapse into describes nothing (as top-level it would even
|
|
405
|
+
# instruct the dump to read a collection with no name). The emptiness test
|
|
406
|
+
# stays last: the preceding conditions establish that calling
|
|
407
|
+
# `collection_name` is meaningful.
|
|
342
408
|
private def concrete(models)
|
|
343
409
|
models.select do |model|
|
|
344
410
|
model.respond_to?(:collection_name) &&
|
|
345
411
|
model.name &&
|
|
346
|
-
!model.name.start_with?("Mongoid::")
|
|
412
|
+
!model.name.start_with?("Mongoid::") &&
|
|
413
|
+
!model.collection_name.to_s.empty?
|
|
347
414
|
end
|
|
348
415
|
end
|
|
349
416
|
|
data/lib/exwiw/version.rb
CHANGED