exwiw 0.3.9 → 0.4.1
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 +9 -0
- data/lib/exwiw/adapter/postgresql_adapter.rb +92 -11
- data/lib/exwiw/mongodb_collection_config.rb +10 -0
- data/lib/exwiw/mongoid_schema_generator.rb +127 -20
- data/lib/exwiw/version.rb +1 -1
- data/lib/tasks/exwiw.rake +6 -0
- 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: a743c4759cdb8b292e4fdfe8ea3d4e10fa66e15a70677dd685366fb9e26a376b
|
|
4
|
+
data.tar.gz: '04048ceca802c0d418495cdf7c002bd47047beb05a53655a451c2c77a4d8270d'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7d7a5ffca1cdae405e88fac5d72e4ed4e52ee3deec8cce0e503dcdc2b4136ec03ca2d84893c5897a3e56a78b0fa9e11dddc5f606566f32c90297815c8ade4702
|
|
7
|
+
data.tar.gz: 8e7742f720c371b41679c311d6323f8182e4f414f1ff199ac90928026ce46c2607a61a3c3a7ed91c23c97ea4b855d838bfee38b593d69c06f1dc743a17916eb0
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.1] - 2026-06-04
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `MongoidSchemaGenerator` gains an opt-in `skip_unsupported:` mode (via the rake task, `EXWIW_SKIP_UNSUPPORTED=1 bundle exec rake exwiw:schema:generate_mongoid`). When enabled, generation no longer aborts on a construct exwiw cannot represent: an unresolvable `belongs_to` (whose target class no longer exists — e.g. a stale relation left behind after the model was removed) is skipped with a stderr warning while its foreign-key column is still kept as a field, and a polymorphic / self-referential-cyclic / unresolvable-parent `embedded_in` collection is emitted as a top-level `ignore: true` config annotated with a `comment` explaining why — so it is not wrongly dumped as its own collection — instead of raising. Off by default, so the existing fail-loud behavior is unchanged for callers that do not opt in. `MongodbCollectionConfig` now also carries an optional collection-level `comment` attribute, preserved across regeneration like the field / belongs_to `comment`.
|
|
10
|
+
|
|
11
|
+
## [0.4.0] - 2026-06-04
|
|
12
|
+
|
|
13
|
+
### Fixed
|
|
14
|
+
|
|
15
|
+
- PostgreSQL: extraction no longer fails with `operator does not exist: character varying = uuid` when a `belongs_to` chain crosses a varchar foreign key and a uuid primary key. The adapter now introspects column types via `pg_attribute`/`pg_type` at query compile time and injects `::text` casts on both sides of the comparison when a uuid/varchar mismatch is detected. Covers JOIN ON conditions, WHERE IN subqueries, and bulk-delete IN clauses. ([#73](https://github.com/heyinc/exwiw/pull/73))
|
|
16
|
+
|
|
5
17
|
## [0.3.9] - 2026-06-03
|
|
6
18
|
|
|
7
19
|
### Added
|
data/README.md
CHANGED
|
@@ -200,6 +200,15 @@ Models in an inheritance hierarchy whose subclasses share the base's collection
|
|
|
200
200
|
|
|
201
201
|
Regeneration preserves hand-edited `replace_with`, `filter`, `ignore`, and `bulk_insert_chunk_size` values, like the ActiveRecord generator. Indexes are not written to the config — they are introspected from the live database at dump time (see [MongoDB notes](#mongodb-notes)). Polymorphic `belongs_to` is not yet expanded by this task.
|
|
202
202
|
|
|
203
|
+
By default the task **aborts** when a model uses a construct exwiw cannot represent: a `belongs_to` whose target class can no longer be resolved (a stale relation left behind after its model was removed), or a polymorphic / self-referential-cyclic / unresolvable-parent `embedded_in` (see the cases above). Set `EXWIW_SKIP_UNSUPPORTED=1` to keep going instead:
|
|
204
|
+
|
|
205
|
+
```bash
|
|
206
|
+
EXWIW_SKIP_UNSUPPORTED=1 bundle exec rake exwiw:schema:generate_mongoid
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- An unresolvable `belongs_to` is dropped from the collection's `belongs_tos` (its foreign-key column is still kept as an ordinary field, like the polymorphic / HABTM cases) and a warning naming the relation is printed to stderr.
|
|
210
|
+
- An unrepresentable `embedded_in` collection is emitted as a **top-level** config marked `"ignore": true` with a `comment` recording why, and a warning is printed. `ignore: true` keeps it out of extraction so it is not wrongly dumped as its own collection; to actually dump/mask such an embedded collection, define its `embedded_in` config by hand (see [Embedded documents](#embedded-documents)). This is useful for bootstrapping a config against a large app where a handful of legacy/polymorphic collections would otherwise block the whole run.
|
|
211
|
+
|
|
203
212
|
### Configuration
|
|
204
213
|
|
|
205
214
|
This is an example of the one table schema:
|
|
@@ -177,8 +177,17 @@ module Exwiw
|
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
foreign_key = first_join.foreign_key
|
|
180
|
-
|
|
181
|
-
|
|
180
|
+
outer_table = select_query_ast.from_table_name
|
|
181
|
+
inner_table = first_join.join_table_name
|
|
182
|
+
inner_column = first_join.primary_key
|
|
183
|
+
cast_to = types_need_cast?(
|
|
184
|
+
column_pg_type(outer_table, foreign_key),
|
|
185
|
+
column_pg_type(inner_table, inner_column)
|
|
186
|
+
) ? 'text' : nil
|
|
187
|
+
subquery_sql = compile_ast(subquery_ast, select_cast_to: cast_to)
|
|
188
|
+
outer_expr = "#{outer_table}.#{foreign_key}"
|
|
189
|
+
outer_expr = "#{outer_expr}::text" if cast_to
|
|
190
|
+
sql += "\nWHERE #{outer_expr} IN (#{subquery_sql})"
|
|
182
191
|
|
|
183
192
|
# first_join.base_where_clauses holds conditions on the outer
|
|
184
193
|
# delete-target table (from_table_name), such as a polymorphic type
|
|
@@ -195,19 +204,30 @@ module Exwiw
|
|
|
195
204
|
sql
|
|
196
205
|
end
|
|
197
206
|
|
|
198
|
-
def compile_ast(query_ast)
|
|
207
|
+
def compile_ast(query_ast, select_cast_to: nil)
|
|
199
208
|
raise NotImplementedError unless query_ast.is_a?(Exwiw::QueryAst::Select)
|
|
200
209
|
|
|
201
210
|
sql = "SELECT "
|
|
202
211
|
sql += if query_ast.select_all
|
|
203
212
|
"*"
|
|
204
213
|
else
|
|
205
|
-
query_ast.columns.map { |col| compile_column_name(query_ast, col) }
|
|
214
|
+
cols = query_ast.columns.map { |col| compile_column_name(query_ast, col) }
|
|
215
|
+
cols = cols.map { |c| "#{c}::#{select_cast_to}" } if select_cast_to
|
|
216
|
+
cols.join(', ')
|
|
206
217
|
end
|
|
207
218
|
sql += " FROM #{query_ast.from_table_name}"
|
|
208
219
|
|
|
209
220
|
query_ast.join_clauses.each do |join|
|
|
210
|
-
|
|
221
|
+
fk_expr = "#{join.base_table_name}.#{join.foreign_key}"
|
|
222
|
+
pk_expr = "#{join.join_table_name}.#{join.primary_key}"
|
|
223
|
+
if types_need_cast?(
|
|
224
|
+
column_pg_type(join.base_table_name, join.foreign_key),
|
|
225
|
+
column_pg_type(join.join_table_name, join.primary_key)
|
|
226
|
+
)
|
|
227
|
+
fk_expr = "#{fk_expr}::text"
|
|
228
|
+
pk_expr = "#{pk_expr}::text"
|
|
229
|
+
end
|
|
230
|
+
sql += " JOIN #{join.join_table_name} ON #{fk_expr} = #{pk_expr}"
|
|
211
231
|
|
|
212
232
|
join.where_clauses.each do |where|
|
|
213
233
|
compiled_where_condition = compile_where_condition(where, join.join_table_name)
|
|
@@ -246,23 +266,54 @@ module Exwiw
|
|
|
246
266
|
"#{key} IN (#{values.join(', ')})"
|
|
247
267
|
end
|
|
248
268
|
elsif where_clause.operator == :in_subquery
|
|
249
|
-
|
|
269
|
+
subquery_sql = compile_subquery(where_clause.value, outer_table: table_name, outer_column: where_clause.column_name)
|
|
270
|
+
cast_to = subquery_cast_to(where_clause.value, table_name, where_clause.column_name)
|
|
271
|
+
outer_key = cast_to ? "#{key}::#{cast_to}" : key
|
|
272
|
+
"#{outer_key} IN (#{subquery_sql})"
|
|
250
273
|
else
|
|
251
274
|
raise "Unsupported operator: #{where_clause.operator}"
|
|
252
275
|
end
|
|
253
276
|
end
|
|
254
277
|
|
|
255
|
-
private def compile_subquery(subquery)
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
278
|
+
private def compile_subquery(subquery, outer_table: nil, outer_column: nil)
|
|
279
|
+
cast_to = subquery_cast_to(subquery, outer_table, outer_column)
|
|
280
|
+
|
|
281
|
+
if subquery.is_a?(Exwiw::QueryAst::SelectSubquery)
|
|
282
|
+
return compile_ast(subquery.query, select_cast_to: cast_to)
|
|
283
|
+
end
|
|
259
284
|
|
|
260
285
|
inner_values = subquery.where_values.map { |v| escape_value(v) }
|
|
261
|
-
"
|
|
286
|
+
select_expr = "#{subquery.table_name}.#{subquery.select_column}"
|
|
287
|
+
select_expr = "#{select_expr}::#{cast_to}" if cast_to
|
|
288
|
+
"SELECT #{select_expr} " \
|
|
262
289
|
"FROM #{subquery.table_name} " \
|
|
263
290
|
"WHERE #{subquery.table_name}.#{subquery.where_column} IN (#{inner_values.join(', ')})"
|
|
264
291
|
end
|
|
265
292
|
|
|
293
|
+
private def subquery_select_target(subquery)
|
|
294
|
+
case subquery
|
|
295
|
+
when Exwiw::QueryAst::SelectSubquery
|
|
296
|
+
q = subquery.query
|
|
297
|
+
col = q.columns.first
|
|
298
|
+
col ? [q.from_table_name, col.name] : [nil, nil]
|
|
299
|
+
when Exwiw::QueryAst::Subquery
|
|
300
|
+
[subquery.table_name, subquery.select_column]
|
|
301
|
+
else
|
|
302
|
+
[nil, nil]
|
|
303
|
+
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
private def subquery_cast_to(subquery, outer_table, outer_column)
|
|
307
|
+
return nil if outer_table.nil? || outer_column.nil?
|
|
308
|
+
|
|
309
|
+
inner_table, inner_column = subquery_select_target(subquery)
|
|
310
|
+
return nil if inner_table.nil?
|
|
311
|
+
|
|
312
|
+
outer_type = column_pg_type(outer_table, outer_column)
|
|
313
|
+
inner_type = column_pg_type(inner_table, inner_column)
|
|
314
|
+
types_need_cast?(outer_type, inner_type) ? 'text' : nil
|
|
315
|
+
end
|
|
316
|
+
|
|
266
317
|
private def escape_value(value)
|
|
267
318
|
case value
|
|
268
319
|
when nil
|
|
@@ -351,6 +402,36 @@ module Exwiw
|
|
|
351
402
|
end
|
|
352
403
|
end
|
|
353
404
|
|
|
405
|
+
private def column_pg_type(table_name, column_name)
|
|
406
|
+
@column_type_cache ||= {}
|
|
407
|
+
cache_key = [table_name, column_name]
|
|
408
|
+
return @column_type_cache[cache_key] if @column_type_cache.key?(cache_key)
|
|
409
|
+
|
|
410
|
+
sql = <<~SQL
|
|
411
|
+
SELECT t.typname
|
|
412
|
+
FROM pg_attribute a
|
|
413
|
+
JOIN pg_class c ON c.oid = a.attrelid
|
|
414
|
+
JOIN pg_type t ON t.oid = a.atttypid
|
|
415
|
+
WHERE c.relname = $1
|
|
416
|
+
AND a.attname = $2
|
|
417
|
+
AND a.attnum > 0
|
|
418
|
+
AND NOT a.attisdropped
|
|
419
|
+
LIMIT 1
|
|
420
|
+
SQL
|
|
421
|
+
|
|
422
|
+
result = connection.exec_params(sql, [table_name, column_name])
|
|
423
|
+
@column_type_cache[cache_key] = result.ntuples > 0 ? result.getvalue(0, 0) : nil
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
private def types_need_cast?(type_a, type_b)
|
|
427
|
+
return false if type_a.nil? || type_b.nil?
|
|
428
|
+
return false if type_a == type_b
|
|
429
|
+
|
|
430
|
+
string_types = %w[varchar text bpchar name].freeze
|
|
431
|
+
(type_a == 'uuid' && string_types.include?(type_b)) ||
|
|
432
|
+
(type_b == 'uuid' && string_types.include?(type_a))
|
|
433
|
+
end
|
|
434
|
+
|
|
354
435
|
private def connection
|
|
355
436
|
@connection ||=
|
|
356
437
|
begin
|
|
@@ -14,6 +14,12 @@ module Exwiw
|
|
|
14
14
|
attribute :fields, array(MongodbField)
|
|
15
15
|
attribute :bulk_insert_chunk_size, optional(Integer), skip_serializing_if_nil: true
|
|
16
16
|
attribute :ignore, Serdes::OptionalType.new(Serdes::ConcreteType.new(Boolean)), skip_serializing_if_nil: true
|
|
17
|
+
# Free-form note. Purely informational — exwiw never reads it — and preserved
|
|
18
|
+
# across `MongoidSchemaGenerator` regeneration like the field / belongs_to
|
|
19
|
+
# `comment`. The generator also emits one when, under `skip_unsupported`, it
|
|
20
|
+
# marks an unrepresentable collection `ignore: true`, to record why extraction
|
|
21
|
+
# was skipped.
|
|
22
|
+
attribute :comment, optional(String), skip_serializing_if_nil: true
|
|
17
23
|
|
|
18
24
|
# Marks this config as physically embedded inside another collection's
|
|
19
25
|
# documents. When set, this config is not processed as a standalone dump
|
|
@@ -62,6 +68,10 @@ module Exwiw
|
|
|
62
68
|
merged.filter = filter
|
|
63
69
|
merged.bulk_insert_chunk_size = bulk_insert_chunk_size
|
|
64
70
|
merged.ignore = ignore
|
|
71
|
+
# A freshly generated comment (e.g. the skip_unsupported marker) wins so
|
|
72
|
+
# it stays accurate; otherwise a hand-added note on a normal collection
|
|
73
|
+
# is kept.
|
|
74
|
+
merged.comment = passed.comment || comment
|
|
65
75
|
merged.embedded_in = passed.embedded_in
|
|
66
76
|
|
|
67
77
|
# Structural facts of each belongs_to come from the freshly generated
|
|
@@ -14,14 +14,38 @@ module Exwiw
|
|
|
14
14
|
# (`fields`, `relations`, `collection_name`), so it does not require a live
|
|
15
15
|
# MongoDB connection.
|
|
16
16
|
class MongoidSchemaGenerator
|
|
17
|
-
|
|
17
|
+
# Raised when an embedded collection's `embedded_in` cannot be expressed as
|
|
18
|
+
# an exwiw config (polymorphic embedding, self-referential/cyclic embedding,
|
|
19
|
+
# or an unresolvable embedding-parent class). A subclass of ArgumentError so
|
|
20
|
+
# the historical `raise_error(ArgumentError, ...)` contract is preserved.
|
|
21
|
+
# Under `skip_unsupported` the generator rescues this and emits an
|
|
22
|
+
# `ignore: true` config instead of aborting the whole run.
|
|
23
|
+
class UnsupportedEmbedding < ArgumentError
|
|
24
|
+
# A concise phrase (as opposed to the long, actionable exception message)
|
|
25
|
+
# recorded as the generated config's `comment`.
|
|
26
|
+
attr_reader :reason
|
|
27
|
+
|
|
28
|
+
def initialize(message, reason:)
|
|
29
|
+
super(message)
|
|
30
|
+
@reason = reason
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# `skip_unsupported`: when true, the generator does not abort on a construct
|
|
35
|
+
# it cannot represent. It skips an unresolvable `belongs_to` (keeping the
|
|
36
|
+
# foreign-key field) and emits an unrepresentable embedded collection as an
|
|
37
|
+
# `ignore: true` top-level config annotated with a `comment`, warning to
|
|
38
|
+
# stderr in both cases. Off by default, so the historical fail-loud behavior
|
|
39
|
+
# is unchanged unless a caller opts in.
|
|
40
|
+
def self.from_rails_application(output_dir:, skip_unsupported: false)
|
|
18
41
|
Rails.application.eager_load!
|
|
19
|
-
new(models: ::Mongoid.models, output_dir: output_dir)
|
|
42
|
+
new(models: ::Mongoid.models, output_dir: output_dir, skip_unsupported: skip_unsupported)
|
|
20
43
|
end
|
|
21
44
|
|
|
22
|
-
def initialize(models:, output_dir:)
|
|
45
|
+
def initialize(models:, output_dir:, skip_unsupported: false)
|
|
23
46
|
@models = models
|
|
24
47
|
@output_dir = output_dir
|
|
48
|
+
@skip_unsupported = skip_unsupported
|
|
25
49
|
end
|
|
26
50
|
|
|
27
51
|
def generate!
|
|
@@ -78,7 +102,31 @@ module Exwiw
|
|
|
78
102
|
# supported (MongodbCollectionConfig rejects them), so embedded configs
|
|
79
103
|
# always carry an empty belongs_tos and instead declare where they live.
|
|
80
104
|
attrs[:belongs_tos] = []
|
|
81
|
-
|
|
105
|
+
begin
|
|
106
|
+
attrs[:embedded_in] = embedded_in_for(ordered.find(&:embedded?))
|
|
107
|
+
rescue => e
|
|
108
|
+
# Known-unrepresentable shapes arrive as UnsupportedEmbedding (with a
|
|
109
|
+
# concise reason). Without skip_unsupported, re-raise so the historical
|
|
110
|
+
# fail-loud behavior is preserved. The broad rescue is a deliberate
|
|
111
|
+
# safety net for skip_unsupported (a best-effort bootstrapping mode):
|
|
112
|
+
# any other error while deriving the embedding is turned into an
|
|
113
|
+
# `ignore: true` config too, so a single odd model never aborts the run.
|
|
114
|
+
raise e unless @skip_unsupported
|
|
115
|
+
|
|
116
|
+
reason =
|
|
117
|
+
if e.is_a?(UnsupportedEmbedding)
|
|
118
|
+
e.reason
|
|
119
|
+
else
|
|
120
|
+
"raised #{e.class} while deriving embedded_in (#{e.message.lines.first&.strip})"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Emit the collection as a top-level config marked `ignore: true` so it
|
|
124
|
+
# is NOT (wrongly) dumped as its own collection, and record why. The
|
|
125
|
+
# user can hand-write its embedded_in config later to dump/mask it.
|
|
126
|
+
warn("exwiw: skip_unsupported: '#{collection_name}' #{reason}; emitting ignore:true (define embedded_in by hand to dump/mask it).")
|
|
127
|
+
attrs[:ignore] = true
|
|
128
|
+
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."
|
|
129
|
+
end
|
|
82
130
|
else
|
|
83
131
|
attrs[:belongs_tos] = aggregate_belongs_tos(ordered)
|
|
84
132
|
end
|
|
@@ -168,10 +216,25 @@ module Exwiw
|
|
|
168
216
|
# same belongs_to twice, so uniq them.
|
|
169
217
|
belongs_to_assocs
|
|
170
218
|
.reject(&:polymorphic?)
|
|
171
|
-
.
|
|
219
|
+
.filter_map { |assoc| belongs_to_for(assoc) }
|
|
172
220
|
.uniq
|
|
173
221
|
end
|
|
174
222
|
|
|
223
|
+
# Resolves a referenced belongs_to to a `{ table_name, foreign_key }` pair.
|
|
224
|
+
# `assoc.klass` raises NameError when the association's target class no longer
|
|
225
|
+
# exists (a stale/legacy `belongs_to`, e.g. pointing at a model removed years
|
|
226
|
+
# ago). Under `skip_unsupported` such a relation is skipped with a warning —
|
|
227
|
+
# its foreign-key column is still tracked as an ordinary field by
|
|
228
|
+
# `aggregate_fields`, mirroring how polymorphic / HABTM relations are dropped.
|
|
229
|
+
private def belongs_to_for(assoc)
|
|
230
|
+
{ table_name: assoc.klass.collection_name.to_s, foreign_key: assoc.foreign_key }
|
|
231
|
+
rescue NameError, ::Mongoid::Errors::MongoidError => e
|
|
232
|
+
raise e unless @skip_unsupported
|
|
233
|
+
|
|
234
|
+
warn("exwiw: skip_unsupported: skipping belongs_to ':#{assoc.name}' that could not be resolved (#{e.class}: #{e.message.lines.first&.strip}); its foreign key '#{assoc.foreign_key}' is still kept as a field.")
|
|
235
|
+
nil
|
|
236
|
+
end
|
|
237
|
+
|
|
175
238
|
# Resolves the `embedded_in` config for an embedded model. Each embedded
|
|
176
239
|
# model points at its *immediate* embedding parent: the parent's collection
|
|
177
240
|
# name plus the single document key (`store_as`, defaulting to the relation
|
|
@@ -196,14 +259,30 @@ module Exwiw
|
|
|
196
259
|
# names exactly one parent collection + path, so this shape cannot be
|
|
197
260
|
# represented; fail loudly with an actionable message instead of crashing.
|
|
198
261
|
if assoc.polymorphic?
|
|
199
|
-
raise
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
262
|
+
raise UnsupportedEmbedding.new(
|
|
263
|
+
"MongoidSchemaGenerator: '#{model.name}' (collection '#{model.collection_name}') " \
|
|
264
|
+
"declares a polymorphic `embedded_in :#{assoc.name}`, which has no single embedding " \
|
|
265
|
+
"parent collection and cannot be expressed as an exwiw `embedded_in` config. " \
|
|
266
|
+
"Define the collection's config by hand, or make the relation non-polymorphic.",
|
|
267
|
+
reason: "has a polymorphic embedded_in :#{assoc.name}",
|
|
268
|
+
)
|
|
204
269
|
end
|
|
205
270
|
|
|
206
|
-
parent =
|
|
271
|
+
parent =
|
|
272
|
+
begin
|
|
273
|
+
assoc.klass
|
|
274
|
+
rescue NameError => e
|
|
275
|
+
# The embedding-parent class named by `class_name` (or inferred from
|
|
276
|
+
# the relation) does not exist — a stale/renamed parent. exwiw cannot
|
|
277
|
+
# name a parent collection it cannot resolve.
|
|
278
|
+
raise UnsupportedEmbedding.new(
|
|
279
|
+
"MongoidSchemaGenerator: '#{model.name}' (collection '#{model.collection_name}') " \
|
|
280
|
+
"declares `embedded_in :#{assoc.name}` whose parent class cannot be resolved " \
|
|
281
|
+
"(#{e.message.lines.first&.strip}). Fix the association's class_name, or define the " \
|
|
282
|
+
"collection's config by hand.",
|
|
283
|
+
reason: "has an embedded_in :#{assoc.name} whose parent class is unresolvable",
|
|
284
|
+
)
|
|
285
|
+
end
|
|
207
286
|
|
|
208
287
|
# A self-referential / cyclic `embedded_in` — Mongoid's
|
|
209
288
|
# `recursively_embeds_many` / `recursively_embeds_one` (which declare a
|
|
@@ -216,19 +295,47 @@ module Exwiw
|
|
|
216
295
|
# `MongodbAdapter#dumpable?` (`!embedded?`) would silently never dump the
|
|
217
296
|
# collection's root documents. Fail loudly instead.
|
|
218
297
|
if parent.collection_name.to_s == model.collection_name.to_s
|
|
219
|
-
raise
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
298
|
+
raise UnsupportedEmbedding.new(
|
|
299
|
+
"MongoidSchemaGenerator: '#{model.name}' (collection '#{model.collection_name}') " \
|
|
300
|
+
"declares a self-referential (cyclic) `embedded_in :#{assoc.name}` that embeds the " \
|
|
301
|
+
"collection inside documents of its own type (e.g. `recursively_embeds_many` / " \
|
|
302
|
+
"`recursively_embeds_one`). " \
|
|
303
|
+
"exwiw represents a collection as either top-level or embedded, not both, so this " \
|
|
304
|
+
"cannot be expressed as an exwiw `embedded_in` config. Define the collection's config " \
|
|
305
|
+
"by hand.",
|
|
306
|
+
reason: "has a self-referential (cyclic) embedded_in :#{assoc.name}",
|
|
307
|
+
)
|
|
227
308
|
end
|
|
228
309
|
|
|
229
310
|
# `store_as` defaults to the relation name and is the actual document key
|
|
230
311
|
# the subdocuments are stored under inside the immediate parent.
|
|
231
|
-
parent_relation =
|
|
312
|
+
parent_relation =
|
|
313
|
+
begin
|
|
314
|
+
parent.relations[assoc.inverse.to_s]
|
|
315
|
+
rescue ::Mongoid::Errors::MongoidError, NameError => e
|
|
316
|
+
# e.g. AmbiguousRelationship: the embedded class is embedded under
|
|
317
|
+
# several document keys in the parent (or otherwise has no single
|
|
318
|
+
# resolvable inverse), so exwiw cannot pick the one path it lives under.
|
|
319
|
+
raise UnsupportedEmbedding.new(
|
|
320
|
+
"MongoidSchemaGenerator: '#{model.name}' (collection '#{model.collection_name}') " \
|
|
321
|
+
"declares `embedded_in :#{assoc.name}` whose inverse on '#{parent.name}' is ambiguous " \
|
|
322
|
+
"or unresolvable (#{e.class}: #{e.message.lines.first&.strip}). Add an `inverse_of:` to " \
|
|
323
|
+
"disambiguate, or define the collection's config by hand.",
|
|
324
|
+
reason: "has an embedded_in :#{assoc.name} with an ambiguous/unresolvable inverse",
|
|
325
|
+
)
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
unless parent_relation
|
|
329
|
+
# `assoc.inverse` resolved to a name that is not an association on the
|
|
330
|
+
# parent (or to nothing), so there is no document key to embed under.
|
|
331
|
+
raise UnsupportedEmbedding.new(
|
|
332
|
+
"MongoidSchemaGenerator: '#{model.name}' (collection '#{model.collection_name}') " \
|
|
333
|
+
"declares `embedded_in :#{assoc.name}` but its inverse relation could not be located on " \
|
|
334
|
+
"'#{parent.name}' (the embedding document key is indeterminable). Add an `inverse_of:`, or " \
|
|
335
|
+
"define the collection's config by hand.",
|
|
336
|
+
reason: "has an embedded_in :#{assoc.name} whose inverse relation could not be located",
|
|
337
|
+
)
|
|
338
|
+
end
|
|
232
339
|
|
|
233
340
|
{ collection_name: parent.collection_name.to_s, path: parent_relation.store_as }
|
|
234
341
|
end
|
data/lib/exwiw/version.rb
CHANGED
data/lib/tasks/exwiw.rake
CHANGED
|
@@ -32,11 +32,17 @@ namespace :exwiw do
|
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
desc "Generate schema from a Mongoid application"
|
|
35
|
+
# Set EXWIW_SKIP_UNSUPPORTED=1 to keep generation going past constructs exwiw
|
|
36
|
+
# cannot represent (an unresolvable `belongs_to`, or a polymorphic / cyclic /
|
|
37
|
+
# unresolvable `embedded_in`): the unresolvable belongs_to is skipped and an
|
|
38
|
+
# unrepresentable embedded collection is emitted as `ignore: true` with a
|
|
39
|
+
# `comment`, each warned to stderr, instead of aborting the whole run.
|
|
35
40
|
task generate_mongoid: :environment do
|
|
36
41
|
require "exwiw"
|
|
37
42
|
|
|
38
43
|
Exwiw::MongoidSchemaGenerator.from_rails_application(
|
|
39
44
|
output_dir: ENV["OUTPUT_DIR_PATH"] || "exwiw",
|
|
45
|
+
skip_unsupported: ENV["EXWIW_SKIP_UNSUPPORTED"] == "1",
|
|
40
46
|
).generate!
|
|
41
47
|
end
|
|
42
48
|
end
|