Skip to content

Commit b801e22

Browse files
committed
feat(tables): persist reference column targets
1 parent 37640c6 commit b801e22

8 files changed

Lines changed: 455 additions & 5 deletions

File tree

‎apps/sim/lib/copilot/tools/server/table/user-table.test.ts‎

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ import { OrchestrationError } from '@/lib/core/orchestration/types'
77
import type { TableDefinition } from '@/lib/table'
88

99
const {
10+
mockAddTableColumn,
1011
mockUpdateColumnType,
1112
mockUpdateColumnOptions,
13+
mockUpdateColumnReference,
1214
mockResolveWorkspaceFileReference,
1315
mockGetBoundWorkspaceFileSecretProvenance,
1416
mockDownloadWorkspaceFile,
@@ -37,8 +39,10 @@ const {
3739
mockResolveWorkflowContext,
3840
fakeEnrichment,
3941
} = vi.hoisted(() => ({
42+
mockAddTableColumn: vi.fn(),
4043
mockUpdateColumnType: vi.fn(),
4144
mockUpdateColumnOptions: vi.fn(),
45+
mockUpdateColumnReference: vi.fn(),
4246
mockResolveWorkspaceFileReference: vi.fn(),
4347
mockGetBoundWorkspaceFileSecretProvenance: vi.fn(),
4448
mockDownloadWorkspaceFile: vi.fn(),
@@ -196,11 +200,13 @@ vi.mock('@/lib/table/workflow-groups/service', () => ({
196200
}))
197201

198202
vi.mock('@/lib/table/columns/service', () => ({
199-
addTableColumn: vi.fn(),
203+
addTableColumn: mockAddTableColumn,
200204
deleteColumn: vi.fn(),
201205
deleteColumns: mockDeleteColumns,
202206
renameColumn: vi.fn(),
203207
updateColumnConstraints: vi.fn(),
208+
updateColumnCurrency: vi.fn(),
209+
updateColumnReference: mockUpdateColumnReference,
204210
updateColumnType: mockUpdateColumnType,
205211
updateColumnOptions: mockUpdateColumnOptions,
206212
}))
@@ -1582,6 +1588,94 @@ describe('userTableServerTool.update_rows_by_filter', () => {
15821588
})
15831589
})
15841590

1591+
describe('userTableServerTool reference column metadata', () => {
1592+
beforeEach(() => {
1593+
vi.clearAllMocks()
1594+
mockGetTableById.mockResolvedValue(buildTable())
1595+
mockAddTableColumn.mockImplementation(
1596+
async (_tableId: string, column: TableDefinition['schema']['columns'][number]) =>
1597+
buildTable({ schema: { columns: [column] } })
1598+
)
1599+
})
1600+
1601+
it('forwards the target when adding a reference column', async () => {
1602+
const result = await userTableServerTool.execute(
1603+
{
1604+
operation: 'add_column',
1605+
args: {
1606+
tableId: 'tbl_1',
1607+
column: {
1608+
name: 'account',
1609+
type: 'reference',
1610+
referenceTableId: 'tbl_accounts',
1611+
},
1612+
},
1613+
},
1614+
buildToolContext()
1615+
)
1616+
1617+
expect(result.success).toBe(true)
1618+
expect(mockAddTableColumn).toHaveBeenCalledWith(
1619+
'tbl_1',
1620+
expect.objectContaining({
1621+
type: 'reference',
1622+
referenceTableId: 'tbl_accounts',
1623+
}),
1624+
expect.any(String),
1625+
{ expectedWorkspaceId: 'workspace-1' }
1626+
)
1627+
})
1628+
1629+
it('forwards a target-only update to the shared reference service', async () => {
1630+
const referenceTable = buildTable({
1631+
schema: {
1632+
columns: [
1633+
{
1634+
id: 'col_account',
1635+
name: 'account',
1636+
type: 'reference',
1637+
referenceTableId: 'tbl_accounts',
1638+
},
1639+
],
1640+
},
1641+
})
1642+
mockGetTableById.mockResolvedValue(referenceTable)
1643+
mockUpdateColumnReference.mockResolvedValue({
1644+
...referenceTable,
1645+
schema: {
1646+
columns: [
1647+
{
1648+
...referenceTable.schema.columns[0],
1649+
referenceTableId: 'tbl_companies',
1650+
},
1651+
],
1652+
},
1653+
})
1654+
1655+
const result = await userTableServerTool.execute(
1656+
{
1657+
operation: 'update_column',
1658+
args: {
1659+
tableId: 'tbl_1',
1660+
columnName: 'account',
1661+
referenceTableId: 'tbl_companies',
1662+
},
1663+
},
1664+
buildToolContext()
1665+
)
1666+
1667+
expect(result.success).toBe(true)
1668+
expect(mockUpdateColumnReference).toHaveBeenCalledWith(
1669+
expect.objectContaining({
1670+
columnName: 'col_account',
1671+
referenceTableId: 'tbl_companies',
1672+
}),
1673+
expect.any(String),
1674+
{ expectedWorkspaceId: 'workspace-1' }
1675+
)
1676+
})
1677+
})
1678+
15851679
describe('userTableServerTool.update_column — select routing', () => {
15861680
const selectTable = buildTable({
15871681
schema: {

‎apps/sim/lib/copilot/tools/server/table/user-table.ts‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
933933
options?: unknown
934934
multiple?: boolean
935935
currencyCode?: string
936+
referenceTableId?: string
936937
}
937938
| undefined
938939
if (!col?.name || !col?.type) {
@@ -1056,17 +1057,21 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
10561057
const rawOptions = (args as Record<string, unknown>).options
10571058
const multiple = (args as Record<string, unknown>).multiple as boolean | undefined
10581059
const currencyCode = (args as Record<string, unknown>).currencyCode as string | undefined
1060+
const referenceTableId = (args as Record<string, unknown>).referenceTableId as
1061+
| string
1062+
| undefined
10591063
if (
10601064
newType === undefined &&
10611065
uniqFlag === undefined &&
10621066
rawOptions === undefined &&
10631067
multiple === undefined &&
1064-
currencyCode === undefined
1068+
currencyCode === undefined &&
1069+
referenceTableId === undefined
10651070
) {
10661071
return {
10671072
success: false,
10681073
message:
1069-
'At least one of newType, unique, options, multiple, or currencyCode must be provided',
1074+
'At least one of newType, unique, options, multiple, currencyCode, or referenceTableId must be provided',
10701075
}
10711076
}
10721077
if (currencyCode !== undefined && !isSupportedCurrencyCode(currencyCode)) {
@@ -1097,6 +1102,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
10971102
...(rawOptions !== undefined ? { options: rawOptions } : {}),
10981103
...(multiple !== undefined ? { multiple } : {}),
10991104
...(currencyCode !== undefined ? { currencyCode } : {}),
1105+
...(referenceTableId !== undefined ? { referenceTableId } : {}),
11001106
},
11011107
},
11021108
{ tableId: args.tableId }

‎apps/sim/lib/table/application/columns.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ export interface AddTableColumnInput extends TableColumnInput {
3636
options?: SelectOption[]
3737
multiple?: boolean
3838
currencyCode?: string
39+
referenceTableId?: string
3940
}
4041
}
4142

@@ -77,6 +78,7 @@ export interface UpdateTableColumnInput extends TableColumnInput {
7778
options?: unknown
7879
multiple?: boolean
7980
currencyCode?: string
81+
referenceTableId?: string
8082
}
8183
}
8284

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
1+
/**
2+
* @vitest-environment node
3+
*/
4+
5+
import { beforeEach, describe, expect, it, vi } from 'vitest'
6+
import type { TableDefinition } from '@/lib/table/types'
7+
8+
const mocks = vi.hoisted(() => ({
9+
withLockedTable: vi.fn(),
10+
set: vi.fn(),
11+
where: vi.fn(),
12+
}))
13+
14+
vi.mock('@/lib/table/service', () => ({ withLockedTable: mocks.withLockedTable }))
15+
16+
import {
17+
addTableColumn,
18+
updateColumnReference,
19+
updateColumnType,
20+
} from '@/lib/table/columns/service'
21+
22+
const BASE_TABLE = {
23+
id: 'tbl_people',
24+
name: 'People',
25+
workspaceId: 'ws_1',
26+
schema: {
27+
columns: [{ id: 'col_name', name: 'Name', type: 'string' }],
28+
},
29+
metadata: null,
30+
rowCount: 0,
31+
} as unknown as TableDefinition
32+
33+
function tableWithReference(referenceTableId = 'tbl_accounts'): TableDefinition {
34+
return {
35+
...BASE_TABLE,
36+
schema: {
37+
columns: [
38+
{
39+
id: 'col_account',
40+
name: 'Account',
41+
type: 'reference',
42+
referenceTableId,
43+
},
44+
],
45+
},
46+
}
47+
}
48+
49+
describe('reference column metadata persistence', () => {
50+
beforeEach(() => {
51+
vi.clearAllMocks()
52+
mocks.where.mockResolvedValue(undefined)
53+
mocks.set.mockReturnValue({ where: mocks.where })
54+
})
55+
56+
function useTable(table: TableDefinition) {
57+
const trx = {
58+
execute: vi.fn().mockResolvedValue([]),
59+
select: vi.fn(() => ({
60+
from: vi.fn(() => ({
61+
where: vi.fn(() => ({
62+
orderBy: vi.fn(() => ({ limit: vi.fn().mockResolvedValue([]) })),
63+
})),
64+
})),
65+
})),
66+
update: vi.fn(() => ({ set: mocks.set })),
67+
}
68+
mocks.withLockedTable.mockImplementationOnce(
69+
async (_tableId, mutate: (locked: TableDefinition, tx: typeof trx) => Promise<unknown>) =>
70+
mutate(table, trx)
71+
)
72+
return trx
73+
}
74+
75+
it('retains referenceTableId when adding a reference column', async () => {
76+
useTable(BASE_TABLE)
77+
78+
const updated = await addTableColumn(
79+
'tbl_people',
80+
{ name: 'Account', type: 'reference', referenceTableId: 'tbl_accounts' },
81+
'req_1'
82+
)
83+
84+
expect(updated.schema.columns.at(-1)).toMatchObject({
85+
name: 'Account',
86+
type: 'reference',
87+
referenceTableId: 'tbl_accounts',
88+
})
89+
})
90+
91+
it('retains the supplied target when converting a column to reference', async () => {
92+
useTable(BASE_TABLE)
93+
94+
const updated = await updateColumnType(
95+
{
96+
tableId: 'tbl_people',
97+
columnName: 'col_name',
98+
newType: 'reference',
99+
referenceTableId: 'tbl_accounts',
100+
},
101+
'req_1'
102+
)
103+
104+
expect(updated.schema.columns[0]).toMatchObject({
105+
id: 'col_name',
106+
type: 'reference',
107+
referenceTableId: 'tbl_accounts',
108+
})
109+
})
110+
111+
it('changes a reference target without reading or rewriting rows', async () => {
112+
const trx = useTable(tableWithReference())
113+
114+
const updated = await updateColumnReference(
115+
{
116+
tableId: 'tbl_people',
117+
columnName: 'col_account',
118+
referenceTableId: 'tbl_companies',
119+
},
120+
'req_1'
121+
)
122+
123+
expect(updated.schema.columns[0]).toMatchObject({ referenceTableId: 'tbl_companies' })
124+
expect(trx.select).not.toHaveBeenCalled()
125+
expect(trx.execute).not.toHaveBeenCalled()
126+
expect(trx.update).toHaveBeenCalledOnce()
127+
})
128+
129+
it('rejects reference metadata on a non-reference column', async () => {
130+
const trx = useTable(BASE_TABLE)
131+
132+
await expect(
133+
updateColumnReference(
134+
{
135+
tableId: 'tbl_people',
136+
columnName: 'col_name',
137+
referenceTableId: 'tbl_accounts',
138+
},
139+
'req_1'
140+
)
141+
).rejects.toMatchObject({ code: 'validation' })
142+
143+
expect(trx.update).not.toHaveBeenCalled()
144+
})
145+
146+
it('returns the locked table unchanged when the target is already set', async () => {
147+
const table = tableWithReference()
148+
const trx = useTable(table)
149+
150+
const updated = await updateColumnReference(
151+
{
152+
tableId: 'tbl_people',
153+
columnName: 'col_account',
154+
referenceTableId: 'tbl_accounts',
155+
},
156+
'req_1'
157+
)
158+
159+
expect(updated).toBe(table)
160+
expect(trx.update).not.toHaveBeenCalled()
161+
})
162+
})

0 commit comments

Comments
 (0)