diff --git a/.opencode/skills/data-parity/SKILL.md b/.opencode/skills/data-parity/SKILL.md index 2bb7fa5df..07e217f83 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,56 @@ 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 + +### 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. SQL Server 2022+ required for date partitioning. | +| `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 +``` + +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` +- `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]`). diff --git a/bun.lock b/bun.lock index 1b06053a5..a6b9ac546 100644 --- a/bun.lock +++ b/bun.lock @@ -43,12 +43,13 @@ "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", "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", @@ -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=="], @@ -1034,7 +1035,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 +1903,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 +2337,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 +2989,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 +3043,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=="], @@ -3140,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=="], @@ -3164,7 +3171,7 @@ "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=="], + "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=="], @@ -3518,6 +3525,10 @@ "@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=="], "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 +3567,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=="], @@ -3632,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=="], @@ -3642,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=="], @@ -3762,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=="], @@ -3778,6 +3809,10 @@ "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=="], "gaxios/rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -3822,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=="], @@ -3848,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=="], @@ -4144,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 98a0112cf..040adf5fa 100644 --- a/packages/drivers/package.json +++ b/packages/drivers/package.json @@ -17,10 +17,18 @@ "@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", "@clickhouse/client": "^1.0.0" + }, + "peerDependencies": { + "@azure/identity": ">=4.0.0" + }, + "peerDependenciesMeta": { + "@azure/identity": { + "optional": true + } } } diff --git a/packages/drivers/src/normalize.ts b/packages/drivers/src/normalize.ts index 5afc20cee..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 = { @@ -104,6 +109,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/src/sqlserver.ts b/packages/drivers/src/sqlserver.ts index 3ea1e390f..5c369306c 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", @@ -24,8 +27,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,7 +40,88 @@ export async function connect(config: ConnectionConfig): Promise { }, } - pool = await mssql.connect(mssqlConfig) + // 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")) { + // 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") { + 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 = { + 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 + } + + // 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 { @@ -62,22 +144,39 @@ 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 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 [k, v] of Object.entries(row)) { + if (k === "" && 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]).filter((k) => !k.startsWith("_")) - : (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 new file mode 100644 index 000000000..5042b8f5b --- /dev/null +++ b/packages/drivers/test/sqlserver-unit.test.ts @@ -0,0 +1,456 @@ +/** + * 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 + */ +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 +} + +function createMockPool(config: any) { + mockConnectCalls.push(config) + return { + connect: async () => {}, + request: () => createMockRequest(), + close: async () => { + mockCloseCalls++ + }, + } +} + +mock.module("mssql", () => ({ + default: { + 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() } + }, +})) + +// 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) + }) + }) + + // --- 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 passes type to tedious (no credential object)", 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("azure-active-directory-default") + expect(cfg.authentication.options.credential).toBeUndefined() + }) + + 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: "azure-active-directory-default", + azure_client_id: "mi-client-id", + }) + await c.connect() + const cfg = mockConnectCalls[0] + 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 () => { + 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) + }) + + 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 --- + + 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("preserves underscore-prefixed columns", async () => { + mockQueryResult = { + recordset: [{ id: 1, _p: "Delivered", name: "x" }], + } + const result = await connector.execute("SELECT * FROM t") + 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/src/altimate/native/connections/data-diff.ts b/packages/opencode/src/altimate/native/connections/data-diff.ts index 294c43745..42f476b80 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})` @@ -526,18 +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, '${escaped}', 23)` default: - return `${expr} = '${partitionValue}'` + return `${expr} = '${escaped}'` } } @@ -623,10 +661,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 +804,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..cc871682c 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", @@ -165,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)"}`) } } 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/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") + }) +}) 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() + }) }) // ---------------------------------------------------------------------------