Skip to content

Commit f09c267

Browse files
committed
feat(tables): persist reference column targets
1 parent a152eb6 commit f09c267

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,
@@ -39,8 +41,10 @@ const {
3941
mockResolveWorkflowContext,
4042
fakeEnrichment,
4143
} = vi.hoisted(() => ({
44+
mockAddTableColumn: vi.fn(),
4245
mockUpdateColumnType: vi.fn(),
4346
mockUpdateColumnOptions: vi.fn(),
47+
mockUpdateColumnReference: vi.fn(),
4448
mockResolveWorkspaceFileReference: vi.fn(),
4549
mockGetBoundWorkspaceFileSecretProvenance: vi.fn(),
4650
mockDownloadWorkspaceFile: vi.fn(),
@@ -201,11 +205,13 @@ vi.mock('@/lib/table/workflow-groups/service', () => ({
201205
}))
202206

203207
vi.mock('@/lib/table/columns/service', () => ({
204-
addTableColumn: vi.fn(),
208+
addTableColumn: mockAddTableColumn,
205209
deleteColumn: vi.fn(),
206210
deleteColumns: mockDeleteColumns,
207211
renameColumn: vi.fn(),
208212
updateColumnConstraints: vi.fn(),
213+
updateColumnCurrency: vi.fn(),
214+
updateColumnReference: mockUpdateColumnReference,
209215
updateColumnType: mockUpdateColumnType,
210216
updateColumnOptions: mockUpdateColumnOptions,
211217
}))
@@ -1694,6 +1700,94 @@ describe('userTableServerTool.update_rows_by_filter', () => {
16941700
})
16951701
})
16961702

1703+
describe('userTableServerTool reference column metadata', () => {
1704+
beforeEach(() => {
1705+
vi.clearAllMocks()
1706+
mockGetTableById.mockResolvedValue(buildTable())
1707+
mockAddTableColumn.mockImplementation(
1708+
async (_tableId: string, column: TableDefinition['schema']['columns'][number]) =>
1709+
buildTable({ schema: { columns: [column] } })
1710+
)
1711+
})
1712+
1713+
it('forwards the target when adding a reference column', async () => {
1714+
const result = await userTableServerTool.execute(
1715+
{
1716+
operation: 'add_column',
1717+
args: {
1718+
tableId: 'tbl_1',
1719+
column: {
1720+
name: 'account',
1721+
type: 'reference',
1722+
referenceTableId: 'tbl_accounts',
1723+
},
1724+
},
1725+
},
1726+
buildToolContext()
1727+
)
1728+
1729+
expect(result.success).toBe(true)
1730+
expect(mockAddTableColumn).toHaveBeenCalledWith(
1731+
'tbl_1',
1732+
expect.objectContaining({
1733+
type: 'reference',
1734+
referenceTableId: 'tbl_accounts',
1735+
}),
1736+
expect.any(String),
1737+
{ expectedWorkspaceId: 'workspace-1' }
1738+
)
1739+
})
1740+
1741+
it('forwards a target-only update to the shared reference service', async () => {
1742+
const referenceTable = buildTable({
1743+
schema: {
1744+
columns: [
1745+
{
1746+
id: 'col_account',
1747+
name: 'account',
1748+
type: 'reference',
1749+
referenceTableId: 'tbl_accounts',
1750+
},
1751+
],
1752+
},
1753+
})
1754+
mockGetTableById.mockResolvedValue(referenceTable)
1755+
mockUpdateColumnReference.mockResolvedValue({
1756+
...referenceTable,
1757+
schema: {
1758+
columns: [
1759+
{
1760+
...referenceTable.schema.columns[0],
1761+
referenceTableId: 'tbl_companies',
1762+
},
1763+
],
1764+
},
1765+
})
1766+
1767+
const result = await userTableServerTool.execute(
1768+
{
1769+
operation: 'update_column',
1770+
args: {
1771+
tableId: 'tbl_1',
1772+
columnName: 'account',
1773+
referenceTableId: 'tbl_companies',
1774+
},
1775+
},
1776+
buildToolContext()
1777+
)
1778+
1779+
expect(result.success).toBe(true)
1780+
expect(mockUpdateColumnReference).toHaveBeenCalledWith(
1781+
expect.objectContaining({
1782+
columnName: 'col_account',
1783+
referenceTableId: 'tbl_companies',
1784+
}),
1785+
expect.any(String),
1786+
{ expectedWorkspaceId: 'workspace-1' }
1787+
)
1788+
})
1789+
})
1790+
16971791
describe('userTableServerTool.update_column — select routing', () => {
16981792
const selectTable = buildTable({
16991793
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
@@ -1007,6 +1007,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
10071007
options?: unknown
10081008
multiple?: boolean
10091009
currencyCode?: string
1010+
referenceTableId?: string
10101011
}
10111012
| undefined
10121013
if (!col?.name || !col?.type) {
@@ -1130,17 +1131,21 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
11301131
const rawOptions = (args as Record<string, unknown>).options
11311132
const multiple = (args as Record<string, unknown>).multiple as boolean | undefined
11321133
const currencyCode = (args as Record<string, unknown>).currencyCode as string | undefined
1134+
const referenceTableId = (args as Record<string, unknown>).referenceTableId as
1135+
| string
1136+
| undefined
11331137
if (
11341138
newType === undefined &&
11351139
uniqFlag === undefined &&
11361140
rawOptions === undefined &&
11371141
multiple === undefined &&
1138-
currencyCode === undefined
1142+
currencyCode === undefined &&
1143+
referenceTableId === undefined
11391144
) {
11401145
return {
11411146
success: false,
11421147
message:
1143-
'At least one of newType, unique, options, multiple, or currencyCode must be provided',
1148+
'At least one of newType, unique, options, multiple, currencyCode, or referenceTableId must be provided',
11441149
}
11451150
}
11461151
if (currencyCode !== undefined && !isSupportedCurrencyCode(currencyCode)) {
@@ -1171,6 +1176,7 @@ export const userTableServerTool: BaseServerTool<UserTableArgs, UserTableResult>
11711176
...(rawOptions !== undefined ? { options: rawOptions } : {}),
11721177
...(multiple !== undefined ? { multiple } : {}),
11731178
...(currencyCode !== undefined ? { currencyCode } : {}),
1179+
...(referenceTableId !== undefined ? { referenceTableId } : {}),
11741180
},
11751181
},
11761182
{ 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)