From 7f8441777c019edca2de195d342e45f6458c1ae2 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 2 Apr 2026 01:25:49 -0700 Subject: [PATCH 01/11] fix: use synchronous DuckDB constructor to avoid bun runtime timeout Bun's runtime never fires native addon async callbacks, so the async `new duckdb.Database(path, opts, callback)` form would hit the 2-second timeout fallback on every connection attempt. Switch to the synchronous constructor form `new duckdb.Database(path)` / `new duckdb.Database(path, opts)` which throws on error and completes immediately in both Node and bun runtimes. Co-Authored-By: Claude Sonnet 4.6 --- packages/drivers/src/duckdb.ts | 51 +++++++++++----------------------- 1 file changed, 16 insertions(+), 35 deletions(-) diff --git a/packages/drivers/src/duckdb.ts b/packages/drivers/src/duckdb.ts index 3ccca467a..f0209b664 100644 --- a/packages/drivers/src/duckdb.ts +++ b/packages/drivers/src/duckdb.ts @@ -51,47 +51,28 @@ export async function connect(config: ConnectionConfig): Promise { return { async connect() { - // altimate_change start — retry with read-only on lock errors - const tryConnect = (accessMode?: string): Promise => - new Promise((resolve, reject) => { - let resolved = false - let timeout: ReturnType | undefined - const opts = accessMode ? { access_mode: accessMode } : undefined - const instance = new duckdb.Database( - dbPath, - opts, - (err: Error | null) => { - if (resolved) { if (instance && typeof instance.close === "function") instance.close(); return } - resolved = true - if (timeout) clearTimeout(timeout) - if (err) { - const msg = err.message || String(err) - if (msg.toLowerCase().includes("locked") || msg.includes("SQLITE_BUSY") || msg.includes("DUCKDB_LOCKED")) { - reject(new Error("DUCKDB_LOCKED")) - } else { - reject(err) - } - } else { - resolve(instance) - } - }, - ) - // Bun: native callback may not fire; fall back after 2s - timeout = setTimeout(() => { - if (!resolved) { - resolved = true - reject(new Error(`Timed out opening DuckDB database "${dbPath}"`)) - } - }, 2000) - }) + // altimate_change start — use synchronous constructor; bun's runtime never fires + // async native callbacks, causing a 2s timeout. The sync form throws on error. + const tryConnect = (accessMode?: string): any => { + const opts = accessMode ? { access_mode: accessMode } : undefined + try { + return opts ? new duckdb.Database(dbPath, opts) : new duckdb.Database(dbPath) + } catch (err: any) { + const msg = (err as Error).message || String(err) + if (msg.toLowerCase().includes("locked") || msg.includes("SQLITE_BUSY") || msg.includes("DUCKDB_LOCKED")) { + throw new Error("DUCKDB_LOCKED") + } + throw err + } + } try { - db = await tryConnect() + db = tryConnect() } catch (err: any) { if (err.message === "DUCKDB_LOCKED" && dbPath !== ":memory:") { // Retry in read-only mode — allows concurrent reads try { - db = await tryConnect("READ_ONLY") + db = tryConnect("READ_ONLY") } catch (retryErr) { throw wrapDuckDBError( retryErr instanceof Error ? retryErr : new Error(String(retryErr)), From 6a9389de2f7bf75ed216e3861fdc5c21b58ae9d1 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Thu, 2 Apr 2026 01:41:25 -0700 Subject: [PATCH 02/11] =?UTF-8?q?revert:=20restore=20async=20DuckDB=20cons?= =?UTF-8?q?tructor=20=E2=80=94=20sync=20change=20was=20bogus?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The async callback form with 2s fallback was already working correctly at e3df5a47a. The timeout was caused by a missing duckdb .node binary, not a bun incompatibility. Co-Authored-By: Claude Sonnet 4.6 --- packages/drivers/src/duckdb.ts | 51 +++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/drivers/src/duckdb.ts b/packages/drivers/src/duckdb.ts index f0209b664..3ccca467a 100644 --- a/packages/drivers/src/duckdb.ts +++ b/packages/drivers/src/duckdb.ts @@ -51,28 +51,47 @@ export async function connect(config: ConnectionConfig): Promise { return { async connect() { - // altimate_change start — use synchronous constructor; bun's runtime never fires - // async native callbacks, causing a 2s timeout. The sync form throws on error. - const tryConnect = (accessMode?: string): any => { - const opts = accessMode ? { access_mode: accessMode } : undefined - try { - return opts ? new duckdb.Database(dbPath, opts) : new duckdb.Database(dbPath) - } catch (err: any) { - const msg = (err as Error).message || String(err) - if (msg.toLowerCase().includes("locked") || msg.includes("SQLITE_BUSY") || msg.includes("DUCKDB_LOCKED")) { - throw new Error("DUCKDB_LOCKED") - } - throw err - } - } + // altimate_change start — retry with read-only on lock errors + const tryConnect = (accessMode?: string): Promise => + new Promise((resolve, reject) => { + let resolved = false + let timeout: ReturnType | undefined + const opts = accessMode ? { access_mode: accessMode } : undefined + const instance = new duckdb.Database( + dbPath, + opts, + (err: Error | null) => { + if (resolved) { if (instance && typeof instance.close === "function") instance.close(); return } + resolved = true + if (timeout) clearTimeout(timeout) + if (err) { + const msg = err.message || String(err) + if (msg.toLowerCase().includes("locked") || msg.includes("SQLITE_BUSY") || msg.includes("DUCKDB_LOCKED")) { + reject(new Error("DUCKDB_LOCKED")) + } else { + reject(err) + } + } else { + resolve(instance) + } + }, + ) + // Bun: native callback may not fire; fall back after 2s + timeout = setTimeout(() => { + if (!resolved) { + resolved = true + reject(new Error(`Timed out opening DuckDB database "${dbPath}"`)) + } + }, 2000) + }) try { - db = tryConnect() + db = await tryConnect() } catch (err: any) { if (err.message === "DUCKDB_LOCKED" && dbPath !== ":memory:") { // Retry in read-only mode — allows concurrent reads try { - db = tryConnect("READ_ONLY") + db = await tryConnect("READ_ONLY") } catch (retryErr) { throw wrapDuckDBError( retryErr instanceof Error ? retryErr : new Error(String(retryErr)), From 5bbf29b794a746c8f2001d73dc7035ffa0afc28c Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Mon, 6 Apr 2026 12:43:37 -0700 Subject: [PATCH 03/11] feat: add MSSQL/Fabric dialect mapping and data-parity support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add `warehouseTypeToDialect()` mapping: sqlserver→tsql, mssql→tsql, fabric→fabric, postgresql→postgres, mariadb→mysql. Fixes critical serde mismatch where Rust engine rejects raw warehouse type names. - Update both `resolveDialect()` functions to use the mapping - Add MSSQL/Fabric cases to `dateTruncExpr()` — DATETRUNC(DAY, col) - Add locale-safe date literal casting via CONVERT(DATE, ..., 23) - Register `fabric` in DRIVER_MAP (reuses sqlserver TDS driver) - Add `fabric` normalize aliases in normalize.ts - Add 15 SQL Server driver unit tests (TOP injection, truncation, schema introspection, connection lifecycle, result format) - Add 9 dialect mapping unit tests Co-Authored-By: Claude Opus 4.6 --- packages/drivers/src/normalize.ts | 1 + packages/drivers/test/sqlserver-unit.test.ts | 211 ++++++++++++++++++ .../altimate/native/connections/data-diff.ts | 49 +++- .../altimate/native/connections/registry.ts | 1 + .../test/altimate/data-diff-dialect.test.ts | 55 +++++ 5 files changed, 311 insertions(+), 6 deletions(-) create mode 100644 packages/drivers/test/sqlserver-unit.test.ts create mode 100644 packages/opencode/test/altimate/data-diff-dialect.test.ts diff --git a/packages/drivers/src/normalize.ts b/packages/drivers/src/normalize.ts index 5afc20cee..71667c044 100644 --- a/packages/drivers/src/normalize.ts +++ b/packages/drivers/src/normalize.ts @@ -104,6 +104,7 @@ const DRIVER_ALIASES: Record = { mariadb: MYSQL_ALIASES, sqlserver: SQLSERVER_ALIASES, mssql: SQLSERVER_ALIASES, + fabric: SQLSERVER_ALIASES, oracle: ORACLE_ALIASES, mongodb: MONGODB_ALIASES, mongo: MONGODB_ALIASES, diff --git a/packages/drivers/test/sqlserver-unit.test.ts b/packages/drivers/test/sqlserver-unit.test.ts new file mode 100644 index 000000000..4a2e38ad8 --- /dev/null +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -0,0 +1,211 @@ +/** + * Unit tests for SQL Server driver logic: + * - TOP injection (vs LIMIT) + * - Truncation detection + * - Schema introspection queries + * - Connection lifecycle + * - Result format mapping + */ +import { describe, test, expect, mock, beforeEach } from "bun:test" + +// --- Mock mssql --- + +let mockQueryCalls: string[] = [] +let mockQueryResult: any = { recordset: [] } +let mockConnectCalls: any[] = [] +let mockCloseCalls = 0 +let mockInputs: Array<{ name: string; value: any }> = [] + +function resetMocks() { + mockQueryCalls = [] + mockQueryResult = { recordset: [] } + mockConnectCalls = [] + mockCloseCalls = 0 + mockInputs = [] +} + +function createMockRequest() { + const req: any = { + input(name: string, value: any) { + mockInputs.push({ name, value }) + return req + }, + async query(sql: string) { + mockQueryCalls.push(sql) + return mockQueryResult + }, + } + return req +} + +mock.module("mssql", () => ({ + default: { + connect: async (config: any) => { + mockConnectCalls.push(config) + return { + request: () => createMockRequest(), + close: async () => { + mockCloseCalls++ + }, + } + }, + }, +})) + +// Import after mocking +const { connect } = await import("../src/sqlserver") + +describe("SQL Server driver unit tests", () => { + let connector: Awaited> + + beforeEach(async () => { + resetMocks() + connector = await connect({ host: "localhost", port: 1433, database: "testdb", user: "sa", password: "pass" }) + await connector.connect() + }) + + // --- TOP injection --- + + describe("TOP injection", () => { + test("injects TOP for SELECT without one", async () => { + mockQueryResult = { recordset: [{ id: 1, name: "a" }] } + await connector.execute("SELECT * FROM t") + expect(mockQueryCalls[0]).toContain("TOP 1001") + }) + + test("does NOT double-TOP when TOP already present", async () => { + mockQueryResult = { recordset: [{ id: 1 }] } + await connector.execute("SELECT TOP 5 * FROM t") + expect(mockQueryCalls[0]).toBe("SELECT TOP 5 * FROM t") + }) + + test("does NOT inject TOP when LIMIT present", async () => { + mockQueryResult = { recordset: [] } + await connector.execute("SELECT * FROM t LIMIT 10") + expect(mockQueryCalls[0]).toBe("SELECT * FROM t LIMIT 10") + }) + + test("noLimit bypasses TOP injection", async () => { + mockQueryResult = { recordset: [] } + await connector.execute("SELECT * FROM t", undefined, undefined, { noLimit: true }) + expect(mockQueryCalls[0]).toBe("SELECT * FROM t") + }) + + test("uses custom limit value", async () => { + mockQueryResult = { recordset: [] } + await connector.execute("SELECT * FROM t", 50) + expect(mockQueryCalls[0]).toContain("TOP 51") + }) + + test("default limit is 1000", async () => { + mockQueryResult = { recordset: [] } + await connector.execute("SELECT * FROM t") + expect(mockQueryCalls[0]).toContain("TOP 1001") + }) + }) + + // --- Truncation --- + + describe("truncation detection", () => { + test("detects truncation when rows exceed limit", async () => { + const rows = Array.from({ length: 11 }, (_, i) => ({ id: i })) + mockQueryResult = { recordset: rows } + const result = await connector.execute("SELECT * FROM t", 10) + expect(result.truncated).toBe(true) + expect(result.rows.length).toBe(10) + }) + + test("no truncation when rows at or below limit", async () => { + mockQueryResult = { recordset: [{ id: 1 }, { id: 2 }] } + const result = await connector.execute("SELECT * FROM t", 10) + expect(result.truncated).toBe(false) + }) + + test("empty result returns correctly", async () => { + mockQueryResult = { recordset: [], recordset_columns: {} } + const result = await connector.execute("SELECT * FROM t") + expect(result.rows).toEqual([]) + expect(result.truncated).toBe(false) + }) + }) + + // --- Schema introspection --- + + describe("schema introspection", () => { + test("listSchemas queries sys.schemas", async () => { + mockQueryResult = { recordset: [{ name: "dbo" }, { name: "sales" }] } + const schemas = await connector.listSchemas() + expect(mockQueryCalls[0]).toContain("sys.schemas") + expect(schemas).toEqual(["dbo", "sales"]) + }) + + test("listTables queries sys.tables and sys.views", async () => { + mockQueryResult = { + recordset: [ + { name: "orders", type: "U " }, + { name: "order_summary", type: "V" }, + ], + } + const tables = await connector.listTables("dbo") + expect(mockQueryCalls[0]).toContain("UNION ALL") + expect(mockQueryCalls[0]).toContain("sys.tables") + expect(mockQueryCalls[0]).toContain("sys.views") + expect(tables).toEqual([ + { name: "orders", type: "table" }, + { name: "order_summary", type: "view" }, + ]) + }) + + test("describeTable queries sys.columns", async () => { + mockQueryResult = { + recordset: [ + { column_name: "id", data_type: "int", is_nullable: 0 }, + { column_name: "name", data_type: "nvarchar", is_nullable: 1 }, + ], + } + const cols = await connector.describeTable("dbo", "users") + expect(mockQueryCalls[0]).toContain("sys.columns") + expect(cols).toEqual([ + { name: "id", data_type: "int", nullable: false }, + { name: "name", data_type: "nvarchar", nullable: true }, + ]) + }) + }) + + // --- Connection lifecycle --- + + describe("connection lifecycle", () => { + test("close is idempotent", async () => { + await connector.close() + await connector.close() + expect(mockCloseCalls).toBe(1) + }) + }) + + // --- Result format --- + + describe("result format", () => { + test("maps recordset to column-ordered arrays", async () => { + mockQueryResult = { + recordset: [ + { id: 1, name: "alice", age: 30 }, + { id: 2, name: "bob", age: 25 }, + ], + } + const result = await connector.execute("SELECT id, name, age FROM t") + expect(result.columns).toEqual(["id", "name", "age"]) + expect(result.rows).toEqual([ + [1, "alice", 30], + [2, "bob", 25], + ]) + }) + + test("filters underscore-prefixed columns", async () => { + mockQueryResult = { + recordset: [{ id: 1, _bucket: 3, name: "x" }], + } + const result = await connector.execute("SELECT * FROM t") + expect(result.columns).toEqual(["id", "name"]) + }) + }) +}) diff --git a/packages/opencode/src/altimate/native/connections/data-diff.ts b/packages/opencode/src/altimate/native/connections/data-diff.ts index 294c43745..8641b22d2 100644 --- a/packages/opencode/src/altimate/native/connections/data-diff.ts +++ b/packages/opencode/src/altimate/native/connections/data-diff.ts @@ -10,6 +10,24 @@ import type { DataDiffParams, DataDiffResult, PartitionDiffResult } from "../types" import * as Registry from "./registry" +// --------------------------------------------------------------------------- +// Dialect mapping — bridge warehouse config types to Rust SqlDialect serde names +// --------------------------------------------------------------------------- + +/** Map warehouse config types to Rust SqlDialect serde names. */ +const WAREHOUSE_TO_DIALECT: Record = { + sqlserver: "tsql", + mssql: "tsql", + fabric: "fabric", + postgresql: "postgres", + mariadb: "mysql", +} + +/** Convert a warehouse config type to the Rust-compatible SqlDialect name. */ +export function warehouseTypeToDialect(warehouseType: string): string { + return WAREHOUSE_TO_DIALECT[warehouseType.toLowerCase()] ?? warehouseType.toLowerCase() +} + // --------------------------------------------------------------------------- // Query-source detection // --------------------------------------------------------------------------- @@ -18,10 +36,17 @@ const SQL_KEYWORDS = /^\s*(SELECT|WITH|VALUES)\b/i /** * Detect whether a string is an arbitrary SQL query (vs a plain table name). - * Plain table names may contain dots (schema.table, db.schema.table) but not spaces. + * + * A SQL query starts with a keyword AND contains whitespace (e.g., "SELECT * FROM ..."). + * A plain table name — even one named "select" or "with" — is a single token without + * internal whitespace (possibly dot-separated like schema.table or db.schema.table). + * + * The \b in SQL_KEYWORDS already prevents matching "with_metadata" or "select_results", + * but the whitespace check additionally handles bare keyword table names like "select". */ function isQuery(input: string): boolean { - return SQL_KEYWORDS.test(input) + const trimmed = input.trim() + return SQL_KEYWORDS.test(trimmed) && /\s/.test(trimmed) } /** @@ -449,6 +474,12 @@ function dateTruncExpr(granularity: string, column: string, dialect: string): st } return `TRUNC(${column}, '${oracleFmt[g] ?? g.toUpperCase()}')` } + case "sqlserver": + case "mssql": + case "tsql": + case "fabric": + // SQL Server 2022+ / Fabric: DATETRUNC expects unquoted datepart keyword + return `DATETRUNC(${g.toUpperCase()}, ${column})` default: // Postgres, Snowflake, Redshift, DuckDB, etc. return `DATE_TRUNC('${g}', ${column})` @@ -536,6 +567,12 @@ function buildPartitionWhereClause( case "mysql": case "mariadb": return `${expr} = '${partitionValue}'` + case "sqlserver": + case "mssql": + case "tsql": + case "fabric": + // Style 23 = ISO-8601 (yyyy-mm-dd), locale-safe + return `${expr} = CONVERT(DATE, '${partitionValue}', 23)` default: return `${expr} = '${partitionValue}'` } @@ -623,10 +660,10 @@ async function runPartitionedDiff(params: DataDiffParams): Promise { if (warehouse) { const cfg = Registry.getConfig(warehouse) - return cfg?.type ?? "generic" + return warehouseTypeToDialect(cfg?.type ?? "generic") } const warehouses = Registry.list().warehouses - return warehouses[0]?.type ?? "generic" + return warehouseTypeToDialect(warehouses[0]?.type ?? "generic") } const sourceDialect = resolveDialect(params.source_warehouse) @@ -766,10 +803,10 @@ export async function runDataDiff(params: DataDiffParams): Promise { if (warehouse) { const cfg = Registry.getConfig(warehouse) - return cfg?.type ?? "generic" + return warehouseTypeToDialect(cfg?.type ?? "generic") } const warehouses = Registry.list().warehouses - return warehouses[0]?.type ?? "generic" + return warehouseTypeToDialect(warehouses[0]?.type ?? "generic") } const dialect1 = resolveDialect(params.source_warehouse) diff --git a/packages/opencode/src/altimate/native/connections/registry.ts b/packages/opencode/src/altimate/native/connections/registry.ts index 617d6685d..40694a59b 100644 --- a/packages/opencode/src/altimate/native/connections/registry.ts +++ b/packages/opencode/src/altimate/native/connections/registry.ts @@ -122,6 +122,7 @@ const DRIVER_MAP: Record = { mariadb: "@altimateai/drivers/mysql", sqlserver: "@altimateai/drivers/sqlserver", mssql: "@altimateai/drivers/sqlserver", + fabric: "@altimateai/drivers/sqlserver", databricks: "@altimateai/drivers/databricks", duckdb: "@altimateai/drivers/duckdb", oracle: "@altimateai/drivers/oracle", diff --git a/packages/opencode/test/altimate/data-diff-dialect.test.ts b/packages/opencode/test/altimate/data-diff-dialect.test.ts new file mode 100644 index 000000000..083c64d57 --- /dev/null +++ b/packages/opencode/test/altimate/data-diff-dialect.test.ts @@ -0,0 +1,55 @@ +/** + * Tests for warehouse-type-to-dialect mapping in the data-diff orchestrator. + * + * The Rust engine's SqlDialect serde deserialization only accepts exact lowercase + * variant names (e.g., "tsql", not "sqlserver"). This mapping bridges the gap + * between warehouse config types and Rust dialect names. + */ +import { describe, test, expect } from "bun:test" + +import { warehouseTypeToDialect } from "../../src/altimate/native/connections/data-diff" + +describe("warehouseTypeToDialect", () => { + // --- Remapped types --- + + test("maps sqlserver to tsql", () => { + expect(warehouseTypeToDialect("sqlserver")).toBe("tsql") + }) + + test("maps mssql to tsql", () => { + expect(warehouseTypeToDialect("mssql")).toBe("tsql") + }) + + test("maps fabric to fabric", () => { + expect(warehouseTypeToDialect("fabric")).toBe("fabric") + }) + + test("maps postgresql to postgres", () => { + expect(warehouseTypeToDialect("postgresql")).toBe("postgres") + }) + + test("maps mariadb to mysql", () => { + expect(warehouseTypeToDialect("mariadb")).toBe("mysql") + }) + + // --- Passthrough types (already match Rust names) --- + + test("passes through postgres unchanged", () => { + expect(warehouseTypeToDialect("postgres")).toBe("postgres") + }) + + test("passes through snowflake unchanged", () => { + expect(warehouseTypeToDialect("snowflake")).toBe("snowflake") + }) + + test("passes through generic unchanged", () => { + expect(warehouseTypeToDialect("generic")).toBe("generic") + }) + + // --- Case insensitivity --- + + test("handles uppercase input", () => { + expect(warehouseTypeToDialect("SQLSERVER")).toBe("tsql") + expect(warehouseTypeToDialect("PostgreSQL")).toBe("postgres") + }) +}) From 16cc04e35e1e2543d6fe84ea47ae69f21cda8d03 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Mon, 6 Apr 2026 13:29:12 -0700 Subject: [PATCH 04/11] feat: add Azure AD authentication to SQL Server driver (7 flows) - Support all 7 Azure AD / Entra ID auth types in `sqlserver.ts`: `azure-active-directory-password`, `access-token`, `service-principal-secret`, `msi-vm`, `msi-app-service`, `azure-active-directory-default`, `token-credential` - Force TLS encryption for all Azure AD connections - Dynamic import of `@azure/identity` for `DefaultAzureCredential` - Add normalize aliases for Azure AD config fields (`authentication`, `azure_tenant_id`, `azure_client_id`, `azure_client_secret`, `access_token`) - Add `fabric: SQLSERVER_ALIASES` to DRIVER_ALIASES - Add 10 Azure AD unit tests covering all auth flows, encryption, and `DefaultAzureCredential` with managed identity Co-Authored-By: Claude Opus 4.6 --- packages/drivers/src/normalize.ts | 5 + packages/drivers/src/sqlserver.ts | 67 +++++++- packages/drivers/test/sqlserver-unit.test.ts | 172 +++++++++++++++++++ 3 files changed, 242 insertions(+), 2 deletions(-) diff --git a/packages/drivers/src/normalize.ts b/packages/drivers/src/normalize.ts index 71667c044..162e376e6 100644 --- a/packages/drivers/src/normalize.ts +++ b/packages/drivers/src/normalize.ts @@ -65,6 +65,11 @@ const SQLSERVER_ALIASES: AliasMap = { ...COMMON_ALIASES, host: ["server", "serverName", "server_name"], trust_server_certificate: ["trustServerCertificate"], + authentication: ["authenticationType", "auth_type", "authentication_type"], + azure_tenant_id: ["tenantId", "tenant_id", "azureTenantId"], + azure_client_id: ["clientId", "client_id", "azureClientId"], + azure_client_secret: ["clientSecret", "client_secret", "azureClientSecret"], + access_token: ["token", "accessToken"], } const ORACLE_ALIASES: AliasMap = { diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index 3ea1e390f..fa473a149 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -24,8 +24,6 @@ export async function connect(config: ConnectionConfig): Promise { server: config.host ?? "127.0.0.1", port: config.port ?? 1433, database: config.database, - user: config.user, - password: config.password, options: { encrypt: config.encrypt ?? false, trustServerCertificate: config.trust_server_certificate ?? true, @@ -39,6 +37,71 @@ export async function connect(config: ConnectionConfig): Promise { }, } + const authType = config.authentication as string | undefined + + if (authType?.startsWith("azure-active-directory") || authType === "token-credential") { + // Azure AD / Entra ID — always encrypt + ;(mssqlConfig.options as any).encrypt = true + + if (authType === "token-credential" || authType === "azure-active-directory-default") { + try { + const { DefaultAzureCredential } = await import("@azure/identity") + mssqlConfig.authentication = { + type: "token-credential", + options: { + credential: new DefaultAzureCredential( + config.azure_client_id + ? { managedIdentityClientId: config.azure_client_id as string } + : undefined, + ), + }, + } + } catch { + throw new Error( + "Azure AD authentication requires @azure/identity. Run: npm install @azure/identity", + ) + } + } else if (authType === "azure-active-directory-password") { + mssqlConfig.authentication = { + type: "azure-active-directory-password", + options: { + userName: config.user, + password: config.password, + clientId: config.azure_client_id, + tenantId: config.azure_tenant_id, + }, + } + } else if (authType === "azure-active-directory-access-token") { + mssqlConfig.authentication = { + type: "azure-active-directory-access-token", + options: { token: config.token ?? config.access_token }, + } + } else if ( + authType === "azure-active-directory-msi-vm" || + authType === "azure-active-directory-msi-app-service" + ) { + mssqlConfig.authentication = { + type: authType, + options: { + ...(config.azure_client_id && { clientId: config.azure_client_id }), + }, + } + } else if (authType === "azure-active-directory-service-principal-secret") { + mssqlConfig.authentication = { + type: "azure-active-directory-service-principal-secret", + options: { + clientId: config.azure_client_id, + clientSecret: config.azure_client_secret, + tenantId: config.azure_tenant_id, + }, + } + } + } else { + // Standard SQL Server user/password + mssqlConfig.user = config.user + mssqlConfig.password = config.password + } + pool = await mssql.connect(mssqlConfig) }, diff --git a/packages/drivers/test/sqlserver-unit.test.ts b/packages/drivers/test/sqlserver-unit.test.ts index 4a2e38ad8..d17ebbb7e 100644 --- a/packages/drivers/test/sqlserver-unit.test.ts +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -2,6 +2,7 @@ * Unit tests for SQL Server driver logic: * - TOP injection (vs LIMIT) * - Truncation detection + * - Azure AD authentication (7 flows) * - Schema introspection queries * - Connection lifecycle * - Result format mapping @@ -52,6 +53,18 @@ mock.module("mssql", () => ({ }, })) +// Mock @azure/identity for Azure AD tests +class MockDefaultAzureCredential { + opts: any + constructor(opts?: any) { + this.opts = opts + } +} + +mock.module("@azure/identity", () => ({ + DefaultAzureCredential: MockDefaultAzureCredential, +})) + // Import after mocking const { connect } = await import("../src/sqlserver") @@ -129,6 +142,165 @@ describe("SQL Server driver unit tests", () => { }) }) + // --- Azure AD authentication --- + + describe("Azure AD authentication", () => { + test("standard auth uses user/password directly", async () => { + resetMocks() + const c = await connect({ host: "localhost", database: "db", user: "sa", password: "pass" }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.user).toBe("sa") + expect(cfg.password).toBe("pass") + expect(cfg.authentication).toBeUndefined() + }) + + test("azure-active-directory-password builds correct auth object", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + user: "user@domain.com", + password: "secret", + authentication: "azure-active-directory-password", + azure_client_id: "client-123", + azure_tenant_id: "tenant-456", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication).toEqual({ + type: "azure-active-directory-password", + options: { + userName: "user@domain.com", + password: "secret", + clientId: "client-123", + tenantId: "tenant-456", + }, + }) + expect(cfg.user).toBeUndefined() + expect(cfg.password).toBeUndefined() + }) + + test("azure-active-directory-access-token passes token", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-access-token", + access_token: "eyJhbGciOi...", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication).toEqual({ + type: "azure-active-directory-access-token", + options: { token: "eyJhbGciOi..." }, + }) + }) + + test("azure-active-directory-service-principal-secret builds SP auth", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-service-principal-secret", + azure_client_id: "sp-client", + azure_client_secret: "sp-secret", + azure_tenant_id: "sp-tenant", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication).toEqual({ + type: "azure-active-directory-service-principal-secret", + options: { + clientId: "sp-client", + clientSecret: "sp-secret", + tenantId: "sp-tenant", + }, + }) + }) + + test("azure-active-directory-msi-vm builds MSI auth with optional clientId", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-msi-vm", + azure_client_id: "msi-client", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication).toEqual({ + type: "azure-active-directory-msi-vm", + options: { clientId: "msi-client" }, + }) + }) + + test("azure-active-directory-msi-app-service works without clientId", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-msi-app-service", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication).toEqual({ + type: "azure-active-directory-msi-app-service", + options: {}, + }) + }) + + test("azure-active-directory-default uses DefaultAzureCredential", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-default", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication.type).toBe("token-credential") + expect(cfg.authentication.options.credential).toBeInstanceOf(MockDefaultAzureCredential) + }) + + test("token-credential uses DefaultAzureCredential with managed identity", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "token-credential", + azure_client_id: "mi-client-id", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication.type).toBe("token-credential") + const cred = cfg.authentication.options.credential as MockDefaultAzureCredential + expect(cred.opts).toEqual({ managedIdentityClientId: "mi-client-id" }) + }) + + test("encryption forced for all Azure AD connections", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "azure-active-directory-password", + user: "u", + password: "p", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.options.encrypt).toBe(true) + }) + + test("standard auth does not force encryption", async () => { + resetMocks() + const c = await connect({ host: "localhost", database: "db", user: "sa", password: "pass" }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.options.encrypt).toBe(false) + }) + }) + // --- Schema introspection --- describe("schema introspection", () => { From eb9f3c3086363002f169e97a75b35cc78156bd90 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Mon, 6 Apr 2026 13:30:55 -0700 Subject: [PATCH 05/11] docs: add MSSQL and Microsoft Fabric documentation to data-parity SKILL.md - Add SQL Server / Fabric schema inspection query in Step 2 - Add "SQL Server and Microsoft Fabric" section with: - Supported configurations table (sqlserver, mssql, fabric) - Fabric connection guide with Azure AD auth types - Algorithm behavior notes (joindiff vs hashdiff selection) Co-Authored-By: Claude Opus 4.6 --- .opencode/skills/data-parity/SKILL.md | 49 +++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/.opencode/skills/data-parity/SKILL.md b/.opencode/skills/data-parity/SKILL.md index 2bb7fa5df..f2a423806 100644 --- a/.opencode/skills/data-parity/SKILL.md +++ b/.opencode/skills/data-parity/SKILL.md @@ -71,6 +71,19 @@ WHERE table_schema = 'mydb' AND table_name = 'orders' ORDER BY ordinal_position ``` +```sql +-- SQL Server / Fabric +SELECT c.name AS column_name, tp.name AS data_type, c.is_nullable, + dc.definition AS column_default +FROM sys.columns c +INNER JOIN sys.types tp ON c.user_type_id = tp.user_type_id +INNER JOIN sys.objects o ON c.object_id = o.object_id +INNER JOIN sys.schemas s ON o.schema_id = s.schema_id +LEFT JOIN sys.default_constraints dc ON c.default_object_id = dc.object_id +WHERE s.name = 'dbo' AND o.name = 'orders' +ORDER BY c.column_id +``` + ```sql -- ClickHouse DESCRIBE TABLE source_db.events @@ -409,3 +422,39 @@ Even when tables match perfectly, state what was checked: **Silently excluding auto-timestamp columns without asking the user** → Always present detected auto-timestamp columns (Step 4) and get explicit confirmation. In migration scenarios, `created_at` should be *identical* — excluding it silently hides real bugs. + +--- + +## SQL Server and Microsoft Fabric + +### Supported Configurations + +| Warehouse Type | Authentication | Notes | +|---|---|---| +| `sqlserver` / `mssql` | User/password or Azure AD | On-prem or Azure SQL. Requires SQL Server 2022+ for `DATETRUNC` and `LEAST`. | +| `fabric` | Azure AD only | Microsoft Fabric SQL endpoint. Always uses TLS encryption. | + +### Connecting to Microsoft Fabric + +Fabric uses the same TDS protocol as SQL Server — no separate driver needed. Configuration: + +``` +type: "fabric" +host: "-.datawarehouse.fabric.microsoft.com" +database: "" +authentication: "azure-active-directory-default" # recommended +``` + +Supported Azure AD authentication types: +- `azure-active-directory-default` — auto-discovers credentials via `DefaultAzureCredential` (recommended) +- `token-credential` — same as above, with optional `azure_client_id` for managed identity +- `azure-active-directory-password` — username/password with `azure_client_id` and `azure_tenant_id` +- `azure-active-directory-access-token` — pre-obtained token (does **not** auto-refresh) +- `azure-active-directory-service-principal-secret` — service principal with `azure_client_id`, `azure_client_secret`, `azure_tenant_id` +- `azure-active-directory-msi-vm` / `azure-active-directory-msi-app-service` — managed identity + +### Algorithm Behavior + +- **Same-warehouse** MSSQL or Fabric → `joindiff` (single FULL OUTER JOIN, most efficient) +- **Cross-warehouse** MSSQL/Fabric ↔ other database → `hashdiff` (automatic when using `auto`) +- The Rust engine maps `sqlserver`/`mssql` to `tsql` dialect and `fabric` to `fabric` dialect — both generate valid T-SQL syntax with bracket quoting (`[schema].[table]`). From 3ed76778c64e48818733bfc962b78fcdc55132c5 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Tue, 7 Apr 2026 12:53:57 -0700 Subject: [PATCH 06/11] fix: delegate Azure AD credential creation to tedious and remove underscore column filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **Azure AD auth**: Pass `azure-active-directory-*` types directly to tedious instead of constructing `DefaultAzureCredential` ourselves. Tedious imports `@azure/identity` internally and creates credentials — avoids bun CJS/ESM `isTokenCredential` boundary issue that caused "not an instance of the token credential class" errors. - **Auth shorthands**: Map `CLI`, `default`, `password`, `service-principal`, `msi`, `managed-identity` to their full tedious type names. - **Column filter**: Remove `_.startsWith("_")` filter from `execute()` result columns — it stripped legitimate aliases like `_p` used by partition discovery, causing partitioned diffs to return empty results. - **Tests**: Remove `@azure/identity` mock (no longer imported by driver), update auth assertions, add shorthand mapping tests, fix column filter test. - **Verified**: All 97 driver tests pass. Full data-diff pipeline tested against real MSSQL server (profile, joindiff, auto, where_clause, partitioned). Co-Authored-By: Claude Opus 4.6 --- packages/drivers/src/sqlserver.ts | 43 ++++++------ packages/drivers/test/sqlserver-unit.test.ts | 74 ++++++++++++++------ 2 files changed, 73 insertions(+), 44 deletions(-) diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index fa473a149..78d633335 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -37,29 +37,30 @@ export async function connect(config: ConnectionConfig): Promise { }, } - const authType = config.authentication as string | undefined + // Normalize shorthand auth values to tedious-compatible types + const AUTH_SHORTHANDS: Record = { + cli: "azure-active-directory-default", + default: "azure-active-directory-default", + password: "azure-active-directory-password", + "service-principal": "azure-active-directory-service-principal-secret", + serviceprincipal: "azure-active-directory-service-principal-secret", + "managed-identity": "azure-active-directory-msi-vm", + msi: "azure-active-directory-msi-vm", + } + const rawAuth = config.authentication as string | undefined + const authType = rawAuth ? (AUTH_SHORTHANDS[rawAuth.toLowerCase()] ?? rawAuth) : undefined - if (authType?.startsWith("azure-active-directory") || authType === "token-credential") { - // Azure AD / Entra ID — always encrypt + if (authType?.startsWith("azure-active-directory")) { + // Azure AD / Entra ID — tedious handles credential creation internally. + // We pass the type + options; tedious imports @azure/identity itself. ;(mssqlConfig.options as any).encrypt = true - if (authType === "token-credential" || authType === "azure-active-directory-default") { - try { - const { DefaultAzureCredential } = await import("@azure/identity") - mssqlConfig.authentication = { - type: "token-credential", - options: { - credential: new DefaultAzureCredential( - config.azure_client_id - ? { managedIdentityClientId: config.azure_client_id as string } - : undefined, - ), - }, - } - } catch { - throw new Error( - "Azure AD authentication requires @azure/identity. Run: npm install @azure/identity", - ) + if (authType === "azure-active-directory-default") { + mssqlConfig.authentication = { + type: "azure-active-directory-default", + options: { + ...(config.azure_client_id && { clientId: config.azure_client_id as string }), + }, } } else if (authType === "azure-active-directory-password") { mssqlConfig.authentication = { @@ -128,7 +129,7 @@ export async function connect(config: ConnectionConfig): Promise { const rows = result.recordset ?? [] const columns = rows.length > 0 - ? Object.keys(rows[0]).filter((k) => !k.startsWith("_")) + ? Object.keys(rows[0]) : (result.recordset?.columns ? Object.keys(result.recordset.columns) : []) diff --git a/packages/drivers/test/sqlserver-unit.test.ts b/packages/drivers/test/sqlserver-unit.test.ts index d17ebbb7e..f0ba9133d 100644 --- a/packages/drivers/test/sqlserver-unit.test.ts +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -53,18 +53,6 @@ mock.module("mssql", () => ({ }, })) -// Mock @azure/identity for Azure AD tests -class MockDefaultAzureCredential { - opts: any - constructor(opts?: any) { - this.opts = opts - } -} - -mock.module("@azure/identity", () => ({ - DefaultAzureCredential: MockDefaultAzureCredential, -})) - // Import after mocking const { connect } = await import("../src/sqlserver") @@ -250,7 +238,7 @@ describe("SQL Server driver unit tests", () => { }) }) - test("azure-active-directory-default uses DefaultAzureCredential", async () => { + test("azure-active-directory-default passes type to tedious (no credential object)", async () => { resetMocks() const c = await connect({ host: "myserver.database.windows.net", @@ -259,23 +247,22 @@ describe("SQL Server driver unit tests", () => { }) await c.connect() const cfg = mockConnectCalls[0] - expect(cfg.authentication.type).toBe("token-credential") - expect(cfg.authentication.options.credential).toBeInstanceOf(MockDefaultAzureCredential) + expect(cfg.authentication.type).toBe("azure-active-directory-default") + expect(cfg.authentication.options.credential).toBeUndefined() }) - test("token-credential uses DefaultAzureCredential with managed identity", async () => { + test("azure-active-directory-default with client_id passes clientId option", async () => { resetMocks() const c = await connect({ host: "myserver.database.windows.net", database: "db", - authentication: "token-credential", + authentication: "azure-active-directory-default", azure_client_id: "mi-client-id", }) await c.connect() const cfg = mockConnectCalls[0] - expect(cfg.authentication.type).toBe("token-credential") - const cred = cfg.authentication.options.credential as MockDefaultAzureCredential - expect(cred.opts).toEqual({ managedIdentityClientId: "mi-client-id" }) + expect(cfg.authentication.type).toBe("azure-active-directory-default") + expect(cfg.authentication.options.clientId).toBe("mi-client-id") }) test("encryption forced for all Azure AD connections", async () => { @@ -299,6 +286,47 @@ describe("SQL Server driver unit tests", () => { const cfg = mockConnectCalls[0] expect(cfg.options.encrypt).toBe(false) }) + + test("'CLI' shorthand maps to azure-active-directory-default", async () => { + resetMocks() + const c = await connect({ + host: "myserver.datawarehouse.fabric.microsoft.com", + database: "migration", + authentication: "CLI", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication.type).toBe("azure-active-directory-default") + expect(cfg.options.encrypt).toBe(true) + }) + + test("'service-principal' shorthand maps correctly", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "service-principal", + azure_client_id: "cid", + azure_client_secret: "csec", + azure_tenant_id: "tid", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication.type).toBe("azure-active-directory-service-principal-secret") + expect(cfg.authentication.options.clientId).toBe("cid") + }) + + test("'msi' shorthand maps to azure-active-directory-msi-vm", async () => { + resetMocks() + const c = await connect({ + host: "myserver.database.windows.net", + database: "db", + authentication: "msi", + }) + await c.connect() + const cfg = mockConnectCalls[0] + expect(cfg.authentication.type).toBe("azure-active-directory-msi-vm") + }) }) // --- Schema introspection --- @@ -372,12 +400,12 @@ describe("SQL Server driver unit tests", () => { ]) }) - test("filters underscore-prefixed columns", async () => { + test("preserves underscore-prefixed columns", async () => { mockQueryResult = { - recordset: [{ id: 1, _bucket: 3, name: "x" }], + recordset: [{ id: 1, _p: "Delivered", name: "x" }], } const result = await connector.execute("SELECT * FROM t") - expect(result.columns).toEqual(["id", "name"]) + expect(result.columns).toEqual(["id", "_p", "name"]) }) }) }) From 5a77db9b9bdee4cf3f53021e002b2666c18f5fcd Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Mon, 13 Apr 2026 10:26:31 -0700 Subject: [PATCH 07/11] fix: upgrade `mssql` to v12 with `ConnectionPool` isolation and row flattening MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Upgrade `mssql` from v11 to v12 (`tedious` 18 → 19) - Use explicit `ConnectionPool` instead of global `mssql.connect()` to isolate multiple simultaneous connections - Flatten unnamed column arrays — `mssql` merges unnamed columns (e.g. `SELECT COUNT(*), SUM(...)`) into a single array under the empty-string key; restore positional column values - Proper column name resolution: compare `namedKeys.length` against flattened row length, fall back to synthetic `col_0`, `col_1`, etc. - Update test mock to export `ConnectionPool` class and `createMockPool` Co-Authored-By: Claude Opus 4.6 --- bun.lock | 24 ++++++--- packages/drivers/package.json | 2 +- packages/drivers/src/sqlserver.ts | 56 ++++++++++++++------ packages/drivers/test/sqlserver-unit.test.ts | 30 +++++++---- 4 files changed, 81 insertions(+), 31 deletions(-) diff --git a/bun.lock b/bun.lock index 1b06053a5..25e43809d 100644 --- a/bun.lock +++ b/bun.lock @@ -48,7 +48,7 @@ "@google-cloud/bigquery": "^8.0.0", "duckdb": "^1.0.0", "mongodb": "^6.0.0", - "mssql": "^11.0.0", + "mssql": "^12.0.0", "mysql2": "^3.0.0", "oracledb": "^6.0.0", "pg": "^8.0.0", @@ -1034,7 +1034,7 @@ "@techteamer/ocsp": ["@techteamer/ocsp@1.0.1", "", { "dependencies": { "asn1.js": "^5.4.1", "asn1.js-rfc2560": "^5.0.1", "asn1.js-rfc5280": "^3.0.0", "async": "^3.2.4", "simple-lru-cache": "^0.0.2" } }, "sha512-q4pW5wAC6Pc3JI8UePwE37CkLQ5gDGZMgjSX4MEEm4D4Di59auDQ8UNIDzC4gRnPNmmcwjpPxozq8p5pjiOmOw=="], - "@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + "@tediousjs/connection-string": ["@tediousjs/connection-string@0.6.0", "", {}, "sha512-GxlsW354Vi6QqbUgdPyQVcQjI7cZBdGV5vOYVYuCVDTylx2wl3WHR2HlhcxxHTrMigbelpXsdcZso+66uxPfow=="], "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], @@ -1902,7 +1902,7 @@ "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], - "mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + "mssql": ["mssql@12.2.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.6.0", "commander": "^11.0.0", "debug": "^4.3.3", "tarn": "^3.0.2", "tedious": "^19.0.0" }, "bin": { "mssql": "bin/mssql" } }, "sha512-TU89g82WatOVcinw3etO/crKbd67ugC3Wm6TJDklHjp7211brVENWIs++UoPC2H+TWvyi0OSlzMou8GY15onOA=="], "multicast-dns": ["multicast-dns@7.2.5", "", { "dependencies": { "dns-packet": "^5.2.2", "thunky": "^1.0.2" }, "bin": { "multicast-dns": "cli.js" } }, "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg=="], @@ -2336,7 +2336,7 @@ "tarn": ["tarn@3.0.2", "", {}, "sha512-51LAVKUSZSVfI05vjPESNc5vwqqZpbXCsU+/+wxlOrUjk2SnFTt97v9ZgQrD4YmxYW1Px6w2KjaDitCfkvgxMQ=="], - "tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "tedious": ["tedious@19.2.1", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.5", "@types/node": ">=18", "bl": "^6.1.4", "iconv-lite": "^0.7.0", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-pk1Q16Yl62iocuQB+RWbg6rFUFkIyzqOFQ6NfysCltRvQqKwfurgj8v/f2X+CKvDhSL4IJ0cCOfCHDg9PWEEYA=="], "teeny-request": ["teeny-request@10.1.0", "", { "dependencies": { "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "node-fetch": "^3.3.2", "stream-events": "^1.0.5" } }, "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw=="], @@ -2988,6 +2988,8 @@ "@smithy/util-waiter/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + "@types/mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "@types/request/form-data": ["form-data@2.5.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.35", "safe-buffer": "^5.2.1" } }, "sha512-jqdObeR2rxZZbPSGL+3VckHMYtu+f9//KXBsVny6JSX/pa38Fy+bGjuG8eW/H6USNQWhLi8Num++cU2yOCNz4A=="], "accepts/negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="], @@ -3040,6 +3042,8 @@ "cross-spawn/which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "drizzle-orm/mssql": ["mssql@11.0.1", "", { "dependencies": { "@tediousjs/connection-string": "^0.5.0", "commander": "^11.0.0", "debug": "^4.3.3", "rfdc": "^1.3.0", "tarn": "^3.0.2", "tedious": "^18.2.1" }, "bin": { "mssql": "bin/mssql" } }, "sha512-KlGNsugoT90enKlR8/G36H0kTxPthDhmtNUCwEHvgRza5Cjpjoj+P2X6eMpFUDN7pFrJZsKadL4x990G8RBE1w=="], + "effect/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "effect/yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], @@ -3164,8 +3168,6 @@ "tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - "tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - "teeny-request/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -3518,6 +3520,8 @@ "@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="], + "@types/mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "@types/request/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "ai-gateway-provider/@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.62", "", { "dependencies": { "@ai-sdk/provider": "2.0.1", "@ai-sdk/provider-utils": "3.0.21" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-I3RhaOEMnWlWnrvjNBOYvUb19Dwf2nw01IruZrVJRDi688886e11wnd5DxrBZLd2V29Gizo3vpOPnnExsA+wTA=="], @@ -3556,6 +3560,12 @@ "cross-spawn/which/isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "drizzle-orm/mssql/@tediousjs/connection-string": ["@tediousjs/connection-string@0.5.0", "", {}, "sha512-7qSgZbincDDDFyRweCIEvZULFAw5iz/DeunhvuxpL31nfntX3P4Yd4HkHBRg9H8CdqY1e5WFN1PZIz/REL9MVQ=="], + + "drizzle-orm/mssql/commander": ["commander@11.1.0", "", {}, "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ=="], + + "drizzle-orm/mssql/tedious": ["tedious@18.6.2", "", { "dependencies": { "@azure/core-auth": "^1.7.2", "@azure/identity": "^4.2.1", "@azure/keyvault-keys": "^4.4.0", "@js-joda/core": "^5.6.1", "@types/node": ">=18", "bl": "^6.0.11", "iconv-lite": "^0.6.3", "js-md4": "^0.3.2", "native-duplexpair": "^1.0.0", "sprintf-js": "^1.1.3" } }, "sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg=="], + "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], @@ -3778,6 +3788,8 @@ "cross-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "drizzle-orm/mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], + "gaxios/rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], "gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], diff --git a/packages/drivers/package.json b/packages/drivers/package.json index 98a0112cf..361c1dd96 100644 --- a/packages/drivers/package.json +++ b/packages/drivers/package.json @@ -17,7 +17,7 @@ "@google-cloud/bigquery": "^8.0.0", "@databricks/sql": "^1.0.0", "mysql2": "^3.0.0", - "mssql": "^11.0.0", + "mssql": "^12.0.0", "oracledb": "^6.0.0", "duckdb": "^1.0.0", "mongodb": "^6.0.0", diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index 78d633335..a5ddcf227 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -6,10 +6,13 @@ import type { ConnectionConfig, Connector, ConnectorResult, ExecuteOptions, Sche export async function connect(config: ConnectionConfig): Promise { let mssql: any + let MssqlConnectionPool: any try { // @ts-expect-error — mssql has no type declarations; installed as optional peerDependency - mssql = await import("mssql") - mssql = mssql.default || mssql + const mod = await import("mssql") + mssql = mod.default || mod + // ConnectionPool is a named export, not on .default + MssqlConnectionPool = mod.ConnectionPool ?? mssql.ConnectionPool } catch { throw new Error( "SQL Server driver not installed. Run: npm install mssql", @@ -103,7 +106,14 @@ export async function connect(config: ConnectionConfig): Promise { mssqlConfig.password = config.password } - pool = await mssql.connect(mssqlConfig) + // Use an explicit ConnectionPool (not the global mssql.connect()) so + // multiple simultaneous connections to different servers are isolated. + if (MssqlConnectionPool) { + pool = new MssqlConnectionPool(mssqlConfig) + await pool.connect() + } else { + pool = await mssql.connect(mssqlConfig) + } }, async execute(sql: string, limit?: number, _binds?: any[], options?: ExecuteOptions): Promise { @@ -126,22 +136,38 @@ export async function connect(config: ConnectionConfig): Promise { } const result = await pool.request().query(query) - const rows = result.recordset ?? [] + const recordset = result.recordset ?? [] + const truncated = effectiveLimit > 0 && recordset.length > effectiveLimit + const limitedRecordset = truncated ? recordset.slice(0, effectiveLimit) : recordset + + // mssql merges unnamed columns (e.g. SELECT COUNT(*), SUM(...)) into a + // single array under the empty-string key: row[""] = [val1, val2, ...]. + // Flatten these arrays to restore positional column values. + const flattenRow = (row: any): any[] => { + const vals: any[] = [] + for (const v of Object.values(row)) { + if (Array.isArray(v)) vals.push(...v) + else vals.push(v) + } + return vals + } + + const rows = limitedRecordset.map(flattenRow) + const sampleFlat = rows.length > 0 ? rows[0] : [] + const namedKeys = recordset.length > 0 ? Object.keys(recordset[0]) : [] const columns = - rows.length > 0 - ? Object.keys(rows[0]) - : (result.recordset?.columns - ? Object.keys(result.recordset.columns) - : []) - const truncated = effectiveLimit > 0 && rows.length > effectiveLimit - const limitedRows = truncated ? rows.slice(0, effectiveLimit) : rows + namedKeys.length === sampleFlat.length + ? namedKeys + : sampleFlat.length > 0 + ? sampleFlat.map((_: any, i: number) => `col_${i}`) + : (result.recordset?.columns + ? Object.keys(result.recordset.columns) + : []) return { columns, - rows: limitedRows.map((row: any) => - columns.map((col) => row[col]), - ), - row_count: limitedRows.length, + rows, + row_count: rows.length, truncated, } }, diff --git a/packages/drivers/test/sqlserver-unit.test.ts b/packages/drivers/test/sqlserver-unit.test.ts index f0ba9133d..61d84e298 100644 --- a/packages/drivers/test/sqlserver-unit.test.ts +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -39,17 +39,29 @@ function createMockRequest() { return req } +function createMockPool(config: any) { + mockConnectCalls.push(config) + return { + connect: async () => {}, + request: () => createMockRequest(), + close: async () => { + mockCloseCalls++ + }, + } +} + mock.module("mssql", () => ({ default: { - connect: async (config: any) => { - mockConnectCalls.push(config) - return { - request: () => createMockRequest(), - close: async () => { - mockCloseCalls++ - }, - } - }, + connect: async (config: any) => createMockPool(config), + }, + ConnectionPool: class { + _pool: any + constructor(config: any) { + this._pool = createMockPool(config) + } + async connect() { return this._pool.connect() } + request() { return this._pool.request() } + async close() { return this._pool.close() } }, })) From 811c2bee22ad2b9ac69ef5ee46269d87d071c517 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Mon, 13 Apr 2026 10:28:21 -0700 Subject: [PATCH 08/11] fix: resolve TypeScript spread-type errors in Azure AD conditional options Use ternary expressions (`x ? {...} : {}`) instead of short-circuit (`x && {...}`) to avoid spreading a boolean value. Co-Authored-By: Claude Opus 4.6 --- packages/drivers/src/sqlserver.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index a5ddcf227..bb4e0f7d6 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -62,7 +62,7 @@ export async function connect(config: ConnectionConfig): Promise { mssqlConfig.authentication = { type: "azure-active-directory-default", options: { - ...(config.azure_client_id && { clientId: config.azure_client_id as string }), + ...(config.azure_client_id ? { clientId: config.azure_client_id as string } : {}), }, } } else if (authType === "azure-active-directory-password") { @@ -87,7 +87,7 @@ export async function connect(config: ConnectionConfig): Promise { mssqlConfig.authentication = { type: authType, options: { - ...(config.azure_client_id && { clientId: config.azure_client_id }), + ...(config.azure_client_id ? { clientId: config.azure_client_id } : {}), }, } } else if (authType === "azure-active-directory-service-principal-secret") { From 93c6559b5ccbd50cf8d76116fb6b5f7fe5afaaed Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Tue, 14 Apr 2026 13:41:52 -0700 Subject: [PATCH 09/11] fix: resolve cubic review findings on MSSQL/Fabric PR - P1: restrict `flattenRow` to only spread the empty-string key (`""`) where mssql merges unnamed columns, preserving legitimate array values - P2: escape single quotes in `partitionValue` for date-mode branches in `buildPartitionWhereClause` (categorical mode already escaped) - P2: add `fabric` to `PASSWORD_DRIVERS` set in registry for consistent password validation alongside `sqlserver`/`mssql` - P2: fallback to `"(no values)"` when `d.values` is nullish to prevent template literal coercing `undefined` to the string `"undefined"` Co-Authored-By: Claude Opus 4.6 --- packages/drivers/src/sqlserver.ts | 7 ++++--- .../src/altimate/native/connections/data-diff.ts | 11 ++++++----- .../src/altimate/native/connections/registry.ts | 1 + packages/opencode/src/altimate/tools/data-diff.ts | 2 +- 4 files changed, 12 insertions(+), 9 deletions(-) diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index bb4e0f7d6..2f66537e9 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -142,11 +142,12 @@ export async function connect(config: ConnectionConfig): Promise { // mssql merges unnamed columns (e.g. SELECT COUNT(*), SUM(...)) into a // single array under the empty-string key: row[""] = [val1, val2, ...]. - // Flatten these arrays to restore positional column values. + // Flatten only the empty-string key to restore positional column values; + // legitimate array values from other keys are preserved as-is. const flattenRow = (row: any): any[] => { const vals: any[] = [] - for (const v of Object.values(row)) { - if (Array.isArray(v)) vals.push(...v) + for (const [k, v] of Object.entries(row)) { + if (k === "" && Array.isArray(v)) vals.push(...v) else vals.push(v) } return vals diff --git a/packages/opencode/src/altimate/native/connections/data-diff.ts b/packages/opencode/src/altimate/native/connections/data-diff.ts index 8641b22d2..42f476b80 100644 --- a/packages/opencode/src/altimate/native/connections/data-diff.ts +++ b/packages/opencode/src/altimate/native/connections/data-diff.ts @@ -557,24 +557,25 @@ function buildPartitionWhereClause( // date mode const expr = dateTruncExpr(granularity!, quotedCol, dialect) + const escaped = partitionValue.replace(/'/g, "''") // Cast the literal appropriately per dialect switch (dialect) { case "bigquery": - return `${expr} = '${partitionValue}'` + return `${expr} = '${escaped}'` case "clickhouse": - return `${expr} = toDate('${partitionValue}')` + return `${expr} = toDate('${escaped}')` case "mysql": case "mariadb": - return `${expr} = '${partitionValue}'` + return `${expr} = '${escaped}'` case "sqlserver": case "mssql": case "tsql": case "fabric": // Style 23 = ISO-8601 (yyyy-mm-dd), locale-safe - return `${expr} = CONVERT(DATE, '${partitionValue}', 23)` + return `${expr} = CONVERT(DATE, '${escaped}', 23)` default: - return `${expr} = '${partitionValue}'` + return `${expr} = '${escaped}'` } } diff --git a/packages/opencode/src/altimate/native/connections/registry.ts b/packages/opencode/src/altimate/native/connections/registry.ts index 40694a59b..cc871682c 100644 --- a/packages/opencode/src/altimate/native/connections/registry.ts +++ b/packages/opencode/src/altimate/native/connections/registry.ts @@ -166,6 +166,7 @@ async function createConnector(name: string, config: ConnectionConfig): Promise< "mariadb", "sqlserver", "mssql", + "fabric", "oracle", "snowflake", "clickhouse", diff --git a/packages/opencode/src/altimate/tools/data-diff.ts b/packages/opencode/src/altimate/tools/data-diff.ts index bf9948748..15b78bdd0 100644 --- a/packages/opencode/src/altimate/tools/data-diff.ts +++ b/packages/opencode/src/altimate/tools/data-diff.ts @@ -203,7 +203,7 @@ function formatOutcome(outcome: any, source: string, target: string): string { lines.push(` Sample differences (first ${Math.min(diffRows.length, 5)}):`) for (const d of diffRows.slice(0, 5)) { const label = d.sign === "-" ? "source only" : "target only" - lines.push(` [${label}] ${d.values?.join(" | ")}`) + lines.push(` [${label}] ${d.values?.join(" | ") ?? "(no values)"}`) } } From 84e882895e9e928fdd18590d8a3f7b6ff7bbe712 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Tue, 14 Apr 2026 13:50:10 -0700 Subject: [PATCH 10/11] test: add fabric connection path and flattenRow coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - sqlserver-unit: 3 tests for unnamed column flattening — verifies only the empty-string key is spread, legitimate named arrays are preserved - driver-normalize: fabric type uses SQLSERVER_ALIASES (server → host, trustServerCertificate → trust_server_certificate) - connections: fabric type is recognized in DRIVER_MAP and listed correctly Co-Authored-By: Claude Opus 4.6 --- packages/drivers/test/sqlserver-unit.test.ts | 33 +++++++++++++++++++ .../test/altimate/connections.test.ts | 17 ++++++++++ .../test/altimate/driver-normalize.test.ts | 13 ++++++++ 3 files changed, 63 insertions(+) diff --git a/packages/drivers/test/sqlserver-unit.test.ts b/packages/drivers/test/sqlserver-unit.test.ts index 61d84e298..5042b8f5b 100644 --- a/packages/drivers/test/sqlserver-unit.test.ts +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -420,4 +420,37 @@ describe("SQL Server driver unit tests", () => { expect(result.columns).toEqual(["id", "_p", "name"]) }) }) + + // --- Unnamed column flattening --- + + describe("unnamed column flattening", () => { + test("flattens unnamed columns merged under empty-string key", async () => { + // mssql merges SELECT COUNT(*), SUM(amount) into row[""] = [42, 1000] + mockQueryResult = { + recordset: [{ "": [42, 1000] }], + } + const result = await connector.execute("SELECT COUNT(*), SUM(amount) FROM t") + expect(result.rows).toEqual([[42, 1000]]) + expect(result.columns).toEqual(["col_0", "col_1"]) + }) + + test("preserves legitimate array values from named columns", async () => { + // A named column containing an array (e.g. from JSON aggregation) + // should NOT be spread — only the empty-string key gets flattened + mockQueryResult = { + recordset: [{ id: 1, tags: ["a", "b", "c"] }], + } + const result = await connector.execute("SELECT * FROM t") + expect(result.columns).toEqual(["id", "tags"]) + expect(result.rows).toEqual([[1, ["a", "b", "c"]]]) + }) + + test("handles mix of named and unnamed columns", async () => { + mockQueryResult = { + recordset: [{ name: "alice", "": [42] }], + } + const result = await connector.execute("SELECT * FROM t") + expect(result.rows).toEqual([["alice", 42]]) + }) + }) }) diff --git a/packages/opencode/test/altimate/connections.test.ts b/packages/opencode/test/altimate/connections.test.ts index f741a8cf1..5c9680297 100644 --- a/packages/opencode/test/altimate/connections.test.ts +++ b/packages/opencode/test/altimate/connections.test.ts @@ -81,6 +81,23 @@ describe("ConnectionRegistry", () => { await expect(Registry.get("mydb")).rejects.toThrow("Supported:") }) + test("fabric type is recognized in DRIVER_MAP and routes to sqlserver driver", () => { + Registry.setConfigs({ + fabricdb: { + type: "fabric", + host: "myserver.datawarehouse.fabric.microsoft.com", + database: "migration", + authentication: "default", + }, + }) + const config = Registry.getConfig("fabricdb") + expect(config).toBeDefined() + expect(config?.type).toBe("fabric") + const result = Registry.list() + expect(result.warehouses).toHaveLength(1) + expect(result.warehouses[0].type).toBe("fabric") + }) + test("getConfig returns config for known connection", () => { Registry.setConfigs({ mydb: { type: "postgres", host: "localhost" }, diff --git a/packages/opencode/test/altimate/driver-normalize.test.ts b/packages/opencode/test/altimate/driver-normalize.test.ts index 95f348289..43b31c4e8 100644 --- a/packages/opencode/test/altimate/driver-normalize.test.ts +++ b/packages/opencode/test/altimate/driver-normalize.test.ts @@ -463,6 +463,19 @@ describe("normalizeConfig — SQL Server", () => { expect(result.host).toBe("myserver") expect(result.user).toBe("sa") }) + + test("fabric type uses SQLSERVER_ALIASES", () => { + const result = normalizeConfig({ + type: "fabric", + server: "myserver.datawarehouse.fabric.microsoft.com", + trustServerCertificate: false, + authentication: "default", + }) + expect(result.host).toBe("myserver.datawarehouse.fabric.microsoft.com") + expect(result.server).toBeUndefined() + expect(result.trust_server_certificate).toBe(false) + expect(result.trustServerCertificate).toBeUndefined() + }) }) // --------------------------------------------------------------------------- From 333a45cc4cf0ddd56287cedcd4eb3dab8d8b3919 Mon Sep 17 00:00:00 2001 From: suryaiyer95 Date: Tue, 14 Apr 2026 14:01:02 -0700 Subject: [PATCH 11/11] docs: document minimum versions and make @azure/identity optional MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add "Minimum Version Requirements" table to SKILL.md covering SQL Server 2022+, mssql v12, and @azure/identity v4 with rationale for each - Document auth shorthands (CLI, default, password, service-principal, msi) - Move @azure/identity from dependencies to optional peerDependencies so it is NOT installed by default — only required for Azure AD auth - Add runtime check in sqlserver driver: if Azure AD auth type is requested but @azure/identity is missing, throw a clear install instruction error Co-Authored-By: Claude Opus 4.6 --- .opencode/skills/data-parity/SKILL.md | 25 ++++++++++-- bun.lock | 57 +++++++++++++++++++++++++-- packages/drivers/package.json | 8 ++++ packages/drivers/src/sqlserver.ts | 8 ++++ 4 files changed, 90 insertions(+), 8 deletions(-) diff --git a/.opencode/skills/data-parity/SKILL.md b/.opencode/skills/data-parity/SKILL.md index f2a423806..07e217f83 100644 --- a/.opencode/skills/data-parity/SKILL.md +++ b/.opencode/skills/data-parity/SKILL.md @@ -427,11 +427,23 @@ Even when tables match perfectly, state what was checked: ## SQL Server and Microsoft Fabric +### Minimum Version Requirements + +| Component | Minimum Version | Why | +|---|---|---| +| **SQL Server** | 2022 (16.x) | `DATETRUNC()` used for date partitioning; `LEAST()`/`GREATEST()` used by Rust engine | +| **Azure SQL Database** | Any current version | Always has `DATETRUNC()` and `LEAST()` | +| **Microsoft Fabric** | Any current version | T-SQL surface includes all required functions | +| **mssql** (npm) | 12.0.0 | `ConnectionPool` isolation for concurrent connections, tedious 19 | +| **@azure/identity** (npm) | 4.0.0 | Required only for Azure AD authentication; tedious imports it internally | + +> **Note:** Date partitioning (`partition_column` + `partition_granularity`) uses `DATETRUNC()` which is **not available on SQL Server 2019 or earlier**. Basic diff operations (joindiff, hashdiff, profile) work on older versions. If you need partitioned diffs on SQL Server < 2022, use numeric or categorical partitioning instead. + ### Supported Configurations | Warehouse Type | Authentication | Notes | |---|---|---| -| `sqlserver` / `mssql` | User/password or Azure AD | On-prem or Azure SQL. Requires SQL Server 2022+ for `DATETRUNC` and `LEAST`. | +| `sqlserver` / `mssql` | User/password or Azure AD | On-prem or Azure SQL. SQL Server 2022+ required for date partitioning. | | `fabric` | Azure AD only | Microsoft Fabric SQL endpoint. Always uses TLS encryption. | ### Connecting to Microsoft Fabric @@ -445,9 +457,14 @@ database: "" authentication: "azure-active-directory-default" # recommended ``` -Supported Azure AD authentication types: -- `azure-active-directory-default` — auto-discovers credentials via `DefaultAzureCredential` (recommended) -- `token-credential` — same as above, with optional `azure_client_id` for managed identity +Auth shorthands (mapped to full tedious type names): +- `CLI` or `default` → `azure-active-directory-default` +- `password` → `azure-active-directory-password` +- `service-principal` → `azure-active-directory-service-principal-secret` +- `msi` or `managed-identity` → `azure-active-directory-msi-vm` + +Full Azure AD authentication types: +- `azure-active-directory-default` — auto-discovers credentials via `DefaultAzureCredential` (recommended; works with `az login`) - `azure-active-directory-password` — username/password with `azure_client_id` and `azure_tenant_id` - `azure-active-directory-access-token` — pre-obtained token (does **not** auto-refresh) - `azure-active-directory-service-principal-secret` — service principal with `azure_client_id`, `azure_client_secret`, `azure_tenant_id` diff --git a/bun.lock b/bun.lock index 25e43809d..a6b9ac546 100644 --- a/bun.lock +++ b/bun.lock @@ -43,6 +43,7 @@ "mongodb": "^6.0.0", }, "optionalDependencies": { + "@azure/identity": "^4.0.0", "@clickhouse/client": "^1.0.0", "@databricks/sql": "^1.0.0", "@google-cloud/bigquery": "^8.0.0", @@ -488,7 +489,7 @@ "@azure/core-xml": ["@azure/core-xml@1.5.0", "", { "dependencies": { "fast-xml-parser": "^5.0.7", "tslib": "^2.8.1" } }, "sha512-D/sdlJBMJfx7gqoj66PKVmhDDaU6TKA49ptcolxdas29X7AfvLTmfAGLjAcIMBK7UZ2o4lygHIqVckOlQU3xWw=="], - "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "@azure/identity": ["@azure/identity@4.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^5.1.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-5C/2WD5Vb1lHnZS16dNQRPMjN6oV/Upba+C9nBIs15PmOi6A3ZGs4Lr2u60zw4S04gi+u3cEXiqTVP7M4Pz3kw=="], "@azure/keyvault-common": ["@azure/keyvault-common@2.0.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.3.0", "@azure/core-client": "^1.5.0", "@azure/core-rest-pipeline": "^1.8.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.10.0", "@azure/logger": "^1.1.4", "tslib": "^2.2.0" } }, "sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w=="], @@ -496,11 +497,11 @@ "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + "@azure/msal-browser": ["@azure/msal-browser@5.6.3", "", { "dependencies": { "@azure/msal-common": "16.4.1" } }, "sha512-sTjMtUm+bJpENU/1WlRzHEsgEHppZDZ1EtNyaOODg/sQBtMxxJzGB+MOCM+T2Q5Qe1fKBrdxUmjyRxm0r7Ez9w=="], - "@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + "@azure/msal-common": ["@azure/msal-common@16.4.1", "", {}, "sha512-Bl8f+w37xkXsYh7QRkAKCFGYtWMYuOVO7Lv+BxILrvGz3HbIEF22Pt0ugyj0QPOl6NLrHcnNUQ9yeew98P/5iw=="], - "@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + "@azure/msal-node": ["@azure/msal-node@5.1.2", "", { "dependencies": { "@azure/msal-common": "16.4.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-DoeSJ9U5KPAIZoHsPywvfEj2MhBniQe0+FSpjLUTdWoIkI999GB5USkW6nNEHnIaLVxROHXvprWA1KzdS1VQ4A=="], "@azure/storage-blob": ["@azure/storage-blob@12.26.0", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.4.0", "@azure/core-client": "^1.6.2", "@azure/core-http-compat": "^2.0.0", "@azure/core-lro": "^2.2.0", "@azure/core-paging": "^1.1.1", "@azure/core-rest-pipeline": "^1.10.1", "@azure/core-tracing": "^1.1.2", "@azure/core-util": "^1.6.1", "@azure/core-xml": "^1.4.3", "@azure/logger": "^1.0.0", "events": "^3.0.0", "tslib": "^2.2.0" } }, "sha512-SriLPKezypIsiZ+TtlFfE46uuBIap2HeaQVS78e1P7rz5OSbq0rsd52WE1mC5f7vAeLiXqv7I7oRhL3WFZEw3Q=="], @@ -3144,6 +3145,8 @@ "snowflake-sdk/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.21", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.18", "@aws-sdk/credential-provider-http": "^3.972.20", "@aws-sdk/credential-provider-ini": "^3.972.20", "@aws-sdk/credential-provider-process": "^3.972.18", "@aws-sdk/credential-provider-sso": "^3.972.20", "@aws-sdk/credential-provider-web-identity": "^3.972.20", "@aws-sdk/types": "^3.973.6", "@smithy/credential-provider-imds": "^4.2.12", "@smithy/property-provider": "^4.2.12", "@smithy/shared-ini-file-loader": "^4.4.7", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-hah8if3/B/Q+LBYN5FukyQ1Mym6PLPDsBOBsIgNEYD6wLyZg0UmUF/OKIVC3nX9XH8TfTPuITK+7N/jenVACWA=="], + "snowflake-sdk/@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "snowflake-sdk/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "snowflake-sdk/open": ["open@7.4.2", "", { "dependencies": { "is-docker": "^2.0.0", "is-wsl": "^2.1.1" } }, "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q=="], @@ -3168,6 +3171,8 @@ "tar-stream/bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + "tedious/@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "teeny-request/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], "teeny-request/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], @@ -3520,6 +3525,8 @@ "@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="], + "@types/mssql/tedious/@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "@types/mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "@types/request/form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], @@ -3642,6 +3649,12 @@ "snowflake-sdk/@aws-sdk/credential-provider-node/@smithy/types": ["@smithy/types@4.13.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g=="], + "snowflake-sdk/@azure/identity/@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + + "snowflake-sdk/@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + + "snowflake-sdk/@azure/identity/open": ["open@10.1.2", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "is-wsl": "^3.1.0" } }, "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw=="], + "snowflake-sdk/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "snowflake-sdk/open/is-docker": ["is-docker@2.2.1", "", { "bin": { "is-docker": "cli.js" } }, "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ=="], @@ -3652,6 +3665,10 @@ "string-width-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + "tedious/@azure/identity/@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + + "tedious/@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + "teeny-request/http-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], "teeny-request/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], @@ -3772,6 +3789,10 @@ "@smithy/util-stream/@smithy/node-http-handler/@smithy/querystring-builder/@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + "@types/mssql/tedious/@azure/identity/@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + + "@types/mssql/tedious/@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + "@types/request/form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -3788,6 +3809,8 @@ "cross-fetch/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "drizzle-orm/mssql/tedious/@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "drizzle-orm/mssql/tedious/iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], "gaxios/rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], @@ -3834,6 +3857,16 @@ "snowflake-sdk/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds/@smithy/url-parser": ["@smithy/url-parser@4.2.12", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.12", "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA=="], + "snowflake-sdk/@azure/identity/@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "snowflake-sdk/@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "tedious/@azure/identity/@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "tedious/@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "tedious/@azure/identity/@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "wide-align/string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], "@aws-sdk/client-sts/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http/@smithy/util-stream/@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.2", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.2", "tslib": "^2.6.2" } }, "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q=="], @@ -3860,10 +3893,20 @@ "@google-cloud/storage/teeny-request/node-fetch/whatwg-url/webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], + "@types/mssql/tedious/@azure/identity/@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "@types/mssql/tedious/@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "@types/mssql/tedious/@azure/identity/@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], "cacache/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "drizzle-orm/mssql/tedious/@azure/identity/@azure/msal-browser": ["@azure/msal-browser@4.28.2", "", { "dependencies": { "@azure/msal-common": "15.14.2" } }, "sha512-6vYUMvs6kJxJgxaCmHn/F8VxjLHNh7i9wzfwPGf8kyBJ8Gg2yvBXx175Uev8LdrD1F5C4o7qHa2CC4IrhGE1XQ=="], + + "drizzle-orm/mssql/tedious/@azure/identity/@azure/msal-node": ["@azure/msal-node@3.8.7", "", { "dependencies": { "@azure/msal-common": "15.14.2", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-a+Xnrae+uwLnlw68bplS1X4kuJ9F/7K6afuMFyRkNIskhjgDezl5Fhrx+1pmAlDmC0VaaAxjRQMp1OmcqVwkIg=="], + "gaxios/rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], "gaxios/rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], @@ -4156,6 +4199,12 @@ "@aws-sdk/middleware-flexible-checksums/@aws-sdk/core/@smithy/smithy-client/@smithy/middleware-endpoint/@smithy/url-parser/@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.12", "", { "dependencies": { "@smithy/types": "^4.13.1", "tslib": "^2.6.2" } }, "sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw=="], + "drizzle-orm/mssql/tedious/@azure/identity/@azure/msal-browser/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "drizzle-orm/mssql/tedious/@azure/identity/@azure/msal-node/@azure/msal-common": ["@azure/msal-common@15.14.2", "", {}, "sha512-n8RBJEUmd5QotoqbZfd+eGBkzuFI1KX6jw2b3WcpSyGjwmzoeI/Jb99opIBPHpb8y312NB+B6+FGi2ZVSR8yfA=="], + + "drizzle-orm/mssql/tedious/@azure/identity/@azure/msal-node/uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], + "gaxios/rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], "gaxios/rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], diff --git a/packages/drivers/package.json b/packages/drivers/package.json index 361c1dd96..040adf5fa 100644 --- a/packages/drivers/package.json +++ b/packages/drivers/package.json @@ -22,5 +22,13 @@ "duckdb": "^1.0.0", "mongodb": "^6.0.0", "@clickhouse/client": "^1.0.0" + }, + "peerDependencies": { + "@azure/identity": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@azure/identity": { + "optional": true + } } } diff --git a/packages/drivers/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index 2f66537e9..5c369306c 100644 --- a/packages/drivers/src/sqlserver.ts +++ b/packages/drivers/src/sqlserver.ts @@ -56,6 +56,14 @@ export async function connect(config: ConnectionConfig): Promise { if (authType?.startsWith("azure-active-directory")) { // Azure AD / Entra ID — tedious handles credential creation internally. // We pass the type + options; tedious imports @azure/identity itself. + // Verify @azure/identity is available before attempting Azure AD auth. + try { + await import("@azure/identity") + } catch { + throw new Error( + "Azure AD authentication requires @azure/identity. Run: npm install @azure/identity", + ) + } ;(mssqlConfig.options as any).encrypt = true if (authType === "azure-active-directory-default") {