Skip to content

Commit 965fb6b

Browse files
kerryluclaude
andcommitted
fix(tables): emit isVirtual in the table list and stop over-blocking locked tables
The `/api/table` list handler maps each table to an explicit field whitelist and never emitted `isVirtual`, so every list-surface gate evaluated `undefined`: `canMutateTable`/`canRenameTable` left Delete, Rename, Import CSV and Move enabled on the Memory row, and the trigger-mode filter in the table selector matched nothing. The existing list test mocked a Memory table without the flag and asserted everything but it, which is why the gap shipped. Two grid gates were also over-broad on persisted tables. The `json` escape in the update-lock check is dead for virtual tables (they early-return above it) and only changed behavior for persisted update-locked ones, contradicting the comment directly above. Routing `onOpenConfig` through the blocked-add-column handler made a schema-locked table's column config unopenable and showed "add column" copy for something that was not an add — opening a column config is metadata, not schema. Finally, a Memory transcript that alone exceeds the page byte budget no longer fails the whole query. This mirrors `fetchRowsBounded` on the persisted path, which always yields at least one row for a bounded page and reserves the throw for unbounded queries where a short page would be silent truncation. `queryMemoryTableRows` is always bounded, so the guard was unreachable by design; without this, one long conversation made the table unreadable with no way to page past it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c31aaf1 commit 965fb6b

5 files changed

Lines changed: 35 additions & 25 deletions

File tree

apps/sim/app/api/table/route.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,7 @@ describe('GET /api/table folder placement', () => {
176176
{
177177
...CREATED_TABLE,
178178
id: 'system_memory_workspace-1',
179+
isVirtual: true,
179180
name: 'Memory',
180181
workspaceId: 'workspace-1',
181182
createdBy: 'user-1',
@@ -197,6 +198,10 @@ describe('GET /api/table folder placement', () => {
197198
expect(json.data.tables).toContainEqual(
198199
expect.objectContaining({
199200
id: 'system_memory_workspace-1',
201+
// The list surfaces (tables page context menu, trigger-mode table
202+
// selector) gate on this flag, so dropping it silently re-enables
203+
// delete/rename/import/move on a read-only table.
204+
isVirtual: true,
200205
name: 'Memory',
201206
workspaceId: 'workspace-1',
202207
locks: {

apps/sim/app/api/table/route.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,7 @@ export const GET = withRouteHandler(async (request: NextRequest) => {
212212
const schemaData = t.schema as TableSchema
213213
return {
214214
id: t.id,
215+
isVirtual: t.isVirtual,
215216
name: t.name,
216217
description: t.description,
217218
schema: {

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2287,7 +2287,7 @@ export function TableGrid({
22872287
// that silently refuses to save. Only for users who could otherwise edit:
22882288
// without write access the lock isn't why they can't, and they still get
22892289
// the read-only expanded viewer below.
2290-
if (canEditRef.current && updateLockedRef.current && column?.type !== 'json') {
2290+
if (canEditRef.current && updateLockedRef.current) {
22912291
onBlockedActionRef.current('edit-cell')
22922292
return
22932293
}
@@ -4225,9 +4225,7 @@ export function TableGrid({
42254225
workflows={workflows}
42264226
workflowGroups={tableWorkflowGroups}
42274227
sourceInfo={columnSourceInfo.get(column.key)}
4228-
onOpenConfig={
4229-
canMutateSchema ? handleConfigureColumn : handleBlockedAddColumn
4230-
}
4228+
onOpenConfig={handleConfigureColumn}
42314229
onViewWorkflow={handleViewWorkflow}
42324230
isPinned={colIsPinned}
42334231
onPinToggle={userPermissions.canEdit ? handlePinToggle : undefined}

apps/sim/lib/virtual-tables/memory-virtual-table.server.test.ts

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -333,22 +333,28 @@ describe('Memory virtual table', () => {
333333
expect(result.hasMore).toBe(true)
334334
})
335335

336-
it('rejects one transcript that cannot fit in a bounded query response', async () => {
337-
queueTableRows(schemaMock.memory, [
338-
{
339-
id: 'memory-1',
340-
key: 'conversation-1',
341-
createdAt: CREATED_AT,
342-
updatedAt: UPDATED_AT,
343-
data: [{ role: 'user', content: 'oversized' }],
344-
messageCount: 1,
345-
rowBytes: 5 * 1024 * 1024 + 1,
346-
},
347-
])
336+
it('still returns one transcript that alone exceeds the query byte budget', async () => {
337+
const oversized = {
338+
id: 'memory-1',
339+
key: 'conversation-1',
340+
createdAt: CREATED_AT,
341+
updatedAt: UPDATED_AT,
342+
data: [{ role: 'user', content: 'oversized' }],
343+
messageCount: 1,
344+
rowBytes: 5 * 1024 * 1024 + 1,
345+
}
346+
queueTableRows(schemaMock.memory, [oversized])
347+
queueTableRows(schemaMock.memory, [{ id: oversized.id, data: oversized.data }])
348348

349-
await expect(
350-
queryMemoryTableRows({ workspaceId: 'workspace-1', limit: 1000, includeTotal: false })
351-
).rejects.toThrow('exceeds the 5MB query response limit')
349+
const result = await queryMemoryTableRows({
350+
workspaceId: 'workspace-1',
351+
limit: 1000,
352+
includeTotal: false,
353+
})
354+
355+
expect(result.rows).toHaveLength(1)
356+
expect(result.rows[0]?.id).toBe(oversized.id)
357+
expect(result.hasMore).toBe(false)
352358
})
353359

354360
it('finds matching cells in one storage query and preserves filtered sort ordinals', async () => {

apps/sim/lib/virtual-tables/memory-virtual-table.server.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -210,12 +210,12 @@ export async function queryMemoryTableRows({
210210
if (!Number.isFinite(rowBytes) || rowBytes < 0) {
211211
throw new TableQueryValidationError('Memory table returned an invalid row size')
212212
}
213-
if (selectedCandidates.length === 0 && rowBytes > pageByteBudget) {
214-
throw new TableQueryValidationError(
215-
`Memory transcript exceeds the ${Math.floor(pageByteBudget / (1024 * 1024))}MB query response limit`,
216-
'TABLE_QUERY_RESULT_TOO_LARGE'
217-
)
218-
}
213+
// Matches `fetchRowsBounded` on the persisted path: a bounded page always
214+
// yields at least one row, even one that alone exceeds the budget. Memory
215+
// transcripts have no write-time size cap (unlike persisted rows, which are
216+
// held to MAX_ROW_SIZE_BYTES), so refusing an over-budget first row would
217+
// make one long conversation render the whole table unreadable with no way
218+
// to page past it.
219219
if (selectedCandidates.length > 0 && selectedBytes + rowBytes > pageByteBudget) {
220220
hasMore = true
221221
break

0 commit comments

Comments
 (0)