chore: import upstream snapshot with attribution
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
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
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
/**
|
||||
* @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)
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user