Skip to content

feat(nextly): create a single's table and its registry row in one service - #556

Merged
mobeenabdullah merged 19 commits into
mainfrom
feat/builder-ddl-services-relocation
Aug 5, 2026
Merged

feat(nextly): create a single's table and its registry row in one service#556
mobeenabdullah merged 19 commits into
mainfrom
feat/builder-ddl-services-relocation

Conversation

@mobeenabdullah

@mobeenabdullah mobeenabdullah commented Aug 5, 2026

Copy link
Copy Markdown
Collaborator

Moves the Schema Builder's single create out of the request handler and into a service that
owns the table change and the registry write together. Delete, schema-update and the field-group
side follow in the next two PRs.

Why

A Single's table was created by the handler: generate the DDL, run it, then write the registry row.
Two consequences:

  • A crash between the two leaves a table nothing has any record of — an orphan findable only by
    guessing at table names.
  • A lock cannot cover the pair. One taken inside the registry service is acquired after the
    tables have already changed. That is what blocks the migration-lock work this belongs to.

Collections already avoid both by owning the halves in one method. This is the same shape for
Singles.

What it guarantees, and what it does not

Recoverable and idempotent. NOT atomic. That is the only honest claim available: MySQL commits
DDL implicitly, so no ordering and no transaction makes the pair atomic there. The migration engine
reached the same conclusion in field-groups/migration/steps.ts and answers it with "sequenced with
repair rather than atomic, and every half idempotent to make that repair possible". Same answer
here, rather than a second one.

The mechanism is write-ahead intent: the row is persisted as pending before the table is
touched and confirmed afterwards, so an interrupted create leaves a durable record of what was being
attempted and recovery is a query rather than an inference.

That query already existed. SingleRegistryService.getPendingMigrations() had zero callers,
because pending never reached the database — migrationStatus was a local variable and the row
was written once, at the end. This connects a design that was already there.

The ordering also moves the registry's own rejections (a taken slug, a name reserved by a global
resource) to before the DDL rather than after it.

The defect this found

🔴 There was no integration test that a createSingle request creates a table. The only
integration coverage of the path asserts the REJECTION leaves no orphan. Nothing asserted a
successful create produces one — which is exactly the gap #536 fell through, where a full unit suite
read identical while MySQL tables were not being created at all.

single-create-schema-change.integration.test.ts closes it, driving the real dispatcher against a
real database per dialect and asserting the registry row and the physical table, because the
failure being guarded against is the two halves disagreeing.

Its third case re-applies a create over a table it already made — the repair scenario the guarantee
is entirely about:

dialect result
SQLite passed
PostgreSQL passed
MySQL failedDuplicate key name 'idx_single_..._created_at'

PostgreSQL and SQLite emit IF NOT EXISTS for the table and its indexes. MySQL has no such form
for CREATE INDEX
, so the second run died on the index and a correct schema was recorded as a
failed migration.

Not introduced here — the handler's bare try/catch behaved identically. What changed is that the
service now claims idempotency, so the claim had to be made true. Each statement now tolerates
isIdempotencyError, the canonical matcher whose own header names MySQL's "Duplicate key name" and
which deliberately refuses to match a duplicate ROW (error 1062), so a genuine data conflict still
fails.

Notes for review

reconcileSingleCompanion is a pure move, proven two ways rather than one: the function body
diffs byte-for-byte against 4ce333cae modulo the import-path rewrite, and both forwarding suites
are green either side with identical stderr.

The service's input is Omit<DynamicSingleInsert, "migrationStatus">, not a hand-listed subset.
The first draft listed fields by hand and silently dropped schemaHash, webhooks and the resolved
versions/revalidate shapes — the create would have written a row missing three settings and
nothing in the types or the tests would have said so.

Registered in DI rather than constructed per request so one wrapper governs every caller. The
migration lock has to enclose both halves, and a lock applied at one call site leaves the others
uncovered.

Two test doubles grew; no assertion changed. Both dispatcher suites gained
getSingleMetadataServiceFromDI, wired to a real service over the same registry and adapter
doubles — a stub would leave the assertions describing the stub, since what a request forwards into
the DDL is now decided inside the service. The registry doubles gained updateMigrationStatus.
single-dispatcher-ddl.test.ts also gained getSchemaRegistryFromDI, which silences a warning that
was already firing on main.

ddlFor now asserts the create's own recorded migration_status === "applied" first, because a
create that gave up part-way still leaves its earlier statements in executed — every assertion
would keep passing while describing a run that never finished. Proven load-bearing: forcing
"failed" fails all 6 with expected 'failed' to be 'applied'.

input.fields as unknown as FieldDefinition[] is carried, not introduced. The
FieldConfig/FieldDefinition boundary is asserted this way at ~10 non-test sites already
(di/register.ts:712, single-query-service.ts:2499, collection-query-service.ts:3106, …).
Aligning those two representations is a much larger change than this PR.

Scope, stated rather than quietly narrowed

The plan had this PR covering create, delete, schema-update, the field-group service and all three
field-group transports. It covers create only, for size rather than difficulty:
updateSingleSchema is ~420 lines in the handler and applySingleSchemaChanges ~215, each carrying
the push pipeline, rename detection and the prompt dispatcher. With the field-group side that is
around +2500/−1500 across ~20 files, which is where this repo's review bots start degrading.

  • Next PRFieldGroupMetadataService and all three field-group transports, which closes the
    P1 in task 137
    , plus converging the companion reconcile.
  • After thatdeleteSingle and updateSingleSchema, each with its own before/after evidence.

Field groups go first because the P1 is live: api/field-groups.ts and
direct-api/namespaces/field-groups.ts both write a registry row, never create the table, and
return 201. Reachable through the public nextly.fieldGroups.create(). The route already writes
migrationStatus: "pending", so it is intent-first with the apply missing — pointing it at the
service is the fix, with no interim duplication.

Also deliberately not done here: the companion reconcile exists three times (singles,
component-dispatcher.ts:243, inline at collection-dispatcher.ts:804) with ~45 identical middle
lines. A shared applyCompanionTransition lands with the field-group PR, which already opens two of
those files.

One more thing this PR carries

The same MySQL retry dead end existed in every Builder path, not just the one being moved:
single-dispatcher schema-update and component-dispatcher field-group create both ran statements
through a private copy of the splitter with no tolerance. Both now use one shared
applyMigrationStatements, which also removes 2 of the 7 hand-rolled splitters task 136 tracks.

CollectionFileManager.runMigration is deliberately left alone: it executes with per-dialect
branches and swallows its errors, which task 136 already records as a separate, larger decision.
Changing error handling in code that currently hides failures should not happen as a side effect.

Verification

Unit baseline re-measured at 4ce333cae in its own installed and built worktree: 363 failures /
7275
(main's #544 rewrote __tests__/fixtures/db.ts; the 397 carried through this programme is
stale). Measured twice, stable both times.

Branch: 363 failed / 6869 passed / 7280 — failure set identical to main, zero new, zero
disappeared.
The 6869 − 6864 = 5 extra passes are exactly the tests this PR adds.

🔴 That comparison needs --testTimeout=60000, and the reason is worth knowing. At the repo's
10s default the branch reports 6–14 extra failures, in files this PR does not touch, with a
different set every run. Every one is Test timed out in 10000ms, never an assertion, and each
passes in isolation. Re-measuring main on the same machine minutes later returned 0 timeouts twice,
so this is not the environment.

The branch's unit suite runs ~24% slower than main under identical conditions (154s vs 124s),
which is what tips the slowest drizzle-kit-loading files over a 10s limit. The first hypothesis —
that the DI registration module statically importing the schema and i18n machinery defeated the 37
await import() calls di/register.ts uses for exactly those modules — was tested and refuted;
making them lazy changed nothing measurable. The lazy imports are kept because the reasoning stands
on its own, but they are not the cause, and the cause is not yet identified.

If CI shows those files timing out, that is this, not a behaviour change — and the fix is the
timeout or the cause, not the tests.

Build clean · tsc --noEmit exit 0 · lint exit 0 · check-drizzle-v1-legacy all checks passed ·
cross-package (plugin-sdk, ui, blocks-engine, plugin-seo) 14/14 green.

Integration, PostgreSQL 17: 1267 passed, 0 failed.
Integration, MySQL: 1202 passed, 2 failed — and those 2 fail identically on main.

🔴 The MySQL integration baseline is not zero against a shared database.
builder-create-converges.integration.test.ts fails with
PUSHSCHEMA_FAILED — resolver(table) was called without a HintsHandler whenever TEST_MYSQL_URL
points at a database holding tables the desired schema does not declare. That suite connects to the
URL directly instead of provisioning its own database, and wires noopPromptDispatcher. Against a
database created for the run it passes. Reproduced on 4ce333cae with no branch involved.
Worth its own task: the suite should provision its own database like the rest of the harness.

Summary by CodeRabbit

  • New Features

    • Added reliable Single creation with migration tracking, retry recovery, schema verification, and localized companion-table provisioning.
    • Added shared migration execution that safely tolerates already-applied changes while reporting other database errors.
    • Added schema validation to detect missing columns and incompatible field types after migrations.
  • Bug Fixes

    • Improved handling of incomplete or failed migrations and existing tables.
    • Enhanced component and collection migrations with consistent execution and verification behavior.

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@mobeenabdullah, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 40 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 441761d3-8053-4911-a80e-041174f1f545

📥 Commits

Reviewing files that changed from the base of the PR and between 6512e2f and 95948af.

⛔ Files ignored due to path filters (1)
  • .changeset/single-create-schema-change.md is excluded by !.changeset/**
📒 Files selected for processing (15)
  • packages/nextly/src/di/register.ts
  • packages/nextly/src/di/registrations/register-singles.ts
  • packages/nextly/src/dispatcher/handlers/__tests__/single-dispatcher-ddl.test.ts
  • packages/nextly/src/dispatcher/handlers/__tests__/single-dispatcher-shapes.test.ts
  • packages/nextly/src/dispatcher/handlers/collection-dispatcher.ts
  • packages/nextly/src/dispatcher/handlers/component-dispatcher.ts
  • packages/nextly/src/dispatcher/handlers/single-dispatcher.ts
  • packages/nextly/src/dispatcher/helpers/di.ts
  • packages/nextly/src/domains/schema/services/__tests__/apply-migration-statements.test.ts
  • packages/nextly/src/domains/schema/services/apply-migration-statements.ts
  • packages/nextly/src/domains/schema/services/verify-applied-shape.ts
  • packages/nextly/src/domains/singles/__tests__/single-create-schema-change.integration.test.ts
  • packages/nextly/src/domains/singles/services/reconcile-single-companion.ts
  • packages/nextly/src/domains/singles/services/single-metadata-service.ts
  • packages/nextly/src/domains/singles/services/single-registry-service.ts
📝 Walkthrough

Walkthrough

Changes

Single metadata and migration flow

Layer / File(s) Summary
Shared migration and shape verification utilities
packages/nextly/src/domains/schema/services/*
Added sequential migration execution with idempotency-error handling. Added physical table shape verification for missing columns and recognized type mismatches.
Single metadata creation and companion reconciliation
packages/nextly/src/domains/singles/services/*, packages/nextly/src/domains/singles/__tests__/*
Added retry-aware SingleMetadataService and companion reconciliation. Creation records pending, failed, or applied migration states and provisions localized companions. Integration tests cover creation, recovery, retries, cleanup, and schema mismatches.
SingleMetadataService dependency injection
packages/nextly/src/di/*, packages/nextly/src/dispatcher/helpers/di.ts
Added the service to the public service map, singleton registrations, and dispatcher DI accessors.
Dispatcher migration and shape handling
packages/nextly/src/dispatcher/handlers/*.ts
Updated Single, Component, and Collection dispatchers to use shared migration execution. Existing tables now undergo shape verification before an applied status is recorded.
Dispatcher test dependency setup
packages/nextly/src/dispatcher/handlers/__tests__/*
Updated test doubles and DI wiring for SingleMetadataService, migration-status updates, fresh adapters, and applied-status assertions.

Estimated code review effort: 5 (Critical) | ~90 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Dispatcher
  participant SingleMetadataService
  participant SingleRegistryService
  participant Database
  Dispatcher->>SingleMetadataService: createSingle(input)
  SingleMetadataService->>SingleRegistryService: write or adopt pending record
  SingleMetadataService->>Database: apply migration statements
  Database-->>SingleMetadataService: table state
  SingleMetadataService->>SingleRegistryService: record migration status
  SingleMetadataService-->>Dispatcher: return record and status
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 55.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: moving Single table and registry creation into one service.
Description check ✅ Passed The description provides detailed summary, rationale, scope, risks, and extensive verification results, covering the core template requirements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/builder-ddl-services-relocation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 48 minutes.

@github-actions github-actions Bot added scope: core nextly type: docs Documentation only labels Aug 5, 2026
@pkg-pr-new

pkg-pr-new Bot commented Aug 5, 2026

Copy link
Copy Markdown

Open in StackBlitz

@nextlyhq/adapter-drizzle

npm i https://pkg.pr.new/@nextlyhq/adapter-drizzle@95948af

@nextlyhq/adapter-mysql

npm i https://pkg.pr.new/@nextlyhq/adapter-mysql@95948af

@nextlyhq/adapter-postgres

npm i https://pkg.pr.new/@nextlyhq/adapter-postgres@95948af

@nextlyhq/adapter-sqlite

npm i https://pkg.pr.new/@nextlyhq/adapter-sqlite@95948af

@nextlyhq/admin

npm i https://pkg.pr.new/@nextlyhq/admin@95948af

@nextlyhq/admin-css

npm i https://pkg.pr.new/@nextlyhq/admin-css@95948af

@nextlyhq/blocks-engine

npm i https://pkg.pr.new/@nextlyhq/blocks-engine@95948af

@nextlyhq/blocks-react

npm i https://pkg.pr.new/@nextlyhq/blocks-react@95948af

create-nextly-app

npm i https://pkg.pr.new/create-nextly-app@95948af

nextly

npm i https://pkg.pr.new/nextly@95948af

@nextlyhq/plugin-form-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-form-builder@95948af

@nextlyhq/plugin-page-builder

npm i https://pkg.pr.new/@nextlyhq/plugin-page-builder@95948af

@nextlyhq/plugin-sdk

npm i https://pkg.pr.new/@nextlyhq/plugin-sdk@95948af

@nextlyhq/plugin-seo

npm i https://pkg.pr.new/@nextlyhq/plugin-seo@95948af

@nextlyhq/storage-s3

npm i https://pkg.pr.new/@nextlyhq/storage-s3@95948af

@nextlyhq/storage-uploadthing

npm i https://pkg.pr.new/@nextlyhq/storage-uploadthing@95948af

@nextlyhq/storage-vercel-blob

npm i https://pkg.pr.new/@nextlyhq/storage-vercel-blob@95948af

@nextlyhq/ui

npm i https://pkg.pr.new/@nextlyhq/ui@95948af

commit: 95948af

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b33d074730

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Pushed 29601b316 fixing the typecheck failure CI caught.

getService is generic over the container KEY, not the service type, so getService<SingleRegistryService>("singleRegistryService") fails the constraint. The underlying reason it was written that way: SingleMetadataService was missing from ServiceMap, so there was no typed path to resolve it. Both are fixed — the service is in the map now, and the calls drop the explicit argument.

Worth recording that my local check missed it. I was running tsc --noEmit -p tsconfig.json, which excludes **/*.test.ts; CI runs pnpm check-types, which includes them via tsconfig.tests.json. That is exactly the hole task 130 describes, and it caught a real error here.

Re-verified at the new head: pnpm check-types --force 20/20 · lint exit 0 · drizzle v1 gate passed · unit 363 failed / 6869 passed / 7280, set identical to main, zero new, zero disappeared · the new integration suite 6 passed on PostgreSQL.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 26 minutes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 29601b316b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Both Codex P2 findings were real and are fixed in 89de03ae0. Both threads replied to and resolved.

1. A rejected create was leaving a row behind. My regression: intent-first moved generateMigrationSQL — which validates as well as renders — to after the registry write. A refused field set then stranded a pending Single with permissions seeded, and the corrected retry collided with the slug it had just created. Planning now runs before anything is persisted, so the plan may reject and the apply never throws, by construction.

One correction to the report: the throwing branch is gated on options.target, not relationTo. The finding is right, the example key is not — a fixture using relationTo creates the Single successfully, which is why the first version of my test passed against broken code.

2. Tolerating a re-run could mask a table that does not match. CREATE TABLE IF NOT EXISTS no-ops on every dialect, so an orphaned table plus a changed field set recorded applied with columns missing. On PostgreSQL and SQLite that predates this PR; on MySQL the duplicate-index error was accidental protection that the tolerance removed. Reverting is not the fix — that restores a dead end where a correct schema reports failed forever. applied now means the columns are present.

Each fix has an integration test on all three dialects, each proven load-bearing by reverting the fix and confirming that test alone fails.

Re-verified: check-types 0 · lint 0 · drizzle gate passed · unit 363 vs 363, zero new, zero disappeared · the suite is now 10 passed on PostgreSQL and 10 on MySQL.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.


Your included review limit is currently reached under our Fair Usage Limits Policy. This review may still proceed through usage-based billing if eligible. Your next included review will be available in 8 minutes.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 89de03ae07

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/dispatcher/handlers/component-dispatcher.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-metadata-service.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-metadata-service.ts Outdated
Comment thread packages/nextly/src/dispatcher/handlers/single-dispatcher.ts
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Third Codex P2, real, fixed in 6e59147c5. Thread replied to and resolved.

Repairing a localized entity failed on every dialect, not just MySQL. Reproduced before changing anything, by writing the localized twin of the reapply test and running it against unfixed code:

sqlite:     duplicate column name: headline
postgresql: column "headline" of relation "single_..._locales" already exists

When the registry row is gone, the create path can only describe the entity as brand new, so the plan asks to ADD every translatable column to a companion that already has them. reconcileSingleCompanion ran plan.statements with no tolerance at all — unlike the main table, and unlike the archive-index repair a few lines above it in the same function.

All three companion paths now share one tolerant runner, so half of a localized entity's storage cannot be re-appliable while the other half is not.

Re-verified at 6e59147c5: build clean · check-types 0 · lint 0 · drizzle gate passed · unit 363 vs 363, zero new, zero disappeared · the suite is 12 passed on PostgreSQL and 12 on MySQL · full PostgreSQL integration running clean.

Running total for this PR: three Codex findings, all three real, all three fixed with a load-bearing cross-dialect test. Two of them were defects this branch introduced; the third was latent and this branch's own repair test is what exposed it.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6e59147c53

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/domains/singles/services/reconcile-single-companion.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/single-metadata-service.ts Outdated
Comment thread packages/nextly/src/domains/singles/services/reconcile-single-companion.ts Outdated
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

Four more Codex P2s, all real, all fixed in 0b0ba5dea. All four threads replied to and resolved.

They were four faces of one design mistake I made: I added a tolerance and did not strengthen the verification that the tolerance made necessary. Once already exists is swallowed, CREATE TABLE IF NOT EXISTS no-oping over a mismatched table becomes silent, and every path that relied on the loud failure needs to check the shape instead.

So rather than four patches, there is now one shared shapeMismatches used by all three paths — single create, field-group create, and the ALTER path. The desired shape comes from buildDesiredTableFromFields / ...FromComponentFields, the same builders the schema diff compares against, which is what makes it:

  • include the system lifecycle columns, which come from create options rather than the field list (finding 2)
  • compare types through the diff engine's own normalizeType rather than column names (finding 3)
  • carry provenance, since a field group's creator reads a text width from a different key than a collection's

Fixing the field-group path also exposed a gap in my own verifier, caught by that dispatcher's unit suite: introspection can THROW, not merely return nothing, and an unreadable catalog would then fail a migration that had succeeded. It now reports nothing when it cannot run — which its docblock already claimed and the code did not do.

Three new cross-dialect tests, each proven load-bearing by disabling the check.

Running total: seven Codex findings on this PR, all seven real.

Verified at 0b0ba5dea: build clean · check-types 0 · lint 0 · drizzle gate passed · unit 363 vs 363 · the create suite 16 passed on PostgreSQL and 16 on MySQL · full PostgreSQL integration 1279 passed, 0 failed · full MySQL integration 2 failed, both builder-create-converges against a shared database, which fails identically on main.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b0ba5dead

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/domains/schema/services/verify-applied-shape.ts Outdated
Comment thread packages/nextly/src/dispatcher/handlers/component-dispatcher.ts Outdated
Comment thread packages/nextly/src/dispatcher/handlers/collection-dispatcher.ts Outdated
@mobeenabdullah
mobeenabdullah force-pushed the feat/builder-ddl-services-relocation branch from 0b0ba5d to 74c8820 Compare August 5, 2026 03:20
…ata service

The service is registered in DI, and the registration module is imported at
boot by anything touching the container. Importing the schema generators, the
runtime-schema builder and the companion reconcile at the top of the file put
that whole graph in front of every consumer, including processes that never
create a Single. di/register.ts already avoids this for the same three modules.
getService is generic over the container KEY, not the service type, so passing
the type failed the typecheck that includes test files. The service was also
missing from ServiceMap, which is what forced the explicit argument in the
first place: without an entry there a caller has no typed way to resolve it.
…le matches

Two ways a create could record a state the database does not have.

The DDL generator validates as well as renders — a required relationship
declaring onDelete set null is refused there — so generating after the registry
row was written let that rejection strand a pending Single with its permissions
seeded, and the corrected retry then collided with the slug it had just created.
Planning now happens before anything is persisted.

CREATE TABLE IF NOT EXISTS is a no-op against a table that already exists, and
the index statements after it are tolerated as already applied, so re-running
over an orphaned table with a changed field set reported success while the
columns were missing. Applied now means the columns are there, not that
something of that name is.
…e it

Repairing a localized entity whose registry row was lost reaches the reconcile
as a brand-new localized entity, because that is all the registry can say. The
plan then asks to ADD every translatable column to a companion that already has
them, and those statements carry no IF NOT EXISTS on any dialect, so the repair
failed on all three rather than only on MySQL.

All three companion paths now run their statements through the shared tolerant
runner, so half of a localized entity's storage cannot be re-appliable while the
other half is not. The matcher still refuses a duplicate row.
Tolerating a re-run removed the loud failure that made a MISMATCHED table
obvious. CREATE TABLE IF NOT EXISTS no-ops on every dialect and the index
statements after it are now tolerated, so a repair over a table an earlier
attempt left behind emitted no error even when the field set, the Draft
Published option or a field type had changed.

One shared check answers it for all three paths, building the desired shape
with the same builder the schema diff compares against, so the system lifecycle
columns are included, translatable columns are excluded, and types are compared
through the diff engine's own normalizer. A check that cannot run reports
nothing, because failing a migration for want of a verification would be worse
than the unverified behaviour it replaces.
Writing the intent before touching the database is only worth doing if
something can finish it. The row owns the slug the moment it is written, so a
create interrupted before it could record its outcome was refused as a
duplicate for ever, leaving no way forward short of editing the registry by
hand. That is worse than the orphan table this ordering prevents, because an
orphan at least left the slug free.

A row that is not applied describes an operation that never reported success,
so a create naming the same slug adopts it and re-runs the idempotent DDL. The
row is re-stated from the new request, because a corrected retry usually
differs from the attempt that failed.
A column the Builder now calls optional while the database still has it NOT
NULL accepts every write the Builder considers valid and then fails the
constraint, at write time and far from the migration that caused it. The
desired spec already carried the flag; only the comparison was missing.
The verifier walked only the desired side, so anything the live table still
carries that the schema no longer declares was accepted. A removed required
field leaves a NOT NULL column that rejects writes omitting it, and a dropped
unique leaves a constraint that rejects writes the Builder now allows.

A failed create also stopped claiming success. The shape verification makes
failed a routine outcome, and the response told the admin to run migrations,
which cannot repair a table whose columns do not match.
Adoption treated any non-applied row as resumable, but pending is equally the
state of a create running right now. Two overlapping requests for one slug
would then have the second overwrite the first's row while its DDL was still
building the first schema, after which the original confirms applied against a
description that is no longer its own.

A failed row is a finished attempt, so taking it over is safe. Serialising the
pending case is the migration lock this relocation exists to unblock, and a
second timestamp-based exclusion here would leave two of them.
The verifier compared types and index names against the diff engine's ideal
schema, but the builder's own generators do not render exactly that, so it
failed creates that work: a float number renders decimal while the descriptor
says float8, a unique field becomes an inline constraint the database names
itself, and the field-group generator emits no created_at index the spec
declares for every component.

Presence and nullability were measured against every fixture on all three
dialects and produce no such false positives, so the check keeps those and
drops the rest until the generators and the descriptor agree.

The companion also stops tolerating a re-run. A half-finished localization
enable reaches that code in the same state as an orphan repair, and the planner
cannot tell them apart, so tolerating it would report success while
default-locale content is still on the main table. A localized repair now fails
loudly instead.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

https://github.com/nextlyhq/nextly/blob/58149acb90166335e018fe8494db2b2967223f4c/packages/nextly/src/domains/schema/services/verify-applied-shape.ts#L182-L187
P2 Badge Align required FK nullability before verifying

For a fresh Single or Component create that contains a required single-target relationship/upload, the direct DDL still emits that column as NOT NULL, while field-column-descriptor reports fkSingle columns as nullable because requiredness is enforced in application code. This new nullability comparison therefore records an otherwise-created table as failed on the normal create path; align the DDL with the descriptor or skip this comparison for those FK columns before using it as the migration outcome gate.

AGENTS.md reference: packages/nextly/AGENTS.md:L30-L32

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/nextly/src/dispatcher/handlers/single-dispatcher.ts
Comment thread packages/nextly/src/domains/schema/services/verify-applied-shape.ts
Comment thread packages/nextly/src/dispatcher/handlers/single-dispatcher.ts
Adoption read the failed status and then wrote, so two retries could both see
it and both run DDL against one slug. updateSingle also writes migration_status
only when the field hash or status flag changes, so the commonest retry of all
— the same payload again — left the row saying failed for the whole time its
DDL was running.

The claim now puts the status in the WHERE clause and marks the row with a
token it reads back. Counting returned rows would not do: MySQL has no
RETURNING and the adapter re-reads with the same predicate, which the claim has
just falsified, so a successful claim reports zero rows there.
@mobeenabdullah
mobeenabdullah force-pushed the feat/builder-ddl-services-relocation branch from 58149ac to 95948af Compare August 5, 2026 05:16
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

95948afb3 — three findings, two fixed as one, one rebutted with a test.

The retry race was narrowed last round, not closed. updateSingle writes migration_status only when the field hash or status flag changes, so a retry with the same payload — the commonest one there is — left the row saying failed for the whole time its DDL ran. And the read-then-write was never atomic anyway.

Now an actual compare-and-swap: the status is in the WHERE clause, so only one caller can match while the row still says failed.

🔴 My first implementation of that CAS was wrong on MySQL, and the test caught it. I counted rows via returning: "*", which passed on PostgreSQL. MySQL has no RETURNING — the adapter emulates it by re-reading with the same predicate, which the claim has just falsified, so a successful claim reported zero rows on the one dialect where it worked. The claim now marks the row with a token and reads it back, which names the winner everywhere with no driver-specific plumbing.

The MySQL title finding no longer reproduces. It depends on the type comparison, which was removed in the commit after the one it was raised on. Measured rather than argued: added a test that creates a Single and then adds a field through updateSingleSchema, asserting applied on every dialect. Kept regardless, so reintroducing type comparison cannot silently break a normal schema update.

Verified at 95948afb3, rebased onto ace13ca0b with a freshly measured baseline: build clean · check-types 0 · lint 0 · drizzle gate passed · unit zero new failures · create suite 26 passed on PostgreSQL, 26 on MySQL.

24 findings on this PR, 24 real. Zero threads open.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@greptileai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. More of your lovely PRs please.

Reviewed commit: 95948afb3a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mobeenabdullah
mobeenabdullah merged commit ccaa140 into main Aug 5, 2026
10 checks passed
@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

cb21c9032the post-apply shape verification is removed entirely. This reverses a decision made two rounds ago, and the reason is that the evidence I gave for it was incomplete.

Codex found a fifth way it fails a working operation: a create with a required upload. Proven, not argued — on the normal create path, no repair involved:

[Singles] Table "single_..._up" does not match this schema: hero is NOT NULL, expected nullable

The descriptor reports every fkSingle column as nullable on purpose, because requiredness is enforced in application code, while the DDL emits NOT NULL.

The full tally:

the check broke the check caught
float creates · unique creates · every field-group create · required relationship/upload creates · localization enables nothing outside the repair scenarios written to exercise it

I narrowed it twice. Each time the false positives moved to whatever comparison remained. That is structural, not a run of oversights: it compares against the diff engine's ideal schema, and the Builder's generators do not render that. Those divergences are real and already tracked as their own work — a post-apply gate is not where they should be discovered.

🔴 My evidence for keeping it was wrong. I said presence and nullability were "measured safe across every fixture on all three dialects". True of my fixtures — which had no required relationship, no required upload, and no localization enable. The decision to keep those rested on that, so correcting it is mine.

The localization case was the worst: marking the migration failed skipped the companion transition while localized: true was already persisted, pointing reads at an unseeded companion with the content still on main. A verification that strands data is worse than no verification.

Every path it broke is now a regression test asserting it stays applied.

The stale-pending finding is answered rather than fixed, on its thread: adopting pending is exactly the race fixed two rounds ago. Intent-first ships without its recovery half, and B1-9b completes it. Stated plainly rather than closed with a heuristic.

Verified at cb21c9032: build clean · check-types 0 · lint 0 · drizzle gate passed · unit zero new failures · create suite 22 passed on PostgreSQL, 22 on MySQL.

28 findings, 28 real. Zero threads open.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ccaa140893

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

d6d5fe64cthe write-ordering change is dropped too. Founder decision, and it is the right one.

Writing the registry row before the DDL is only better once something can finish an interrupted attempt. Without that, the half-written row owns the slug and refuses every retry — a worse state than the orphan table it prevents, because an orphan at least leaves the slug free. Codex pressed this twice and was right both times; the answer was to stop shipping half a mechanism, not to guess at the other half.

So the row is written last with the outcome already known, exactly as the handler did. The planning step stays first, so a request the generator refuses still leaves nothing behind at all — that part was a genuine fix and it survives.

Removed with it: the adoption branch, the atomic claim, the relaxed owner check, and their three tests. Nothing is left in the codebase that no longer has a caller.

What this PR is now, precisely

  • The Single create moved out of the request handler into a DI-registered service — −356 lines from the handler. This is the whole point: a lock cannot wrap two separate steps.
  • reconcileSingleCompanion extracted, proven a byte-identical move.
  • One shared statement runner replacing two private copies, fixing the MySQL retry dead end where a half-applied migration reported failed for ever.
  • 445 lines of integration tests proving a create actually creates a table, on all three dialects. There were none before.
  • An honest failed-create response.

It claims nothing it cannot prove. Every behaviour change beyond the relocation was either measured or removed.

Verified at d6d5fe64c: build clean · check-types 0 · lint 0 · drizzle gate passed · unit zero new failures · create suite 16 passed on PostgreSQL, 16 on MySQL · dispatcher suites back to the main baseline exactly.

28 findings, 28 real, zero threads open.

@codex please review this PR

@mobeenabdullah

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. Hooray!

Reviewed commit: ccaa140893

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

scope: core nextly type: docs Documentation only

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant