Skip to content

Commit 93a733b

Browse files
refactor(tables): centralize row read/order/paginate/position-writes in table-wrapper
1 parent c786ada commit 93a733b

6 files changed

Lines changed: 609 additions & 424 deletions

File tree

apps/sim/app/api/table/[tableId]/rows/route.ts

Lines changed: 17 additions & 105 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { db } from '@sim/db'
2-
import { tableRowExecutions, userTableRows } from '@sim/db/schema'
31
import { createLogger } from '@sim/logger'
42
import { toError } from '@sim/utils/errors'
5-
import { and, eq, inArray, sql } from 'drizzle-orm'
63
import { type NextRequest, NextResponse } from 'next/server'
74
import {
85
type BatchInsertTableRowsBodyInput,
@@ -17,27 +14,20 @@ import { isZodError, validationErrorResponse } from '@/lib/api/server/validation
1714
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
1815
import { generateRequestId } from '@/lib/core/utils/request'
1916
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
20-
import type {
21-
Filter,
22-
RowData,
23-
RowExecutionMetadata,
24-
RowExecutions,
25-
Sort,
26-
TableSchema,
27-
} from '@/lib/table'
17+
import type { Filter, RowData, Sort, TableSchema } from '@/lib/table'
2818
import {
2919
batchInsertRows,
3020
batchUpdateRows,
3121
deleteRowsByFilter,
3222
deleteRowsByIds,
3323
insertRow,
34-
USER_TABLE_ROWS_SQL_NAME,
3524
updateRowsByFilter,
3625
validateBatchRows,
3726
validateRowData,
3827
validateRowSize,
3928
} from '@/lib/table'
40-
import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql'
29+
import { TableQueryValidationError } from '@/lib/table/sql'
30+
import { queryRows } from '@/lib/table/table-wrapper'
4131
import { accessError, checkAccess } from '@/app/api/table/utils'
4232

4333
const logger = createLogger('TableRowsAPI')
@@ -268,113 +258,35 @@ export const GET = withRouteHandler(
268258
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
269259
}
270260

271-
const baseConditions = [
272-
eq(userTableRows.tableId, tableId),
273-
eq(userTableRows.workspaceId, validated.workspaceId),
274-
]
275-
276-
const schema = table.schema as TableSchema
277-
278-
if (validated.filter) {
279-
const filterClause = buildFilterClause(
280-
validated.filter as Filter,
281-
USER_TABLE_ROWS_SQL_NAME,
282-
schema.columns
283-
)
284-
if (filterClause) {
285-
baseConditions.push(filterClause)
286-
}
287-
}
288-
289-
let query = db
290-
.select({
291-
id: userTableRows.id,
292-
data: userTableRows.data,
293-
position: userTableRows.position,
294-
createdAt: userTableRows.createdAt,
295-
updatedAt: userTableRows.updatedAt,
296-
})
297-
.from(userTableRows)
298-
.where(and(...baseConditions))
299-
300-
if (validated.sort) {
301-
const sortClause = buildSortClause(validated.sort, USER_TABLE_ROWS_SQL_NAME, schema.columns)
302-
if (sortClause) {
303-
query = query.orderBy(sortClause) as typeof query
304-
} else {
305-
query = query.orderBy(userTableRows.position) as typeof query
306-
}
307-
} else {
308-
query = query.orderBy(userTableRows.position) as typeof query
309-
}
310-
311-
let totalCount: number | null = null
312-
if (validated.includeTotal) {
313-
const [{ count }] = await db
314-
.select({ count: sql<number>`count(*)` })
315-
.from(userTableRows)
316-
.where(and(...baseConditions))
317-
totalCount = Number(count)
318-
}
319-
320-
const rows = await query.limit(validated.limit).offset(validated.offset)
321-
322-
// Sidecar: fetch per-(row, group) execution state and group into a map
323-
// so the response preserves the legacy `row.executions[groupId]` wire
324-
// shape. One indexed-IN scan against table_row_executions.
325-
const executionsByRow = new Map<string, RowExecutions>()
326-
if (rows.length > 0) {
327-
const execRows = await db
328-
.select()
329-
.from(tableRowExecutions)
330-
.where(
331-
inArray(
332-
tableRowExecutions.rowId,
333-
rows.map((r) => r.id)
334-
)
335-
)
336-
for (const e of execRows) {
337-
const existing = executionsByRow.get(e.rowId) ?? {}
338-
const meta: RowExecutionMetadata = {
339-
status: e.status as RowExecutionMetadata['status'],
340-
executionId: e.executionId ?? null,
341-
jobId: e.jobId ?? null,
342-
workflowId: e.workflowId,
343-
error: e.error ?? null,
344-
...(e.runningBlockIds && e.runningBlockIds.length > 0
345-
? { runningBlockIds: e.runningBlockIds }
346-
: {}),
347-
...(e.blockErrors && Object.keys(e.blockErrors as Record<string, string>).length > 0
348-
? { blockErrors: e.blockErrors as Record<string, string> }
349-
: {}),
350-
...(e.cancelledAt ? { cancelledAt: e.cancelledAt.toISOString() } : {}),
351-
}
352-
existing[e.groupId] = meta
353-
executionsByRow.set(e.rowId, existing)
354-
}
355-
}
261+
const result = await queryRows(table, {
262+
filter: validated.filter as Filter | undefined,
263+
sort: validated.sort,
264+
limit: validated.limit,
265+
offset: validated.offset,
266+
includeTotal: validated.includeTotal,
267+
})
356268

357269
logger.info(
358-
`[${requestId}] Queried ${rows.length} rows from table ${tableId} (total: ${totalCount ?? 'n/a'})`
270+
`[${requestId}] Queried ${result.rowCount} rows from table ${tableId} (total: ${result.totalCount ?? 'n/a'})`
359271
)
360272

361273
return NextResponse.json({
362274
success: true,
363275
data: {
364-
rows: rows.map((r) => ({
276+
rows: result.rows.map((r) => ({
365277
id: r.id,
366278
data: r.data,
367-
executions: executionsByRow.get(r.id) ?? {},
279+
executions: r.executions,
368280
position: r.position,
369281
createdAt:
370282
r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt),
371283
updatedAt:
372284
r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
373285
})),
374-
rowCount: rows.length,
375-
totalCount,
376-
limit: validated.limit,
377-
offset: validated.offset,
286+
rowCount: result.rowCount,
287+
totalCount: result.totalCount,
288+
limit: result.limit,
289+
offset: result.offset,
378290
},
379291
})
380292
} catch (error) {

apps/sim/app/api/v1/tables/[tableId]/rows/route.ts

Lines changed: 15 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
1-
import { db } from '@sim/db'
2-
import { userTableRows } from '@sim/db/schema'
31
import { createLogger } from '@sim/logger'
42
import { toError } from '@sim/utils/errors'
5-
import { and, eq, sql } from 'drizzle-orm'
63
import { type NextRequest, NextResponse } from 'next/server'
74
import {
85
type V1BatchInsertTableRowsBody,
@@ -24,13 +21,13 @@ import {
2421
deleteRowsByFilter,
2522
deleteRowsByIds,
2623
insertRow,
27-
USER_TABLE_ROWS_SQL_NAME,
2824
updateRowsByFilter,
2925
validateBatchRows,
3026
validateRowData,
3127
validateRowSize,
3228
} from '@/lib/table'
33-
import { buildFilterClause, buildSortClause, TableQueryValidationError } from '@/lib/table/sql'
29+
import { TableQueryValidationError } from '@/lib/table/sql'
30+
import { queryRows } from '@/lib/table/table-wrapper'
3431
import { accessError, checkAccess } from '@/app/api/table/utils'
3532
import {
3633
checkRateLimit,
@@ -153,92 +150,29 @@ export const GET = withRouteHandler(async (request: NextRequest, context: TableR
153150
return NextResponse.json({ error: 'Invalid workspace ID' }, { status: 400 })
154151
}
155152

156-
const baseConditions = [
157-
eq(userTableRows.tableId, tableId),
158-
eq(userTableRows.workspaceId, validated.workspaceId),
159-
]
160-
161-
const schema = table.schema as TableSchema
162-
163-
if (validated.filter) {
164-
const filterClause = buildFilterClause(
165-
validated.filter as Filter,
166-
USER_TABLE_ROWS_SQL_NAME,
167-
schema.columns
168-
)
169-
if (filterClause) {
170-
baseConditions.push(filterClause)
171-
}
172-
}
173-
174-
let query = db
175-
.select({
176-
id: userTableRows.id,
177-
data: userTableRows.data,
178-
position: userTableRows.position,
179-
createdAt: userTableRows.createdAt,
180-
updatedAt: userTableRows.updatedAt,
181-
})
182-
.from(userTableRows)
183-
.where(and(...baseConditions))
184-
185-
if (validated.sort) {
186-
const sortClause = buildSortClause(validated.sort, USER_TABLE_ROWS_SQL_NAME, schema.columns)
187-
if (sortClause) {
188-
query = query.orderBy(sortClause) as typeof query
189-
} else {
190-
query = query.orderBy(userTableRows.position) as typeof query
191-
}
192-
} else {
193-
query = query.orderBy(userTableRows.position) as typeof query
194-
}
195-
196-
const rowsPromise = query.limit(validated.limit).offset(validated.offset)
197-
198-
let totalCount: number | null = null
199-
if (validated.includeTotal) {
200-
const countQuery = db
201-
.select({ count: sql<number>`count(*)` })
202-
.from(userTableRows)
203-
.where(and(...baseConditions))
204-
const [countResult, rows] = await Promise.all([countQuery, rowsPromise])
205-
totalCount = Number(countResult[0].count)
206-
return NextResponse.json({
207-
success: true,
208-
data: {
209-
rows: rows.map((r) => ({
210-
id: r.id,
211-
data: r.data,
212-
position: r.position,
213-
createdAt:
214-
r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt),
215-
updatedAt:
216-
r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
217-
})),
218-
rowCount: rows.length,
219-
totalCount,
220-
limit: validated.limit,
221-
offset: validated.offset,
222-
},
223-
})
224-
}
225-
226-
const rows = await rowsPromise
153+
const result = await queryRows(table, {
154+
filter: validated.filter as Filter | undefined,
155+
sort: validated.sort,
156+
limit: validated.limit,
157+
offset: validated.offset,
158+
includeTotal: validated.includeTotal,
159+
withExecutions: false,
160+
})
227161

228162
return NextResponse.json({
229163
success: true,
230164
data: {
231-
rows: rows.map((r) => ({
165+
rows: result.rows.map((r) => ({
232166
id: r.id,
233167
data: r.data,
234168
position: r.position,
235169
createdAt: r.createdAt instanceof Date ? r.createdAt.toISOString() : String(r.createdAt),
236170
updatedAt: r.updatedAt instanceof Date ? r.updatedAt.toISOString() : String(r.updatedAt),
237171
})),
238-
rowCount: rows.length,
239-
totalCount,
240-
limit: validated.limit,
241-
offset: validated.offset,
172+
rowCount: result.rowCount,
173+
totalCount: result.totalCount,
174+
limit: result.limit,
175+
offset: result.offset,
242176
},
243177
})
244178
} catch (error) {

apps/sim/lib/copilot/request/tools/tables.ts

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ import { TraceEvent } from '@/lib/copilot/generated/trace-events-v1'
1212
import { TraceSpan } from '@/lib/copilot/generated/trace-spans-v1'
1313
import { withCopilotSpan } from '@/lib/copilot/request/otel'
1414
import type { ExecutionContext, ToolCallResult } from '@/lib/copilot/request/types'
15+
import type { RowData } from '@/lib/table'
1516
import { getTableById } from '@/lib/table/service'
17+
import { buildOrderedRowValues } from '@/lib/table/table-wrapper'
1618

1719
const logger = createLogger('CopilotToolResultTables')
1820

@@ -107,16 +109,15 @@ export async function maybeWriteOutputToTable(
107109
throw new Error('Request aborted before tool mutation could be applied')
108110
}
109111
const chunk = rows.slice(i, i + BATCH_CHUNK_SIZE)
110-
const values = chunk.map((rowData, j) => ({
111-
id: `row_${generateId().replace(/-/g, '')}`,
112+
const values = buildOrderedRowValues({
112113
tableId: outputTable,
113114
workspaceId: context.workspaceId!,
114-
data: rowData,
115-
position: i + j,
116-
createdAt: now,
117-
updatedAt: now,
115+
rows: chunk as RowData[],
116+
startPosition: i,
117+
now,
118118
createdBy: context.userId,
119-
}))
119+
makeId: () => `row_${generateId().replace(/-/g, '')}`,
120+
})
120121
await tx.insert(userTableRows).values(values)
121122
}
122123
})
@@ -251,16 +252,15 @@ export async function maybeWriteReadCsvToTable(
251252
throw new Error('Request aborted before tool mutation could be applied')
252253
}
253254
const chunk = rows.slice(i, i + BATCH_CHUNK_SIZE)
254-
const values = chunk.map((rowData, j) => ({
255-
id: `row_${generateId().replace(/-/g, '')}`,
255+
const values = buildOrderedRowValues({
256256
tableId: outputTable,
257257
workspaceId: context.workspaceId!,
258-
data: rowData,
259-
position: i + j,
260-
createdAt: now,
261-
updatedAt: now,
258+
rows: chunk as RowData[],
259+
startPosition: i,
260+
now,
262261
createdBy: context.userId,
263-
}))
262+
makeId: () => `row_${generateId().replace(/-/g, '')}`,
263+
})
264264
await tx.insert(userTableRows).values(values)
265265
}
266266
})

0 commit comments

Comments
 (0)