@prisma/orm-target-mongo 8.0.0-rc.17-dev.28 → 8.0.0-rc.17-dev.30

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.
Files changed (58) hide show
  1. package/dist/adapter.mjs +1 -1
  2. package/dist/adapter.mjs.map +1 -1
  3. package/dist/adapter__control.mjs +3 -3
  4. package/dist/adapter__control.mjs.map +1 -1
  5. package/dist/adapter__runtime.mjs +2 -2
  6. package/dist/adapter__runtime.mjs.map +1 -1
  7. package/dist/{codecs-I_SlLNTn-BsshuqIx.mjs → codecs-D6yLMoD2-Bn8-UhA5.mjs} +4 -7
  8. package/dist/codecs-D6yLMoD2-Bn8-UhA5.mjs.map +1 -0
  9. package/dist/{control-B0JolOVp.mjs → control-BcqDWXwi.mjs} +648 -107
  10. package/dist/control-BcqDWXwi.mjs.map +1 -0
  11. package/dist/{control-B1h4Nci1.d.mts → control-PcpFueeY.d.mts} +24 -7
  12. package/dist/control-PcpFueeY.d.mts.map +1 -0
  13. package/dist/{descriptor-meta-BtMd3yW0--oRvmHvL.mjs → descriptor-meta-BtglG_aY-DhbxL8Gw.mjs} +3 -3
  14. package/dist/{descriptor-meta-BtMd3yW0--oRvmHvL.mjs.map → descriptor-meta-BtglG_aY-DhbxL8Gw.mjs.map} +1 -1
  15. package/dist/{descriptor-meta-runtime-DpdkUA8--CvurCIDn.mjs → descriptor-meta-runtime-CwIqqZ8J-C3UNM2K8.mjs} +3 -3
  16. package/dist/{descriptor-meta-runtime-DpdkUA8--CvurCIDn.mjs.map → descriptor-meta-runtime-CwIqqZ8J-C3UNM2K8.mjs.map} +1 -1
  17. package/dist/driver.d.mts +1 -1
  18. package/dist/driver.mjs +1 -1
  19. package/dist/driver__control.d.mts +1 -1
  20. package/dist/driver__control.mjs +1 -1
  21. package/dist/driver__control.mjs.map +1 -1
  22. package/dist/{migration-C9WLWp1C.d.mts → migration-CPNxGxSK.d.mts} +12 -5
  23. package/dist/migration-CPNxGxSK.d.mts.map +1 -0
  24. package/dist/{migration-factories-CgyFxFZT-D4wek2HG.mjs → migration-factories-BmDfC5c5-BI-Wusuk.mjs} +139 -4
  25. package/dist/migration-factories-BmDfC5c5-BI-Wusuk.mjs.map +1 -0
  26. package/dist/{mongo-adapter-BW-kRaXm-B74CBt9J.mjs → mongo-adapter-DWNaz2UT-HiI8mJ7J.mjs} +10 -8
  27. package/dist/mongo-adapter-DWNaz2UT-HiI8mJ7J.mjs.map +1 -0
  28. package/dist/{mongo-driver-BTjYMBKs-CN2niXFQ.d.mts → mongo-driver-CTyvKDUg-bwRH7EDL.d.mts} +4 -3
  29. package/dist/{mongo-driver-BTjYMBKs-CN2niXFQ.d.mts.map → mongo-driver-CTyvKDUg-bwRH7EDL.d.mts.map} +1 -1
  30. package/dist/{mongo-driver-DwpObyCr-C_iD-Yx0.mjs → mongo-driver-rY9vL9BT-DABUWbRC.mjs} +11 -4
  31. package/dist/mongo-driver-rY9vL9BT-DABUWbRC.mjs.map +1 -0
  32. package/dist/mongo-target-errors-Btf3ML1w-B5EMYbz1.mjs +9 -0
  33. package/dist/mongo-target-errors-Btf3ML1w-B5EMYbz1.mjs.map +1 -0
  34. package/dist/{op-factory-call-DgkZzsyP-j04fJDt5.d.mts → op-factory-call-BoYr7vvM-jFNdzIK6.d.mts} +40 -6
  35. package/dist/op-factory-call-BoYr7vvM-jFNdzIK6.d.mts.map +1 -0
  36. package/dist/{prisma6-binding-DoapgPvm.mjs → prisma6-binding-i6ORtPD9.mjs} +2 -2
  37. package/dist/{prisma6-binding-DoapgPvm.mjs.map → prisma6-binding-i6ORtPD9.mjs.map} +1 -1
  38. package/dist/target.d.mts +4 -4
  39. package/dist/target.mjs +5 -5
  40. package/dist/target__codecs.mjs +1 -1
  41. package/dist/target__control.d.mts +3 -3
  42. package/dist/target__control.mjs +2 -2
  43. package/dist/target__migration.d.mts +2 -2
  44. package/dist/target__migration.mjs +2 -2
  45. package/dist/target__pack.mjs +1 -1
  46. package/dist/target__pack.mjs.map +1 -1
  47. package/dist/target__prisma6-binding.mjs +1 -1
  48. package/dist/target__runtime.mjs +1 -1
  49. package/dist/target__runtime.mjs.map +1 -1
  50. package/package.json +9 -9
  51. package/dist/codecs-I_SlLNTn-BsshuqIx.mjs.map +0 -1
  52. package/dist/control-B0JolOVp.mjs.map +0 -1
  53. package/dist/control-B1h4Nci1.d.mts.map +0 -1
  54. package/dist/migration-C9WLWp1C.d.mts.map +0 -1
  55. package/dist/migration-factories-CgyFxFZT-D4wek2HG.mjs.map +0 -1
  56. package/dist/mongo-adapter-BW-kRaXm-B74CBt9J.mjs.map +0 -1
  57. package/dist/mongo-driver-DwpObyCr-C_iD-Yx0.mjs.map +0 -1
  58. package/dist/op-factory-call-DgkZzsyP-j04fJDt5.d.mts.map +0 -1
@@ -1,19 +1,20 @@
1
- import { a as mongoTargetError } from "./codecs-I_SlLNTn-BsshuqIx.mjs";
2
- import { t as mongoTargetDescriptorMeta } from "./descriptor-meta-BtMd3yW0--oRvmHvL.mjs";
3
- import { a as dropCollection, n as createCollection, o as dropIndex, r as createIndex, t as collMod } from "./migration-factories-CgyFxFZT-D4wek2HG.mjs";
1
+ import { t as mongoTargetError } from "./mongo-target-errors-Btf3ML1w-B5EMYbz1.mjs";
2
+ import { t as mongoTargetDescriptorMeta } from "./descriptor-meta-BtglG_aY-DhbxL8Gw.mjs";
3
+ import { a as dropCollection, c as renameField, d as unsetField, n as createCollection, o as dropIndex, r as createIndex, s as renameCollection, t as collMod, u as unrewritableFieldNameReason } from "./migration-factories-BmDfC5c5-BI-Wusuk.mjs";
4
4
  import { type } from "arktype";
5
5
  import { blindCast } from "@prisma/orm-framework/utils/casts";
6
6
  import { ifDefined } from "@prisma/orm-framework/utils/defined";
7
7
  import { NamespaceBase, UNBOUND_NAMESPACE_ID, elementCoordinates, freezeNode, hydrateNamespaceEntities } from "@prisma/orm-framework/components/ir";
8
- import { APP_SPACE_ID, VERIFY_CODE_SCHEMA_FAILURE, contractSnapshotJsonSpecifier, contractSnapshotTypesSpecifier, describeMigrationStatement, issueOutcome } from "@prisma/orm-framework/components/control";
9
- import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr } from "@prisma/orm-family-mongo/query-ast/control";
10
- import { canonicalizeSchemasForVerification, contractToMongoSchemaIR, diffMongoSchemas } from "@prisma/orm-family-mongo/family/control";
8
+ import { APP_SPACE_ID, VERIFY_CODE_SCHEMA_FAILURE, contractSnapshotJsonSpecifier, contractSnapshotTypesSpecifier, describeMigrationStatement, issueOutcome, modelDisplayName } from "@prisma/orm-framework/components/control";
9
+ import { CollModCommand, CreateCollectionCommand, CreateIndexCommand, DropCollectionCommand, DropIndexCommand, ListCollectionsCommand, ListIndexesCommand, MongoAndExpr, MongoExistsExpr, MongoFieldFilter, MongoNotExpr, MongoOrExpr, RenameCollectionCommand, isMongoDmlOperation } from "@prisma/orm-family-mongo/query-ast/control";
10
+ import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma/orm-family-mongo/query-ast/execution";
11
+ import { canonicalizeSchemasForVerification, contractToMongoSchemaIR, diffMongoSchemas, fieldRewriteOf } from "@prisma/orm-family-mongo/family/control";
11
12
  import { notOk, ok } from "@prisma/orm-framework/utils/result";
12
13
  import { MongoContractSerializerBase, MongoSchemaVerifierBase } from "@prisma/orm-family-mongo/family/ir";
13
14
  import { MongoStorage } from "@prisma/orm-family-mongo/contract";
14
15
  import { composeMongoEntityKinds } from "@prisma/orm-family-mongo/contract/entity-kinds";
15
- import { asNamespaceId } from "@prisma/orm-framework/contract/types";
16
- import { canonicalize, deepEqual } from "@prisma/orm-family-mongo/schema-ir";
16
+ import { asNamespaceId, effectiveControlPolicy } from "@prisma/orm-framework/contract/types";
17
+ import { MongoSchemaCollection, MongoSchemaIR, canonicalize, deepEqual } from "@prisma/orm-family-mongo/schema-ir";
17
18
  import { TsExpression, jsonToTsSource, renderImports } from "@prisma/orm-framework/ts-render";
18
19
  import { Migration } from "@prisma/orm-toolchain/migration-tools/migration";
19
20
  import { resolveRequirementSpecifiers } from "@prisma/orm-framework/components/emission";
@@ -21,7 +22,6 @@ import { detectScaffoldRuntime, shebangLineFor } from "@prisma/orm-toolchain/mig
21
22
  import { errorRunnerFailed } from "@prisma/orm-framework/errors/execution";
22
23
  import { verifyMongoSchema } from "@prisma/orm-family-mongo/family/schema-verify";
23
24
  import { InternalError } from "@prisma/orm-framework/utils/internal-error";
24
- import { AggregateCommand, MongoAddFieldsStage, MongoLimitStage, MongoLookupStage, MongoMatchStage, MongoMergeStage, MongoProjectStage, MongoSortStage, RawAggregateCommand, RawDeleteManyCommand, RawDeleteOneCommand, RawFindOneAndDeleteCommand, RawFindOneAndUpdateCommand, RawInsertManyCommand, RawInsertOneCommand, RawUpdateManyCommand, RawUpdateOneCommand } from "@prisma/orm-family-mongo/query-ast/execution";
25
25
  //#region ../../../3-mongo-target/1-mongo-target/dist/control.mjs
26
26
  /**
27
27
  * Mongo target `Namespace` concretion. In Mongo the "namespace" concept
@@ -252,6 +252,75 @@ var DropCollectionCall = class extends OpFactoryCallNode {
252
252
  return `dropCollection(${jsonToTsSource(this.collection)})`;
253
253
  }
254
254
  };
255
+ var RenameCollectionCall = class extends OpFactoryCallNode {
256
+ factoryName = "renameCollection";
257
+ operationClass = "widening";
258
+ from;
259
+ to;
260
+ label;
261
+ constructor(from, to) {
262
+ super();
263
+ this.from = from;
264
+ this.to = to;
265
+ this.label = `Rename collection ${from} to ${to}`;
266
+ this.freeze();
267
+ }
268
+ toOp() {
269
+ return renameCollection(this.from, this.to);
270
+ }
271
+ renderTypeScript() {
272
+ return `renameCollection(${jsonToTsSource(this.from)}, ${jsonToTsSource(this.to)})`;
273
+ }
274
+ };
275
+ function renderFilterOption(filter) {
276
+ return filter === void 0 ? "" : `, ${jsonToTsSource({ filter })}`;
277
+ }
278
+ var RenameFieldCall = class extends OpFactoryCallNode {
279
+ factoryName = "renameField";
280
+ operationClass = "widening";
281
+ collection;
282
+ from;
283
+ to;
284
+ filter;
285
+ label;
286
+ constructor(collection, from, to, filter) {
287
+ super();
288
+ this.collection = collection;
289
+ this.from = from;
290
+ this.to = to;
291
+ this.filter = filter;
292
+ this.label = this.toOp().label;
293
+ this.freeze();
294
+ }
295
+ toOp() {
296
+ return renameField(this.collection, this.from, this.to, ifDefined("filter", this.filter));
297
+ }
298
+ renderTypeScript() {
299
+ return `renameField(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.from)}, ${jsonToTsSource(this.to)}${renderFilterOption(this.filter)})`;
300
+ }
301
+ };
302
+ var UnsetFieldCall = class extends OpFactoryCallNode {
303
+ factoryName = "unsetField";
304
+ operationClass = "destructive";
305
+ collection;
306
+ field;
307
+ filter;
308
+ label;
309
+ constructor(collection, field, filter) {
310
+ super();
311
+ this.collection = collection;
312
+ this.field = field;
313
+ this.filter = filter;
314
+ this.label = this.toOp().label;
315
+ this.freeze();
316
+ }
317
+ toOp() {
318
+ return unsetField(this.collection, this.field, ifDefined("filter", this.filter));
319
+ }
320
+ renderTypeScript() {
321
+ return `unsetField(${jsonToTsSource(this.collection)}, ${jsonToTsSource(this.field)}${renderFilterOption(this.filter)})`;
322
+ }
323
+ };
255
324
  var CollModCall = class extends OpFactoryCallNode {
256
325
  factoryName = "collMod";
257
326
  collection;
@@ -309,6 +378,322 @@ function schemaCollectionToCreateCollectionOptions(coll) {
309
378
  ...ifDefined("changeStreamPreAndPostImages", opts?.changeStreamPreAndPostImages)
310
379
  };
311
380
  }
381
+ /** The collection a model stores its documents in; a model without `@@map` names it verbatim. */
382
+ function collectionOf(contract, coordinate) {
383
+ const collection = contract?.domain.namespaces[coordinate.namespaceId]?.models[coordinate.model]?.storage["collection"];
384
+ return typeof collection === "string" ? collection : coordinate.model;
385
+ }
386
+ function modelAt(contract, coordinate) {
387
+ const namespace = contract.domain.namespaces[coordinate.namespaceId];
388
+ return namespace !== void 0 && Object.hasOwn(namespace.models, coordinate.model) ? namespace.models[coordinate.model] : void 0;
389
+ }
390
+ /**
391
+ * The condition that selects a variant's documents in the collection it shares with its base: the
392
+ * base's discriminator field holding the variant's value. A model that is not a variant has none.
393
+ */
394
+ function variantFilter(contract, coordinate) {
395
+ const base = modelAt(contract, coordinate)?.base;
396
+ if (base === void 0) return void 0;
397
+ const baseModel = contract.domain.namespaces[base.namespace]?.models[base.model];
398
+ const field = baseModel?.discriminator?.field;
399
+ const value = baseModel?.variants?.[coordinate.model]?.value;
400
+ return field === void 0 || value === void 0 ? void 0 : { [field]: value };
401
+ }
402
+ function collectionControlPolicy(contract, collection) {
403
+ return effectiveControlPolicy(Object.values(contract.storage.namespaces).map((namespace) => namespace?.entries.collection?.[collection]).find((entry) => entry !== void 0)?.control, contract.defaultControlPolicy);
404
+ }
405
+ /** Why MongoDB would refuse a collection name, if it would. */
406
+ function invalidCollectionNameReason(name) {
407
+ if (name === "") return "it is empty";
408
+ if (name.includes("$")) return "it contains \"$\"";
409
+ if (name.includes("\0")) return "it contains a null character";
410
+ if (name.startsWith("system.")) return "it starts with \"system.\", which MongoDB reserves";
411
+ }
412
+ function isEmbedRelation(relation) {
413
+ return !("on" in relation) && !("through" in relation);
414
+ }
415
+ function storedField(model, field) {
416
+ if (model === void 0) return { kind: "absent" };
417
+ if (Object.hasOwn(model.fields, field)) return { kind: "field" };
418
+ const relation = Object.hasOwn(model.relations, field) ? model.relations[field] : void 0;
419
+ if (relation === void 0) return { kind: "absent" };
420
+ return isEmbedRelation(relation) ? { kind: "embed" } : { kind: "reference" };
421
+ }
422
+ function refused(statement, label, reason, why) {
423
+ return {
424
+ kind: "statementRefused",
425
+ summary: `${label}: ${reason}`,
426
+ why,
427
+ refusedStatement: statement
428
+ };
429
+ }
430
+ const LEAVE_OUT = "Leave out this statement.";
431
+ const TIME_SERIES_UPDATES = "MongoDB updates the documents of a time-series collection only through its metaField";
432
+ /**
433
+ * A removal the plan cannot carry out because its collection is a time-series collection, whose
434
+ * documents MongoDB does not let an update change except through the metaField.
435
+ */
436
+ function timeSeriesRemovalRefusals(removals, schema) {
437
+ return removals.filter((removal) => schema.collection(removal.call.collection)?.options?.timeseries !== void 0).map((removal) => ({
438
+ kind: "fieldRemovalRefused",
439
+ summary: `Cannot remove field ${JSON.stringify(removal.call.field)} from collection ${JSON.stringify(removal.call.collection)}: it is a time-series collection`,
440
+ why: `${TIME_SERIES_UPDATES}, so the field cannot be removed from the documents that hold it. Keep the field in the contract, as an optional field if new documents leave it out, then run this command again.`
441
+ }));
442
+ }
443
+ /**
444
+ * Plans the rename each statement states, in order: a model statement whose collection changes renames the
445
+ * collection in the schema the diff then starts from, and a field statement whose stored name
446
+ * changes renames the field in the documents that hold it.
447
+ */
448
+ function planStatements(input) {
449
+ let schema = input.schema;
450
+ const collectionRenames = [];
451
+ const fieldRenames = [];
452
+ const callOfStatement = [];
453
+ const conflicts = [];
454
+ const renamedCollections = /* @__PURE__ */ new Map();
455
+ for (const statement of input.statements) {
456
+ const label = `Cannot ${describeMigrationStatement(statement, input.fromContract, input.contract)}`;
457
+ const outcome = statement.entity === "model" ? planModelStatement(statement, label, schema, input) : planFieldStatement(statement, label, renamedCollections, schema, input);
458
+ if (outcome.kind === "refused") {
459
+ conflicts.push(outcome.conflict);
460
+ callOfStatement.push(void 0);
461
+ continue;
462
+ }
463
+ if (outcome.kind === "renameCollection") {
464
+ schema = renameInSchema(schema, outcome.call.from, outcome.call.to);
465
+ renamedCollections.set(outcome.call.from, outcome.call.to);
466
+ collectionRenames.push(outcome.call);
467
+ }
468
+ if (outcome.kind === "renameField") fieldRenames.push(outcome.call);
469
+ callOfStatement.push(outcome.kind === "unchanged" ? void 0 : outcome.call);
470
+ }
471
+ return conflicts.length > 0 ? notOk(conflicts) : ok({
472
+ schema,
473
+ collectionRenames,
474
+ fieldRenames,
475
+ callOfStatement
476
+ });
477
+ }
478
+ function renameInSchema(schema, from, to) {
479
+ return new MongoSchemaIR(schema.collections.map((collection) => collection.name === from ? new MongoSchemaCollection({
480
+ name: to,
481
+ indexes: collection.indexes,
482
+ ...collection.validator === void 0 ? {} : { validator: collection.validator },
483
+ ...collection.options === void 0 ? {} : { options: collection.options }
484
+ }) : collection));
485
+ }
486
+ function controlPolicyRefusal(statement, label, contract, collection) {
487
+ const policy = collectionControlPolicy(contract, collection);
488
+ if (policy === "managed") return void 0;
489
+ return {
490
+ kind: "refused",
491
+ conflict: refused(statement, label, `collection "${collection}" has control policy "${policy}"`, `Statements rename only collections, and fields of collections, whose control policy is "managed". Make the change in the database yourself and leave out this statement.`)
492
+ };
493
+ }
494
+ /**
495
+ * The reason and advice when a statement renames a variant whose discriminator value changes: its
496
+ * documents keep the old value, which the new validator does not accept.
497
+ */
498
+ function discriminatorValueChange(statement, input) {
499
+ const before = variantFilter(input.fromContract, statement.from);
500
+ const after = variantFilter(input.contract, statement.to);
501
+ const base = modelAt(input.contract, statement.to)?.base;
502
+ if (before === void 0 || after === void 0 || base === void 0) return void 0;
503
+ const [field, oldValue] = Object.entries(before)[0] ?? [];
504
+ const newValue = Object.values(after)[0];
505
+ if (field === void 0 || oldValue === newValue) return void 0;
506
+ const fromName = modelDisplayName(statement.from);
507
+ const toName = modelDisplayName(statement.to);
508
+ return [`its discriminator value changes from ${JSON.stringify(oldValue)} to ${JSON.stringify(newValue)}`, `The documents of "${fromName}" hold ${field} ${JSON.stringify(oldValue)}, and this release does not rewrite a discriminator value, so they would no longer match "${toName}" and would fail validation. Keep the value: declare "${toName}" with @@base(${base.model}, ${JSON.stringify(oldValue)}). A later release changes a stored value with --convert.`];
509
+ }
510
+ function planModelStatement(statement, label, schema, input) {
511
+ const discriminatorChange = discriminatorValueChange(statement, input);
512
+ if (discriminatorChange !== void 0) return {
513
+ kind: "refused",
514
+ conflict: refused(statement, label, ...discriminatorChange)
515
+ };
516
+ const from = collectionOf(input.fromContract, statement.from);
517
+ const to = collectionOf(input.contract, statement.to);
518
+ if (from === to) return { kind: "unchanged" };
519
+ const policyRefusal = controlPolicyRefusal(statement, label, input.contract, to);
520
+ if (policyRefusal !== void 0) return policyRefusal;
521
+ const refuse = (reason, why) => ({
522
+ kind: "refused",
523
+ conflict: refused(statement, label, reason, why)
524
+ });
525
+ const source = schema.collection(from);
526
+ if (source === void 0) return refuse(`the schema being planned from has no collection "${from}"`, `The database has no collection "${from}", or "${from}" is a view, which MongoDB cannot rename. Inspect it with prisma db schema, or ${LEAVE_OUT.toLowerCase()}`);
527
+ if (source.options?.timeseries !== void 0) return refuse(`collection "${from}" is a time-series collection`, `MongoDB cannot rename a time-series collection. Leave out this statement. To keep the documents, first create collection "${to}" yourself with the same time-series options and copy them into it; the plan then drops "${from}" once --delete names the model.`);
528
+ const invalid = invalidCollectionNameReason(to);
529
+ if (invalid !== void 0) return refuse(`MongoDB does not accept the collection name "${to}": ${invalid}`, "Give the model a collection name MongoDB accepts with @@map, then run this command again.");
530
+ if (schema.collection(to) !== void 0) return refuse(`the schema being planned from already has a collection "${to}"`, `A rename cannot replace a collection that already exists. Rename or drop collection "${to}" first, or ${LEAVE_OUT.toLowerCase()}`);
531
+ return {
532
+ kind: "renameCollection",
533
+ call: new RenameCollectionCall(from, to)
534
+ };
535
+ }
536
+ function planFieldStatement(statement, label, renamedCollections, schema, input) {
537
+ const refuse = (reason, why) => ({
538
+ kind: "refused",
539
+ conflict: refused(statement, label, reason, why)
540
+ });
541
+ const fromField = storedField(modelAt(input.fromContract, statement.from), statement.from.field);
542
+ const toField = storedField(modelAt(input.contract, statement.to), statement.to.field);
543
+ if (fromField.kind === "embed" || toField.kind === "embed") return refuse("the field is an embedded relation", `Renaming an embedded relation is not supported in this release. ${LEAVE_OUT}`);
544
+ if (fromField.kind !== "field" && toField.kind !== "field") return { kind: "unchanged" };
545
+ if (fromField.kind !== "field" || toField.kind !== "field") return refuse("the field is stored in documents on one side of the statement only", `A relation is not stored in documents, so a statement cannot rename a field to or from one. ${LEAVE_OUT}`);
546
+ if (statement.from.field === "_id" || statement.to.field === "_id") return refuse("it renames _id", `MongoDB does not let a document change its _id. ${LEAVE_OUT}`);
547
+ const unsafeFrom = unrewritableFieldNameReason(statement.from.field);
548
+ if (unsafeFrom !== void 0) return refuse(`the stored field name "${statement.from.field}" cannot be rewritten`, `${unsafeFrom}, so a statement cannot move its values. Rename it in the database yourself, or ${LEAVE_OUT.toLowerCase()}`);
549
+ const unsafeTo = unrewritableFieldNameReason(statement.to.field);
550
+ if (unsafeTo !== void 0) return refuse(`the stored field name "${statement.to.field}" cannot be written`, `${unsafeTo}. Give the field a stored name without "." that does not start with "$" (with @map), or ${LEAVE_OUT.toLowerCase()}`);
551
+ const originCollection = collectionOf(input.fromContract, statement.from);
552
+ const collection = renamedCollections.get(originCollection) ?? originCollection;
553
+ const destinationCollection = collectionOf(input.contract, statement.to);
554
+ if (collection !== destinationCollection) return refuse(`the model's collection changes from "${originCollection}" to "${destinationCollection}", and no statement renames it`, collectionChangeAdvice(statement, originCollection, destinationCollection));
555
+ if (statement.from.field === statement.to.field) return { kind: "unchanged" };
556
+ if (schema.collection(collection)?.options?.timeseries !== void 0) return refuse(`collection "${collection}" is a time-series collection`, `${TIME_SERIES_UPDATES}, so a statement cannot move the values of a field in them. Keep the stored name: name the field ${JSON.stringify(statement.from.field)} in the contract (with @map(${JSON.stringify(statement.from.field)}) in PSL), then run this command again without this statement.`);
557
+ const policyRefusal = controlPolicyRefusal(statement, label, input.contract, collection);
558
+ if (policyRefusal !== void 0) return policyRefusal;
559
+ const filter = variantFilter(input.fromContract, statement.from) ?? rootDocumentsLosing(input.contract, statement.to, statement.from.field)?.filter;
560
+ return {
561
+ kind: "renameField",
562
+ call: new RenameFieldCall(collection, statement.from.field, statement.to.field, filter)
563
+ };
564
+ }
565
+ function collectionChangeAdvice(statement, from, to) {
566
+ if (!sameModel(statement.from, statement.to)) return `Add --rename ${modelDisplayName(statement.from)}:${modelDisplayName(statement.to)} before this statement, so the collection is renamed with its documents.`;
567
+ return `A statement cannot rename the collection of a model that keeps its name. Rename the collection in a step of its own first: change only @@map and emit the contract. ${renameCollectionByHandSteps(statement.from, from, to)} Then change the field and run this statement again.`;
568
+ }
569
+ function renameCollectionByHandSteps(model, from, to) {
570
+ return `With db update, rename the collection in the database yourself, for example db.getCollection(${JSON.stringify(from)}).renameCollection(${JSON.stringify(to)}) in mongosh, then run db update. With migration plan, plan it, passing --delete ${modelDisplayName(model)} to answer its question; in its migration.ts make renameCollection(${JSON.stringify(from)}, ${JSON.stringify(to)}) the first operation, imported with the other operations, delete the drop of "${from}", and delete the create of "${to}" if there is one; run node on that migration.ts so it writes ops.json again; then apply it with prisma db migrate.`;
571
+ }
572
+ /**
573
+ * How to keep the documents of a model that keeps its name while its collection changes from
574
+ * `from` to `to`: no statement can rename that collection, so the user renames it by hand.
575
+ */
576
+ function keepCollectionByHandAdvice(model, from, to) {
577
+ return `To keep its documents, rename collection "${from}" to "${to}" instead; a statement cannot rename the collection of a model that keeps its name. ${renameCollectionByHandSteps(model, from, to)}`;
578
+ }
579
+ function modelCoordinates(contract) {
580
+ return Object.entries(contract.domain?.namespaces ?? {}).flatMap(([namespaceId, namespace]) => Object.keys(namespace.models).map((model) => ({
581
+ namespaceId: asNamespaceId(namespaceId),
582
+ model
583
+ })));
584
+ }
585
+ function sameModel(a, b) {
586
+ return a.namespaceId === b.namespaceId && a.model === b.model;
587
+ }
588
+ /**
589
+ * The destination model an origin model became: the one a statement renamed it to, else the one of
590
+ * the same name, else the only one stored the same way (same collection, same variant value).
591
+ */
592
+ function destinationModelOf(origin, input) {
593
+ const renamed = input.statements.find((statement) => statement.entity === "model" && sameModel(statement.from, origin));
594
+ if (renamed !== void 0) return renamed.to;
595
+ if (modelAt(input.contract, origin) !== void 0) return origin;
596
+ const collection = input.collectionAfterRenames(collectionOf(input.fromContract, origin));
597
+ const filter = JSON.stringify(variantFilter(input.fromContract, origin) ?? null);
598
+ const stored = modelCoordinates(input.contract).filter((candidate) => collectionOf(input.contract, candidate) === collection && JSON.stringify(variantFilter(input.contract, candidate) ?? null) === filter);
599
+ const [only] = stored;
600
+ return stored.length === 1 ? only : void 0;
601
+ }
602
+ function renamedByStatement(input, field) {
603
+ return input.statements.some((statement) => statement.entity === "field" && sameModel(statement.from, field) && statement.from.field === field.field);
604
+ }
605
+ function storedFieldsOf(contract, coordinate) {
606
+ const model = modelAt(contract, coordinate);
607
+ if (model === void 0) return /* @__PURE__ */ new Set();
608
+ const base = model.base === void 0 ? void 0 : modelAt(contract, {
609
+ namespaceId: asNamespaceId(model.base.namespace),
610
+ model: model.base.model
611
+ });
612
+ return /* @__PURE__ */ new Set([...Object.keys(model.fields), ...Object.keys(base?.fields ?? {})]);
613
+ }
614
+ /**
615
+ * The documents of a root model that lose `field`: all of them, or, when some of its destination
616
+ * variants now store the field themselves, those whose discriminator value is none of theirs.
617
+ * `undefined` when the root model itself still stores it.
618
+ */
619
+ function rootDocumentsLosing(contract, root, field) {
620
+ const model = modelAt(contract, root);
621
+ if (model === void 0 || Object.hasOwn(model.fields, field)) return void 0;
622
+ const discriminator = model.discriminator?.field;
623
+ const keeping = Object.entries(model.variants ?? {}).filter(([variant]) => Object.hasOwn(modelAt(contract, {
624
+ ...root,
625
+ model: variant
626
+ })?.fields ?? {}, field)).map(([, entry]) => entry.value).sort();
627
+ if (discriminator === void 0 || keeping.length === 0) return { filter: void 0 };
628
+ return { filter: { [discriminator]: { $nin: keeping } } };
629
+ }
630
+ /**
631
+ * Each field an origin model stores that its destination model no longer stores, in a collection
632
+ * the plan keeps: removing it is data loss, carried out by unsetting it from the documents that
633
+ * hold it, in a collection whose control policy is `managed`. A variant's field is unset only from that variant's documents, unless its destination
634
+ * base now stores it. A base field is unset from the documents of every variant that does not now
635
+ * store it itself.
636
+ */
637
+ function planFieldRemovals(input) {
638
+ return modelCoordinates(input.fromContract).flatMap((origin) => {
639
+ const collection = input.collectionAfterRenames(collectionOf(input.fromContract, origin));
640
+ if (!input.keptCollections.has(collection)) return [];
641
+ if (collectionControlPolicy(input.contract, collection) !== "managed") return [];
642
+ const destination = destinationModelOf(origin, input);
643
+ if (destination === void 0) return [];
644
+ const model = modelAt(input.fromContract, origin);
645
+ if (model === void 0) return [];
646
+ const variant = variantFilter(input.fromContract, origin);
647
+ const losing = (field) => {
648
+ if (variant !== void 0) return storedFieldsOf(input.contract, destination).has(field) ? void 0 : { filter: variant };
649
+ return rootDocumentsLosing(input.contract, destination, field);
650
+ };
651
+ return Object.keys(model.fields).filter((field) => unrewritableFieldNameReason(field) === void 0 && !renamedByStatement(input, {
652
+ ...origin,
653
+ field
654
+ })).sort().flatMap((field) => {
655
+ const lost = losing(field);
656
+ return lost === void 0 ? [] : [{
657
+ call: new UnsetFieldCall(collection, field, lost.filter),
658
+ subject: {
659
+ kind: "field",
660
+ ...origin,
661
+ field
662
+ }
663
+ }];
664
+ });
665
+ });
666
+ }
667
+ function isRecord$1(value) {
668
+ return typeof value === "object" && value !== null && !Array.isArray(value);
669
+ }
670
+ function propertyNames(schema) {
671
+ const properties = isRecord$1(schema) ? schema["properties"] : void 0;
672
+ return isRecord$1(properties) ? Object.keys(properties) : [];
673
+ }
674
+ function branchPropertyNames(schema) {
675
+ const branches = isRecord$1(schema) ? schema["oneOf"] : void 0;
676
+ return Array.isArray(branches) ? branches.flatMap(propertyNames) : [];
677
+ }
678
+ /**
679
+ * Without an origin contract, a field is known removed when the origin validator declares it at the
680
+ * top level and the destination validator declares it nowhere. It is named by its storage name.
681
+ */
682
+ function planValidatorFieldRemovals(input) {
683
+ return input.origin.collections.flatMap((originCollection) => {
684
+ if (collectionControlPolicy(input.contract, originCollection.name) !== "managed") return [];
685
+ const destination = input.destination.collection(originCollection.name);
686
+ if (destination?.validator === void 0 || originCollection.validator === void 0) return [];
687
+ const kept = /* @__PURE__ */ new Set([...propertyNames(destination.validator?.jsonSchema), ...branchPropertyNames(destination.validator?.jsonSchema)]);
688
+ return propertyNames(originCollection.validator.jsonSchema).filter((field) => unrewritableFieldNameReason(field) === void 0 && !kept.has(field)).sort().map((field) => ({
689
+ call: new UnsetFieldCall(originCollection.name, field),
690
+ subject: {
691
+ kind: "storage",
692
+ name: `${originCollection.name}.${field}`
693
+ }
694
+ }));
695
+ });
696
+ }
312
697
  function renderOps(calls) {
313
698
  return calls.map((call) => call.toOp());
314
699
  }
@@ -467,6 +852,35 @@ var PlannerProducedMongoMigration = class extends Migration {
467
852
  });
468
853
  }
469
854
  };
855
+ const SAME_HASH = "the migration would start and end at the same contract hash";
856
+ function why(collection) {
857
+ return `The contract gives collection ${JSON.stringify(collection)} no validator, so the names of its fields are not part of the contract's storage hash; a contract written in TypeScript has none yet. A migration that only renames or removes a field would start and end at the same hash, and db migrate refuses such a migration. Use prisma db update for this change instead: it carries the change out with the same --rename or --delete and writes no migration. Or write the migration by hand: run prisma migration new --from <hash of the migration the database is at>, and carry the change out in a dataTransform operation of its migration.ts, since db migrate accepts a migration that starts and ends at the same hash when it runs a dataTransform.`;
858
+ }
859
+ /**
860
+ * Refuses, for a migration package, each field rename and field removal of a plan whose contract
861
+ * storage hash does not change: the collection has no validator in the contract, so its field
862
+ * names are not in the hash, and `db migrate` refuses a migration that starts and ends at the same
863
+ * hash. `db update` writes no migration and plans without an origin, so it is not refused.
864
+ */
865
+ function sameHashFieldChangeRefusals(input) {
866
+ if (input.origin === null || input.origin.storageHash !== input.contract.storage.storageHash) return [];
867
+ const renames = input.statements.flatMap((statement, index) => {
868
+ const call = input.callOfStatement[index];
869
+ if (!(call instanceof RenameFieldCall)) return [];
870
+ return [{
871
+ kind: "statementRefused",
872
+ summary: `Cannot ${describeMigrationStatement(statement, input.fromContract, input.contract)}: ${SAME_HASH}`,
873
+ why: why(call.collection),
874
+ refusedStatement: statement
875
+ }];
876
+ });
877
+ const removals = input.removals.map(({ call }) => ({
878
+ kind: "fieldRemovalRefused",
879
+ summary: `Cannot remove field ${JSON.stringify(call.field)} from collection ${JSON.stringify(call.collection)}: ${SAME_HASH}`,
880
+ why: why(call.collection)
881
+ }));
882
+ return [...renames, ...removals];
883
+ }
470
884
  function buildIndexLookupKey(index) {
471
885
  const keys = index.keys.map((k) => `${k.field}:${k.direction}`).join(",");
472
886
  const opts = [
@@ -509,20 +923,41 @@ function validatorUpdateLabel(collName, origin, dest) {
509
923
  ];
510
924
  return parts.length === 0 ? `Update validator on ${collName}` : `Update validator on ${collName} (${parts.join("; ")})`;
511
925
  }
926
+ /**
927
+ * The keys of an option MongoDB fills in when a collection is created without them, each with a
928
+ * test for the value it fills in. `listCollections` reports a time-series collection's granularity
929
+ * as "seconds" and its bucketMaxSpanSeconds when they were not given, and bucketRoundingSeconds only
930
+ * for custom bucketing, which a contract cannot state; and a clustered index's v, key and unique, and
931
+ * its name as "_id_".
932
+ */
933
+ const SERVER_FILLED = {
934
+ timeseries: {
935
+ granularity: (value) => value === "seconds",
936
+ bucketMaxSpanSeconds: () => true,
937
+ bucketRoundingSeconds: () => true
938
+ },
939
+ clusteredIndex: {
940
+ v: () => true,
941
+ key: () => true,
942
+ unique: () => true,
943
+ name: (value) => value === "_id_"
944
+ }
945
+ };
946
+ /** The origin's value of an option without the keys the server filled in and the destination leaves out. */
947
+ function withoutServerFilled(option, origin, dest) {
948
+ if (!isPlainObject(origin)) return origin;
949
+ const filled = SERVER_FILLED[option];
950
+ return Object.fromEntries(Object.entries(origin).filter(([key, value]) => isPlainObject(dest) && Object.hasOwn(dest, key) || !filled[key]?.(value)));
951
+ }
512
952
  function hasImmutableOptionChange(origin, dest) {
513
953
  if (canonicalize(origin?.capped) !== canonicalize(dest?.capped)) return "capped";
514
- if (canonicalize(origin?.timeseries) !== canonicalize(dest?.timeseries)) return "timeseries";
954
+ if (canonicalize(withoutServerFilled("timeseries", origin?.timeseries, dest?.timeseries)) !== canonicalize(dest?.timeseries)) return "timeseries";
515
955
  if (canonicalize(origin?.collation) !== canonicalize(dest?.collation)) return "collation";
516
- if (canonicalize(origin?.clusteredIndex) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
956
+ if (canonicalize(withoutServerFilled("clusteredIndex", origin?.clusteredIndex, dest?.clusteredIndex)) !== canonicalize(dest?.clusteredIndex)) return "clusteredIndex";
517
957
  }
518
958
  function collectionHasOptions(coll) {
519
959
  return !!(coll.options || coll.validator);
520
960
  }
521
- /** The collection a model stores its documents in; a model without `@@map` names it verbatim. */
522
- function collectionOf(contract, coordinate) {
523
- const collection = contract?.domain.namespaces[coordinate.namespaceId]?.models[coordinate.model]?.storage["collection"];
524
- return typeof collection === "string" ? collection : coordinate.model;
525
- }
526
961
  /**
527
962
  * The model of `fromContract` that stores its documents in `collection`, if any: the root model
528
963
  * when variants share the collection with it.
@@ -540,67 +975,47 @@ function modelStoredIn(fromContract, collection) {
540
975
  }));
541
976
  return (storing.find(({ isRoot }) => isRoot) ?? storing[0])?.coordinate;
542
977
  }
543
- /** Each collection drop of the plan, with the model whose documents it loses. */
544
- function collectionDrops(calls, fromContract) {
978
+ /**
979
+ * Each collection drop of the plan, with the model whose documents it loses. When that model keeps
980
+ * its name and the plan brings its new collection into existence, the drop says how to rename the
981
+ * collection by hand instead.
982
+ */
983
+ function collectionDrops(calls, fromContract, contract, collections) {
984
+ const created = (collection) => collections.origin.collection(collection) === void 0 && collections.destination.collection(collection) !== void 0;
545
985
  return calls.flatMap((call, operationIndex) => {
546
986
  if (!(call instanceof DropCollectionCall)) return [];
547
987
  const model = fromContract === null ? void 0 : modelStoredIn(fromContract, call.collection);
548
- return [{
988
+ if (model === void 0) return [{
549
989
  operationIndex,
550
- subject: model === void 0 ? {
990
+ subject: {
551
991
  kind: "storage",
552
992
  name: call.collection
553
- } : {
554
- kind: "model",
555
- ...model
556
993
  }
557
994
  }];
995
+ const subject = {
996
+ kind: "model",
997
+ ...model
998
+ };
999
+ const destinationCollection = modelStoredInDestination(contract, model);
1000
+ return [destinationCollection !== void 0 && created(destinationCollection) ? {
1001
+ operationIndex,
1002
+ subject,
1003
+ keepDataAdvice: keepCollectionByHandAdvice(model, call.collection, destinationCollection)
1004
+ } : {
1005
+ operationIndex,
1006
+ subject
1007
+ }];
558
1008
  });
559
1009
  }
560
- const NOT_IN_THIS_RELEASE = "MongoDB cannot carry out rename statements in this release.";
561
- function shellString(name) {
562
- return JSON.stringify(name);
563
- }
564
- /**
565
- * How to keep the documents of a subject a plan would drop, since the planner carries out no
566
- * rename: rename the collection by hand before a plan that drops it is applied. Right for both
567
- * `db update`, which then finds nothing to drop, and `migration plan`, whose written migration
568
- * still drops it.
569
- */
570
- function keepDataByHand(subject, fromContract) {
571
- const collection = subject.kind === "storage" ? subject.name : collectionOf(fromContract, subject);
572
- return `If it was renamed, keep its documents instead: rename collection "${collection}" by hand on each database before a plan that drops it is applied there, for example with db.getCollection(${shellString(collection)}).renameCollection("<new collection>") in mongosh. db update then drops nothing; a migration written by migration plan still drops "${collection}", so remove that operation from its migration.ts, or do not apply it where the collection was renamed.`;
573
- }
574
- /**
575
- * What a plan made without the statement does to the data, and how to keep it. Right for both
576
- * `db update` and `migration plan`, since the planner does not know which command it serves.
577
- * Mongo contracts key a model's fields by their stored names, so the statement's field names are
578
- * the names a `$rename` needs.
579
- */
580
- function keepTheData(statement, fromContract, contract) {
581
- const from = collectionOf(fromContract, statement.from);
582
- if (statement.entity === "field") {
583
- const { field } = statement.from;
584
- const newField = statement.to.field;
585
- const collection = `db.getCollection(${shellString(from)})`;
586
- return `${NOT_IN_THIS_RELEASE} Without the statement, the documents in collection "${from}" keep their values under "${field}", and nothing moves them to "${newField}". To move them, run these in mongosh on each database before a plan made without the statement is applied there, using the field names as they are stored. First turn off the collection's validator, which still requires "${field}": db.runCommand({ collMod: ${shellString(from)}, validationLevel: "off" }). Then drop each unique index that includes "${field}", or the move fails once two documents have lost it, for example ${collection}.dropIndex(${shellString(`${field}_1`)}). Then move the values: ${collection}.updateMany({}, { $rename: { ${shellString(field)}: ${shellString(newField)} } }). Applying the plan then creates the indexes on "${newField}" and turns the validator back on.`;
587
- }
588
- const to = collectionOf(contract, statement.to);
589
- if (from === to) return `${NOT_IN_THIS_RELEASE} Both models store their documents in collection "${from}", so a plan made without the statement keeps them.`;
590
- return `${NOT_IN_THIS_RELEASE} Without the statement, a plan drops collection "${from}" with its documents and creates collection "${to}". To keep the documents, rename the collection by hand on each database before a plan made without the statement is applied there, for example with db.getCollection(${shellString(from)}).renameCollection(${shellString(to)}) in mongosh. A migration written by migration plan without the statement still drops "${from}" wherever it is applied, so check its operations first.`;
591
- }
592
- function statementNotApplied(statement, fromContract, contract) {
593
- return {
594
- kind: "statementRefused",
595
- summary: `MongoDB does not apply rename statements in this release, so nothing was planned: ${describeMigrationStatement(statement, fromContract ?? contract, contract)}`,
596
- why: keepTheData(statement, fromContract, contract),
597
- refusedStatement: statement
598
- };
1010
+ /** The collection the destination stores `model` in, when the destination still has that model. */
1011
+ function modelStoredInDestination(contract, model) {
1012
+ const namespace = contract.domain.namespaces[model.namespaceId];
1013
+ return namespace !== void 0 && Object.hasOwn(namespace.models, model.model) ? collectionOf(contract, model) : void 0;
599
1014
  }
600
1015
  var MongoMigrationPlanner = class {
601
1016
  planCalls(options) {
602
1017
  const contract = blindCast(options.contract);
603
- const originIR = blindCast(options.schema);
1018
+ const originIR = options.statementCalls?.schema ?? blindCast(options.schema);
604
1019
  const destinationIR = contractToMongoSchemaIR(contract);
605
1020
  const collCreates = [];
606
1021
  const drops = [];
@@ -643,8 +1058,10 @@ var MongoMigrationPlanner = class {
643
1058
  conflicts
644
1059
  };
645
1060
  const allCalls = [
1061
+ ...options.statementCalls?.collectionRenames ?? [],
646
1062
  ...collCreates,
647
1063
  ...drops,
1064
+ ...options.statementCalls?.rewrites ?? [],
648
1065
  ...creates,
649
1066
  ...validatorOps,
650
1067
  ...mutableOptionOps,
@@ -667,21 +1084,75 @@ var MongoMigrationPlanner = class {
667
1084
  }
668
1085
  plan(options) {
669
1086
  const contract = blindCast(options.contract);
670
- const [statement] = options.statements;
671
- if (statement !== void 0) return {
1087
+ const originIR = blindCast(options.schema);
1088
+ const fromContract = options.fromContract;
1089
+ const statementPlan = planStatements({
1090
+ statements: options.statements,
1091
+ fromContract: fromContract ?? contract,
1092
+ contract,
1093
+ schema: originIR
1094
+ });
1095
+ if (!statementPlan.ok) return {
1096
+ kind: "failure",
1097
+ conflicts: statementPlan.failure
1098
+ };
1099
+ const { schema, collectionRenames, fieldRenames, callOfStatement } = statementPlan.value;
1100
+ const destinationIR = contractToMongoSchemaIR(contract);
1101
+ const renamedTo = new Map(collectionRenames.map((call) => [call.from, call.to]));
1102
+ const removals = fromContract === null ? planValidatorFieldRemovals({
1103
+ origin: schema,
1104
+ destination: destinationIR,
1105
+ contract
1106
+ }) : planFieldRemovals({
1107
+ statements: options.statements,
1108
+ fromContract,
1109
+ contract,
1110
+ collectionAfterRenames: (collection) => renamedTo.get(collection) ?? collection,
1111
+ keptCollections: new Set(schema.collectionNames.filter((name) => destinationIR.collection(name) !== void 0))
1112
+ });
1113
+ const removalRefusals = [...timeSeriesRemovalRefusals(removals, schema), ...sameHashFieldChangeRefusals({
1114
+ origin: options.origin,
1115
+ fromContract: fromContract ?? contract,
1116
+ contract,
1117
+ statements: options.statements,
1118
+ callOfStatement,
1119
+ removals
1120
+ })];
1121
+ if (removalRefusals.length > 0) return {
672
1122
  kind: "failure",
673
- conflicts: [statementNotApplied(statement, options.fromContract, contract)]
1123
+ conflicts: removalRefusals
674
1124
  };
675
- const result = this.planCalls(options);
1125
+ const result = this.planCalls({
1126
+ ...options,
1127
+ statementCalls: {
1128
+ schema,
1129
+ collectionRenames,
1130
+ rewrites: [...fieldRenames, ...removals.map((removal) => removal.call)]
1131
+ }
1132
+ });
676
1133
  if (result.kind === "failure") return result;
1134
+ const { calls } = result;
1135
+ const fieldLoss = removals.map((removal) => ({
1136
+ operationIndex: calls.indexOf(removal.call),
1137
+ subject: removal.subject
1138
+ }));
677
1139
  return {
678
1140
  kind: "success",
679
- plan: new PlannerProducedMongoMigration(result.calls, {
1141
+ plan: new PlannerProducedMongoMigration(calls, {
680
1142
  from: options.origin?.storageHash ?? null,
681
1143
  to: contract.storage.storageHash
682
1144
  }, options.snapshotsImportPath),
683
- appliedStatements: [],
684
- dataLoss: collectionDrops(result.calls, options.fromContract),
1145
+ appliedStatements: options.statements.map((statement, index) => {
1146
+ const call = callOfStatement[index];
1147
+ return {
1148
+ statement,
1149
+ operationIndexes: call === void 0 ? [] : [calls.indexOf(call)]
1150
+ };
1151
+ }),
1152
+ dataLoss: [...collectionDrops(calls, fromContract, contract, {
1153
+ origin: schema,
1154
+ destination: destinationIR
1155
+ }), ...fieldLoss].sort((a, b) => a.operationIndex - b.operationIndex),
685
1156
  accessWidening: []
686
1157
  };
687
1158
  }
@@ -846,6 +1317,11 @@ const DropCollectionJson = type({
846
1317
  kind: "\"dropCollection\"",
847
1318
  collection: "string"
848
1319
  });
1320
+ const RenameCollectionJson = type({
1321
+ kind: "\"renameCollection\"",
1322
+ collection: "string",
1323
+ to: "string"
1324
+ });
849
1325
  const CollModJson = type({
850
1326
  kind: "\"collMod\"",
851
1327
  collection: "string",
@@ -902,7 +1378,8 @@ const RawUpdateManyJson = type({
902
1378
  kind: "\"rawUpdateMany\"",
903
1379
  collection: "string",
904
1380
  filter: "Record<string, unknown>",
905
- update: "Record<string, unknown> | Record<string, unknown>[]"
1381
+ update: "Record<string, unknown> | Record<string, unknown>[]",
1382
+ "bypassDocumentValidation?": "boolean"
906
1383
  });
907
1384
  const RawDeleteOneJson = type({
908
1385
  kind: "\"rawDeleteOne\"",
@@ -995,16 +1472,16 @@ const DdlOperationJson = type({
995
1472
  execute: "Record<string, unknown>[]",
996
1473
  postcheck: "Record<string, unknown>[]"
997
1474
  });
998
- const DataTransformCheckJson = type({
1475
+ const DmlCheckJson = type({
999
1476
  description: "string",
1000
1477
  source: "Record<string, unknown>",
1001
1478
  filter: "Record<string, unknown>",
1002
1479
  expect: "\"exists\" | \"notExists\""
1003
1480
  });
1004
- const DataTransformOperationJson = type({
1481
+ const DmlOperationJson = type({
1005
1482
  id: "string",
1006
1483
  label: "string",
1007
- operationClass: "\"data\"",
1484
+ operationClass: "\"additive\" | \"widening\" | \"destructive\" | \"data\"",
1008
1485
  name: "string",
1009
1486
  precheck: "Record<string, unknown>[]",
1010
1487
  run: "Record<string, unknown>[]",
@@ -1153,7 +1630,7 @@ function deserializeDmlCommand(json) {
1153
1630
  }
1154
1631
  case "rawUpdateMany": {
1155
1632
  const data = validate(RawUpdateManyJson, json, "rawUpdateMany command");
1156
- return new RawUpdateManyCommand(data.collection, data.filter, data.update);
1633
+ return new RawUpdateManyCommand(data.collection, data.filter, data.update, ifDefined("bypassDocumentValidation", data.bypassDocumentValidation));
1157
1634
  }
1158
1635
  case "rawDeleteOne": {
1159
1636
  const data = validate(RawDeleteOneJson, json, "rawDeleteOne command");
@@ -1242,6 +1719,10 @@ function deserializeDdlCommand(json) {
1242
1719
  });
1243
1720
  }
1244
1721
  case "dropCollection": return new DropCollectionCommand(validate(DropCollectionJson, json, "dropCollection command").collection);
1722
+ case "renameCollection": {
1723
+ const data = validate(RenameCollectionJson, json, "renameCollection command");
1724
+ return new RenameCollectionCommand(data.collection, data.to);
1725
+ }
1245
1726
  case "collMod": {
1246
1727
  const data = validate(CollModJson, json, "collMod command");
1247
1728
  return new CollModCommand(data.collection, {
@@ -1286,8 +1767,8 @@ function deserializeStep(json) {
1286
1767
  command: deserializeDdlCommand(data.command)
1287
1768
  };
1288
1769
  }
1289
- function isDataTransformJson(json) {
1290
- return recordValue(json, "operationClass", "migration operation") === "data";
1770
+ function isDmlOperationJson(json) {
1771
+ return Array.isArray(recordValue(json, "run", "migration operation"));
1291
1772
  }
1292
1773
  function deserializeDdlOp(json) {
1293
1774
  const data = validate(DdlOperationJson, json, "migration operation");
@@ -1300,8 +1781,8 @@ function deserializeDdlOp(json) {
1300
1781
  postcheck: data.postcheck.map(deserializeCheck)
1301
1782
  };
1302
1783
  }
1303
- function deserializeDataTransformCheck(json) {
1304
- const data = validate(DataTransformCheckJson, json, "data transform check");
1784
+ function deserializeDmlCheck(json) {
1785
+ const data = validate(DmlCheckJson, json, "DML operation check");
1305
1786
  return {
1306
1787
  description: data.description,
1307
1788
  source: deserializeMongoQueryPlan(data.source),
@@ -1309,20 +1790,20 @@ function deserializeDataTransformCheck(json) {
1309
1790
  expect: data.expect
1310
1791
  };
1311
1792
  }
1312
- function deserializeDataTransformOp(json) {
1313
- const data = validate(DataTransformOperationJson, json, "data transform operation");
1793
+ function deserializeDmlOp(json) {
1794
+ const data = validate(DmlOperationJson, json, "DML operation");
1314
1795
  return {
1315
1796
  id: data.id,
1316
1797
  label: data.label,
1317
- operationClass: "data",
1798
+ operationClass: data.operationClass,
1318
1799
  name: data.name,
1319
- precheck: data.precheck.map(deserializeDataTransformCheck),
1800
+ precheck: data.precheck.map(deserializeDmlCheck),
1320
1801
  run: data.run.map(deserializeMongoQueryPlan),
1321
- postcheck: data.postcheck.map(deserializeDataTransformCheck)
1802
+ postcheck: data.postcheck.map(deserializeDmlCheck)
1322
1803
  };
1323
1804
  }
1324
1805
  function deserializeMongoOp(json) {
1325
- if (isDataTransformJson(json)) return deserializeDataTransformOp(json);
1806
+ if (isDmlOperationJson(json)) return deserializeDmlOp(json);
1326
1807
  return deserializeDdlOp(json);
1327
1808
  }
1328
1809
  function deserializeMongoOps(json) {
@@ -1339,6 +1820,59 @@ function runnerFailure(code, summary, opts) {
1339
1820
  ...opts
1340
1821
  });
1341
1822
  }
1823
+ /**
1824
+ * The failure when a field rename's before-check finds documents that already hold the new name,
1825
+ * which `$rename` would overwrite: what to look for and fix, and that the plan resumes.
1826
+ */
1827
+ function renameOverwriteRefusal(op) {
1828
+ const rewrite = fieldRewriteOf(op);
1829
+ if (rewrite === void 0) return void 0;
1830
+ const renames = Reflect.get(rewrite.command.update, "$rename");
1831
+ const to = typeof renames === "object" && renames !== null ? Reflect.get(renames, rewrite.field) : void 0;
1832
+ if (typeof to !== "string") return void 0;
1833
+ const [check] = op.precheck;
1834
+ const source = check?.source.command;
1835
+ const match = source instanceof RawAggregateCommand ? Reflect.get(source.pipeline[0] ?? {}, "$match") : void 0;
1836
+ const find = `db.getCollection(${JSON.stringify(rewrite.collection)}).find(${JSON.stringify(match ?? {})})`;
1837
+ return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} stopped: some documents in collection ${rewrite.collection} already hold ${to} as well as ${rewrite.field}`, {
1838
+ why: `Renaming ${rewrite.field} to ${to} would overwrite their ${to}. Find them with ${find} in mongosh, and move or remove one of the two values in each. Nothing was renamed. The operations before this one in the plan already ran, such as dropping an index on ${rewrite.field}; once the documents are fixed, run the command again and the plan finishes from here.`,
1839
+ meta: {
1840
+ operationId: op.id,
1841
+ name: op.name
1842
+ }
1843
+ });
1844
+ }
1845
+ /**
1846
+ * The failure when a collection rename's before-check finds that the new collection already exists.
1847
+ */
1848
+ function renameTargetExistsRefusal(op, failed) {
1849
+ const rename = op.execute.find((step) => step.command instanceof RenameCollectionCommand)?.command;
1850
+ if (!(rename instanceof RenameCollectionCommand) || failed.expect !== "notExists") return;
1851
+ const { collection: from, to } = rename;
1852
+ return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} stopped: collection ${to} already exists, so collection ${from} cannot be renamed to it`, {
1853
+ why: `Nothing was renamed. If ${to} holds nothing you need, drop it, then run the command again; operations that already ran are skipped. If an earlier operation of the same migration created ${to}, as a createIndex on ${to} does, make renameCollection(${JSON.stringify(from)}, ${JSON.stringify(to)}) the first operation of its migration.ts, run node on that migration.ts, and then drop ${to} and run the command again.`,
1854
+ meta: { operationId: op.id }
1855
+ });
1856
+ }
1857
+ const MONGO_UNAUTHORIZED = 13;
1858
+ /**
1859
+ * The failure for a rewrite that skips validation when the database user lacks the
1860
+ * `bypassDocumentValidation` action, as a `readWrite` user on a hosted cluster does.
1861
+ */
1862
+ function bypassValidationRefusal(op, plan, error) {
1863
+ if (!(plan.command instanceof RawUpdateManyCommand) || !plan.command.bypassDocumentValidation) return;
1864
+ if (typeof error !== "object" || error === null) return void 0;
1865
+ if (Reflect.get(error, "code") !== MONGO_UNAUTHORIZED) return void 0;
1866
+ const codeName = Reflect.get(error, "codeName");
1867
+ return runnerFailure("MIGRATION.EXECUTION_FAILED", `Operation ${op.id} failed: the database user may not skip document validation`, {
1868
+ why: `Renaming or removing a field rewrites documents with bypassDocumentValidation, because the old validator requires the old field. MongoDB refused it with error ${MONGO_UNAUTHORIZED} (${String(codeName)}). Grant the user the bypassDocumentValidation action on the database, for example with the dbAdmin or restore role, then run the command again; operations that already ran are skipped.`,
1869
+ meta: {
1870
+ operationId: op.id,
1871
+ mongoCode: MONGO_UNAUTHORIZED,
1872
+ codeName
1873
+ }
1874
+ });
1875
+ }
1342
1876
  var MongoMigrationRunner = class {
1343
1877
  deps;
1344
1878
  constructor(deps) {
@@ -1362,22 +1896,22 @@ var MongoMigrationRunner = class {
1362
1896
  for (const operation of operations) {
1363
1897
  options.callbacks?.onOperationStart?.(operation);
1364
1898
  try {
1365
- if (operation.operationClass === "data") {
1366
- const result = await this.executeDataTransform(blindCast(operation), adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks);
1899
+ if (isMongoDmlOperation(operation)) {
1900
+ const result = await this.executeDmlOperation(operation, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks);
1367
1901
  if (result.failure) return result.failure;
1368
1902
  if (result.executed) operationsExecuted += 1;
1369
1903
  continue;
1370
1904
  }
1371
- const ddlOp = blindCast(operation);
1372
1905
  if (runPostchecks && runIdempotency) {
1373
- if (await this.allChecksSatisfied(ddlOp.postcheck, inspectionExecutor, filterEvaluator)) continue;
1906
+ if (await this.allChecksSatisfied(operation.postcheck, inspectionExecutor, filterEvaluator)) continue;
1374
1907
  }
1375
1908
  if (runPrechecks) {
1376
- if (!await this.evaluateChecks(ddlOp.precheck, inspectionExecutor, filterEvaluator)) return runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${operation.id} failed during precheck`, { meta: { operationId: operation.id } });
1909
+ const failed = await this.firstFailingCheck(operation.precheck, inspectionExecutor, filterEvaluator);
1910
+ if (failed !== void 0) return renameTargetExistsRefusal(operation, failed) ?? runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${operation.id} failed during precheck`, { meta: { operationId: operation.id } });
1377
1911
  }
1378
- for (const step of ddlOp.execute) await executeDdl(step.command);
1912
+ for (const step of operation.execute) await executeDdl(step.command);
1379
1913
  if (runPostchecks) {
1380
- if (!await this.evaluateChecks(ddlOp.postcheck, inspectionExecutor, filterEvaluator)) return runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${operation.id} failed during postcheck`, { meta: { operationId: operation.id } });
1914
+ if (!await this.evaluateChecks(operation.postcheck, inspectionExecutor, filterEvaluator)) return runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${operation.id} failed during postcheck`, { meta: { operationId: operation.id } });
1381
1915
  }
1382
1916
  operationsExecuted += 1;
1383
1917
  } finally {
@@ -1449,14 +1983,14 @@ var MongoMigrationRunner = class {
1449
1983
  });
1450
1984
  }
1451
1985
  }
1452
- async executeDataTransform(op, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks) {
1986
+ async executeDmlOperation(op, adapter, driver, filterEvaluator, runIdempotency, runPrechecks, runPostchecks) {
1453
1987
  if (runPostchecks && runIdempotency && op.postcheck.length > 0) {
1454
- if (await this.evaluateDataTransformChecks(op.postcheck, adapter, driver, filterEvaluator)) return { executed: false };
1988
+ if (await this.evaluateDmlChecks(op.postcheck, adapter, driver, filterEvaluator)) return { executed: false };
1455
1989
  }
1456
1990
  if (runPrechecks && op.precheck.length > 0) {
1457
- if (!await this.evaluateDataTransformChecks(op.precheck, adapter, driver, filterEvaluator)) return {
1991
+ if (!await this.evaluateDmlChecks(op.precheck, adapter, driver, filterEvaluator)) return {
1458
1992
  executed: false,
1459
- failure: runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} failed during precheck`, { meta: {
1993
+ failure: renameOverwriteRefusal(op) ?? runnerFailure("MIGRATION.PRECHECK_FAILED", `Operation ${op.id} failed during precheck`, { meta: {
1460
1994
  operationId: op.id,
1461
1995
  name: op.name
1462
1996
  } })
@@ -1464,10 +1998,19 @@ var MongoMigrationRunner = class {
1464
1998
  }
1465
1999
  for (const plan of op.run) {
1466
2000
  const wireCommand = await adapter.lower(plan, {});
1467
- for await (const _ of driver.execute(wireCommand));
2001
+ try {
2002
+ for await (const _ of driver.execute(wireCommand));
2003
+ } catch (error) {
2004
+ const refusal = bypassValidationRefusal(op, plan, error);
2005
+ if (refusal === void 0) throw error;
2006
+ return {
2007
+ executed: false,
2008
+ failure: refusal
2009
+ };
2010
+ }
1468
2011
  }
1469
2012
  if (runPostchecks && op.postcheck.length > 0) {
1470
- if (!await this.evaluateDataTransformChecks(op.postcheck, adapter, driver, filterEvaluator)) return {
2013
+ if (!await this.evaluateDmlChecks(op.postcheck, adapter, driver, filterEvaluator)) return {
1471
2014
  executed: false,
1472
2015
  failure: runnerFailure("MIGRATION.POSTCHECK_FAILED", `Operation ${op.id} failed during postcheck`, { meta: {
1473
2016
  operationId: op.id,
@@ -1477,7 +2020,7 @@ var MongoMigrationRunner = class {
1477
2020
  }
1478
2021
  return { executed: true };
1479
2022
  }
1480
- async evaluateDataTransformChecks(checks, adapter, driver, filterEvaluator) {
2023
+ async evaluateDmlChecks(checks, adapter, driver, filterEvaluator) {
1481
2024
  for (const check of checks) {
1482
2025
  const commandKind = check.source.command.kind;
1483
2026
  if (!READ_ONLY_CHECK_COMMAND_KINDS.has(commandKind)) throw errorRunnerFailed(`Data-transform check rejected: command kind "${commandKind}" is not read-only`, {
@@ -1500,11 +2043,13 @@ var MongoMigrationRunner = class {
1500
2043
  return true;
1501
2044
  }
1502
2045
  async evaluateChecks(checks, inspectionExecutor, filterEvaluator) {
2046
+ return await this.firstFailingCheck(checks, inspectionExecutor, filterEvaluator) === void 0;
2047
+ }
2048
+ async firstFailingCheck(checks, inspectionExecutor, filterEvaluator) {
1503
2049
  for (const check of checks) {
1504
2050
  const matchFound = (await check.source.accept(inspectionExecutor)).some((doc) => filterEvaluator.evaluate(check.filter, doc));
1505
- if (!(check.expect === "exists" ? matchFound : !matchFound)) return false;
2051
+ if (!(check.expect === "exists" ? matchFound : !matchFound)) return check;
1506
2052
  }
1507
- return true;
1508
2053
  }
1509
2054
  async allChecksSatisfied(checks, inspectionExecutor, filterEvaluator) {
1510
2055
  if (checks.length === 0) return false;
@@ -1628,10 +2173,6 @@ const mongoTargetDescriptor = {
1628
2173
  contractSerializer: new MongoTargetContractSerializer(),
1629
2174
  schemaVerifier: new MongoTargetSchemaVerifier(),
1630
2175
  migrations: {
1631
- renameStatements: {
1632
- refused: true,
1633
- keepDataByHand
1634
- },
1635
2176
  createPlanner(_adapter) {
1636
2177
  return new MongoMigrationPlanner();
1637
2178
  },
@@ -1681,6 +2222,6 @@ const mongoTargetDescriptor = {
1681
2222
  }
1682
2223
  };
1683
2224
  //#endregion
1684
- export { renderCallsToTypeScript as _, DropIndexCall as a, schemaIndexToCreateIndexOptions as b, MongoMigrationRunner as c, MongoTargetSchemaVerifier as d, MongoTargetUnboundDatabase as f, mongoTargetDescriptor as g, deserializeMongoOps as h, DropCollectionCall as i, MongoTargetContractSerializer as l, deserializeMongoOp as m, CreateCollectionCall as n, FilterEvaluator as o, PlannerProducedMongoMigration as p, CreateIndexCall as r, MongoMigrationPlanner as s, CollModCall as t, MongoTargetDatabase as u, renderOps as v, serializeMongoOps as x, schemaCollectionToCreateCollectionOptions as y };
2225
+ export { schemaIndexToCreateIndexOptions as C, schemaCollectionToCreateCollectionOptions as S, deserializeMongoOp as _, DropIndexCall as a, renderCallsToTypeScript as b, MongoMigrationRunner as c, MongoTargetSchemaVerifier as d, MongoTargetUnboundDatabase as f, UnsetFieldCall as g, RenameFieldCall as h, DropCollectionCall as i, MongoTargetContractSerializer as l, RenameCollectionCall as m, CreateCollectionCall as n, FilterEvaluator as o, PlannerProducedMongoMigration as p, CreateIndexCall as r, MongoMigrationPlanner as s, CollModCall as t, MongoTargetDatabase as u, deserializeMongoOps as v, serializeMongoOps as w, renderOps as x, mongoTargetDescriptor as y };
1685
2226
 
1686
- //# sourceMappingURL=control-B0JolOVp.mjs.map
2227
+ //# sourceMappingURL=control-BcqDWXwi.mjs.map