Skip to content

refactor(tables): make lib/table/orchestration the single implementation - #6134

Merged
TheodoreSpeaks merged 10 commits into
improvement/v2-endpointsfrom
improvement/orchestration-migration
Aug 1, 2026
Merged

refactor(tables): make lib/table/orchestration the single implementation#6134
TheodoreSpeaks merged 10 commits into
improvement/v2-endpointsfrom
improvement/orchestration-migration

Conversation

@TheodoreSpeaks

@TheodoreSpeaksTheodoreSpeaks commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Stacked on #5273base is improvement/v2-endpoints, not staging.

The repo's convention is that business logic lives in lib/[resource]/orchestration so the Sim UI, the public API, and the copilot tools share one implementation. lib/table/orchestration exported exactly one function (performRestoreTable); everything else was implemented per-caller. Column update existed four times — the UI route, v1, v2, and the copilot table tool — each calling the same column services but owning its own guards, error mapping, and audit.

The copies had drifted, and the drift was the bug. This PR extracts the logic and deletes the copies; the fixes fall out of the extraction rather than being patched at each call site.

performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own the logic, and all ten call sites reduce to auth → parse → call → render. Net ~570 lines deleted from routes.

Behavior this consolidates

Each of these was previously true on only some paths:

  • The typeChanging guard.updateColumnType early-returns on an unchanged type and drops any options sent with it, so restating the current type alongside new options silently discarded them. v2 had no guard at all — and since its contract shares v1's body schema, it acceptedoptions and ignored them, returning 200.
  • The select-unique guard. Each write is its own locked transaction, so a rename or type change paired with a constraint write that is going to fail commits first and then throws, leaving a half-applied schema change. Missing from v2.
  • Stable select-option ids. Cells reference the option id, so an edit that re-sends an option by name must reuse it or every cell holding it is orphaned. Only the copilot path did this. normalizeSelectOptionsInput moves to lib/table/select-options and now covers every caller — it preserves a supplied id, so it is a no-op for the fully-formed options the HTTP contracts accept and a repair for name-only options.
  • required forwarded into the type and options writes, so a conversion validates against the constraint the same request is setting rather than the column's current one.
  • An audit on every successful update. The UI route and the copilot tool emitted none — column edits from the Sim UI were silently unaudited.
  • Single-row delete through the row service. v2 did a raw db.delete on user_table_rows, skipping assertRowDelete and deleteOrderedRow: a delete-locked table returned 200 and the row-count/order bookkeeping never ran. v1 carries a comment warning against exactly this.
  • The delete actor handed to deleteTable, which audits only when a row was actually archived — omitting the actor is how rollback callers opt out. v1 and v2 omitted it and audited themselves outside that check, emitting TABLE_DELETED for a no-op delete of an already-archived table.

Error contract

Failure classes come back as OrchestrationErrorCode. The shared contract moves out of lib/workflows/orchestration/types into lib/core/orchestration/types — resource-neutral code (lib/folders) already had to import it from a workflow path — and gains a locked class mapping to 423, since both tables and workflows have a lock that forbids a mutation and every caller was translating that itself.

v2 renders these through a new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1/UI surfaces, so a given failure maps to the same status on every surface.

Review guide

The guard logic is in lib/table/orchestration/columns.ts — read that once instead of four route diffs. The route diffs are mechanical deletions.

Tests follow the same split: lib/table/orchestration/columns.test.ts (12 cases) and tables.test.ts cover the guards, classification, and audit; the route tests are now thin, asserting delegation and the failure-class → envelope mapping.

Not in scope

performCreateTable (where the copilot path has no explicit permission check of its own and the UI has folder validation the others lack) and the knowledge resource (where the copilot path emits no audit for any KB mutation, defaults minSize: 1 against the API's 100, and KB search has ~1200 duplicated lines) follow in the next PR on this branch.

One user-visible bug found but left for the connector consolidation: the copilot's delete_connector reports "Associated documents have been removed", but reaches the route by internal HTTP self-call without the deleteDocuments param, which defaults to false — the documents are kept.

Verification

app/api/v2 + app/api/v1/tables + app/api/table + lib/table + lib/copilot1838 pass.

bun run check:api-validation, bun run check:openapi (6 specs, 57 operations, 48 contracts), bun run type-check, and full bun run lint:check all clean.

🤖 Generated with Claude Code

@vercel

vercelBot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
ProjectDeploymentActionsUpdated (UTC)
docsSkippedSkippedAug 1, 2026 12:49am

Request Review

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursor

cursorBot commented Jul 31, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Broad refactor across UI, v1, v2, and copilot table APIs with intentional behavior fixes (locks, audits, column guards); regression risk is mitigated by orchestration tests but many HTTP paths changed.

Overview
Moves table column updates, deletes, renames, folder moves, and lock changes into lib/table/orchestration, so the Sim UI routes, v1/v2 APIs, copilot tools, and VFS mutations all call the same functions instead of four divergent column-update implementations (~570 route lines removed).

performUpdateTableColumn owns the guards that had drifted: unchanged-type + options routing, pre-write validation (currency, rename collision, workflow-output constraints, select-as-unique), folding renames into the last transactional write, and normalizeSelectOptionsInput for stable select option ids. Successful updates now always recordAudit.

performDeleteTable / performDeleteTableRow centralize archive/delete (audit and PostHog only when a row was actually archived), and v2 row delete no longer uses a raw db.delete that skipped delete locks and row bookkeeping.

Error handling introduces shared OrchestrationError and lib/core/orchestration/types (moved out of workflows). Routes replace message-substring status mapping with orchestrationErrorResponse / statusForOrchestrationError and v2 v2ErrorForOrchestration / v2CaughtOrchestrationError. Table/column/import services throw classified errors instead of bare Error where failures are caller-fixable.

Reviewed by Cursor Bugbot for commit d449610. Bugbot is set up for automated code reviews on this repo. Configure here.

@greptile-apps

greptile-appsBot commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

The PR consolidates table mutation behavior into shared orchestration functions.

  • Routes now delegate column updates, table deletion, and row deletion to shared implementations.
  • Column writes combine renames, typed changes, and constraints within one transaction while normalizing select-option IDs.
  • HTTP callers forward request context so successful mutation audits retain client provenance.
  • Shared orchestration errors provide consistent status and response-envelope mapping across UI, v1, and v2 APIs.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains; the previously reported audit-provenance and partial-column-update paths are addressed at the current head.

Important Files Changed

FilenameOverview
apps/sim/lib/table/orchestration/columns.tsCentralizes column-update guards, option normalization, transactional write selection, error classification, and auditing.
apps/sim/lib/table/columns/service.tsApplies pending renames and requested constraints inside the same locked transaction as type, options, or currency updates.
apps/sim/lib/table/orchestration/tables.tsCentralizes table and row deletion behavior while forwarding actor and request context to successful audits.
apps/sim/lib/table/select-options.tsNormalizes name-only select options while preserving supplied or existing stable option IDs.
apps/sim/lib/core/orchestration/types.tsDefines resource-neutral orchestration errors, status mapping, and optional HTTP request context.
apps/sim/app/api/v2/lib/response.tsAdds consistent v2 response-envelope rendering for orchestration failure classes.
apps/sim/app/api/v2/tables/[tableId]/columns/route.tsReduces the v2 column endpoint to authorization, parsing, orchestration delegation, and response rendering.
apps/sim/app/api/v2/tables/[tableId]/rows/[rowId]/route.tsDelegates single-row deletion to shared orchestration instead of bypassing row-lock and ordering services.

Sequence Diagram

sequenceDiagram
participant Caller as UI / v1 / v2 / Copilot
participant Route as Route or Tool Handler
participant Orch as Table Orchestration
participant Service as Table Service
participant DB as Database
participant Audit as Audit Log
Caller->>Route: Authenticated table mutation
Route->>Orch: Parsed input + actor + optional request
Orch->>Orch: Normalize and preflight guards
Orch->>Service: Single coordinated mutation
Service->>DB: Locked transaction
DB-->>Service: Updated table
Service-->>Orch: Mutation result
Orch->>Audit: Record successful mutation
Orch-->>Route: Typed outcome
Route-->>Caller: Surface-specific response
Loading

Reviews (5): Last reviewed commit: "refactor(tables): classify failures by t..." | Re-trigger Greptile

Comment threadapps/sim/app/api/v2/tables/[tableId]/route.ts Outdated
Comment threadapps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
Comment threadapps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
Comment threadapps/sim/app/api/v2/tables/[tableId]/columns/route.ts Outdated
TheodoreSpeaksand others added 2 commits July 31, 2026 14:47
…rkflows
OrchestrationErrorCode and statusForOrchestrationError are the contract every
lib/[resource]/orchestration module returns against, but they lived inside the
workflows module, so resource-neutral code (lib/folders) already had to import
from a workflow path. Moved to lib/core/orchestration/types.
Adds a 'locked' class mapping to 423. Both tables and workflows have a lock
that forbids a mutation, and each caller was translating that to a status
itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Column update was implemented four times — the UI route, v1, v2, and the
copilot table tool — each calling the same column services but owning its own
guards, error mapping, and audit. The copies had drifted, and the drift was the
bug: v2 was missing both guards, only the copilot copy minted stable option
ids, and only v1/v2 audited.
performUpdateTableColumn, performDeleteTable, and performDeleteTableRow now own
that logic; all ten call sites reduce to auth, parse, call, render. The guards
are asserted once in lib/table/orchestration rather than four times against
four routes.
Behavior this consolidates, previously true on only some paths:
- The typeChanging guard. updateColumnType early-returns on an unchanged type
and drops any options sent with it, so restating the current type alongside
new options silently discarded them. v2 had no guard at all and, since its
contract shares v1's body schema, accepted options and ignored them.
- The select-unique guard. Each write is its own locked transaction, so a
rename or type change paired with a constraint write that is going to fail
commits first and then throws, half-applying the schema change.
- Stable select-option ids. Cells reference the option id, so an edit that
re-sends an option by name has to reuse it or every cell holding it is
orphaned. Only the copilot path did this; normalizeSelectOptionsInput moves to
lib/table/select-options and now covers every caller. It preserves a supplied
id, so it is a no-op for the fully-formed options the HTTP contracts accept.
- required forwarded into the type and options writes, so a conversion
validates against the constraint the same request is setting.
- An audit on every successful update. The UI route and the copilot tool
emitted none.
- Single-row delete through the row service. v2 did a raw db.delete, skipping
assertRowDelete and deleteOrderedRow, so a delete-locked table returned 200
and the row-count bookkeeping never ran.
- The delete actor handed to deleteTable, which audits only when a row was
actually archived. v1 and v2 omitted it and audited themselves outside that
check, emitting TABLE_DELETED for a no-op delete of an archived table.
Failure classes come back as OrchestrationErrorCode; v2 renders them through a
new v2ErrorForOrchestration, mirroring statusForOrchestrationError on the v1
and UI surfaces, so a given failure maps to the same status everywhere.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@TheodoreSpeaks
TheodoreSpeaksforce-pushed the improvement/orchestration-migration branch from 569f89f to 9736288CompareJuly 31, 2026 21:48
@TheodoreSpeaksTheodoreSpeaks changed the title fix(tables): correctness gaps found auditing the orchestration conventionrefactor(tables): make lib/table/orchestration the single implementationJul 31, 2026
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 9736288. Configure here.

TheodoreSpeaksand others added 4 commits July 31, 2026 16:02
The base rewrote column update while this branch was extracting it: writes now
address the column by stable id, a rename and constraint change ride inside the
typed write's transaction instead of following it, and `currencyCode` joins the
update shape for the new `currency` column type.
performUpdateTableColumn is rebuilt from that implementation rather than the
one this branch originally extracted, so the extraction carries the base's
logic forward instead of regressing it. All four callers delegate unchanged.
lib/table/select-options.ts arrived independently on the base as a client-safe
leaf module for option-id resolution; normalizeSelectOptionsInput joins it there
rather than replacing it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The base's route tests assert which column service each payload reaches — the
behavior that now lives in performUpdateTableColumn. They mocked the `@/lib/table`
barrel; the orchestration module imports the service directly, so they mock that
too and keep asserting the same thing through the extracted implementation.
The orchestration tests move onto the base's semantics: writes address the
stable column id, a rename rides inside the write it accompanies rather than
running first, and the currency guards replace the non-select options guard the
service now owns.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment threadapps/sim/lib/table/orchestration/tables.ts
`lib/table/service.ts` wrote its own audit rows, so whether an operation was
audited depended on which function a caller reached for rather than on a user
having performed it. That is what let v1 and v2 audit a no-op delete, and what
made `deleteTable`'s optional `actingUserId` double as an audit opt-out flag.
Worse, most sites fell back to `actingUserId ?? createdBy`, so an unattributed
call was logged against the table's *creator*. The copilot `mv` path passed no
actor at all: renaming someone else's table recorded them as the renamer.
Audit now lives in the orchestration functions — performDeleteTable,
performRenameTable, performMoveTableToFolder, performUpdateTableLocks — and
the services just write. Internal callers (folder cascade, import rollback)
keep calling the service and are silent by construction rather than by
remembering to omit an argument.
Two services now return what the audit needs: `deleteTable` reports whether it
actually archived a row, so a repeat delete logs nothing; `updateTableLocks`
returns the before/after locks, since only the locked write can observe the
transition its description names.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment threadapps/sim/lib/table/orchestration/tables.ts
Comment threadapps/sim/lib/table/orchestration/tables.ts
Comment threadapps/sim/lib/copilot/tools/server/table/user-table.ts
Comment threadapps/sim/lib/table/orchestration/columns.ts
…ation
Moving the audits into the orchestration functions dropped three things the
routes had been carrying, and added one the orchestration now owns twice.
- The v1 and v2 column-update routes passed `request` to `recordAudit`, so
their audit rows recorded the caller's IP and user-agent. The orchestration
function had no way to receive it. Every table orchestration function now
takes an optional `OrchestrationRequestContext` and every HTTP route
forwards it; the copilot and VFS callers, which have no request, omit it.
- `classifyTableMutation` matched `TableConflictError` on "already exists"
appearing in the message and reported it as `validation`, turning the UI
route's 409 on a duplicate table rename into a 400. It now matches the type,
the way `performRestoreTable` already did.
- `captureServerEvent` ran on every delete while the audit was gated on a row
actually being archived, so a repeat delete of an archived table still
reported `table_deleted`. Both now hang off the same evidence.
- The copilot delete path kept its own `captureServerEvent` from when the
service did not emit one, double-counting every copilot table delete.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

Comment threadapps/sim/lib/copilot/tools/server/table/user-table.ts Outdated
A copilot `update_column` payload whose only content was the column's current
type used to return success with the live schema, while the v1, v2, and UI
routes rejected the same payload with "No updates specified". Delegating to
`performUpdateTableColumn` unified them onto the routes' rejection — correct,
but the message tells the caller its request was empty when it named a type.
The orchestration function now reports the same thing `updateColumnType` reports
when it loses this race concurrently: the column is already that type, re-issue
without the type change. An empty payload still reads "No updates specified".
Drops the copilot's `outcome.table ?? tableForUpdate` fallback with it — the
comment described the no-op that can no longer reach that line, and a success
always carries a table.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 0ee4499. Configure here.

The table module decided HTTP statuses by searching error messages for
phrases. `VALIDATION_MESSAGE_FRAGMENTS` and `ROW_WRITE_ERROR_PATTERNS` held 32
substrings between them, and fifteen more lists were inlined in routes — 83
matchers over 17 files, each its own copy of the guesswork and already drifted
apart. It made message wording load-bearing: `TableRowLimitError`'s own doc
comment noted that its text had to contain "row limit" for a route to answer
400, and adding "already exists" to a rename message silently demoted a 409 to
a 400 (the bug fixed one commit ago, by adding another special case).
Services now throw `OrchestrationError`, which carries the transport-neutral
`OrchestrationErrorCode` the layers above already speak. Classification is one
`instanceof` in `orchestrationErrorResponse` (UI + v1) and
`v2CaughtOrchestrationError` (v2). Every pattern list is gone. Wording is free
to change; an unclassified error still becomes a generic 500, which is what an
unexpected fault should be.
`asOrchestrationError` walks the `cause` chain rather than testing the caught
value directly: drizzle wraps a throw raised inside a transaction callback in a
`DrizzleQueryError` whose own message is the failed SQL, so a bare `instanceof`
would drop every failure raised inside `withLockedTable`. That is the same
reason `rootErrorMessage` had to dig for a root cause before.
Three throws stay bare `Error` deliberately — `Table ID mismatch`, `Workspace
ID mismatch`, and `Failed to build upsert conflict predicate` are internal
invariants no consumer classified, and they keep falling through to a 500.
`Insufficient capacity` was in the pattern list with no producer anywhere in
the codebase.
Status changes, all deliberate:
- `'forbidden'` joins the code union so the table-row-limit ceiling keeps its
403; without it this refactor would have flattened it to 400.
- import-async's table-limit rejection: 400 -> 403, matching the two other
create routes it had drifted from.
- Renaming a table to an invalid name: 500 -> 400. `validateTableName`
messages don't contain "Invalid", so no matcher ever caught them.
- Restoring a table that isn't archived, or into an archived workspace:
500 -> 400.
- A duplicate *column* name stays `validation`/400 rather than becoming a 409
like a duplicate table name. Both v1 and the orchestration have always
answered 400 for it; changing a published status is not this refactor's job.
The twelve tests that changed were asserting the substring mechanism itself,
constructing plain `Error`s with magic strings. They now assert the real
contract, plus new cases pinning that identical wording carrying no
classification stays internal and keeps its message off the wire.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YGzbVDZpe2dEALbu2BUU8a
@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@greptile

@TheodoreSpeaks

Copy link
Copy Markdown
CollaboratorAuthor

@cursor review

@cursorcursorBot 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.

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit d449610. Configure here.

@TheodoreSpeaks
TheodoreSpeaks merged commit 5fea5f7 into improvement/v2-endpointsAug 1, 2026
5 checks passed
@TheodoreSpeaks
TheodoreSpeaks deleted the improvement/orchestration-migration branch August 1, 2026 01:24
TheodoreSpeaks added a commit that referenced this pull request Aug 1, 2026
#6150 branched before #6134, so skill-lifecycle.ts imports
@/lib/workflows/orchestration/types — the module #6134 moved to
@/lib/core/orchestration/types. Git merged a file deletion on one side with a
new file referencing it on the other: no textual conflict, broken build.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@TheodoreSpeaks