Skip to content

Commit 31e746d

Browse files
fix(tables): allow unbounded v1 row queries
1 parent 41923b8 commit 31e746d

8 files changed

Lines changed: 87 additions & 24 deletions

File tree

apps/docs/content/docs/en/integrations/table.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ Tables are created from the **Tables** section in the sidebar. Each table requir
5454

5555
## Usage Instructions
5656

57-
Create and manage custom data tables. Store, query, and manipulate structured data within workflows.
57+
Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB.
5858

5959

6060

@@ -213,7 +213,7 @@ Query rows from a table with filtering, sorting, and pagination
213213
| `tableId` | string | Yes | Table ID |
214214
| `filter` | object | No | Filter conditions \(MongoDB-style operators: $eq, $ne, $gt, $gte, $lt, $lte, $in, $nin, $contains, $ncontains, $startsWith, $endsWith, $empty\) |
215215
| `sort` | object | No | Sort order as \{field: "asc"\|"desc"\} |
216-
| `limit` | number | No | Maximum rows to return \(default: $\{TABLE_LIMITS.DEFAULT_QUERY_LIMIT\}, max: $\{TABLE_LIMITS.MAX_QUERY_LIMIT\}\) |
216+
| `limit` | number | No | Maximum rows to return. Omit to return every matching row; the query fails if the result exceeds the 5MB response budget. |
217217
| `offset` | number | No | Number of rows to skip \(default: 0\) |
218218

219219
#### Output
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
import { describe, expect, it, vi } from 'vitest'
5+
6+
vi.mock('@/triggers', () => ({
7+
getTrigger: vi.fn(() => ({ subBlocks: [] })),
8+
}))
9+
10+
import { TableBlock } from '@/blocks/blocks/table'
11+
12+
function params(input: Record<string, unknown>): Record<string, unknown> {
13+
return TableBlock.tools.config?.params?.(input as never) as Record<string, unknown>
14+
}
15+
16+
describe('table query_rows transformer', () => {
17+
it('keeps an omitted limit unbounded', () => {
18+
expect(params({ operation: 'query_rows', tableId: 'table-1' }).limit).toBeUndefined()
19+
})
20+
21+
it('parses and validates an explicit limit', () => {
22+
expect(params({ operation: 'query_rows', tableId: 'table-1', limit: '25' }).limit).toBe(25)
23+
expect(params({ operation: 'query_rows', tableId: 'table-1', limit: '1000000' }).limit).toBe(
24+
1000000
25+
)
26+
expect(() => params({ operation: 'query_rows', tableId: 'table-1', limit: 'abc' })).toThrow(
27+
/Invalid number for Limit/
28+
)
29+
})
30+
})

apps/sim/blocks/blocks/table.ts

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { toError } from '@sim/utils/errors'
33
import { TABLE_LIMITS } from '@/lib/table/constants'
44
import { filterRulesToFilter, sortRulesToSort } from '@/lib/table/query-builder/converters'
55
import type { BlockConfig } from '@/blocks/types'
6+
import { parseOptionalNumberInput } from '@/blocks/utils'
67
import type { TableQueryResponse } from '@/tools/table/types'
78
import { getTrigger } from '@/triggers'
89

@@ -113,7 +114,11 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
113114
tableId: params.tableId,
114115
filter,
115116
data: parseJSON(params.data, 'Row Data'),
116-
limit: params.limit ? Number.parseInt(params.limit) : undefined,
117+
limit: parseOptionalNumberInput(params.limit, 'Limit', {
118+
integer: true,
119+
min: 1,
120+
max: TABLE_LIMITS.MAX_BULK_OPERATION_SIZE,
121+
}),
117122
}
118123
},
119124

@@ -136,7 +141,11 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
136141
return {
137142
tableId: params.tableId,
138143
filter,
139-
limit: params.limit ? Number.parseInt(params.limit) : undefined,
144+
limit: parseOptionalNumberInput(params.limit, 'Limit', {
145+
integer: true,
146+
min: 1,
147+
max: TABLE_LIMITS.MAX_BULK_OPERATION_SIZE,
148+
}),
140149
}
141150
},
142151

@@ -171,8 +180,11 @@ const paramTransformers: Record<string, (params: TableBlockParams) => ParsedPara
171180
tableId: params.tableId,
172181
filter,
173182
sort,
174-
limit: params.limit ? Number.parseInt(params.limit) : 100,
175-
offset: params.offset ? Number.parseInt(params.offset) : 0,
183+
limit: parseOptionalNumberInput(params.limit, 'Limit', {
184+
integer: true,
185+
min: 1,
186+
}),
187+
offset: parseOptionalNumberInput(params.offset, 'Offset', { integer: true, min: 0 }) ?? 0,
176188
}
177189
},
178190
}
@@ -197,7 +209,7 @@ export const TableBlock: BlockConfig<TableQueryResponse> = {
197209
name: 'Table',
198210
description: 'User-defined data tables',
199211
longDescription:
200-
'Create and manage custom data tables. Store, query, and manipulate structured data within workflows.',
212+
'Create and manage custom data tables. Store, query, and manipulate structured data within workflows. Query Rows returns every matching row when Limit is omitted and fails if the result exceeds 5MB.',
201213
docsLink: 'https://docs.sim.ai/integrations/table',
202214
category: 'blocks',
203215
bgColor: '#10B981',
@@ -652,7 +664,7 @@ Return ONLY the sort JSON:`,
652664
id: 'limit',
653665
title: 'Limit',
654666
type: 'short-input',
655-
placeholder: '100',
667+
placeholder: 'Leave empty for all rows (fails over 5MB)',
656668
condition: {
657669
field: 'operation',
658670
value: ['query_rows', 'update_rows_by_filter', 'delete_rows_by_filter'],
@@ -726,7 +738,11 @@ Return ONLY the sort JSON:`,
726738
description: 'Visual filter builder conditions for bulk operations',
727739
},
728740
filter: { type: 'json', description: 'Filter criteria for query/update/delete operations' },
729-
limit: { type: 'number', description: 'Query or bulk operation limit' },
741+
limit: {
742+
type: 'number',
743+
description:
744+
'Optional query row limit; omit to return every matching row (fails over 5MB). Also caps bulk update/delete operations.',
745+
},
730746
builderMode: {
731747
type: 'string',
732748
description: 'Input mode for filter and sort (builder or json)',

apps/sim/lib/api/contracts/tables.test.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,20 @@ describe('tableRowsQuerySchema includeTotal', () => {
4343
})
4444
})
4545

46+
describe('tableRowsQuerySchema limit', () => {
47+
it('leaves an omitted or empty limit unbounded', () => {
48+
expect(tableRowsQuerySchema.parse({ workspaceId: 'ws-1' }).limit).toBeUndefined()
49+
expect(tableRowsQuerySchema.parse({ workspaceId: 'ws-1', limit: '' }).limit).toBeUndefined()
50+
})
51+
52+
it('still parses and validates an explicit limit', () => {
53+
expect(tableRowsQuerySchema.parse({ workspaceId: 'ws-1', limit: '25' }).limit).toBe(25)
54+
expect(tableRowsQuerySchema.parse({ workspaceId: 'ws-1', limit: '1000000' }).limit).toBe(
55+
1000000
56+
)
57+
})
58+
})
59+
4660
describe('tableEventStreamQuerySchema', () => {
4761
it('parses an explicit cursor', () => {
4862
expect(tableEventStreamQuerySchema.parse({ from: '7' })).toEqual({ from: 7 })

apps/sim/lib/api/contracts/tables.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -820,11 +820,21 @@ export const tableRowsQueryBaseSchema = z.object({
820820
.default(true),
821821
})
822822

823-
export const tableRowsQuerySchema = tableRowsQueryBaseSchema.refine(
824-
(data) => !(data.after && data.sort),
825-
{ message: 'after cursor cannot be combined with sort — cursors paginate the default order' }
823+
const unboundedTableRowsLimitSchema = z.preprocess(
824+
(value) => (value === null || value === undefined || value === '' ? undefined : Number(value)),
825+
z
826+
.number({ error: 'Limit must be a number' })
827+
.int('Limit must be an integer')
828+
.min(1, 'Limit must be at least 1')
829+
.optional()
826830
)
827831

832+
export const tableRowsQuerySchema = tableRowsQueryBaseSchema
833+
.extend({ limit: unboundedTableRowsLimitSchema })
834+
.refine((data) => !(data.after && data.sort), {
835+
message: 'after cursor cannot be combined with sort — cursors paginate the default order',
836+
})
837+
828838
export const updateRowsByFilterBodySchema = z.object({
829839
workspaceId: workspaceIdSchema,
830840
filter: bulkFilterSchema,
@@ -1063,14 +1073,7 @@ export const rowQueryBodySchema = z.object({
10631073
// Omitted limit returns the ENTIRE matching result, failing fast (400) when
10641074
// it exceeds the response byte budget. An explicit limit caps the page row
10651075
// count; the byte budget may still end a page early with nextCursor set.
1066-
limit: z.preprocess(
1067-
(value) => (value === null || value === undefined || value === '' ? undefined : Number(value)),
1068-
z
1069-
.number({ error: 'Limit must be a number' })
1070-
.int('Limit must be an integer')
1071-
.min(1, 'Limit must be at least 1')
1072-
.optional()
1073-
),
1076+
limit: unboundedTableRowsLimitSchema,
10741077
cursor: z.string().min(1, 'cursor must be a non-empty token').optional(),
10751078
})
10761079

apps/sim/lib/table/llm/enrichment.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ export function enrichTableToolParameters(
161161
if (enrichedProperties.limit && toolId === 'table_query_rows') {
162162
enrichedProperties.limit = {
163163
...enrichedProperties.limit,
164-
description: `Maximum rows to return (min: 1, max: 1000, default: 100). For ranking queries: use limit=1 for highest/lowest, limit=2 for second highest, etc.`,
164+
description: `Maximum rows to return (min: 1). Omit to return every matching row; the query fails if the result exceeds 5MB. For ranking queries: use limit=1 for highest/lowest, limit=2 for second highest, etc.`,
165165
}
166166
}
167167

apps/sim/tools/generated/tool-metadata.ts

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

apps/sim/tools/table/query_rows.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { TABLE_LIMITS } from '@/lib/table/constants'
21
import { enrichTableToolSchema } from '@/tools/schema-enrichers'
32
import type { TableQueryResponse, TableRowQueryParams } from '@/tools/table/types'
43
import type { ToolConfig } from '@/tools/types'
@@ -38,7 +37,8 @@ export const tableQueryRowsTool: ToolConfig<TableRowQueryParams, TableQueryRespo
3837
limit: {
3938
type: 'number',
4039
required: false,
41-
description: `Maximum rows to return (default: ${TABLE_LIMITS.DEFAULT_QUERY_LIMIT}, max: ${TABLE_LIMITS.MAX_QUERY_LIMIT})`,
40+
description:
41+
'Maximum rows to return. Omit to return every matching row; the query fails if the result exceeds the 5MB response budget.',
4242
visibility: 'user-or-llm',
4343
},
4444
offset: {

0 commit comments

Comments
 (0)