@fabricorg/databricks-testkit 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1256,6 +1256,213 @@ function autoLoaderIngestionCheck(options = {}) {
1256
1256
  }
1257
1257
  };
1258
1258
  }
1259
+ function safeName(prefix) {
1260
+ return `${prefix}_${crypto.randomUUID().replaceAll("-", "_")}`;
1261
+ }
1262
+ function performanceBudgetCheck() {
1263
+ return {
1264
+ id: "performance",
1265
+ description: "SQL Statement Execution p95 stays within the configured latency budget",
1266
+ configured: (env) => Boolean(env.DBX_TEST_PERFORMANCE_P95_MS),
1267
+ async run({ env, warehouse }) {
1268
+ const budgetMs = Number(env.DBX_TEST_PERFORMANCE_P95_MS);
1269
+ const runs = Number(env.DBX_TEST_PERFORMANCE_RUNS ?? 7);
1270
+ if (!Number.isFinite(budgetMs) || budgetMs <= 0) {
1271
+ throw new Error("DBX_TEST_PERFORMANCE_P95_MS must be a positive number");
1272
+ }
1273
+ if (!Number.isInteger(runs) || runs < 3 || runs > 100) {
1274
+ throw new Error("DBX_TEST_PERFORMANCE_RUNS must be an integer from 3 to 100");
1275
+ }
1276
+ await warehouse.query("SELECT 1 AS warmup");
1277
+ const durations = [];
1278
+ for (let index = 0; index < runs; index += 1) {
1279
+ const started = performance.now();
1280
+ await warehouse.query("SELECT 1 AS performance_probe");
1281
+ durations.push(performance.now() - started);
1282
+ }
1283
+ durations.sort((left, right) => left - right);
1284
+ const p95 = durations[Math.ceil(durations.length * 0.95) - 1] ?? 0;
1285
+ if (p95 > budgetMs) {
1286
+ throw new Error(`SQL p95 ${Math.round(p95)}ms exceeded ${budgetMs}ms budget`);
1287
+ }
1288
+ return {
1289
+ runs,
1290
+ p50Ms: Math.round(durations[Math.floor(durations.length * 0.5)] ?? 0),
1291
+ p95Ms: Math.round(p95),
1292
+ budgetMs
1293
+ };
1294
+ }
1295
+ };
1296
+ }
1297
+ function failureRecoveryCheck() {
1298
+ return {
1299
+ id: "failure-recovery",
1300
+ description: "A failed statement is isolated and a subsequent statement recovers",
1301
+ async run({ warehouse }) {
1302
+ let failedAsExpected = false;
1303
+ try {
1304
+ await warehouse.query("SELECT * FROM __fabric_experiments_expected_missing_table__");
1305
+ } catch {
1306
+ failedAsExpected = true;
1307
+ }
1308
+ if (!failedAsExpected) throw new Error("Injected failure unexpectedly succeeded");
1309
+ const rows = await warehouse.query("SELECT 1 AS recovered");
1310
+ if (Number(rows[0]?.recovered) !== 1) {
1311
+ throw new Error("Statement execution did not recover after the injected failure");
1312
+ }
1313
+ return { injectedFailure: true, recovered: true };
1314
+ }
1315
+ };
1316
+ }
1317
+ function secretRotationCheck(options = {}) {
1318
+ return {
1319
+ id: "secret-rotation",
1320
+ description: "Databricks secret put, overwrite, metadata verification, and cleanup",
1321
+ configured: (env) => Boolean(env.DBX_TEST_ROTATION_SECRET_SCOPE),
1322
+ async run({ env, client }) {
1323
+ const scope = env.DBX_TEST_ROTATION_SECRET_SCOPE;
1324
+ const key = options.randomId?.() ?? safeName("fx_rotation_probe");
1325
+ const put = async (value) => client.post("/api/2.0/secrets/put", { scope, key, string_value: value });
1326
+ try {
1327
+ await put(crypto.randomBytes(32).toString("base64url"));
1328
+ await put(crypto.randomBytes(32).toString("base64url"));
1329
+ const listed = await client.get("/api/2.0/secrets/list", {
1330
+ scope
1331
+ });
1332
+ if (!(listed.secrets ?? []).some((secret) => secret.key === key)) {
1333
+ throw new Error(`Rotated secret metadata was not visible in scope ${scope}`);
1334
+ }
1335
+ return { scope, rotated: true, cleanedUp: true };
1336
+ } finally {
1337
+ await client.post("/api/2.0/secrets/delete", { scope, key }).catch(() => void 0);
1338
+ }
1339
+ }
1340
+ };
1341
+ }
1342
+ function backupRestoreCheck() {
1343
+ return {
1344
+ id: "backup-restore",
1345
+ description: "Delta DEEP CLONE backup restores a dropped scratch table",
1346
+ async run({ warehouse }) {
1347
+ const source = warehouse.qual(safeName("backup_source"));
1348
+ const backup = warehouse.qual(safeName("backup_clone"));
1349
+ try {
1350
+ await warehouse.exec(`CREATE TABLE ${source} USING DELTA AS SELECT 7 AS value`);
1351
+ await warehouse.exec(`CREATE TABLE ${backup} DEEP CLONE ${source}`);
1352
+ await warehouse.exec(`DROP TABLE ${source}`);
1353
+ await warehouse.exec(`CREATE TABLE ${source} DEEP CLONE ${backup}`);
1354
+ const rows = await warehouse.query(`SELECT value FROM ${source}`);
1355
+ if (Number(rows[0]?.value) !== 7) throw new Error("Restored Delta table lost its data");
1356
+ return { restoredRows: rows.length, deepClone: true };
1357
+ } finally {
1358
+ await warehouse.exec(`DROP TABLE IF EXISTS ${source}`).catch(() => void 0);
1359
+ await warehouse.exec(`DROP TABLE IF EXISTS ${backup}`).catch(() => void 0);
1360
+ }
1361
+ }
1362
+ };
1363
+ }
1364
+ function rollbackCheck() {
1365
+ return {
1366
+ id: "rollback",
1367
+ description: "Delta time-travel rollback restores a known-good table version",
1368
+ async run({ warehouse }) {
1369
+ const table = warehouse.qual(safeName("rollback_probe"));
1370
+ try {
1371
+ await warehouse.exec(`CREATE TABLE ${table} (value INT) USING DELTA`);
1372
+ await warehouse.exec(`INSERT INTO ${table} VALUES (1)`);
1373
+ const history = await warehouse.query(`DESCRIBE HISTORY ${table} LIMIT 1`);
1374
+ const knownGoodVersion = Number(history[0]?.version);
1375
+ if (!Number.isInteger(knownGoodVersion)) {
1376
+ throw new Error("Delta history did not return a known-good version");
1377
+ }
1378
+ await warehouse.exec(`INSERT INTO ${table} VALUES (999)`);
1379
+ await warehouse.exec(`RESTORE TABLE ${table} TO VERSION AS OF ${knownGoodVersion}`);
1380
+ const rows = await warehouse.query(`SELECT value FROM ${table} ORDER BY value`);
1381
+ if (rows.length !== 1 || Number(rows[0]?.value) !== 1) {
1382
+ throw new Error("Delta rollback did not restore the known-good contents");
1383
+ }
1384
+ return { knownGoodVersion, restoredRows: rows.length };
1385
+ } finally {
1386
+ await warehouse.exec(`DROP TABLE IF EXISTS ${table}`).catch(() => void 0);
1387
+ }
1388
+ }
1389
+ };
1390
+ }
1391
+ function serverlessComputeCheck() {
1392
+ return {
1393
+ id: "azure-serverless",
1394
+ description: "Azure Databricks serverless SQL warehouse is enabled and executes SQL",
1395
+ configured: (env) => Boolean(env.DATABRICKS_WAREHOUSE_ID),
1396
+ async run({ env, client, warehouse }) {
1397
+ const id = env.DATABRICKS_WAREHOUSE_ID;
1398
+ const details = await client.get(`/api/2.0/sql/warehouses/${encodeURIComponent(id)}`);
1399
+ if (details.enable_serverless_compute !== true) {
1400
+ throw new Error(`SQL warehouse ${id} is not configured for serverless compute`);
1401
+ }
1402
+ const rows = await warehouse.query("SELECT current_catalog() AS catalog");
1403
+ return { warehouseId: id, name: details.name, state: details.state, rows: rows.length };
1404
+ }
1405
+ };
1406
+ }
1407
+ function classicComputeCheck() {
1408
+ return {
1409
+ id: "azure-classic",
1410
+ description: "Azure Databricks classic job cluster starts and completes a notebook workload",
1411
+ configured: (env) => Boolean(
1412
+ env.DBX_TEST_CLASSIC_SPARK_VERSION && env.DBX_TEST_CLASSIC_NODE_TYPE && env.DBX_TEST_NOTEBOOK_PATH
1413
+ ),
1414
+ async run({ env, client }) {
1415
+ const numWorkers = Number(env.DBX_TEST_CLASSIC_WORKERS ?? 1);
1416
+ const alternateNodeTypes = (env.DBX_TEST_CLASSIC_ALTERNATE_NODE_TYPES ?? "").split(",").map((value) => value.trim()).filter(Boolean);
1417
+ const singleNode = numWorkers === 0;
1418
+ const submitted = await client.post("/api/2.1/jobs/runs/submit", {
1419
+ run_name: `fabric-experiments-classic-certification-${Date.now()}`,
1420
+ tasks: [
1421
+ {
1422
+ task_key: "classic_notebook",
1423
+ new_cluster: {
1424
+ spark_version: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1425
+ node_type_id: env.DBX_TEST_CLASSIC_NODE_TYPE,
1426
+ num_workers: numWorkers,
1427
+ data_security_mode: env.DBX_TEST_CLASSIC_SECURITY_MODE ?? "SINGLE_USER",
1428
+ ...env.DBX_TEST_CLASSIC_SINGLE_USER_NAME ? { single_user_name: env.DBX_TEST_CLASSIC_SINGLE_USER_NAME } : {},
1429
+ ...singleNode ? {
1430
+ spark_conf: {
1431
+ "spark.databricks.cluster.profile": "singleNode",
1432
+ "spark.master": "local[*]"
1433
+ },
1434
+ custom_tags: { ResourceClass: "SingleNode" }
1435
+ } : {},
1436
+ ...alternateNodeTypes.length ? {
1437
+ worker_node_type_flexibility: {
1438
+ alternate_node_type_ids: alternateNodeTypes
1439
+ },
1440
+ driver_node_type_flexibility: {
1441
+ alternate_node_type_ids: alternateNodeTypes
1442
+ }
1443
+ } : {}
1444
+ },
1445
+ notebook_task: { notebook_path: env.DBX_TEST_NOTEBOOK_PATH }
1446
+ }
1447
+ ]
1448
+ });
1449
+ if (!submitted.run_id) throw new Error("Classic compute submit returned no run_id");
1450
+ const run = await databricks.waitForDatabricksRun(client, submitted.run_id, {
1451
+ timeoutMs: Number(env.DBX_TEST_CLASSIC_TIMEOUT_MS ?? 18e5),
1452
+ pollIntervalMs: Number(env.DBX_TEST_POLL_INTERVAL_MS ?? 15e3)
1453
+ });
1454
+ const result = run.state?.result_state;
1455
+ if (result !== "SUCCESS") {
1456
+ throw new Error(`Classic compute run ended in ${result ?? "UNKNOWN"}`);
1457
+ }
1458
+ return {
1459
+ runId: submitted.run_id,
1460
+ sparkVersion: env.DBX_TEST_CLASSIC_SPARK_VERSION,
1461
+ nodeType: env.DBX_TEST_CLASSIC_NODE_TYPE
1462
+ };
1463
+ }
1464
+ };
1465
+ }
1259
1466
 
1260
1467
  Object.defineProperty(exports, "waitForDatabricksRun", {
1261
1468
  enumerable: true,
@@ -1268,6 +1475,8 @@ Object.defineProperty(exports, "waitForDatabricksStatement", {
1268
1475
  exports.appHealthCheck = appHealthCheck;
1269
1476
  exports.assertDeltaContract = assertDeltaContract;
1270
1477
  exports.autoLoaderIngestionCheck = autoLoaderIngestionCheck;
1478
+ exports.backupRestoreCheck = backupRestoreCheck;
1479
+ exports.classicComputeCheck = classicComputeCheck;
1271
1480
  exports.compareToGolden = compareToGolden;
1272
1481
  exports.createClientFromEnv = createClientFromEnv;
1273
1482
  exports.createDuckDbContext = createDuckDbContext;
@@ -1285,6 +1494,7 @@ exports.deltaContractCheck = deltaContractCheck;
1285
1494
  exports.ensureVolumeDirectory = ensureVolumeDirectory;
1286
1495
  exports.expectQueryToMatchGolden = expectQueryToMatchGolden;
1287
1496
  exports.expectTable = expectTable;
1497
+ exports.failureRecoveryCheck = failureRecoveryCheck;
1288
1498
  exports.jobRunCheck = jobRunCheck;
1289
1499
  exports.lakebaseCredentialCheck = lakebaseCredentialCheck;
1290
1500
  exports.lakebaseRoundTripCheck = lakebaseRoundTripCheck;
@@ -1293,6 +1503,7 @@ exports.normalizeVolumeRoot = normalizeVolumeRoot;
1293
1503
  exports.notebookSubmitCheck = notebookSubmitCheck;
1294
1504
  exports.parseFixtureColumns = parseFixtureColumns;
1295
1505
  exports.parseStatementRows = parseStatementRows;
1506
+ exports.performanceBudgetCheck = performanceBudgetCheck;
1296
1507
  exports.pipelineRefreshCheck = pipelineRefreshCheck;
1297
1508
  exports.renderJUnit = renderJUnit;
1298
1509
  exports.resolveFixtureRows = resolveFixtureRows;
@@ -1300,8 +1511,11 @@ exports.resolveLakebaseProject = resolveLakebaseProject;
1300
1511
  exports.resolveProfile = resolveProfile;
1301
1512
  exports.resolveTokenProvider = resolveTokenProvider;
1302
1513
  exports.restrictedPrincipalCheck = restrictedPrincipalCheck;
1514
+ exports.rollbackCheck = rollbackCheck;
1303
1515
  exports.runLiveSuite = runLiveSuite;
1516
+ exports.secretRotationCheck = secretRotationCheck;
1304
1517
  exports.secretScopeCheck = secretScopeCheck;
1518
+ exports.serverlessComputeCheck = serverlessComputeCheck;
1305
1519
  exports.sqlRoundTripCheck = sqlRoundTripCheck;
1306
1520
  exports.toLakebaseBranchId = toLakebaseBranchId;
1307
1521
  exports.toNamedParameters = toNamedParameters;