d25d482dc2
Publish CLI Package / publish-npm (push) Waiting to run
Publish Python SDK / publish-pypi (push) Waiting to run
Publish TypeScript SDK / publish-npm (push) Waiting to run
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
108 lines
4.1 KiB
TypeScript
108 lines
4.1 KiB
TypeScript
/**
|
|
* @vitest-environment node
|
|
*
|
|
* Lock-order regression guard: a column-creating CSV import must acquire the
|
|
* per-table row-order advisory lock (`user_table_rows_pos`) BEFORE writing
|
|
* `user_table_definitions`, matching the rows_pos → definitions order that plain
|
|
* inserts take via the row-count trigger. The opposite order deadlocks
|
|
* concurrent inserts on the same table.
|
|
*/
|
|
import { userTableDefinitions } from '@sim/db/schema'
|
|
import { dbChainMock, dbChainMockFns, resetDbChainMock } from '@sim/testing'
|
|
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { importAppendRows } from '@/lib/table/import-data'
|
|
import type { TableDefinition } from '@/lib/table/types'
|
|
|
|
vi.mock('@sim/db', () => dbChainMock)
|
|
|
|
vi.mock('@/lib/core/config/feature-flags', () => ({
|
|
isFeatureEnabled: vi.fn().mockResolvedValue(false),
|
|
}))
|
|
|
|
vi.mock('@/lib/table/validation', () => ({
|
|
validateRowSize: vi.fn(() => ({ valid: true, errors: [] })),
|
|
validateRowAgainstSchema: vi.fn(() => ({ valid: true, errors: [] })),
|
|
coerceRowToSchema: vi.fn(() => ({ valid: true, errors: [] })),
|
|
coerceRowValues: vi.fn((row) => row),
|
|
validateTableName: vi.fn(() => ({ valid: true, errors: [] })),
|
|
validateTableSchema: vi.fn(() => ({ valid: true, errors: [] })),
|
|
getUniqueColumns: vi.fn(() => []),
|
|
checkUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })),
|
|
checkBatchUniqueConstraintsDb: vi.fn(async () => ({ valid: true, errors: [] })),
|
|
}))
|
|
|
|
const TABLE: TableDefinition = {
|
|
id: 'tbl-1',
|
|
name: 'People',
|
|
description: null,
|
|
schema: { columns: [{ name: 'name', type: 'string' }] },
|
|
metadata: null,
|
|
rowCount: 0,
|
|
maxRows: 1000,
|
|
workspaceId: 'ws-1',
|
|
createdBy: 'user-1',
|
|
archivedAt: null,
|
|
createdAt: new Date('2024-01-01'),
|
|
updatedAt: new Date('2024-01-01'),
|
|
}
|
|
|
|
/**
|
|
* invocationCallOrder of the first `tx.execute(...)` whose SQL contains
|
|
* `substring` — checks the template strings, the interpolated values (the
|
|
* advisory key is passed as a value), and `sql.raw` output.
|
|
*/
|
|
function executeOrderContaining(substring: string): number {
|
|
const { calls, invocationCallOrder } = dbChainMockFns.execute.mock
|
|
for (let i = 0; i < calls.length; i++) {
|
|
const arg = calls[i][0] as { strings?: unknown; values?: unknown; rawSql?: unknown } | undefined
|
|
const haystacks: string[] = []
|
|
if (Array.isArray(arg?.strings)) {
|
|
haystacks.push(
|
|
...(arg.strings as unknown[]).filter((s): s is string => typeof s === 'string')
|
|
)
|
|
}
|
|
if (Array.isArray(arg?.values)) {
|
|
haystacks.push(...(arg.values as unknown[]).filter((v): v is string => typeof v === 'string'))
|
|
}
|
|
if (typeof arg?.rawSql === 'string') haystacks.push(arg.rawSql)
|
|
if (haystacks.some((s) => s.includes(substring))) return invocationCallOrder[i]
|
|
}
|
|
return -1
|
|
}
|
|
|
|
/** invocationCallOrder of the first `tx.update(table)` call. */
|
|
function updateOrderForTable(table: unknown): number {
|
|
const { calls, invocationCallOrder } = dbChainMockFns.update.mock
|
|
for (let i = 0; i < calls.length; i++) {
|
|
if (calls[i][0] === table) return invocationCallOrder[i]
|
|
}
|
|
return -1
|
|
}
|
|
|
|
describe('table import lock ordering', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
resetDbChainMock()
|
|
})
|
|
|
|
it('acquires the rows_pos advisory before the user_table_definitions write when adding columns', async () => {
|
|
// The lock pre-acquire and the column-creating write both run at the top of
|
|
// the import, before the batch-insert loop. The loop's order-key aggregates
|
|
// aren't fully wired in this unit mock, so tolerate a downstream error after
|
|
// the locks under test have been recorded.
|
|
await importAppendRows(
|
|
TABLE,
|
|
[{ name: 'new_col', type: 'string' }],
|
|
[{ name: 'Alice', new_col: 'x' }],
|
|
{ workspaceId: 'ws-1', userId: 'user-1', requestId: 'req-1' }
|
|
).catch(() => {})
|
|
|
|
const rowsPosLockOrder = executeOrderContaining('user_table_rows_pos')
|
|
const definitionsWriteOrder = updateOrderForTable(userTableDefinitions)
|
|
|
|
expect(rowsPosLockOrder).toBeGreaterThan(0)
|
|
expect(definitionsWriteOrder).toBeGreaterThan(0)
|
|
expect(rowsPosLockOrder).toBeLessThan(definitionsWriteOrder)
|
|
})
|
|
})
|