Skip to content

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

Merged
TheodoreSpeaks merged 9 commits into
stagingfrom
feat/table-v2-block
Jun 18, 2026
Merged

improvement(block): table empty-state filter/sort builders + upsert conflict-column selection#5123
TheodoreSpeaks merged 9 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:34pm

Request Review

@cursor

cursor Bot commented Jun 17, 2026

Copy link
Copy Markdown

PR Summary

Medium Risk
Upsert and table API limit behavior affect data writes and enforcement; changes are mostly UX and optional conflict targeting with unchanged matching rules when a column is omitted.

Overview
Filter and sort builders no longer inject a default blank row. They start empty with dashed Add filter condition / Add sort buttons; deleting the last rule clears the list instead of resetting a phantom row. filterRulesToFilter skips rows with no column (while still honoring OR boundaries) so empty UI state cannot emit { '': … } filters.

Upsert Row adds optional Conflict Column (basic column-selector over unique columns, advanced manual id), wired through the block and table_upsert_row as conflictTarget. Selector plumbing adds table.columns, column-selector sub-block type, tableId in selector context, and shared getTableDetailQueryOptions. Multi–unique-column upserts without a target still fail; the service error message is clearer only.

Table GET returns maxRows from getWorkspaceTableLimits (current plan) instead of the stale value stored on the table record. Get schema tool/block outputs gain stable column **id**s via getColumnId, plus columnCount, rowCount, and maxRows.

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

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

greptile-apps Bot commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR improves the table block in three areas: filter/sort builders now render an empty "+ Add condition" affordance instead of a phantom blank row; the filter converter skips blank-column rules to prevent { '': … } predicates from being serialized; and upsert gains an optional Conflict Column selector so users can pick which unique column to match on without knowing the internal API parameter name.

  • Filter/sort UX: Empty state replaced phantom blank row; removing the last rule now clears the builder rather than resetting to a blank row. The filterRulesToFilter converter correctly skips rules with no column while still respecting OR boundaries.
  • Upsert Conflict Column: New column-selector subblock fetches only unique columns for the selected table via the new table.columns selector definition. The backend error message is updated to surface friendly field names instead of the internal conflictTarget param name.
  • Get Schema enrichment: The tool response now includes columnCount, rowCount, and a live maxRows sourced from the workspace's current plan rather than the stale value stored on the table record.

Confidence Score: 5/5

Safe to merge; all three feature areas are backwards-compatible and the logic changes are well-scoped.

The filter/sort empty-state refactor only affects how the initial store value is rendered — existing serialized rule arrays are handled identically. The converter's blank-column skip is additive and guarded by the existing empty-array fast-path. The upsert conflict-target wiring is fully optional, the backend resolution logic is unchanged, and the error wording is the only behavioral diff. The live-plan maxRows fetch replaces a stale stored value without touching any write paths.

apps/sim/hooks/selectors/providers/sim/selectors.ts — the fetchById path does not filter by col.unique, leaving it mildly inconsistent with fetchList.

Important Files Changed

Filename Overview
apps/sim/lib/table/query-builder/converters.ts OR boundary is honored before blank-row skip; all-blank and empty-array paths both return null correctly.
apps/sim/hooks/selectors/providers/sim/selectors.ts New table.columns selector fetches unique columns via ensureQueryData; fetchById does not filter by col.unique, but that is a cosmetic mismatch since the backend enforces uniqueness at upsert time.
apps/sim/blocks/blocks/table.ts conflictColumnSelector correctly uses canonicalParamId + selectorKey; dependsOn: ['tableSelector'] wires the canonical tableId into the selector context through the canonical index; manualConflictColumn mirrors the pattern used by other advanced-mode inputs.
apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/filter-builder/filter-builder.tsx Empty-state button calls addRule, rules array is correctly seeded from store; all hooks are called before early returns, preserving React rules-of-hooks compliance.
apps/sim/app/api/table/[tableId]/route.ts maxRows is now resolved from the workspace's live plan (getWorkspaceTableLimits) instead of the stale value stored on the table record.
apps/sim/tools/table/get_schema.ts Adds columnCount, rowCount, maxRows to the output and ensures every column has a stable id via getColumnId; consistent with the route-level change.
apps/sim/hooks/queries/tables.ts getTableDetailQueryOptions correctly shares the same query key as useTable so ensureQueryData reuses cached data.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[FilterBuilder / SortBuilder renders] --> B{rules.length > 0?}
    B -- No --> C{isReadOnly?}
    C -- Yes --> D[return null]
    C -- No --> E[Show Add condition button]
    E -->|click| F[addRule adds new rule]
    F --> B

    B -- Yes --> G[Render rule rows]
    G -->|remove last rule| H[removeRule sets rules to empty]
    H --> B

    I[upsert_row block] --> J{conflictTarget provided?}
    J -- Yes --> K{Is it a unique column?}
    K -- Yes --> L[Use as conflict target]
    K -- No --> M[Throw: not a unique column]
    J -- No --> N{uniqueColumns.length == 1?}
    N -- Yes --> O[Auto-resolve to that column]
    N -- No --> P[Throw: specify conflict column]

    Q[table.columns selector] -->|fetchList| R[GET table detail]
    R --> S[Filter col.unique === true]
    S --> T[Return unique cols as options]
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"}}}%%
flowchart TD
    A[FilterBuilder / SortBuilder renders] --> B{rules.length > 0?}
    B -- No --> C{isReadOnly?}
    C -- Yes --> D[return null]
    C -- No --> E[Show Add condition button]
    E -->|click| F[addRule adds new rule]
    F --> B

    B -- Yes --> G[Render rule rows]
    G -->|remove last rule| H[removeRule sets rules to empty]
    H --> B

    I[upsert_row block] --> J{conflictTarget provided?}
    J -- Yes --> K{Is it a unique column?}
    K -- Yes --> L[Use as conflict target]
    K -- No --> M[Throw: not a unique column]
    J -- No --> N{uniqueColumns.length == 1?}
    N -- Yes --> O[Auto-resolve to that column]
    N -- No --> P[Throw: specify conflict column]

    Q[table.columns selector] -->|fetchList| R[GET table detail]
    R --> S[Filter col.unique === true]
    S --> T[Return unique cols as options]
Loading

Reviews (5): Last reviewed commit: "fix(tables): source workspaceId for colu..." | Re-trigger Greptile

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

Copy link
Copy Markdown
Collaborator Author

@greptile review

Comment thread apps/sim/lib/table/query-builder/converters.ts Outdated
Comment thread apps/sim/app/api/table/[tableId]/route.ts
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@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 033d416. Configure here.

Comment thread apps/sim/hooks/selectors/providers/sim/selectors.ts Outdated
@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

@greptile review

@TheodoreSpeaks

Copy link
Copy Markdown
Collaborator Author

Re: Bugbot's "Live maxRows mismatches insert cap" — this is a false positive after merging staging. Insert enforcement is no longer the frozen table.max_rows trigger: migration 0241_drop_table_row_cap_guard.sql (from #5120, now in the base) drops the row cap from the trigger, and inserts enforce assertRowCapacity against the workspace's current plan (getMaxRowsPerTable). The GET route now reports that same live plan limit, so reported and enforced caps are consistent. Bugbot flags it because it only sees this PR's diff (the route change) and not the enforcement change, which is now in the staging base.

@TheodoreSpeaks TheodoreSpeaks changed the title improvement(tables): empty-state filter/sort builders + upsert conflict-column selection improvement(block): table empty-state filter/sort builders + upsert conflict-column selection Jun 18, 2026
@TheodoreSpeaks TheodoreSpeaks merged commit 63fdc47 into staging Jun 18, 2026
17 checks passed
@TheodoreSpeaks TheodoreSpeaks deleted the feat/table-v2-block branch June 18, 2026 22:15
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