fix(tables): enforce row limits against the current plan, not a frozen per-table cap#5120
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub. |
PR SummaryMedium Risk Overview Every write path is wired to the new check: single/batch insert, upsert (insert branch), replace, table creation with starter rows, sync append import (pre-check via plan limit), async import runner (per batch), and import append/replace helpers. Plan resolution runs outside transactions; tx-bound insert helpers no longer enforce caps internally. Table-creation and CSV import routes stop passing Import and API UX fixes: streaming sync CSV import passes a running UI: the row-number gutter width is based on Reviewed by Cursor Bugbot for commit c2a1b30. Bugbot is set up for automated code reviews on this repo. Configure here. |
|
@greptile review |
Greptile SummaryThis PR replaces the per-table frozen
Confidence Score: 4/5Safe to merge with one fix: a The apps/sim/app/api/table/route.ts — the catch block needs a Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[Row write request] --> B{Path}
B -->|insertRow / batchInsertRows / replaceTableRows| C[assertRowCapacity before TX opens]
B -->|upsertRow| D[getMaxRowsPerTable before TX opens]
B -->|Sync CSV import - new table| E[batchInsertRows with running currentRowCount]
B -->|Sync CSV import - existing table| F[importAppendRows or importReplaceRows pre-TX assertRowCapacity]
B -->|Async CSV import| G[per-batch assertRowCapacity in streaming loop]
C --> H{wouldExceedRowLimit?}
D --> I{wouldExceedRowLimit inside TX}
E --> H
F --> H
G --> H
H -->|No| J[Execute insert in TX trigger maintains row_count]
H -->|Yes| K[throw TableRowLimitError]
I -->|Yes| K
K --> L[rowWriteErrorResponse or message.includes row limit match in catch block]
L --> M[400 with plan limit message + Upgrade toast on client]
subgraph Cache
N[getWorkspaceTableLimits]
O[limitsCache Map 30s TTL max 5000 entries expiry sweep + LRU eviction]
N --> O
end
H --> N
I --> N
%%{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[Row write request] --> B{Path}
B -->|insertRow / batchInsertRows / replaceTableRows| C[assertRowCapacity before TX opens]
B -->|upsertRow| D[getMaxRowsPerTable before TX opens]
B -->|Sync CSV import - new table| E[batchInsertRows with running currentRowCount]
B -->|Sync CSV import - existing table| F[importAppendRows or importReplaceRows pre-TX assertRowCapacity]
B -->|Async CSV import| G[per-batch assertRowCapacity in streaming loop]
C --> H{wouldExceedRowLimit?}
D --> I{wouldExceedRowLimit inside TX}
E --> H
F --> H
G --> H
H -->|No| J[Execute insert in TX trigger maintains row_count]
H -->|Yes| K[throw TableRowLimitError]
I -->|Yes| K
K --> L[rowWriteErrorResponse or message.includes row limit match in catch block]
L --> M[400 with plan limit message + Upgrade toast on client]
subgraph Cache
N[getWorkspaceTableLimits]
O[limitsCache Map 30s TTL max 5000 entries expiry sweep + LRU eviction]
N --> O
end
H --> N
I --> N
|
Greptile SummaryThis PR replaces the frozen per-table
Confidence Score: 4/5Safe to merge. The enforcement shift from a frozen DB trigger to a live application check is well-scoped, consistently applied across all insert paths, and the migration is backward compatible. The core enforcement redesign is sound and consistently applied across all insert paths. Two small gaps: the cache bound is softer than its comment implies, and the gutter-width calculation can shift mid-session as row counts cross digit boundaries. apps/sim/lib/table/billing.ts (cache eviction comment vs. actual behaviour) and apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/utils.ts (dynamic gutter sizing). Important Files Changed
Sequence Diagram%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
participant Caller
participant assertRowCapacity
participant limitsCache
participant BillingDB
participant InsertTx
Caller->>assertRowCapacity: workspaceId, currentRowCount, addedRows
assertRowCapacity->>limitsCache: get(workspaceId)
alt cache hit (within 30s TTL)
limitsCache-->>assertRowCapacity: cached limits
else cache miss / expired
assertRowCapacity->>BillingDB: getWorkspaceBilledAccountUserId + getHighestPrioritySubscription
BillingDB-->>assertRowCapacity: plan limits
assertRowCapacity->>limitsCache: "set(workspaceId, limits, TTL=30s)"
end
assertRowCapacity->>assertRowCapacity: wouldExceedRowLimit(limit, currentRowCount, addedRows)
alt exceeds limit
assertRowCapacity-->>Caller: throw TableRowLimitError
else within limit
assertRowCapacity-->>Caller: void
Caller->>InsertTx: open transaction and insert rows
InsertTx->>InsertTx: trigger increments row_count (no cap check)
InsertTx-->>Caller: inserted rows
end
%%{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 Caller
participant assertRowCapacity
participant limitsCache
participant BillingDB
participant InsertTx
Caller->>assertRowCapacity: workspaceId, currentRowCount, addedRows
assertRowCapacity->>limitsCache: get(workspaceId)
alt cache hit (within 30s TTL)
limitsCache-->>assertRowCapacity: cached limits
else cache miss / expired
assertRowCapacity->>BillingDB: getWorkspaceBilledAccountUserId + getHighestPrioritySubscription
BillingDB-->>assertRowCapacity: plan limits
assertRowCapacity->>limitsCache: "set(workspaceId, limits, TTL=30s)"
end
assertRowCapacity->>assertRowCapacity: wouldExceedRowLimit(limit, currentRowCount, addedRows)
alt exceeds limit
assertRowCapacity-->>Caller: throw TableRowLimitError
else within limit
assertRowCapacity-->>Caller: void
Caller->>InsertTx: open transaction and insert rows
InsertTx->>InsertTx: trigger increments row_count (no cap check)
InsertTx-->>Caller: inserted rows
end
Reviews (2): Last reviewed commit: "fix(tables): enforce row limits against ..." | Re-trigger Greptile |
…lan, harden limits cache bound
|
Addressed the review feedback in 498bd20:
360 tests pass; tsc, lint, and |
|
@greptile review |
|
Round 2 (ce6d431):
266 table/copilot tests pass; tsc, lint clean. |
|
@greptile review |
|
@greptile review |
|
@greptile review |
|
@greptile review |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit c8f4149. Configure here.
| workspaceId, | ||
| currentRowCount: existingRowCount + inserted, | ||
| addedRows: coerced.length, | ||
| }) |
There was a problem hiding this comment.
Replace import deletes then caps
High Severity
Async CSV replace deletes all existing rows before streaming inserts, but the new plan row-limit check runs per batch in flush only after that delete. When the file (or an early batch) exceeds the workspace limit, the job fails after the table is already empty or only partially refilled, unlike the sync replace path that validates total size before writing.
Reviewed by Cursor Bugbot for commit c8f4149. Configure here.
There was a problem hiding this comment.
unrelated to this pr.
|
@greptile review |


Summary
Type of Change
Testing
Checklist