Skip to content

Commit 8e897e3

Browse files
committed
feat(tables): persist reference column targets
1 parent bb174af commit 8e897e3

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(),
@@ -197,11 +201,13 @@ vi.mock('@/lib/table/workflow-groups/service', () => ({
197201
}))
198202

199203
vi.mock('@/lib/table/columns/service', () => ({
200-
addTableColumn: vi.fn(),
204+
addTableColumn: mockAddTableColumn,
201205
deleteColumn: vi.fn(),
202206
deleteColumns: mockDeleteColumns,
203207
renameColumn: vi.fn(),
204208
updateColumnConstraints: vi.fn(),
209+
updateColumnCurrency: vi.fn(),
210+
updateColumnReference: mockUpdateColumnReference,
205211
updateColumnType: mockUpdateColumnType,
206212
updateColumnOptions: mockUpdateColumnOptions,
207213
}))
@@ -1682,6 +1688,94 @@ describe('userTableServerTool.update_rows_by_filter', () => {
16821688
})
16831689
})
16841690

1691+
describe('userTableServerTool reference column metadata', () => {
1692+
beforeEach(() => {
1693+
vi.clearAllMocks()
1694+
mockGetTableById.mockResolvedValue(buildTable())
1695+
mockAddTableColumn.mockImplementation(
1696+
async (_tableId: string, column: TableDefinition['schema']['columns'][number]) =>
1697+
buildTable({ schema: { columns: [column] } })
1698+
)
1699+
})
1700+
1701+
it('forwards the target when adding a reference column', async () => {
1702+
const result = await userTableServerTool.execute(
1703+
{
1704+
operation: 'add_column',
1705+
args: {
1706+
tableId: 'tbl_1',
1707+
column: {
1708+
name: 'account',
1709+
type: 'reference',
1710+
referenceTableId: 'tbl_accounts',
1711+
},
1712+
},
1713+
},
1714+
buildToolContext()
1715+
)
1716+
1717+
expect(result.success).toBe(true)
1718+
expect(mockAddTableColumn).toHaveBeenCalledWith(
1719+
'tbl_1',
1720+
expect.objectContaining({
1721+
type: 'reference',
1722+
referenceTableId: 'tbl_accounts',
1723+
}),
1724+
expect.any(String),
1725+
{ expectedWorkspaceId: 'workspace-1' }
1726+
)
1727+
})
1728+
1729+
it('forwards a target-only update to the shared reference service', async () => {
1730+
const referenceTable = buildTable({
1731+
schema: {
1732+
columns: [
1733+
{
1734+
id: 'col_account',
1735+
name: 'account',
1736+
type: 'reference',
1737+
referenceTableId: 'tbl_accounts',
1738+
},
1739+
],
1740+
},
1741+
})
1742+
mockGetTableById.mockResolvedValue(referenceTable)
1743+
mockUpdateColumnReference.mockResolvedValue({
1744+
...referenceTable,
1745+
schema: {
1746+
columns: [
1747+
{
1748+
...referenceTable.schema.columns[0],
1749+
referenceTableId: 'tbl_companies',
1750+
},
1751+
],
1752+
},
1753+
})
1754+
1755+
const result = await userTableServerTool.execute(
1756+
{
1757+
operation: 'update_column',
1758+
args: {
1759+
tableId: 'tbl_1',
1760+
columnName: 'account',
1761+
referenceTableId: 'tbl_companies',
1762+
},
1763+
},
1764+
buildToolContext()
1765+
)
1766+
1767+
expect(result.success).toBe(true)
1768+
expect(mockUpdateColumnReference).toHaveBeenCalledWith(
1769+
expect.objectContaining({
1770+
columnName: 'col_account',
1771+
referenceTableId: 'tbl_companies',
1772+
}),
1773+
expect.any(String),
1774+
{ expectedWorkspaceId: 'workspace-1' }
1775+
)
1776+
})
1777+
})
1778+
16851779
describe('userTableServerTool.update_column — select routing', () => {
16861780
const selectTable = buildTable({
16871781
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
@@ -967,6 +967,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
967967
options?: unknown
968968
multiple?: boolean
969969
currencyCode?: string
970+
referenceTableId?: string
970971
}
971972
| undefined
972973
if (!col?.name || !col?.type) {
@@ -1090,17 +1091,21 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
10901091
const rawOptions = (args as Record<string, unknown>).options
10911092
const multiple = (args as Record<string, unknown>).multiple as boolean | undefined
10921093
const currencyCode = (args as Record<string, unknown>).currencyCode as string | undefined
1094+
const referenceTableId = (args as Record<string, unknown>).referenceTableId as
1095+
| string
1096+
| undefined
10931097
if (
10941098
newType === undefined &&
10951099
uniqFlag === undefined &&
10961100
rawOptions === undefined &&
10971101
multiple === undefined &&
1098-
currencyCode === undefined
1102+
currencyCode === undefined &&
1103+
referenceTableId === undefined
10991104
) {
11001105
return {
11011106
success: false,
11021107
message:
1103-
'At least one of newType, unique, options, multiple, or currencyCode must be provided',
1108+
'At least one of newType, unique, options, multiple, currencyCode, or referenceTableId must be provided',
11041109
}
11051110
}
11061111
if (currencyCode !== undefined && !isSupportedCurrencyCode(currencyCode)) {
@@ -1131,6 +1136,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
11311136
...(rawOptions !== undefined ? { options: rawOptions } : {}),
11321137
...(multiple !== undefined ? { multiple } : {}),
11331138
...(currencyCode !== undefined ? { currencyCode } : {}),
1139+
...(referenceTableId !== undefined ? { referenceTableId } : {}),
11341140
},
11351141
},
11361142
{ 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)