Skip to content

improvement(tables): empty-state filter/sort builders + upsert conflict-column selection#5123

Open
TheodoreSpeaks wants to merge 6 commits into
stagingfrom
feat/table-v2-block
Open

improvement(tables): empty-state filter/sort builders + upsert conflict-column selection#5123
TheodoreSpeaks wants to merge 6 commits into
stagingfrom
feat/table-v2-block

Conversation

@TheodoreSpeaks

@TheodoreSpeaks TheodoreSpeaks commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Filter/sort builders now start empty with a "+ Add condition" affordance instead of a phantom blank row; removing the last row clears it
  • Filter converter skips blank-column rows so an empty builder can't serialize to a { '': … } predicate
  • Upsert Row gains an optional Conflict Column field to pick which unique column to match on
  • Upsert still auto-resolves when the table has a single unique column; when there are multiple and none is specified it throws an explicit error (clearer wording) instead of silently guessing — backend matching logic is otherwise unchanged from staging

Backwards compatibility

  • No behavior change for existing upserts: the Conflict Column field is optional and the backend logic (single unique → auto, multiple-without-target → throw) is identical to staging. Only the error message wording changed.

Type of Change

  • Improvement

Testing

Tested manually; lint, api-validation:strict, and tsc --noEmit pass; table row tests pass (20/20)

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

TheodoreSpeaks and others added 3 commits June 12, 2026 10:45
…me/drop prompt

`drizzle-kit push --force` only suppresses the data-loss confirm, not the
rename-vs-drop disambiguation prompt. That prompt fires whenever a diff both
adds and drops tables/columns at once (e.g. migration 0231 created
sim_trigger_state while dropping the workspace_notification_* tables), and in
CI it crashes with a bare "Interactive prompts require a TTY" stack trace.

Catch that specific failure in the dev push step and emit a GitHub error
annotation explaining the cause and the fix (drop the stale objects on the dev
DB to match schema.ts — the same DROPs the versioned migration already applied
to staging/prod), instead of leaving an opaque trace. Exit status is preserved
either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

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

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped Jun 18, 2026 9:06pm

Request Review

@cursor

cursor Bot commented Jun 17, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Upsert and table-detail responses change what clients see (conflict column, live maxRows), but existing upserts without a conflict column behave the same; maxRows correction could surprise UIs that relied on the stored cap.

Overview
Filter and sort builders no longer inject a default blank rule. When there are no rules, editable blocks show dashed Add filter condition / Add sort buttons; removing the last rule clears the list instead of resetting a phantom row. The filter rule→API converter skips rows with no column, so an empty or half-filled builder cannot emit a { '': … } predicate.

Upsert Row gains an optional Conflict Column (basic column-selector over unique columns, advanced manual id), wired through the block and table_upsert_row tool as conflictTarget. Backend upsert behavior is unchanged (single unique column still auto-resolves); only the multi-unique error wording is clearer.

Supporting changes: new column-selector sub-block type and table.columns selector provider (unique columns from cached table detail via getTableDetailQueryOptions); tableId in selector context from sub-blocks. GET table detail returns maxRows from the workspace’s current plan via getWorkspaceTableLimits, not the stale value stored on the table. Get schema tool/block outputs add stable column ids, columnCount, rowCount, and maxRows.

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

Comment thread apps/sim/lib/table/query-builder/converters.ts
@greptile-apps

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves the table block UX and backend correctness across three areas: empty-state filter/sort builders now render a dashed "+ Add condition" button instead of a phantom blank row, the filter converter skips blank-column rows to prevent { '': ... } predicates, and upsert gains an optional Conflict Column field to let users pick which unique column to match on when a table has more than one.

  • Filter/sort builders: start empty, removing the last rule now clears the builder to the empty state; filterRulesToFilter guards against blank-column rows serializing as { '': ... } predicates.
  • Upsert Conflict Column: introduces a column-selector sub-block type backed by a new table.columns selector that fetches only unique columns; the conflictTarget field is threaded through the block transformer, tool body builder, and service layer; GET /api/table/[tableId] now sources maxRows from the live billing plan instead of the stale value stored at table creation.
  • get_schema tool: enriched response now includes columnCount, rowCount, and maxRows, and each column is guaranteed to carry a stable id via getColumnId.

Confidence Score: 5/5

Safe to merge — all three feature areas are additive, backward-compatible, and the backend upsert logic is unchanged from staging.

The conflict-column field is optional and the service-layer branching (auto-resolve on single unique, throw on multiple without target) is identical to the prior behaviour. The empty-state builder changes are purely UI and the blank-column skip in filterRulesToFilter is a safe guard with correct null propagation. The live-plan maxRows lookup is intentional and well-scoped.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/blocks/blocks/table.ts Adds conflictColumn param, conflictColumnSelector/manualConflictColumn sub-blocks, passes conflictTarget through paramTransformers, and expands get_schema outputs to include columnCount/maxRows/rowCount.
apps/sim/hooks/selectors/providers/sim/selectors.ts Adds table.columns selector; fetchList restricts to unique columns, fetchById resolves any column by ID (correct for display of stale references), workspaceId sourced from Zustand store getState().
apps/sim/lib/table/query-builder/converters.ts Adds blank-column skip in filterRulesToFilter; correct handling of empty-state and all-blank rule arrays.
apps/sim/tools/table/upsert_row.ts Adds optional conflictTarget param (user-only visibility) and conditionally includes it in the request body.
apps/sim/app/api/table/[tableId]/route.ts GET now calls getWorkspaceTableLimits for a live maxRows value instead of reading the stale stored value; adds one async call per GET but is intentional for correctness.
apps/sim/hooks/queries/tables.ts Extracts getTableDetailQueryOptions so non-component callers can ensureQueryData the same cache entry; staleTime matches useTable (30s).

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant UI as Block UI
    participant Selector as table.columns selector
    participant API as GET /api/table/[tableId]
    participant Tool as upsert_row tool
    participant Service as rows/service.ts

    UI->>Selector: "fetchList({ context.tableId })"
    Selector->>API: ensureQueryData(tableDetailQueryOptions)
    API-->>Selector: table schema (live maxRows from billing)
    Selector-->>UI: unique columns only

    UI->>UI: User selects conflict column stored as conflictColumn

    UI->>Tool: "execute({ data, conflictTarget })"
    Tool->>Service: "POST /api/table/[id]/rows/upsert { data, conflictTarget }"
    Service->>Service: find column by id OR name in uniqueColumns
    alt conflictTarget provided
        Service->>Service: validate it is a unique column
    else single unique column
        Service->>Service: auto-resolve targetColumnKey
    else multiple unique, no target
        Service-->>UI: Error: specify a conflict column
    end
    Service-->>UI: "{ row, operation: insert|update }"
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant UI as Block UI
    participant Selector as table.columns selector
    participant API as GET /api/table/[tableId]
    participant Tool as upsert_row tool
    participant Service as rows/service.ts

    UI->>Selector: "fetchList({ context.tableId })"
    Selector->>API: ensureQueryData(tableDetailQueryOptions)
    API-->>Selector: table schema (live maxRows from billing)
    Selector-->>UI: unique columns only

    UI->>UI: User selects conflict column stored as conflictColumn

    UI->>Tool: "execute({ data, conflictTarget })"
    Tool->>Service: "POST /api/table/[id]/rows/upsert { data, conflictTarget }"
    Service->>Service: find column by id OR name in uniqueColumns
    alt conflictTarget provided
        Service->>Service: validate it is a unique column
    else single unique column
        Service->>Service: auto-resolve targetColumnKey
    else multiple unique, no target
        Service-->>UI: Error: specify a conflict column
    end
    Service-->>UI: "{ row, operation: insert|update }"
Loading

Reviews (3): Last reviewed commit: "improvement(tables): unique-column picke..." | Re-trigger Greptile

Comment thread apps/sim/lib/table/rows/service.ts Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

for (const rule of rules) {
// Skip incomplete rows (no column selected) so a blank builder row never
// serializes to a `{ '': ... }` predicate.
if (!rule.column) continue

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Skipped rows break OR grouping

Medium Severity

When filterRulesToFilter skips a rule with no column, it ignores that rule’s logicalOperator. An incomplete row marked or between two valid conditions no longer starts a new OR group, so those conditions can be AND-combined instead of OR-combined at execution time.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 4d057c6. Configure here.

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

Cursor Bugbot has reviewed your changes and found 1 potential issue.

There are 2 total unresolved issues (including 1 from previous review).

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit bfaa366. Configure here.

metadata: table.metadata ?? null,
rowCount: table.rowCount,
maxRows: table.maxRows,
maxRows: maxRowsPerTable,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Live maxRows mismatches insert cap

Medium Severity

The table detail GET now returns maxRows from the workspace’s current billing plan, but row inserts still enforce the max_rows value stored on the table record (DB trigger). After a plan upgrade, API and get_schema can report a higher cap while inserts keep failing at the old limit, so capacity checks based on the response are wrong.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit bfaa366. Configure here.

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

Sign up for free to 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