Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (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 / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

735 lines
27 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { SnowflakeBlock } from '@/blocks/blocks/snowflake'
import {
buildAlterWarehouse,
buildCallProcedure,
buildCancelTaskRun,
buildDeleteRows,
buildGetTask,
buildGetTaskRun,
buildGetTaskRunOutput,
buildGetWarehouse,
buildInsertRows,
buildIntrospectSchema,
buildListCopyHistory,
buildListDatabases,
buildListQueryHistory,
buildListSchemas,
buildListTables,
buildListTaskRuns,
buildListTasks,
buildListWarehouses,
buildLoadData,
buildResumeTask,
buildResumeWarehouse,
buildRunTask,
buildSuspendTask,
buildSuspendWarehouse,
buildUnloadData,
buildUpdateRows,
buildUpsertRows,
identifier,
normalizeBindings,
qualifiedIdentifier,
} from '@/tools/snowflake/sql'
const context = { oauthCredential: 'cred-1' }
const table = { ...context, database: 'ANALYTICS', schema: 'PUBLIC', table: 'EVENTS' }
const queryId = '01b71944-0301-b428-0000-69f706bf0001'
/** ISO instants inside the history retention windows, so assertions never age out. */
const historyStart = new Date(Date.now() - 2 * 86_400_000).toISOString()
const historyEnd = new Date(Date.now() - 86_400_000).toISOString()
const copyStart = new Date(Date.now() - 3 * 86_400_000).toISOString()
/** The separator the row-shape and match-key encodings join on; legal inside a quoted identifier. */
const SEPARATOR = String.fromCharCode(0)
describe('Snowflake SQL builders', () => {
it('accepts safe identifiers and rejects SQL fragments', () => {
expect(identifier('safe_name')).toBe('safe_name')
expect(identifier('"Case Sensitive"')).toBe('"Case Sensitive"')
expect(qualifiedIdentifier('DB', 'SCHEMA', 'TABLE')).toBe('DB.SCHEMA.TABLE')
expect(() => identifier('users; DROP TABLE users')).toThrow('Invalid Snowflake identifier')
})
it('validates explicit bindings', () => {
expect(normalizeBindings({ '1': { type: 'DATE', value: '2026-01-01' } })).toEqual({
'1': { type: 'DATE', value: '2026-01-01' },
})
expect(() => normalizeBindings({ '0': { type: 'TEXT', value: 'x' } })).toThrow('positive')
expect(() => normalizeBindings({ '1': { type: 'NOPE', value: 'x' } } as never)).toThrow(
'Unsupported'
)
expect(() => normalizeBindings([] as never)).toThrow('JSON object')
expect(() => normalizeBindings({ '1': [] } as never)).toThrow('contain type and value')
expect(SnowflakeBlock.inputs.bindings.description).toContain(
'object keyed by 1-based positions'
)
expect(SnowflakeBlock.inputs.procedureArguments.description).toContain('ordered JSON array')
})
it('only coerces fields used by the selected block operation', () => {
const mapParams = SnowflakeBlock.tools.config.params
if (!mapParams) throw new Error('Snowflake block must map tool parameters')
expect(() =>
mapParams({
operation: 'execute_sql',
rows: '{invalid',
filters: '{invalid',
procedureArguments: '{invalid',
onError: 'SKIP_FILE_NUMBER',
})
).not.toThrow()
expect(() =>
mapParams({
operation: 'delete_rows',
rows: '{invalid',
filters: '{"id":1}',
})
).not.toThrow()
expect(() =>
mapParams({
operation: 'load_data',
onError: 'SKIP_FILE_NUMBER',
})
).toThrow('threshold')
})
it('maps overlapping block fields according to the selected operation', () => {
const mapParams = SnowflakeBlock.tools.config.params
if (!mapParams) throw new Error('Snowflake block must map tool parameters')
const finalParams = (params: Record<string, unknown>) => ({
...params,
...mapParams(params),
})
const staleFields = {
database: 'OBJECT_DB',
schema: 'OBJECT_SCHEMA',
taskName: 'TASK_DEFINITION',
}
expect(finalParams({ operation: 'execute_sql', ...staleFields })).toMatchObject({
database: 'OBJECT_DB',
schema: 'OBJECT_SCHEMA',
})
expect(finalParams({ operation: 'insert_rows', ...staleFields, rows: '[]' })).toMatchObject({
database: 'OBJECT_DB',
schema: 'OBJECT_SCHEMA',
})
expect(finalParams({ operation: 'list_task_runs', ...staleFields })).toMatchObject({
taskName: 'TASK_DEFINITION',
})
expect(finalParams({ operation: 'get_task', ...staleFields })).toMatchObject({
database: 'OBJECT_DB',
schema: 'OBJECT_SCHEMA',
taskName: 'TASK_DEFINITION',
})
})
it('builds a bound multi-row INSERT in stable column order', () => {
const result = buildInsertRows({
...table,
rows: [
{ id: 1, name: 'Ada' },
{ id: 2, name: null },
],
})
expect(result.statement).toBe(
'INSERT INTO ANALYTICS.PUBLIC.EVENTS (id, name) VALUES (?, ?), (?, NULL)'
)
expect(result.bindings).toEqual({
'1': { type: 'FIXED', value: '1' },
'2': { type: 'TEXT', value: 'Ada' },
'3': { type: 'FIXED', value: '2' },
})
})
it('lifts PARSE_JSON out of the VALUES clause into a projecting SELECT', () => {
const result = buildInsertRows({
...table,
rows: [
{ id: 1, payload: { ok: true } },
{ id: 2, payload: null },
],
})
expect(result.statement).toBe(
'INSERT INTO ANALYTICS.PUBLIC.EVENTS (id, payload) SELECT C1 AS id, PARSE_JSON(C2) AS payload FROM (VALUES (?, ?), (?, NULL)) AS v (C1, C2)'
)
expect(result.statement).not.toContain('VALUES (?, PARSE_JSON(?))')
expect(result.bindings).toEqual({
'1': { type: 'FIXED', value: '1' },
'2': { type: 'TEXT', value: '{"ok":true}' },
'3': { type: 'FIXED', value: '2' },
})
})
it('projects semi-structured MERGE source columns outside the VALUES clause', () => {
const upsert = buildUpsertRows({
...table,
rows: [{ id: 1, payload: { ok: true } }],
matchColumns: ['id'],
})
expect(upsert.statement).toContain(
'USING (SELECT C1 AS id, PARSE_JSON(C2) AS payload FROM (VALUES (?, ?)) AS v (C1, C2)) AS source'
)
expect(upsert.statement).not.toContain('VALUES (?, PARSE_JSON(?))')
})
it('escapes both backslashes and quotes in string literals', () => {
expect(
buildLoadData({ ...table, stagePath: '@RAW_STAGE', pattern: '.*\\' }).statement
).toContain("PATTERN = '.*\\\\'")
expect(buildListWarehouses("ETL\\' OR TRUE --").statement).toBe(
"SHOW WAREHOUSES LIKE 'ETL\\\\'' OR TRUE --'"
)
})
it('routes a mixed semi-structured column through a single whole-column PARSE_JSON', () => {
const result = buildInsertRows({
...table,
rows: [{ payload: 'text' }, { payload: { a: 1 } }],
})
expect(result.statement).toBe(
'INSERT INTO ANALYTICS.PUBLIC.EVENTS (payload) SELECT PARSE_JSON(C1) AS payload FROM (VALUES (?), (?)) AS v (C1)'
)
expect(result.bindings).toEqual({
'1': { type: 'TEXT', value: '"text"' },
'2': { type: 'TEXT', value: '{"a":1}' },
})
})
it('builds update-only and upsert MERGE statements with bound values', () => {
const params = { ...table, rows: [{ id: 1, name: 'Ada' }], matchColumns: ['ID'] }
const update = buildUpdateRows(params)
const upsert = buildUpsertRows(params)
expect(update.statement).toContain('ON target.id = source.id')
expect(update.statement).not.toContain('EQUAL_NULL')
expect(update.statement).toContain('WHEN MATCHED THEN UPDATE SET target.name = source.name')
expect(update.statement).not.toContain('WHEN NOT MATCHED')
expect(upsert.statement).toContain('WHEN NOT MATCHED THEN INSERT (id, name)')
expect(upsert.bindings).toEqual({
'1': { type: 'FIXED', value: '1' },
'2': { type: 'TEXT', value: 'Ada' },
})
expect(() => buildUpdateRows({ ...params, matchColumns: ['id', 'ID'] })).toThrow('duplicate')
expect(() => buildUpdateRows({ ...params, matchColumns: ['id', 'name', 'extra'] })).toThrow(
'cannot exceed'
)
expect(() => buildUpdateRows({ ...params, matchColumns: [7] as never })).toThrow(
'column name string'
)
})
it('rejects match keys a MERGE cannot resolve deterministically', () => {
expect(() =>
buildUpsertRows({
...table,
rows: [
{ id: 1, name: 'Ada' },
{ id: 1, name: 'Grace' },
],
matchColumns: ['id'],
})
).toThrow('duplicate match key values for id: number:1')
expect(() =>
buildUpsertRows({
...table,
rows: [{ id: null, name: 'Ada' }],
matchColumns: ['id'],
})
).toThrow('match column cannot be null in a row: id')
})
it('encodes composite match keys unambiguously across column boundaries', () => {
expect(() =>
buildUpsertRows({
...table,
rows: [
{ a: 'p', b: `q${SEPARATOR}string:r`, c: 1 },
{ a: `p${SEPARATOR}string:q`, b: 'r', c: 2 },
],
matchColumns: ['a', 'b'],
})
).not.toThrow()
expect(() =>
buildUpsertRows({
...table,
rows: [
{ a: '1', b: 'x', c: 1 },
{ a: 1, b: 'x', c: 2 },
],
matchColumns: ['a', 'b'],
})
).toThrow('duplicate match key values')
})
it('rejects malformed structured writes', () => {
expect(() => buildInsertRows({ ...table, rows: [] })).toThrow('non-empty')
expect(() => buildInsertRows({ ...table, rows: [{ id: 1 }, { other: 2 }] })).toThrow(
'same columns'
)
expect(() => buildInsertRows({ ...table, rows: [{ id: 1 }, { ID: 2 }] })).toThrow(
'same columns'
)
expect(() => buildInsertRows({ ...table, rows: [{ id: 1, ID: 2 }] })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(() => buildInsertRows({ ...table, rows: [{ ID: 1, '"ID"': 2 }] })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(() =>
buildInsertRows({ ...table, rows: [{ id: Number.MAX_SAFE_INTEGER + 1 }] })
).toThrow('safe integers')
expect(() =>
buildInsertRows({
...table,
rows: [{ [`"a${SEPARATOR}b"`]: 1 }, { '"a': 9, 'b"': 8 }],
})
).toThrow('same columns')
expect(() => buildInsertRows({ ...table, rows: [{ id: 1, name: 'a' }, { id: 2 }] })).toThrow(
'same columns'
)
})
it('requires delete filters and binds every filter value', () => {
expect(() => buildDeleteRows({ ...table, filters: {} })).toThrow('cannot be empty')
expect(() => buildDeleteRows({ ...table, filters: { id: 1, ID: 2 } })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(() => buildDeleteRows({ ...table, filters: { ID: 1, '"ID"': 2 } })).toThrow(
'duplicate Snowflake column identifiers'
)
expect(buildDeleteRows({ ...table, filters: { id: 7, deleted_at: null } })).toEqual({
statement: 'DELETE FROM ANALYTICS.PUBLIC.EVENTS WHERE id = ? AND deleted_at IS NULL',
bindings: { '1': { type: 'FIXED', value: '7' } },
})
})
it('builds COPY INTO with the supported stage and copy options', () => {
expect(
buildLoadData({
...table,
stagePath: '@RAW_STAGE/2026/08',
fileFormat: 'ANALYTICS.PUBLIC.CSV_FORMAT',
pattern: '.*[.]csv',
onError: 'CONTINUE',
purge: true,
force: false,
matchByColumnName: 'CASE_INSENSITIVE',
}).statement
).toBe(
"COPY INTO ANALYTICS.PUBLIC.EVENTS FROM @RAW_STAGE/2026/08 PATTERN = '.*[.]csv' FILE_FORMAT = (FORMAT_NAME = 'ANALYTICS.PUBLIC.CSV_FORMAT') ON_ERROR = 'CONTINUE' PURGE = TRUE FORCE = FALSE MATCH_BY_COLUMN_NAME = CASE_INSENSITIVE"
)
expect(() =>
buildLoadData({ ...table, stagePath: '@RAW_STAGE/--', pattern: '.*[.]csv' })
).toThrow('SQL line comment')
expect(() => buildLoadData({ ...table, stagePath: '@RAW_STAGE/a--b/c' })).toThrow(
'SQL line comment'
)
expect(() => buildLoadData({ ...table, stagePath: '@stage/path; DROP TABLE x' })).toThrow(
'stagePath'
)
expect(
buildLoadData({
...table,
stagePath: '@"Raw Stage"/daily',
fileFormat: 'ANALYTICS.PUBLIC."CSV Format"',
}).statement
).toContain(
`FROM @"Raw Stage"/daily FILE_FORMAT = (FORMAT_NAME = 'ANALYTICS.PUBLIC."CSV Format"')`
)
expect(
buildLoadData({
...table,
stagePath: '@ANALYTICS.PUBLIC.%EVENTS/daily',
onError: 'SKIP_FILE_10',
}).statement
).toContain("FROM @ANALYTICS.PUBLIC.%EVENTS/daily ON_ERROR = 'SKIP_FILE_10'")
expect(
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_25%' }).statement
).toContain("ON_ERROR = 'SKIP_FILE_25%'")
expect(
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_25%' }).statement
).toContain("ON_ERROR = 'SKIP_FILE_25%'")
expect(() =>
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_2.5%' })
).toThrow('onError')
expect(() =>
buildLoadData({ ...table, stagePath: '@RAW_STAGE', onError: 'SKIP_FILE_0%' })
).toThrow('onError')
})
it('builds warehouse statements', () => {
expect(buildListWarehouses('ETL%').statement).toBe("SHOW WAREHOUSES LIKE 'ETL%'")
expect(buildGetWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
`SHOW WAREHOUSES ->> SELECT * FROM $1 WHERE "name" = 'ETL_WH'`
)
expect(buildGetWarehouse({ ...context, warehouseName: `"etl_%'s"` }).statement).toBe(
`SHOW WAREHOUSES ->> SELECT * FROM $1 WHERE "name" = 'etl_%''s'`
)
expect(buildResumeWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
'ALTER WAREHOUSE ETL_WH RESUME IF SUSPENDED'
)
expect(buildSuspendWarehouse({ ...context, warehouseName: 'ETL_WH' }).statement).toBe(
'ALTER WAREHOUSE ETL_WH SUSPEND'
)
})
it('bounds every object listing and scopes it to the requested container', () => {
expect(buildListDatabases({ ...context, limit: 25 }).statement).toBe('SHOW DATABASES LIMIT 25')
expect(buildListDatabases({ ...context, nameLike: "AN'X%" }).statement).toBe(
"SHOW DATABASES LIKE 'AN''X%' LIMIT 1000"
)
expect(buildListSchemas({ ...context, database: 'ANALYTICS', limit: 5 }).statement).toBe(
'SHOW SCHEMAS IN DATABASE ANALYTICS LIMIT 5'
)
expect(
buildListTables({ ...context, database: 'ANALYTICS', schema: 'PUBLIC', nameLike: 'E%' })
.statement
).toBe("SHOW TABLES LIKE 'E%' IN SCHEMA ANALYTICS.PUBLIC LIMIT 1000")
expect(() => buildListTables({ ...context, database: 'A; DROP', schema: 'PUBLIC' })).toThrow(
/Invalid Snowflake identifier/
)
})
it('builds task state changes and rejects unqualifiable names', () => {
const task = { ...context, database: 'ANALYTICS', schema: 'PUBLIC', taskName: 'DAILY_LOAD' }
expect(buildResumeTask(task).statement).toBe('ALTER TASK ANALYTICS.PUBLIC.DAILY_LOAD RESUME')
expect(buildSuspendTask(task).statement).toBe('ALTER TASK ANALYTICS.PUBLIC.DAILY_LOAD SUSPEND')
expect(() => buildResumeTask({ ...task, taskName: 'A RESUME; DROP' })).toThrow(
/Invalid Snowflake identifier/
)
})
/**
* An untouched switch serializes as `null`, and in advanced mode the
* serializer emits every advanced sub-block. The generic handler merges
* `{...inputs, ...mapParams(inputs)}`, so a null survives unless the params
* function explicitly overwrites it with undefined — and the builders'
* `!== undefined` tests would then fire, turning a resize into a permanent
* `AUTO_RESUME = FALSE` on the warehouse.
*/
it('drops untouched switches instead of emitting their clauses', () => {
const mapParams = SnowflakeBlock.tools.config.params
if (!mapParams) throw new Error('Snowflake block must map tool parameters')
const merged = (params: Record<string, unknown>) => ({ ...params, ...mapParams(params) })
const altered = merged({
operation: 'alter_warehouse',
warehouseSize: '',
autoSuspendSeconds: null,
autoResume: null,
})
expect(altered.autoResume).toBeUndefined()
expect(() =>
buildAlterWarehouse({ ...context, warehouseName: 'COMPUTE_WH', ...altered })
).toThrow(/at least one of/)
const unloaded = merged({
operation: 'unload_data',
header: null,
overwrite: null,
singleFile: null,
})
expect(unloaded.header).toBeUndefined()
expect(unloaded.singleFile).toBeUndefined()
expect(
buildUnloadData({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
stagePath: '@EXPORTS/daily',
table: 'EVENTS',
...unloaded,
}).statement
).toBe('COPY INTO @EXPORTS/daily FROM ANALYTICS.PUBLIC.EVENTS OVERWRITE = FALSE')
})
it('builds warehouse alterations and rejects unsupported settings', () => {
expect(
buildAlterWarehouse({
...context,
warehouseName: 'ETL_WH',
warehouseSize: 'xlarge',
autoSuspendSeconds: 0,
autoResume: false,
}).statement
).toBe(
'ALTER WAREHOUSE ETL_WH SET WAREHOUSE_SIZE = XLARGE AUTO_SUSPEND = 0 AUTO_RESUME = FALSE'
)
// Every documented spelling of a size normalizes to the bare keyword.
for (const spelling of ['2X-LARGE', "'2X-LARGE'", 'X2LARGE', 'xxlarge']) {
expect(
buildAlterWarehouse({ ...context, warehouseName: 'ETL_WH', warehouseSize: spelling })
.statement
).toBe('ALTER WAREHOUSE ETL_WH SET WAREHOUSE_SIZE = XXLARGE')
}
expect(() =>
buildAlterWarehouse({ ...context, warehouseName: 'ETL_WH', warehouseSize: 'HUGE' })
).toThrow(/warehouseSize must be one of/)
expect(() =>
buildAlterWarehouse({ ...context, warehouseName: 'ETL_WH', autoSuspendSeconds: -1 })
).toThrow(/autoSuspendSeconds/)
// Snowflake documents no upper bound, so a long idle window must be allowed.
expect(
buildAlterWarehouse({ ...context, warehouseName: 'ETL_WH', autoSuspendSeconds: 900_000 })
.statement
).toBe('ALTER WAREHOUSE ETL_WH SET AUTO_SUSPEND = 900000')
expect(() => buildAlterWarehouse({ ...context, warehouseName: 'ETL_WH' })).toThrow(
/at least one of/
)
})
/**
* The source is a table name, never an inline query. An inlined query sits
* directly before the copy-option slot, so anything escaping its parentheses
* becomes a copy clause — a guard for that has to match Snowflake's tokenizer
* exactly, and three versions of one were each defeated. `qualifiedIdentifier`
* removes the class instead of re-guarding it.
*/
it('unloads a table and orders the COPY INTO clauses', () => {
const base = {
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
stagePath: '@EXPORTS/daily',
}
expect(
buildUnloadData({
...base,
table: 'EVENTS',
fileFormat: 'ANALYTICS.PUBLIC.CSV_FORMAT',
header: true,
overwrite: false,
singleFile: true,
maxFileSizeBytes: 16_777_216,
}).statement
).toBe(
// OVERWRITE/SINGLE/MAX_FILE_SIZE are all copyOptions members, so their
// order among themselves is free; HEADER must stay last.
"COPY INTO @EXPORTS/daily FROM ANALYTICS.PUBLIC.EVENTS FILE_FORMAT = (FORMAT_NAME = 'ANALYTICS.PUBLIC.CSV_FORMAT') OVERWRITE = FALSE SINGLE = TRUE MAX_FILE_SIZE = 16777216 HEADER = TRUE"
)
// OVERWRITE is always emitted so the option can never be left to a default.
expect(buildUnloadData({ ...base, table: 'EVENTS' }).statement).toBe(
'COPY INTO @EXPORTS/daily FROM ANALYTICS.PUBLIC.EVENTS OVERWRITE = FALSE'
)
expect(() => buildUnloadData({ ...base, table: '' })).toThrow(/table is required/)
// No SQL text can reach the statement, so no breakout is expressible.
expect(() =>
buildUnloadData({ ...base, table: 'EVENTS) OVERWRITE = TRUE FILE_FORMAT = (TYPE = CSV' })
).toThrow(/Invalid Snowflake identifier/)
expect(() =>
buildUnloadData({ ...base, table: 'EVENTS', maxFileSizeBytes: 5_368_709_121 })
).toThrow(/maxFileSizeBytes/)
})
it('emits history filters as literals so none can be silently dropped', () => {
expect(
buildListQueryHistory({
...context,
limit: 50,
userName: 'analyst_svc',
startTime: historyStart,
endTime: historyEnd,
errorOnly: true,
})
).toEqual({
statement: `SELECT * FROM TABLE(SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY_BY_USER(RESULT_LIMIT => 50, USER_NAME => 'ANALYST_SVC', END_TIME_RANGE_START => TO_TIMESTAMP_LTZ('${historyStart}'), END_TIME_RANGE_END => TO_TIMESTAMP_LTZ('${historyEnd}'))) WHERE UPPER(EXECUTION_STATUS) IN ('FAILED_WITH_ERROR', 'FAILED_WITH_INCIDENT') ORDER BY END_TIME DESC`,
})
expect(buildListQueryHistory({ ...context, warehouseName: 'ETL_WH' }).statement).toContain(
"SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY_BY_WAREHOUSE(RESULT_LIMIT => 1000, WAREHOUSE_NAME => 'ETL_WH')"
)
expect(buildListQueryHistory({ ...context }).statement).toContain(
'SNOWFLAKE.INFORMATION_SCHEMA.QUERY_HISTORY(RESULT_LIMIT => 1000)'
)
expect(() => buildListQueryHistory({ ...context, userName: 'A', warehouseName: 'B' })).toThrow(
/by user or by warehouse/
)
expect(() => buildListQueryHistory({ ...context, startTime: 'yesterday' })).toThrow(
/ISO-8601 timestamp within the last 7 days/
)
// The retention window is enforced, not just advertised in the message.
expect(() => buildListQueryHistory({ ...context, startTime: '2020-01-01T00:00:00Z' })).toThrow(
/within the last 7 days/
)
})
it('requires a start time for copy history and bounds it outside the table function', () => {
expect(
buildListCopyHistory({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
table: 'EVENTS',
startTime: copyStart,
limit: 20,
}).statement
).toBe(
`SELECT * FROM TABLE(ANALYTICS.INFORMATION_SCHEMA.COPY_HISTORY(TABLE_NAME => 'ANALYTICS.PUBLIC.EVENTS', START_TIME => TO_TIMESTAMP_LTZ('${copyStart}'))) ORDER BY LAST_LOAD_TIME DESC LIMIT 20`
)
expect(() =>
buildListCopyHistory({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
table: 'EVENTS',
startTime: 'last tuesday',
})
).toThrow(/ISO-8601 timestamp within the last 14 days/)
expect(() =>
buildListCopyHistory({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
table: 'EVENTS',
startTime: '2020-01-01T00:00:00Z',
})
).toThrow(/within the last 14 days/)
})
it('builds task definition and execution statements', () => {
const task = { ...context, database: 'ANALYTICS', schema: 'PUBLIC', taskName: 'DAILY_LOAD' }
expect(buildListTasks({ ...task, limit: 25, nameLike: 'DAILY%' }).statement).toBe(
"SHOW TASKS LIKE 'DAILY%' IN SCHEMA ANALYTICS.PUBLIC LIMIT 25"
)
expect(buildGetTask(task).statement).toBe('DESCRIBE TASK ANALYTICS.PUBLIC.DAILY_LOAD')
expect(buildRunTask({ ...task, retryLast: true }).statement).toBe(
'EXECUTE TASK ANALYTICS.PUBLIC.DAILY_LOAD RETRY LAST'
)
})
it('builds bounded task history, run lookup, cancellation, and output statements', () => {
const history = buildListTaskRuns({
...context,
taskName: 'DAILY_LOAD',
startTime: historyStart,
errorOnly: true,
limit: 50,
})
expect(history.statement).toContain('RESULT_LIMIT => 50, ERROR_ONLY => TRUE')
expect(history.bindings).toEqual({ '1': { type: 'TEXT', value: 'DAILY_LOAD' } })
const run = buildGetTaskRun({
...context,
queryId,
taskName: 'DAILY_LOAD',
startTime: historyStart,
})
expect(run.statement).toContain('TASK_NAME => ?')
expect(run.statement).toContain('WHERE QUERY_ID = ?')
expect(run.bindings).toEqual({
'1': { type: 'TEXT', value: 'DAILY_LOAD' },
'2': { type: 'TEXT', value: queryId },
})
expect(() =>
buildListTaskRuns({ ...context, taskName: 'ANALYTICS.PUBLIC.DAILY_LOAD' })
).toThrow('unqualified task name')
/**
* TASK_HISTORY silently drops a bind in its time-range arguments, so the window has to
* reach Snowflake as a literal or the filter becomes a no-op with no error.
*/
const window = buildListTaskRuns({
...context,
startTime: historyStart,
endTime: historyEnd,
})
expect(window.statement).toContain(
`SCHEDULED_TIME_RANGE_START => TO_TIMESTAMP_LTZ('${historyStart}')`
)
expect(window.statement).toContain(
`SCHEDULED_TIME_RANGE_END => TO_TIMESTAMP_LTZ('${historyEnd}')`
)
expect(window.statement).not.toContain('TO_TIMESTAMP_LTZ(?)')
expect(window.bindings).toEqual({})
expect(() => buildListTaskRuns({ ...context, startTime: 'not-a-timestamp' })).toThrow(
'startTime must be an ISO-8601 timestamp'
)
expect(() => buildGetTaskRun({ ...context, queryId, endTime: 'nope' })).toThrow(
'endTime must be an ISO-8601 timestamp'
)
expect(() =>
buildGetTaskRun({ ...context, queryId, taskName: 'ANALYTICS.PUBLIC.DAILY_LOAD' })
).toThrow('unqualified task name')
expect(buildListTaskRuns({ ...context, taskName: 'daily_load' }).bindings).toMatchObject({
'1': { type: 'TEXT', value: 'DAILY_LOAD' },
})
expect(buildListTaskRuns({ ...context, taskName: '"my.task"' }).bindings).toMatchObject({
'1': { type: 'TEXT', value: 'my.task' },
})
expect(buildGetTaskRun({ ...context, queryId, taskName: '"my.task"' }).bindings).toMatchObject({
'1': { type: 'TEXT', value: 'my.task' },
})
expect(buildCancelTaskRun({ ...context, queryId }).statement).toContain('SYSTEM$CANCEL_QUERY')
expect(buildGetTaskRunOutput({ ...context, queryId }).statement).toContain('RESULT_SCAN')
expect(() => buildGetTaskRun({ ...context, queryId: "x' OR TRUE" })).toThrow('UUID')
})
it('builds bound schema introspection and typed procedure calls', () => {
const schema = buildIntrospectSchema({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
table: 'EVENTS',
})
expect(schema.statement).toContain('ANALYTICS.INFORMATION_SCHEMA.COLUMNS')
expect(schema.statement).toContain("t.TABLE_TYPE NOT IN ('VIEW', 'MATERIALIZED VIEW')")
expect(schema.statement).not.toContain("t.TABLE_TYPE = 'BASE TABLE'")
expect(schema.bindings).toEqual({
'1': { type: 'TEXT', value: 'PUBLIC' },
'2': { type: 'TEXT', value: 'EVENTS' },
})
expect(
buildIntrospectSchema({
...context,
database: 'analytics',
schema: 'public',
table: 'events',
}).bindings
).toEqual({
'1': { type: 'TEXT', value: 'PUBLIC' },
'2': { type: 'TEXT', value: 'EVENTS' },
})
expect(
buildIntrospectSchema({
...context,
database: '"Analytics DB"',
schema: '"Mixed Schema"',
table: '"events"',
}).bindings
).toEqual({
'1': { type: 'TEXT', value: 'Mixed Schema' },
'2': { type: 'TEXT', value: 'events' },
})
expect(
buildCallProcedure({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
procedureName: 'REFRESH_MODEL',
procedureArguments: [
{ type: 'TEXT', value: 'daily' },
{ type: 'BOOLEAN', value: 'true' },
],
})
).toEqual({
statement: 'CALL ANALYTICS.PUBLIC.REFRESH_MODEL(?, ?)',
bindings: {
'1': { type: 'TEXT', value: 'daily' },
'2': { type: 'BOOLEAN', value: 'true' },
},
})
expect(() =>
buildCallProcedure({
...context,
database: 'ANALYTICS',
schema: 'PUBLIC',
procedureName: 'REFRESH_MODEL',
procedureArguments: { type: 'TEXT', value: 'x' } as never,
})
).toThrow('JSON array')
})
})