Skip to content

Commit 6ffe37e

Browse files
docs(spec,cli): seed replace deletes nothing, and the AppPlugin loads skip a landed row a person deleted (#31135)
<!-- claude-projects-attribution: {"github_login":"marchtian"} --> _Requested by **田騏** · [project thread](https://claude.ai/code/project/chan_01VmuUhXU9SSUzqJYyXSmYSi?thread=cmsg_01VmuUhXU9SSUzqJYyXSmYSiQSToiq8oWUfF5GruxpTNDG)_ Fixes #31099 Clause-②: no Metadata-protocol: none ## 维护者速读 - **改了什么:** 只改文字。`seed.zod.ts` 里 `replace` 不再写成"先删整表再插入",改为"不删任何东西,只插入,给调用方已清空的缓存表用";`insert` 不再写"重复时失败",改为"每次重放都再插一遍";`mode` 的说明补上已落地行的记录规则和 `{ reseed: true }` 这扇门。`os diff` 对 `replace` 数据集的提示同步改正。 - **为什么改:** 旧文字和加载器实际行为相反:作者或 AI 读到"replace 会清表",会以为重放能刷新数据,实际只会把行再插一遍。上一张卡已经改了技能包和文档,spec 的 `.describe()` 和 CLI 提示还停在旧说法。 - **风险与回滚:** 无行为变化,只动注释、`.describe()`、一条 CLI 提示字符串和它的测试,以及自动生成的 4 个参考页。回滚即 revert。 - **先看:** `packages/spec/src/data/seed.zod.ts`(`SeedMode` 注释、`mode` 的 JSDoc 新段落和 `.describe()`);`packages/cli/src/commands/diff.ts` 第 81 行。 - **两分钟验证:** `pnpm --filter @objectstack/cli exec vitest run --project unit test/diff-upgrade-plan.test.ts`,预期 7 个测试全过,`replace` 的提示为 "a replay deletes nothing and inserts these again"。 - **需要你做的:** 无,席位落地。 ## What changes Text only; no behaviour change. Every sentence was worded from the loader and AppPlugin code on `main` (5f00374), not from the old text. - `packages/spec/src/data/seed.zod.ts` - `SeedMode` comments: `insert` no longer says "fail on duplicate" — it inserts every record with no match, so each replay inserts them again, and a row the database refuses is that row's error (`SeedLoaderService.writeRecord` / `decideWriteAction` insert unconditionally; a refused row is pushed to the load's errors while the rest land). `replace` no longer says "Delete ALL records in object then insert": it inserts every record, no match, and deletes nothing (its `writeRecord` arm is a bare insert; nothing in the loader deletes). - `mode` docstring: a new paragraph on the landed-keys rule. The loads AppPlugin runs for an organization (single-tenant boot seed, per-organization replay through `seed-replayer`, dev hot reload), where the composition registers `sys_seed_landed_key`, record the key of every keyed row an `ignore` or `upsert` dataset landed and do not insert again a recorded row that is now gone; a never-recorded key still lands; a seed publish and a package install or upgrade keep no record; `seed-replayer(organizationId, { reseed: true })` inserts every missing row. - `mode` `.describe()`: the same facts, compressed; no tracker number. The `ignore`-is-default text (ruling 6631016988 on #30081) is unchanged. - The `history` string at L116–117 is left as is (historical, card: optional). - `packages/cli/src/commands/diff.ts` `seedModeNote`: `replace` now reads "a replay deletes nothing and inserts these again, so rows already in the table stay". `packages/cli/test/diff-upgrade-plan.test.ts` pins the new string (it pinned the old one). - `content/docs/references/**`: 4 pages regenerated by `gen:schema && gen:docs` from the `.describe()`. - Changeset: `@objectstack/spec` patch + `@objectstack/cli` patch. Wording is in step with the already-reviewed `content/docs/data-modeling/seed-data.mdx` ("A Deleted Seed Row Stays Deleted") and `skills/objectstack-data/references/seeds.md`. ## Tests - `pnpm --filter @objectstack/cli exec vitest run --project unit` on 89162ff: Test Files 396 passed, Tests 5576 passed. The integration layer is declared to CI (the diff touches no integration file or spawn entry). - After merging `origin/main` (head 68060aa, closure rebuilt): `diff-upgrade-plan` and the incoming `connect-agent-both-halves-wire.pin` (unit layer): 2 files, 9 tests passed. The incoming `authoring-rule-command-parity` is an integration-layer file, declared to CI. - eslint on the three touched source/test files: exit 0. ## Acceptance notes - Independent Fable review of the whole diff: PASS with should-fix (name the `sys_seed_landed_key` condition and "keyed row" in the docstring; sharpen the `insert` comment). Both applied in 89162ff; delta review PASS. Verdict comment to follow on this PR. - Left alone, noted: `seedModeNote` says nothing for `insert`, which also inserts every record again on each replay. Outside this card's item 2 (the `replace` line only). - `docs/qa/platform-checklist/areas/platform-core.json` quotes the old `replace` text inside a historical "declared ≠ implemented" clause; out of the card's file surface, left as is. ## Verification <details><summary>Derived gates (dispatch-gates --commands) — 108 run on 89162ff, all exit 0</summary> - `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --ran ran.txt` :: exit 0 — "108 derived famil(ies) accounted for — 108 run, 0 NOT-MEASURED". - First pass: 12 families exited 3 (PREREQUISITE NOT MET: `packages/spec/dist` stale after the docstring commit, a shallow clone for `check-plugin-teardown-shape --self-test`, and unbuilt workspace dists). After `pnpm --filter @objectstack/spec build` and `git fetch --unshallow origin main`, all 12 reran with exit 0. - `pnpm --filter @objectstack/spec run check:generated` :: exit 0 (also post-merge: "All 14 generated artifacts are up to date"). - `pnpm check:doc-authoring` :: exit 0 (no tracker number in `.describe()`). </details> <details><summary>Merge of origin/main</summary> Merged once before opening (68060aa). The incoming commits touch no seed, spec or `diff.ts` file. Re-checked: the CLI closure rebuilt, `check:generated` exit 0, the CLI test files above, and `dispatch-gates --ran` still 108/108. </details> ## Human Minimum - [x] 1. Schemas start as Zod; `packages/spec` holds no runtime logic. - [x] 2. Every name I added uses the casing for its kind (no names added). - [x] 3. No formatter ran; eslint passes on the touched files (`pnpm lint` repo-wide is CI's). - [x] 4. One issue, one branch off an up-to-date `main`, one pull request. - [x] 5. A change that publishes carries a changeset, and both declaration lines are filled in here and in the changeset. - [x] 6. `packages/spec` touched: its generated artifacts are regenerated and committed. - [x] 7. No hand edits to `content/docs/references/`, `packages/*/CHANGELOG.md` or `content/docs/releases/`. - [x] 8. Title, body and comments are in English (the 维护者速读 block is the repo's own Chinese reader section). - [x] The *Before Submitting* list in CONTRIBUTING.md is done. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_017ihL2dzed5sJnRTmwXHWV7 --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent d5fd948 commit 6ffe37e

8 files changed

Lines changed: 37 additions & 10 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': patch
3+
'@objectstack/cli': patch
4+
---
5+
6+
docs(spec,cli): the seed mode text now says what the loader does. `replace` deletes nothing: it inserts every record with no match, for a table the caller cleared, and `insert` inserts every record again on each replay. The `mode` docstring and `.describe()` name the landed-keys rule: the loads AppPlugin runs for an organization (the single-tenant boot seed, the `seed-replayer` replay, the dev hot reload) do not insert again an `ignore` or `upsert` row that landed once and was later deleted, a seed publish or package install keeps no such record, and `seed-replayer(organizationId, { reseed: true })` inserts every missing row. `os diff`'s note on a `replace` dataset no longer says a replay deletes every row of the object.
7+
8+
Clause-②: no
9+
Metadata-protocol: none

‎content/docs/references/api/package-api.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,7 @@ Package manifest installed through POST /api/v1/packages (AUTHORING stage; scope
243243
| :--- | :--- | :--- | :--- |
244244
| **object** | `string` | ✅ | Target Object Name |
245245
| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check |
246-
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
246+
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, so each replay inserts them again, and `replace` does the same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row that landed once and was later deleted is not inserted again; a publish or package install keeps no such record, and seed-replayer called with `{ reseed: true }` inserts every missing row. |
247247
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
248248
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
249249
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |
@@ -351,7 +351,7 @@ Package manifest installed through POST /api/v1/packages (AUTHORING stage; scope
351351
| :--- | :--- | :--- | :--- |
352352
| **object** | `string` | ✅ | Target Object Name |
353353
| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check |
354-
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
354+
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, so each replay inserts them again, and `replace` does the same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row that landed once and was later deleted is not inserted again; a publish or package install keeps no such record, and seed-replayer called with `{ reseed: true }` inserts every missing row. |
355355
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
356356
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
357357
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |

‎content/docs/references/data/seed-loader.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ Seed loader request with datasets and configuration
230230
| :--- | :--- | :--- | :--- |
231231
| **object** | `string` | ✅ | Target Object Name |
232232
| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check |
233-
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
233+
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, so each replay inserts them again, and `replace` does the same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row that landed once and was later deleted is not inserted again; a publish or package install keeps no such record, and seed-replayer called with `{ reseed: true }` inserts every missing row. |
234234
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
235235
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
236236
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |

‎content/docs/references/data/seed.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ const result = SeedSchema.parse(data);
3030
| :--- | :--- | :--- | :--- |
3131
| **object** | `string` | ✅ | Target Object Name |
3232
| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check |
33-
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
33+
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, so each replay inserts them again, and `replace` does the same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row that landed once and was later deleted is not inserted again; a publish or package install keeps no such record, and seed-replayer called with `{ reseed: true }` inserts every missing row. |
3434
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
3535
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
3636
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |

‎content/docs/references/kernel/manifest.mdx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ Install mode: managed (registered as code, upgradeable, sealed) | unmanaged (cop
9797
| :--- | :--- | :--- | :--- |
9898
| **object** | `string` | ✅ | Target Object Name |
9999
| **externalId** | `string \| string[]` | optional (default: `"name"`) | Field (or composite list of fields) matched for the uniqueness check |
100-
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, and `replace` inserts into a table the caller cleared. |
100+
| **mode** | `Enum<'insert' \| 'update' \| 'upsert' \| 'replace' \| 'ignore'>` | optional (default: `"ignore"`) | What a load does with a record whose `externalId` matches an existing row. `ignore` (default) is create-only: a missing row is inserted and an existing row is left as it is, so a replay (every boot, a package install or upgrade, a publish) never writes a seeded value back over a later edit. `upsert` also writes every declared column back onto the matched row on each replay; declare it for reference data the package keeps current. `update` writes matched rows only, `insert` inserts every record with no match, so each replay inserts them again, and `replace` does the same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row that landed once and was later deleted is not inserted again; a publish or package install keeps no such record, and seed-replayer called with `{ reseed: true }` inserts every missing row. |
101101
| **columns** | `Record<string, { applyOn?: Enum<'create' \| 'always'> }>` | optional | Per-column write policy, keyed by field name: `{ status: { applyOn: 'create' } }` writes `status` only when the seed creates the row. Omitted, every column is written on create and update. |
102102
| **env** | `Enum<'prod' \| 'dev' \| 'test'>[]` | optional (default: `["prod","dev","test"]`) | Applicable environments |
103103
| **locale** | `string[]` | optional | Applicable locales (BCP-47 tags); omitted applies to every locale. The boot seed filters on the app's i18n.defaultLocale; the marketplace install, sample-data reseed, restart heal and per-organization replay filter on the locale resolved for the load (the localization.locale setting when set, then the locale of the installing, reseeding or founding user, then the app's i18n.defaultLocale where the load names one); package apply and draft publish do not filter by locale — they load every dataset and warn |

‎packages/cli/src/commands/diff.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function diffNamedArrays(
7878

7979
function seedModeNote(mode: string): string {
8080
if (seedModeOverwritesMatchedRows(mode)) return '; a replay writes its columns over the rows it matches';
81-
if (mode === 'replace') return '; a replay deletes every row of the object and inserts these';
81+
if (mode === 'replace') return '; a replay deletes nothing and inserts these again, so rows already in the table stay';
8282
return '';
8383
}
8484

‎packages/cli/test/diff-upgrade-plan.test.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ describe('os diff — what the upgrade plan adds', () => {
144144
{ type: 'removed', category: 'data', name: 'acme_old', breaking: false, detail: 'seed dataset removed, mode insert' },
145145
{
146146
type: 'added', category: 'data', name: 'acme_tier', breaking: false,
147-
detail: 'seed dataset added: 1 record(s); mode replace; a replay deletes every row of the object and inserts these',
147+
detail: 'seed dataset added: 1 record(s); mode replace; a replay deletes nothing and inserts these again, so rows already in the table stay',
148148
},
149149
{
150150
type: 'modified', category: 'data', name: 'acme_stage', breaking: false,

‎packages/spec/src/data/seed.zod.ts‎

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ import {
2121
isSeedFileReference,
2222
} from './seed-file.zod';
2323
export const SeedMode = z.enum([
24-
'insert', // Try to insert, fail on duplicate
24+
'insert', // Insert every record, no match: each replay inserts them again (a row the database refuses is that row's error)
2525
'update', // Only update found records, ignore new
2626
'upsert', // Create new or update existing: every replay writes the declared columns back
27-
'replace', // Delete ALL records in object then insert (Dangerous - use for cache tables)
27+
'replace', // Insert every record, no match, deleting nothing: for a cache table the caller cleared
2828
'ignore' // Create-only (the default): insert missing rows, leave existing rows alone
2929
]);
3030

@@ -177,6 +177,20 @@ export const SeedSchema = lazySchema(() => strictObject({
177177
* (ruling 6631016988 on objectstack#30081, which measured exactly that on a
178178
* production boot and on both upgrade paths while the default was `upsert`).
179179
*
180+
* A row a person deleted is not brought back by every replay. The loads
181+
* AppPlugin (`@objectstack/runtime`) runs for an organization — the
182+
* single-tenant boot seed, the per-organization replay (the `seed-replayer`
183+
* service) and the dev hot reload, where the composition registers
184+
* `sys_seed_landed_key` (`@objectstack/platform-objects`) — record, per
185+
* organization, the key of every keyed row an `ignore` or `upsert` dataset
186+
* landed, and do not insert again a recorded row that is now gone. A key
187+
* never recorded still lands, so a row an upgrade adds to the seed is
188+
* inserted. A seed publish and a package
189+
* install or upgrade (`os package install`, the marketplace install) keep no
190+
* record, so the row comes back on those. The deliberate re-seed,
191+
* `seed-replayer(organizationId, { reseed: true })`, inserts every missing
192+
* row whatever the record says.
193+
*
180194
* An author who wants a replay to refresh the rows — reference data the
181195
* package keeps current — declares `mode: 'upsert'`, and can still exempt
182196
* the columns the customer or a job owns after the first load with
@@ -197,7 +211,11 @@ export const SeedSchema = lazySchema(() => strictObject({
197211
'(every boot, a package install or upgrade, a publish) never writes a seeded value back over a ' +
198212
'later edit. `upsert` also writes every declared column back onto the matched row on each replay; ' +
199213
'declare it for reference data the package keeps current. `update` writes matched rows only, ' +
200-
'`insert` inserts every record with no match, and `replace` inserts into a table the caller cleared.',
214+
'`insert` inserts every record with no match, so each replay inserts them again, and `replace` does the ' +
215+
'same and deletes nothing: it is for a table the caller cleared. On the loads AppPlugin runs for an ' +
216+
'organization (the boot seed, the seed-replayer replay, the dev hot reload), an `ignore` or `upsert` row ' +
217+
'that landed once and was later deleted is not inserted again; a publish or package install keeps no such ' +
218+
'record, and seed-replayer called with { reseed: true } inserts every missing row.',
201219
),
202220

203221
/**

0 commit comments

Comments
 (0)