From ccb2dc7c26d838230384cabfb619ed108c3cf1c6 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Thu, 27 Nov 2025 16:05:50 +0300 Subject: [PATCH 01/21] Add MSSQL Server support to db0 --- docs/2.connectors/1.index.md | 1 + docs/2.connectors/mssql.md | 30 +++ package.json | 7 +- pnpm-lock.yaml | 449 +++++++++++++++++++++++++++++++++- scripts/gen-connectors.ts | 6 +- src/_connectors.ts | 5 +- src/connectors/mssql.ts | 207 ++++++++++++++++ src/types.ts | 2 +- test/connectors/_tests.ts | 9 + test/connectors/mssql.test.ts | 122 +++++++++ 10 files changed, 831 insertions(+), 7 deletions(-) create mode 100644 docs/2.connectors/mssql.md create mode 100644 src/connectors/mssql.ts create mode 100644 test/connectors/mssql.test.ts diff --git a/docs/2.connectors/1.index.md b/docs/2.connectors/1.index.md index f993a90..447ba5e 100644 --- a/docs/2.connectors/1.index.md +++ b/docs/2.connectors/1.index.md @@ -15,6 +15,7 @@ Currently supported connectors: - [PostgreSQL](/connectors/postgresql) - [MySQL](/connectors/mysql) - [SQLite](/connectors/sqlite) +- [MSSQL](/connectors/mssql) ::read-more{to="https://github.com/unjs/db0/issues/32"} See [unjs/db0#32](https://github.com/unjs/db0/issues/32) for the list of upcoming connectors. diff --git a/docs/2.connectors/mssql.md b/docs/2.connectors/mssql.md new file mode 100644 index 0000000..090c96c --- /dev/null +++ b/docs/2.connectors/mssql.md @@ -0,0 +1,30 @@ +--- +icon: devicon-plain:microsoftsqlserver +--- + +# MSSQL + +> Connect DB0 to MSSQL Database using `tedious` + +## Usage + +For this connector, you need to install [`tedious`](https://www.npmjs.com/package/tedious) dependency: + +:pm-install{name="tedious"} + +Use `mssql` connector: + +```js +import { createDatabase } from "db0"; +import mssql from "db0/connectors/mssql"; + +const db = createDatabase( + mssql({ + /* options */ + }), +); +``` + +## Options + +:read-more{to="https://tediousjs.github.io/tedious/api-connection.html#function_newConnection"} diff --git a/package.json b/package.json index af8793b..1c3be8b 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "pg": "^8.16.3", "prettier": "^3.6.2", "scule": "^1.3.0", + "tedious": "^18.6.1", "typescript": "^5.9.3", "vitest": "^4.0.12", "wrangler": "^4.49.1" @@ -75,7 +76,8 @@ "better-sqlite3": "*", "drizzle-orm": "*", "mysql2": "*", - "sqlite3": "*" + "sqlite3": "*", + "tedious": "*" }, "peerDependenciesMeta": { "@libsql/client": { @@ -90,6 +92,9 @@ "mysql2": { "optional": true }, + "tedious": { + "optional": true + }, "@electric-sql/pglite": { "optional": true }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dff25f4..a2ed0f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -81,6 +81,9 @@ importers: scule: specifier: ^1.3.0 version: 1.3.0 + tedious: + specifier: ^18.6.1 + version: 18.6.2 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -108,6 +111,74 @@ importers: packages: + '@azure-rest/core-client@2.5.1': + resolution: {integrity: sha512-EHaOXW0RYDKS5CFffnixdyRPak5ytiCtU7uXDcP/uiY+A6jFRwNGzzJBiznkCzvi5EYpY+YWinieqHb0oY916A==} + engines: {node: '>=20.0.0'} + + '@azure/abort-controller@2.1.2': + resolution: {integrity: sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA==} + engines: {node: '>=18.0.0'} + + '@azure/core-auth@1.10.1': + resolution: {integrity: sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg==} + engines: {node: '>=20.0.0'} + + '@azure/core-client@1.10.1': + resolution: {integrity: sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w==} + engines: {node: '>=20.0.0'} + + '@azure/core-http-compat@2.3.1': + resolution: {integrity: sha512-az9BkXND3/d5VgdRRQVkiJb2gOmDU8Qcq4GvjtBmDICNiQ9udFmDk4ZpSB5Qq1OmtDJGlQAfBaS4palFsazQ5g==} + engines: {node: '>=20.0.0'} + + '@azure/core-lro@2.7.2': + resolution: {integrity: sha512-0YIpccoX8m/k00O7mDDMdJpbr6mf1yWo2dfmxt5A8XVZVVMz2SSKaEbMCeJRvgQ0IaSlqhjT47p4hVIRRy90xw==} + engines: {node: '>=18.0.0'} + + '@azure/core-paging@1.6.2': + resolution: {integrity: sha512-YKWi9YuCU04B55h25cnOYZHxXYtEvQEbKST5vqRga7hWY9ydd3FZHdeQF8pyh+acWZvppw13M/LMGx0LABUVMA==} + engines: {node: '>=18.0.0'} + + '@azure/core-rest-pipeline@1.22.2': + resolution: {integrity: sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg==} + engines: {node: '>=20.0.0'} + + '@azure/core-tracing@1.3.1': + resolution: {integrity: sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ==} + engines: {node: '>=20.0.0'} + + '@azure/core-util@1.13.1': + resolution: {integrity: sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A==} + engines: {node: '>=20.0.0'} + + '@azure/identity@4.13.0': + resolution: {integrity: sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw==} + engines: {node: '>=20.0.0'} + + '@azure/keyvault-common@2.0.0': + resolution: {integrity: sha512-wRLVaroQtOqfg60cxkzUkGKrKMsCP6uYXAOomOIysSMyt1/YM0eUn9LqieAWM8DLcU4+07Fio2YGpPeqUbpP9w==} + engines: {node: '>=18.0.0'} + + '@azure/keyvault-keys@4.10.0': + resolution: {integrity: sha512-eDT7iXoBTRZ2n3fLiftuGJFD+yjkiB1GNqzU2KbY1TLYeXeSPVTVgn2eJ5vmRTZ11978jy2Kg2wI7xa9Tyr8ag==} + engines: {node: '>=18.0.0'} + + '@azure/logger@1.3.0': + resolution: {integrity: sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA==} + engines: {node: '>=20.0.0'} + + '@azure/msal-browser@4.26.2': + resolution: {integrity: sha512-F2U1mEAFsYGC5xzo1KuWc/Sy3CRglU9Ql46cDUx8x/Y3KnAIr1QAq96cIKCk/ZfnVxlvprXWRjNKoEpgLJXLhg==} + engines: {node: '>=0.8.0'} + + '@azure/msal-common@15.13.2': + resolution: {integrity: sha512-cNwUoCk3FF8VQ7Ln/MdcJVIv3sF73/OT86cRH81ECsydh7F4CNfIo2OAx6Cegtg8Yv75x4506wN4q+Emo6erOA==} + engines: {node: '>=0.8.0'} + + '@azure/msal-node@3.8.3': + resolution: {integrity: sha512-Ul7A4gwmaHzYWj2Z5xBDly/W8JSC1vnKgJ898zPMZr0oSf1ah0tiL15sytjycU/PMhDZAlkWtEL1+MzNMU6uww==} + engines: {node: '>=16'} + '@babel/generator@7.28.5': resolution: {integrity: sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==} engines: {node: '>=6.9.0'} @@ -981,6 +1052,9 @@ packages: '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@js-joda/core@5.6.5': + resolution: {integrity: sha512-3zwefSMwHpu8iVUW8YYz227sIv6UFqO31p1Bf1ZH/Vom7CmNyUsXjDBlnNzcuhmOL1XfxZ3nvND42kR23XlbcQ==} + '@libsql/client@0.15.15': resolution: {integrity: sha512-twC0hQxPNHPKfeOv3sNT6u2pturQjLcI+CnpTM0SjRpocEGgfiZ7DWKXLNnsothjyJmDqEsBQJ5ztq9Wlu470w==} @@ -1676,6 +1750,9 @@ packages: '@types/react@19.1.13': resolution: {integrity: sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==} + '@types/readable-stream@4.0.22': + resolution: {integrity: sha512-/FFhJpfCLAPwAcN3mFycNUa77ddnr8jTgF5VmSNetaemWB2cIlfCA9t0YTM3JAT0wOcv8D4tjPo7pkDhK3EJIg==} + '@types/unist@2.0.11': resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} @@ -1741,6 +1818,10 @@ packages: resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@typespec/ts-http-runtime@0.3.2': + resolution: {integrity: sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg==} + engines: {node: '>=20.0.0'} + '@vitest/coverage-v8@4.0.12': resolution: {integrity: sha512-d+w9xAFJJz6jyJRU4BUU7MH409Ush7FWKNkxJU+jASKg6WX33YT0zc+YawMR1JesMWt9QRFQY/uAD3BTn23FaA==} peerDependencies: @@ -1782,6 +1863,10 @@ packages: abbrev@1.1.1: resolution: {integrity: sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==} + abort-controller@3.0.0: + resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==} + engines: {node: '>=6.5'} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -1805,6 +1890,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -1877,6 +1966,9 @@ packages: bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + bl@6.1.5: + resolution: {integrity: sha512-XylDt2P3JBttAwLpORq/hOEX9eJzP0r6Voa46C/WVvad8D1J0jW5876txB8FnzKtbdnU6X4Y1vOEvC6PllJrDg==} + blake3-wasm@2.1.5: resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==} @@ -1895,12 +1987,18 @@ packages: engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true + buffer-equal-constant-time@1.0.1: + resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + buffer@6.0.3: + resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==} + builtin-modules@5.0.0: resolution: {integrity: sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==} engines: {node: '>=18.20'} @@ -2331,6 +2429,9 @@ packages: oxc-resolver: optional: true + ecdsa-sig-formatter@1.0.11: + resolution: {integrity: sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==} + electron-to-chromium@1.5.258: resolution: {integrity: sha512-rHUggNV5jKQ0sSdWwlaRDkFc3/rRJIVnOSe9yR4zrR07m3ZxhP4N27Hlg8VeJGGYgFTxK5NqDmWI4DSH72vIJg==} @@ -2482,6 +2583,14 @@ packages: event-emitter@0.3.5: resolution: {integrity: sha512-D9rRn9y7kLPnJ+hMq7S/nhvoKwwvVJahBi2BPmx3bvbsEdK3W9ii8cBSGjP+72/LnM4n6fo3+dkCX5FeTQruXA==} + event-target-shim@5.0.1: + resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} + engines: {node: '>=6'} + + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + exit-hook@2.2.1: resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==} engines: {node: '>=6'} @@ -2657,10 +2766,18 @@ packages: resolution: {integrity: sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==} engines: {node: '>= 6'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -2808,6 +2925,9 @@ packages: js-base64@3.7.8: resolution: {integrity: sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==} + js-md4@0.3.2: + resolution: {integrity: sha512-/GDnfQYsltsjRswQhN9fhv3EMw2sCpUdrdxyWDOUK7eyD++r3gRhzgiQgc/x4MAv2i1iuQ4lxO5mvqM3vj4bwA==} + js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} @@ -2838,6 +2958,16 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + jsonwebtoken@9.0.2: + resolution: {integrity: sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ==} + engines: {node: '>=12', npm: '>=6'} + + jwa@1.4.2: + resolution: {integrity: sha512-eeH5JO+21J78qMvTIDdBXidBd6nG2kZjg5Ohz/1fpa28Z4CcsWUzJ1ZZyFq/3z3N17aZy+ZuBoHljASbL1WfOw==} + + jws@3.2.2: + resolution: {integrity: sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2864,9 +2994,30 @@ packages: lodash.deburr@4.1.0: resolution: {integrity: sha512-m/M1U1f3ddMCs6Hq2tAsYThTBDaAKFDX3dwDo97GEYzamXi9SqUpjWi/Rrj/gf3X2n8ktwgZrlP1z6E3v/IExQ==} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + lodash.throttle@4.1.1: resolution: {integrity: sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==} @@ -3027,6 +3178,9 @@ packages: napi-build-utils@2.0.0: resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + native-duplexpair@1.0.0: + resolution: {integrity: sha512-E7QQoM+3jvNtlmyfqRZ0/U75VFgCls+fSkbml2MpgWkWyz3ox8Y58gNhfuziuQYGNNQAbFZJQck55LHCnCK6CA==} + natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} @@ -3248,6 +3402,10 @@ packages: resolution: {integrity: sha512-nODzvTiYVRGRqAOvE84Vk5JDPyyxsVk0/fbA/bq7RqlnhksGpset09XTxbpvLTIjoaF7K8Z8DG8yHtKGTPSYRw==} engines: {node: '>=20'} + process@0.11.10: + resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} + engines: {node: '>= 0.6.0'} + promise-inflight@1.0.1: resolution: {integrity: sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==} peerDependencies: @@ -3284,6 +3442,10 @@ packages: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} + readable-stream@4.7.0: + resolution: {integrity: sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + readdirp@4.1.2: resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} engines: {node: '>= 14.18.0'} @@ -3429,6 +3591,9 @@ packages: resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} engines: {node: '>= 10.x'} + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + sqlite3@5.1.7: resolution: {integrity: sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==} @@ -3496,6 +3661,10 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + tedious@18.6.2: + resolution: {integrity: sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==} + engines: {node: '>=18'} + timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} engines: {node: '>=0.12'} @@ -3591,6 +3760,10 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@8.3.2: + resolution: {integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==} + hasBin: true + vite@7.2.4: resolution: {integrity: sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3765,6 +3938,151 @@ packages: snapshots: + '@azure-rest/core-client@2.5.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@typespec/ts-http-runtime': 0.3.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/abort-controller@2.1.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-auth@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-client@1.10.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-http-compat@2.3.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + transitivePeerDependencies: + - supports-color + + '@azure/core-lro@2.7.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-paging@1.6.2': + dependencies: + tslib: 2.8.1 + + '@azure/core-rest-pipeline@1.22.2': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@typespec/ts-http-runtime': 0.3.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/core-tracing@1.3.1': + dependencies: + tslib: 2.8.1 + + '@azure/core-util@1.13.1': + dependencies: + '@azure/abort-controller': 2.1.2 + '@typespec/ts-http-runtime': 0.3.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/identity@4.13.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + '@azure/msal-browser': 4.26.2 + '@azure/msal-node': 3.8.3 + open: 10.2.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-common@2.0.0': + dependencies: + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-client': 1.10.1 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/keyvault-keys@4.10.0': + dependencies: + '@azure-rest/core-client': 2.5.1 + '@azure/abort-controller': 2.1.2 + '@azure/core-auth': 1.10.1 + '@azure/core-http-compat': 2.3.1 + '@azure/core-lro': 2.7.2 + '@azure/core-paging': 1.6.2 + '@azure/core-rest-pipeline': 1.22.2 + '@azure/core-tracing': 1.3.1 + '@azure/core-util': 1.13.1 + '@azure/keyvault-common': 2.0.0 + '@azure/logger': 1.3.0 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/logger@1.3.0': + dependencies: + '@typespec/ts-http-runtime': 0.3.2 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + + '@azure/msal-browser@4.26.2': + dependencies: + '@azure/msal-common': 15.13.2 + + '@azure/msal-common@15.13.2': {} + + '@azure/msal-node@3.8.3': + dependencies: + '@azure/msal-common': 15.13.2 + jsonwebtoken: 9.0.2 + uuid: 8.3.2 + '@babel/generator@7.28.5': dependencies: '@babel/parser': 7.28.5 @@ -4309,6 +4627,8 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@js-joda/core@5.6.5': {} + '@libsql/client@0.15.15': dependencies: '@libsql/core': 0.15.15 @@ -4786,6 +5106,10 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/readable-stream@4.0.22': + dependencies: + '@types/node': 24.10.1 + '@types/unist@2.0.11': {} '@types/ws@8.18.1': @@ -4885,6 +5209,14 @@ snapshots: '@typescript-eslint/types': 8.47.0 eslint-visitor-keys: 4.2.1 + '@typespec/ts-http-runtime@0.3.2': + dependencies: + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + tslib: 2.8.1 + transitivePeerDependencies: + - supports-color + '@vitest/coverage-v8@4.0.12(vitest@4.0.12(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.7.1))': dependencies: '@bcoe/v8-coverage': 1.0.2 @@ -4944,6 +5276,10 @@ snapshots: abbrev@1.1.1: optional: true + abort-controller@3.0.0: + dependencies: + event-target-shim: 5.0.1 + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -4961,6 +5297,8 @@ snapshots: - supports-color optional: true + agent-base@7.1.4: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -5057,6 +5395,13 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 + bl@6.1.5: + dependencies: + '@types/readable-stream': 4.0.22 + buffer: 6.0.3 + inherits: 2.0.4 + readable-stream: 4.7.0 + blake3-wasm@2.1.5: {} brace-expansion@1.1.12: @@ -5080,6 +5425,8 @@ snapshots: node-releases: 2.0.27 update-browserslist-db: 1.1.4(browserslist@4.28.0) + buffer-equal-constant-time@1.0.1: {} + buffer-from@1.1.2: {} buffer@5.7.1: @@ -5087,6 +5434,11 @@ snapshots: base64-js: 1.5.1 ieee754: 1.2.1 + buffer@6.0.3: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + builtin-modules@5.0.0: {} bun-types@1.3.2(@types/react@19.1.13): @@ -5379,6 +5731,10 @@ snapshots: dts-resolver@2.1.3: {} + ecdsa-sig-formatter@1.0.11: + dependencies: + safe-buffer: 5.2.1 + electron-to-chromium@1.5.258: {} emoji-regex@8.0.0: @@ -5675,6 +6031,10 @@ snapshots: d: 1.0.2 es5-ext: 0.10.64 + event-target-shim@5.0.1: {} + + events@3.3.0: {} + exit-hook@2.2.1: {} expand-template@2.0.3: {} @@ -5851,6 +6211,13 @@ snapshots: - supports-color optional: true + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -5859,6 +6226,13 @@ snapshots: - supports-color optional: true + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -5867,7 +6241,6 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 - optional: true iconv-lite@0.7.0: dependencies: @@ -5982,6 +6355,8 @@ snapshots: js-base64@3.7.8: {} + js-md4@0.3.2: {} + js-tokens@9.0.1: {} js-yaml@4.1.1: @@ -6004,6 +6379,30 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + jsonwebtoken@9.0.2: + dependencies: + jws: 3.2.2 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + + jwa@1.4.2: + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: 5.2.1 + + jws@3.2.2: + dependencies: + jwa: 1.4.2 + safe-buffer: 5.2.1 + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -6038,8 +6437,22 @@ snapshots: lodash.deburr@4.1.0: {} + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + lodash.merge@4.6.2: {} + lodash.once@4.1.1: {} + lodash.throttle@4.1.1: {} long@5.3.2: {} @@ -6252,6 +6665,8 @@ snapshots: napi-build-utils@2.0.0: {} + native-duplexpair@1.0.0: {} + natural-compare@1.4.0: {} negotiator@0.6.4: @@ -6555,6 +6970,8 @@ snapshots: pretty-bytes@7.1.0: {} + process@0.11.10: {} + promise-inflight@1.0.1: optional: true @@ -6593,6 +7010,14 @@ snapshots: string_decoder: 1.3.0 util-deprecate: 1.0.2 + readable-stream@4.7.0: + dependencies: + abort-controller: 3.0.0 + buffer: 6.0.3 + events: 3.3.0 + process: 0.11.10 + string_decoder: 1.3.0 + readdirp@4.1.2: {} regexp-tree@0.1.27: {} @@ -6779,6 +7204,8 @@ snapshots: split2@4.2.0: {} + sprintf-js@1.1.3: {} + sqlite3@5.1.7: dependencies: bindings: 1.5.0 @@ -6860,6 +7287,21 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tedious@18.6.2: + dependencies: + '@azure/core-auth': 1.10.1 + '@azure/identity': 4.13.0 + '@azure/keyvault-keys': 4.10.0 + '@js-joda/core': 5.6.5 + '@types/node': 24.10.1 + bl: 6.1.5 + iconv-lite: 0.6.3 + js-md4: 0.3.2 + native-duplexpair: 1.0.0 + sprintf-js: 1.1.3 + transitivePeerDependencies: + - supports-color + timers-ext@0.1.8: dependencies: es5-ext: 0.10.64 @@ -6886,8 +7328,7 @@ snapshots: dependencies: typescript: 5.9.3 - tslib@2.8.1: - optional: true + tslib@2.8.1: {} tunnel-agent@0.6.0: dependencies: @@ -6956,6 +7397,8 @@ snapshots: util-deprecate@1.0.2: {} + uuid@8.3.2: {} + vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(yaml@2.7.1): dependencies: esbuild: 0.25.12 diff --git a/scripts/gen-connectors.ts b/scripts/gen-connectors.ts index be734de..8fb6095 100644 --- a/scripts/gen-connectors.ts +++ b/scripts/gen-connectors.ts @@ -50,6 +50,10 @@ const connectors: { optionsTName?: string; }[] = []; +const connectorOptionsNameAliases: Record = { + "mssql": "MSSQL" +}; + for (const entry of connectorEntries) { const pathName = entry.replace(/\.ts$/, ""); const name = pathName.replace(/\/|\\/g, "-"); @@ -67,7 +71,7 @@ for (const entry of connectorEntries) { const names = [...new Set([name, ...alternativeNames])]; - const optionsTName = upperFirst(safeName) + "Options"; + const optionsTName = (connectorOptionsNameAliases[name] || upperFirst(safeName)) + "Options"; connectors.push({ name, diff --git a/src/_connectors.ts b/src/_connectors.ts index e0171c6..e09db47 100644 --- a/src/_connectors.ts +++ b/src/_connectors.ts @@ -9,6 +9,7 @@ import type { ConnectorOptions as LibSQLCoreOptions } from "db0/connectors/libsq import type { ConnectorOptions as LibSQLHttpOptions } from "db0/connectors/libsql/http"; import type { ConnectorOptions as LibSQLNodeOptions } from "db0/connectors/libsql/node"; import type { ConnectorOptions as LibSQLWebOptions } from "db0/connectors/libsql/web"; +import type { ConnectorOptions as MSSQLOptions } from "db0/connectors/mssql"; import type { ConnectorOptions as MySQL2Options } from "db0/connectors/mysql2"; import type { ConnectorOptions as NodeSQLiteOptions } from "db0/connectors/node-sqlite"; import type { ConnectorOptions as PgliteOptions } from "db0/connectors/pglite"; @@ -16,7 +17,7 @@ import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/plan import type { ConnectorOptions as PostgreSQLOptions } from "db0/connectors/postgresql"; import type { ConnectorOptions as SQLite3Options } from "db0/connectors/sqlite3"; -export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; +export type ConnectorName = "better-sqlite3" | "bun-sqlite" | "bun" | "cloudflare-d1" | "cloudflare-hyperdrive-mysql" | "cloudflare-hyperdrive-postgresql" | "libsql-core" | "libsql-http" | "libsql-node" | "libsql" | "libsql-web" | "mssql" | "mysql2" | "node-sqlite" | "sqlite" | "pglite" | "planetscale" | "postgresql" | "sqlite3"; export type ConnectorOptions = { "better-sqlite3": BetterSQLite3Options; @@ -32,6 +33,7 @@ export type ConnectorOptions = { /** alias of libsql-node */ "libsql": LibSQLNodeOptions; "libsql-web": LibSQLWebOptions; + "mssql": MSSQLOptions; "mysql2": MySQL2Options; "node-sqlite": NodeSQLiteOptions; /** alias of node-sqlite */ @@ -56,6 +58,7 @@ export const connectors: Record = Object.freeze({ /** alias of libsql-node */ "libsql": "db0/connectors/libsql/node", "libsql-web": "db0/connectors/libsql/web", + "mssql": "db0/connectors/mssql", "mysql2": "db0/connectors/mysql2", "node-sqlite": "db0/connectors/node-sqlite", /** alias of node-sqlite */ diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts new file mode 100644 index 0000000..640efa4 --- /dev/null +++ b/src/connectors/mssql.ts @@ -0,0 +1,207 @@ +import { + Connection, + Request, + Connection as TediousConnection, + type ConnectionConfiguration, + TYPES, +} from "tedious"; +import type { DataType } from "tedious/lib/data-type"; + +import type { Connector, Statement } from "../types"; + +export type ConnectorOptions = ConnectionConfiguration; + +export default function mssqlConnector(opts: ConnectorOptions) { + let _client: undefined | TediousConnection; + async function getClient(): Promise { + if (_client && _client.state === _client.STATE.LOGGED_IN) { + return _client; + } + + return new Promise((resolve, reject) => { + const client = new Connection(opts); + client.connect((error) => { + if (error) { + reject(error); + } + + _client = client; + }); + + client.on('connect', () => resolve(_client)); + client.on('error', reject); + }); + }; + + async function _run(sql: string, parameters?: unknown[]) { + if (!sql) { + throw new Error('SQL query must be provided'); + } + + const connection = await getClient(); + const { + sql: _sql, + parameters: _parameters, + } = prepareSqlParameters(sql, parameters); + + const query = new Promise<{ rows: unknown[], success: boolean }>((resolve, reject) => { + let success = false; + const request = new Request(_sql, (error) => { + if (error) { + reject(error); + } else { + success = true; + } + }); + + const parameterKeys = Object.keys(_parameters); + for (const key of parameterKeys) { + const parameter = _parameters[key]; + + request.addParameter( + parameter.name, + parameter.type, + parameter.value + ); + } + + const rows: unknown[] = []; + request.on('row', (columns = []) => { + const currentRow = {}; + for (const column of columns) { + const { value, metadata } = column; + const { colName } = metadata; + + currentRow[colName] = value; + } + + rows.push(currentRow); + }); + + request.on('requestCompleted', () => { + connection.close(); + resolve({ rows, success }); + }); + + request.on('error', (error) => { + connection.close(); + reject(error); + }); + + connection.execSql(request); + }); + + try { + const { + rows, + success, + } = await query; + + return { + rows, + success, + }; + } catch (error) { + error.sql = _sql; + error.parameters = parameters; + console.error(error); + } + } + + return >{ + name: "mssql", + dialect: "mssql", + exec(sql: string) { + return _run(sql, []); + }, + prepare(sql: string) { + const statement = { + _sql: sql, + _params: [], + bind(...params) { + if (params.length > 0) { + this._params = params; + } + return statement; + }, + async all(...params) { + const { rows } = await _run(this._sql, params || this._params); + return rows; + }, + async run(...params) { + const { success = false } = await _run(this._sql, params || this._params) || {}; + return { + success, + }; + }, + async get(...params) { + const { rows: [ row ] } = await _run(this._sql, params || this._params); + return row; + }, + }; + + return statement; + }, + }; +}; + +// taken from the `kysely` library: https://github.com/kysely-org/kysely/blob/413a88516c20be42dc8cbebade68c27669a3ac1a/src/dialect/mssql/mssql-driver.ts#L440 +export function getTediousDataType(value: unknown): DataType { + if (value === null || value === undefined || typeof value === 'string') { + return TYPES.NVarChar; + } + + if (typeof value === 'bigint' || (typeof value === 'number' && value % 1 === 0)) { + return value < -2_147_483_648 || value > 2_147_483_647 ? TYPES.BigInt : TYPES.Int; + } + + if (typeof value === 'number') { + return TYPES.Float; + } + + if (typeof value === 'boolean') { + return TYPES.Bit; + } + + if (value instanceof Date) { + return TYPES.DateTime2; + } + + if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { + return TYPES.VarBinary; + } + + return TYPES.NVarChar; +}; + +// replace `?` placeholders with `@1`, `@2`, etc. +export function prepareSqlParameters(sql: string, parameters: unknown[]) { + const parameterIndexes = []; + const tokens = [...sql]; + + // find all `?` placeholders in the SQL string + for (const [i, token] of tokens.entries()) { + + if (token === '?') { + parameterIndexes.push(i); + } + }; + + const namedParameters = {}; + for (const [index, parameterIndex] of parameterIndexes.entries()) { + const incrementedIndex = index + 1; + // replace `?` placeholder with index-based parameter name + tokens[parameterIndex] = `@${incrementedIndex}`; + // store the parameter value and type with the index-based parameter name + namedParameters[`@${incrementedIndex}`] = { + name: String(incrementedIndex), + type: getTediousDataType(parameters[index]), + value: parameters[index], + }; + } + + return { + sql: tokens.join(''), // join the tokens back into a SQL string + parameters: namedParameters, + }; +}; diff --git a/src/types.ts b/src/types.ts index c80bcc0..c3bc5dd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -3,7 +3,7 @@ */ export type Primitive = string | number | boolean | undefined | null; -export type SQLDialect = "mysql" | "postgresql" | "sqlite" | "libsql"; +export type SQLDialect = "mysql" | "postgresql" | "sqlite" | "libsql" | "mssql"; export type Statement = { /** diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 7f42d78..48fdc6d 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -44,6 +44,15 @@ export function testConnector(opts: { await db.sql`CREATE TABLE users (\`id\` VARCHAR(4) PRIMARY KEY, \`firstName\` TEXT, \`lastName\` TEXT, \`email\` TEXT)`; break; } + case "mssql": { + await db.sql`CREATE TABLE users ( + [id] NVARCHAR(4) PRIMARY KEY, + [firstName] NVARCHAR(255), + [lastName] NVARCHAR(255), + [email] NVARCHAR(255) + )`; + break; + } default: { await db.sql`CREATE TABLE users ("id" TEXT PRIMARY KEY, "firstName" TEXT, "lastName" TEXT, "email" TEXT)`; break; diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts new file mode 100644 index 0000000..ffad5ad --- /dev/null +++ b/test/connectors/mssql.test.ts @@ -0,0 +1,122 @@ +import { describe, it, expect } from "vitest"; +import { TYPES } from "tedious"; + +import { getTediousDataType, prepareSqlParameters } from "../../src/connectors/mssql"; +import connector from "../../src/connectors/mssql"; +import { testConnector } from "./_tests"; + +describe.runIf( + process.env.MSSQL_HOST + && process.env.MSSQL_DB_NAME + && process.env.MSSQL_USERNAME + && process.env.MSSQL_PASSWORD +)( + "connectors: mssql.test", + () => { + testConnector({ + dialect: "mssql", + connector: connector({ + server: process.env.MSSQL_HOST!, + authentication: { + type: 'default', + options: { + userName: process.env.MSSQL_USERNAME!, + password: process.env.MSSQL_PASSWORD!, + }, + }, + options: { + database: process.env.MSSQL_DB_NAME!, + port: Number.parseInt(process.env.MSSQL_PORT || '1433', 10), + trustServerCertificate: true, + encrypt: false, + }, + }), + }); + }, +); + +describe("getTediousDataType", () => { + it("should return NVarChar for null", () => { + expect(getTediousDataType(null)).toBe(TYPES.NVarChar); + }); + + it("should return NVarChar for undefined", () => { + expect(getTediousDataType(undefined)).toBe(TYPES.NVarChar); + }); + + it("should return NVarChar for strings", () => { + expect(getTediousDataType("test")).toBe(TYPES.NVarChar); + }); + + it("should return Int for numbers", () => { + expect(getTediousDataType(123)).toBe(TYPES.Int); + }); + + it("should return BigInt for large integer numbers", () => { + expect(getTediousDataType(2_147_483_648)).toBe(TYPES.BigInt); + }); + + it("should return Float for floating point numbers", () => { + expect(getTediousDataType(123.45)).toBe(TYPES.Float); + }); + + it("should return Bit for boolean values", () => { + expect(getTediousDataType(true)).toBe(TYPES.Bit); + }); + + it("should return DateTime for Date objects", () => { + expect(getTediousDataType(new Date())).toBe(TYPES.DateTime2); + }); + + it("should return VarBinary for Buffer objects", () => { + expect(getTediousDataType(Buffer.from("test"))).toBe(TYPES.VarBinary); + }); + + it("should return NVarChar by default for other types", () => { + expect(getTediousDataType({})).toBe(TYPES.NVarChar); + }); +}); + +describe("prepareSqlParameters", () => { + it("should replace ? with @1, @2, etc.", () => { + const sql = "SELECT * FROM users WHERE id = ? AND name = ?"; + const parameters = [1, "John"]; + const result = prepareSqlParameters(sql, parameters); + expect(result.sql).toBe("SELECT * FROM users WHERE id = @1 AND name = @2"); + expect(result.parameters).toEqual({ + "@1": { name: "1", type: TYPES.Int, value: 1 }, + "@2": { name: "2", type: TYPES.NVarChar, value: "John" }, + }); + }); + + it("should handle no parameters", () => { + const sql = "SELECT * FROM users"; + const parameters: unknown[] = []; + const result = prepareSqlParameters(sql, parameters); + expect(result.sql).toBe("SELECT * FROM users"); + expect(result.parameters).toEqual({}); + }); + + it("should handle multiple parameters of different types", () => { + const sql = "SELECT * FROM users WHERE id = ? AND age = ? AND active = ?"; + const parameters = [1, 30, true]; + const result = prepareSqlParameters(sql, parameters); + expect(result.sql).toBe("SELECT * FROM users WHERE id = @1 AND age = @2 AND active = @3"); + expect(result.parameters).toEqual({ + "@1": { name: "1", type: TYPES.Int, value: 1 }, + "@2": { name: "2", type: TYPES.Int, value: 30 }, + "@3": { name: "3", type: TYPES.Bit, value: true }, + }); + }); + + it("should handle null and undefined parameters", () => { + const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; + const parameters = [null, undefined]; + const result = prepareSqlParameters(sql, parameters); + expect(result.sql).toBe("SELECT * FROM users WHERE name = @1 AND email = @2"); + expect(result.parameters).toEqual({ + "@1": { name: "1", type: TYPES.NVarChar, value: null }, + "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, + }); + }); +}); \ No newline at end of file From 351643ddc4ac922843fe5da11b333e845daf8f69 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Thu, 27 Nov 2025 19:13:20 +0300 Subject: [PATCH 02/21] testing fixed MSSQL support --- src/connectors/mssql.ts | 200 +++++++++++++++++----------------- test/connectors/_tests.ts | 5 + test/connectors/mssql.test.ts | 89 ++++++++------- 3 files changed, 154 insertions(+), 140 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index 640efa4..b7e7c72 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -1,9 +1,9 @@ import { - Connection, - Request, - Connection as TediousConnection, - type ConnectionConfiguration, - TYPES, + Connection, + Request, + Connection as TediousConnection, + type ConnectionConfiguration, + TYPES, } from "tedious"; import type { DataType } from "tedious/lib/data-type"; @@ -17,100 +17,96 @@ export default function mssqlConnector(opts: ConnectorOptions) { if (_client && _client.state === _client.STATE.LOGGED_IN) { return _client; } - + return new Promise((resolve, reject) => { const client = new Connection(opts); client.connect((error) => { if (error) { reject(error); } - + _client = client; }); - - client.on('connect', () => resolve(_client)); - client.on('error', reject); + + client.on("connect", () => resolve(_client)); + client.on("error", reject); }); - }; - + } + async function _run(sql: string, parameters?: unknown[]) { if (!sql) { - throw new Error('SQL query must be provided'); + throw new Error("SQL query must be provided"); } - + const connection = await getClient(); - const { - sql: _sql, - parameters: _parameters, - } = prepareSqlParameters(sql, parameters); - - const query = new Promise<{ rows: unknown[], success: boolean }>((resolve, reject) => { - let success = false; - const request = new Request(_sql, (error) => { - if (error) { - reject(error); - } else { - success = true; - } - }); - - const parameterKeys = Object.keys(_parameters); - for (const key of parameterKeys) { - const parameter = _parameters[key]; - - request.addParameter( - parameter.name, - parameter.type, - parameter.value - ); - } - - const rows: unknown[] = []; - request.on('row', (columns = []) => { - const currentRow = {}; - for (const column of columns) { - const { value, metadata } = column; - const { colName } = metadata; - - currentRow[colName] = value; + const { sql: _sql, parameters: _parameters } = prepareSqlParameters( + sql, + parameters, + ); + + const query = new Promise<{ rows: unknown[]; success: boolean }>( + (resolve, reject) => { + let success = false; + const request = new Request(_sql, (error) => { + if (error) { + reject(error); + } else { + success = true; + } + }); + + const parameterKeys = Object.keys(_parameters); + for (const key of parameterKeys) { + const parameter = _parameters[key]; + + request.addParameter(parameter.name, parameter.type, parameter.value); } - - rows.push(currentRow); - }); - - request.on('requestCompleted', () => { - connection.close(); - resolve({ rows, success }); - }); - - request.on('error', (error) => { - connection.close(); - reject(error); - }); - - connection.execSql(request); - }); - + + const rows: unknown[] = []; + request.on("row", (columns = []) => { + const currentRow = {}; + for (const column of columns) { + const { value, metadata } = column; + const { colName } = metadata; + + currentRow[colName] = value; + } + + rows.push(currentRow); + }); + + request.on("requestCompleted", () => { + connection.close(); + resolve({ rows, success }); + }); + + request.on("error", (error) => { + connection.close(); + reject(error); + }); + + connection.execSql(request); + }, + ); + try { - const { - rows, - success, - } = await query; - + const { rows, success } = await query; + return { rows, success, }; - } catch (error) { + } catch (error: any) { error.sql = _sql; error.parameters = parameters; - console.error(error); + throw error; } } - + return >{ name: "mssql", dialect: "mssql", + getInstance: () => getClient(), exec(sql: string) { return _run(sql, []); }, @@ -129,64 +125,71 @@ export default function mssqlConnector(opts: ConnectorOptions) { return rows; }, async run(...params) { - const { success = false } = await _run(this._sql, params || this._params) || {}; + const { success = false } = + (await _run(this._sql, params || this._params)) || {}; return { success, }; }, async get(...params) { - const { rows: [ row ] } = await _run(this._sql, params || this._params); + const { + rows: [row], + } = await _run(this._sql, params || this._params); return row; }, }; - + return statement; }, }; -}; +} // taken from the `kysely` library: https://github.com/kysely-org/kysely/blob/413a88516c20be42dc8cbebade68c27669a3ac1a/src/dialect/mssql/mssql-driver.ts#L440 export function getTediousDataType(value: unknown): DataType { - if (value === null || value === undefined || typeof value === 'string') { + if (value === null || value === undefined || typeof value === "string") { return TYPES.NVarChar; } - - if (typeof value === 'bigint' || (typeof value === 'number' && value % 1 === 0)) { - return value < -2_147_483_648 || value > 2_147_483_647 ? TYPES.BigInt : TYPES.Int; + + if ( + typeof value === "bigint" || + (typeof value === "number" && value % 1 === 0) + ) { + return value < -2_147_483_648 || value > 2_147_483_647 + ? TYPES.BigInt + : TYPES.Int; } - - if (typeof value === 'number') { + + if (typeof value === "number") { return TYPES.Float; } - - if (typeof value === 'boolean') { + + if (typeof value === "boolean") { return TYPES.Bit; } - + if (value instanceof Date) { return TYPES.DateTime2; } - - if (typeof Buffer !== 'undefined' && Buffer.isBuffer(value)) { + + if (typeof Buffer !== "undefined" && Buffer.isBuffer(value)) { return TYPES.VarBinary; } - + return TYPES.NVarChar; -}; +} // replace `?` placeholders with `@1`, `@2`, etc. export function prepareSqlParameters(sql: string, parameters: unknown[]) { const parameterIndexes = []; const tokens = [...sql]; - + // find all `?` placeholders in the SQL string for (const [i, token] of tokens.entries()) { - - if (token === '?') { + if (token === "?") { parameterIndexes.push(i); } - }; - + } + const namedParameters = {}; for (const [index, parameterIndex] of parameterIndexes.entries()) { const incrementedIndex = index + 1; @@ -199,9 +202,10 @@ export function prepareSqlParameters(sql: string, parameters: unknown[]) { value: parameters[index], }; } - + return { - sql: tokens.join(''), // join the tokens back into a SQL string + sql: tokens.join(""), // join the tokens back into a SQL string parameters: namedParameters, }; -}; +} + diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index 48fdc6d..a4e7764 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -66,6 +66,10 @@ export function testConnector(opts: { await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; break; } + case "mssql": { + await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '')`; + break; + } default: { const { rows } = await db.sql`INSERT INTO users VALUES (${userId}, 'John', 'Doe', '') RETURNING *`; @@ -102,3 +106,4 @@ export function testConnector(opts: { ); }); } + diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index ffad5ad..08d392e 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -1,77 +1,77 @@ import { describe, it, expect } from "vitest"; import { TYPES } from "tedious"; -import { getTediousDataType, prepareSqlParameters } from "../../src/connectors/mssql"; +import { + getTediousDataType, + prepareSqlParameters, +} from "../../src/connectors/mssql"; import connector from "../../src/connectors/mssql"; import { testConnector } from "./_tests"; describe.runIf( - process.env.MSSQL_HOST - && process.env.MSSQL_DB_NAME - && process.env.MSSQL_USERNAME - && process.env.MSSQL_PASSWORD -)( - "connectors: mssql.test", - () => { - testConnector({ - dialect: "mssql", - connector: connector({ - server: process.env.MSSQL_HOST!, - authentication: { - type: 'default', - options: { - userName: process.env.MSSQL_USERNAME!, - password: process.env.MSSQL_PASSWORD!, - }, - }, + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("connectors: mssql.test", () => { + testConnector({ + dialect: "mssql", + connector: connector({ + server: process.env.MSSQL_HOST!, + authentication: { + type: "default", options: { - database: process.env.MSSQL_DB_NAME!, - port: Number.parseInt(process.env.MSSQL_PORT || '1433', 10), - trustServerCertificate: true, - encrypt: false, + userName: process.env.MSSQL_USERNAME!, + password: process.env.MSSQL_PASSWORD!, }, - }), - }); - }, -); + }, + options: { + database: process.env.MSSQL_DB_NAME!, + port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), + trustServerCertificate: true, + encrypt: false, + }, + }), + }); +}); describe("getTediousDataType", () => { it("should return NVarChar for null", () => { expect(getTediousDataType(null)).toBe(TYPES.NVarChar); }); - + it("should return NVarChar for undefined", () => { expect(getTediousDataType(undefined)).toBe(TYPES.NVarChar); }); - + it("should return NVarChar for strings", () => { expect(getTediousDataType("test")).toBe(TYPES.NVarChar); }); - + it("should return Int for numbers", () => { expect(getTediousDataType(123)).toBe(TYPES.Int); }); - + it("should return BigInt for large integer numbers", () => { expect(getTediousDataType(2_147_483_648)).toBe(TYPES.BigInt); }); - + it("should return Float for floating point numbers", () => { expect(getTediousDataType(123.45)).toBe(TYPES.Float); }); - + it("should return Bit for boolean values", () => { expect(getTediousDataType(true)).toBe(TYPES.Bit); }); - + it("should return DateTime for Date objects", () => { expect(getTediousDataType(new Date())).toBe(TYPES.DateTime2); }); - + it("should return VarBinary for Buffer objects", () => { expect(getTediousDataType(Buffer.from("test"))).toBe(TYPES.VarBinary); }); - + it("should return NVarChar by default for other types", () => { expect(getTediousDataType({})).toBe(TYPES.NVarChar); }); @@ -88,7 +88,7 @@ describe("prepareSqlParameters", () => { "@2": { name: "2", type: TYPES.NVarChar, value: "John" }, }); }); - + it("should handle no parameters", () => { const sql = "SELECT * FROM users"; const parameters: unknown[] = []; @@ -96,27 +96,32 @@ describe("prepareSqlParameters", () => { expect(result.sql).toBe("SELECT * FROM users"); expect(result.parameters).toEqual({}); }); - + it("should handle multiple parameters of different types", () => { const sql = "SELECT * FROM users WHERE id = ? AND age = ? AND active = ?"; const parameters = [1, 30, true]; const result = prepareSqlParameters(sql, parameters); - expect(result.sql).toBe("SELECT * FROM users WHERE id = @1 AND age = @2 AND active = @3"); + expect(result.sql).toBe( + "SELECT * FROM users WHERE id = @1 AND age = @2 AND active = @3", + ); expect(result.parameters).toEqual({ "@1": { name: "1", type: TYPES.Int, value: 1 }, "@2": { name: "2", type: TYPES.Int, value: 30 }, "@3": { name: "3", type: TYPES.Bit, value: true }, }); }); - + it("should handle null and undefined parameters", () => { const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; const parameters = [null, undefined]; const result = prepareSqlParameters(sql, parameters); - expect(result.sql).toBe("SELECT * FROM users WHERE name = @1 AND email = @2"); + expect(result.sql).toBe( + "SELECT * FROM users WHERE name = @1 AND email = @2", + ); expect(result.parameters).toEqual({ "@1": { name: "1", type: TYPES.NVarChar, value: null }, "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, }); }); -}); \ No newline at end of file +}); + From ccb0822394303897e9491d85d3ffc7084ce968ab Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Thu, 27 Nov 2025 20:49:37 +0300 Subject: [PATCH 03/21] added stored procedure tests --- src/connectors/mssql.ts | 1 - test/connectors/_tests.ts | 1 - test/connectors/mssql.test.ts | 107 +++++++++++++++++++++++++++++++++- 3 files changed, 105 insertions(+), 4 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index b7e7c72..bb39d62 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -208,4 +208,3 @@ export function prepareSqlParameters(sql: string, parameters: unknown[]) { parameters: namedParameters, }; } - diff --git a/test/connectors/_tests.ts b/test/connectors/_tests.ts index a4e7764..0535a13 100644 --- a/test/connectors/_tests.ts +++ b/test/connectors/_tests.ts @@ -106,4 +106,3 @@ export function testConnector(opts: { ); }); } - diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 08d392e..bfdf144 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expect } from "vitest"; +import { describe, it, expect, beforeAll, afterAll } from "vitest"; import { TYPES } from "tedious"; import { @@ -7,6 +7,7 @@ import { } from "../../src/connectors/mssql"; import connector from "../../src/connectors/mssql"; import { testConnector } from "./_tests"; +import { createDatabase } from "../../src"; describe.runIf( process.env.MSSQL_HOST && @@ -35,6 +36,109 @@ describe.runIf( }); }); +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("callProcedure", () => { + const db = createDatabase( + connector({ + server: process.env.MSSQL_HOST!, + authentication: { + type: "default", + options: { + userName: process.env.MSSQL_USERNAME!, + password: process.env.MSSQL_PASSWORD!, + }, + }, + options: { + database: process.env.MSSQL_DB_NAME!, + port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), + trustServerCertificate: true, + encrypt: false, + }, + }), + ); + + beforeAll(async () => { + // Drop procedure if it exists + await db.sql` + IF OBJECT_ID('dbo.GetUserCount', 'P') IS NOT NULL + DROP PROCEDURE dbo.GetUserCount + `; + + // Drop procedure if it exists + await db.sql` + IF OBJECT_ID('dbo.AddNumbers', 'P') IS NOT NULL + DROP PROCEDURE dbo.AddNumbers + `; + + // Create a simple stored procedure that returns user count + await db.sql` + CREATE PROCEDURE dbo.GetUserCount + @minAge INT + AS + BEGIN + SELECT COUNT(*) as userCount + FROM (VALUES (1, 25), (2, 30), (3, 35)) AS Users(id, age) + WHERE age >= @minAge + END + `; + + // Create a stored procedure that adds two numbers + await db.sql` + CREATE PROCEDURE dbo.AddNumbers + @a INT, + @b INT + AS + BEGIN + SELECT (@a + @b) as result + END + `; + }); + + afterAll(async () => { + // Clean up procedures + await db.sql` + IF OBJECT_ID('dbo.GetUserCount', 'P') IS NOT NULL + DROP PROCEDURE dbo.GetUserCount + `; + await db.sql` + IF OBJECT_ID('dbo.AddNumbers', 'P') IS NOT NULL + DROP PROCEDURE dbo.AddNumbers + `; + await db.dispose(); + }); + + it("should call a stored procedure with parameters", async () => { + const stmt = db.prepare("EXEC dbo.GetUserCount ?"); + const rows = await stmt.all(30); + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect(rows[0]).toHaveProperty("userCount"); + expect((rows[0] as { userCount: number }).userCount).toBe(2); + }); + + it("should call a stored procedure with multiple parameters", async () => { + const stmt = db.prepare("EXEC dbo.AddNumbers ?, ?"); + const rows = await stmt.all(10, 20); + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect(rows[0]).toHaveProperty("result"); + expect((rows[0] as { result: number }).result).toBe(30); + }); + + it("should call a stored procedure using prepare", async () => { + const stmt = db.prepare("EXEC dbo.AddNumbers ?, ?"); + const rows = await stmt.all(5, 15); + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect(rows[0]).toHaveProperty("result"); + expect((rows[0] as { result: number }).result).toBe(20); + }); +}); + describe("getTediousDataType", () => { it("should return NVarChar for null", () => { expect(getTediousDataType(null)).toBe(TYPES.NVarChar); @@ -124,4 +228,3 @@ describe("prepareSqlParameters", () => { }); }); }); - From f0555c96fd7b4f9665f9901bf9d669994b0cb296 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Thu, 27 Nov 2025 21:21:33 +0300 Subject: [PATCH 04/21] added test for json auto --- test/connectors/mssql.test.ts | 77 +++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index bfdf144..03864be 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -137,6 +137,83 @@ describe.runIf( expect(rows[0]).toHaveProperty("result"); expect((rows[0] as { result: number }).result).toBe(20); }); + + it("should return JSON data using FOR JSON PATH", async () => { + const stmt = db.prepare(` + SELECT + id, + firstName, + lastName, + email + FROM ( + VALUES + (1, 'John', 'Doe', 'john@example.com'), + (2, 'Jane', 'Smith', 'jane@example.com') + ) AS Users(id, firstName, lastName, email) + FOR JSON PATH + `); + const rows = await stmt.all(); + expect(rows).toBeDefined(); + expect(rows.length).toBeGreaterThan(0); + + // SQL Server returns JSON as a single column result + // The JSON data is in the first column (usually named "JSON_F52E2B61-18A1-11d1-B105-00805F49916B") + const jsonColumn = Object.keys(rows[0] as object)[0]; + const jsonString = (rows[0] as Record)[jsonColumn]; + + expect(jsonString).toBeDefined(); + const jsonData = JSON.parse(jsonString); + expect(Array.isArray(jsonData)).toBe(true); + expect(jsonData.length).toBe(2); + expect(jsonData[0]).toMatchObject({ + id: 1, + firstName: "John", + lastName: "Doe", + email: "john@example.com", + }); + expect(jsonData[1]).toMatchObject({ + id: 2, + firstName: "Jane", + lastName: "Smith", + email: "jane@example.com", + }); + }); + + it("should return JSON data with nested structure using FOR JSON PATH", async () => { + const stmt = db.prepare(` + SELECT + id, + firstName, + lastName, + ( + SELECT email, phone + FROM (VALUES ('john@example.com', '555-1234')) AS Contact(email, phone) + FOR JSON PATH + ) AS contact + FROM (VALUES (1, 'John', 'Doe')) AS Users(id, firstName, lastName) + FOR JSON PATH + `); + const rows = await stmt.all(); + expect(rows).toBeDefined(); + + const jsonColumn = Object.keys(rows[0] as object)[0]; + const jsonString = (rows[0] as Record)[jsonColumn]; + const jsonData = JSON.parse(jsonString); + + expect(Array.isArray(jsonData)).toBe(true); + expect(jsonData[0]).toHaveProperty("id", 1); + expect(jsonData[0]).toHaveProperty("firstName", "John"); + expect(jsonData[0]).toHaveProperty("contact"); + + // The nested contact is already a JSON string that needs to be parsed + const contactData = jsonData[0].contact; + const contact = typeof contactData === "string" ? JSON.parse(contactData) : contactData; + expect(Array.isArray(contact)).toBe(true); + expect(contact[0]).toMatchObject({ + email: "john@example.com", + phone: "555-1234", + }); + }); }); describe("getTediousDataType", () => { From bb523acbedc422bc254f61cae9c7780b701f008f Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Fri, 28 Nov 2025 19:37:25 +0300 Subject: [PATCH 05/21] .env.example updated --- .env.example | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.env.example b/.env.example index 3101dee..4881a20 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,10 @@ PLANETSCALE_PASSWORD=password # Cloudflare Hyperdrive WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_POSTGRESQL=postgresql://test:test@localhost:5432/db0 WRANGLER_HYPERDRIVE_LOCAL_CONNECTION_STRING_MYSQL=mysql://test:test@localhost:3306/db0 + +# MSSQL +MSSQL_HOST=localhost +MSSQL_DB_NAME=TestDB +MSSQL_PORT=1433 +MSSQL_USERNAME=sa +MSSQL_PASSWORD=MyStrong!Passw0rd \ No newline at end of file From 081db359f4966d95e867f804ae8846dc5ffa2173 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 13:12:05 +0300 Subject: [PATCH 06/21] added sql server in docker-compose.yml --- docker-compose.yaml | 11 +++++++++++ scripts/mssql-init.sh | 9 +++++++++ 2 files changed, 20 insertions(+) create mode 100755 scripts/mssql-init.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index dd5a711..9abee9a 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -16,3 +16,14 @@ services: MYSQL_DATABASE: db0 MYSQL_USER: test MYSQL_PASSWORD: test + mssql: + # https://hub.docker.com/_/microsoft-mssql-server + image: mcr.microsoft.com/mssql/server:2022-latest + network_mode: "host" + environment: + ACCEPT_EULA: "Y" + MSSQL_SA_PASSWORD: "MyStrong!Passw0rd" + MSSQL_PID: "Developer" + volumes: + - ./scripts/mssql-init.sh:/docker-entrypoint-initdb.d/mssql-init.sh + command: /bin/bash -c "/docker-entrypoint-initdb.d/mssql-init.sh & /opt/mssql/bin/sqlservr" diff --git a/scripts/mssql-init.sh b/scripts/mssql-init.sh new file mode 100755 index 0000000..c36256a --- /dev/null +++ b/scripts/mssql-init.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +# Wait for SQL Server to start +sleep 30s + +# Run the setup script to create the database +/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MyStrong!Passw0rd -d master -Q "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'TestDB') CREATE DATABASE TestDB" + +echo "TestDB database created successfully" From 30de0386d99c6c6c12cc4423fb4fbd949402f9b7 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 13:29:32 +0300 Subject: [PATCH 07/21] Added create database test --- test/connectors/mssql.test.ts | 104 ++++++++++++++++++++++++++++++++++ 1 file changed, 104 insertions(+) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 03864be..f9a4ee1 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -216,6 +216,110 @@ describe.runIf( }); }); +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("createDatabase", () => { + const testDbName = "TestDB_CreateTest"; + let db: ReturnType; + + beforeAll(() => { + // Connect to master database to create/drop test database + db = createDatabase( + connector({ + server: process.env.MSSQL_HOST!, + authentication: { + type: "default", + options: { + userName: process.env.MSSQL_USERNAME!, + password: process.env.MSSQL_PASSWORD!, + }, + }, + options: { + database: "master", + port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), + trustServerCertificate: true, + encrypt: false, + }, + }), + ); + }); + + afterAll(async () => { + // Clean up: drop the test database if it exists + // try { + // await db.exec(` + // IF EXISTS (SELECT * FROM sys.databases WHERE name = '${testDbName}') + // BEGIN + // ALTER DATABASE [${testDbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + // DROP DATABASE [${testDbName}]; + // END + // `); + // } catch (error) { + // // Ignore errors if database doesn't exist + // } + await db.dispose(); + }); + + it("should create a new database", async () => { + // Drop database if it exists from previous failed test + try { + await db.exec(` + IF EXISTS (SELECT * FROM sys.databases WHERE name = '${testDbName}') + BEGIN + ALTER DATABASE [${testDbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [${testDbName}]; + END + `); + } catch (error) { + // Ignore errors if database doesn't exist + } + + // Create the database + await db.exec(`CREATE DATABASE [${testDbName}]`); + + // Verify the database exists + const stmt = db.prepare( + "SELECT name FROM sys.databases WHERE name = ?", + ); + const rows = await stmt.all(testDbName); + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect((rows[0] as { name: string }).name).toBe(testDbName); + }); + + it("should check if database exists", async () => { + const stmt = db.prepare(` + SELECT CASE + WHEN EXISTS (SELECT * FROM sys.databases WHERE name = ?) + THEN 1 + ELSE 0 + END as dbExists + `); + const rows = await stmt.all(testDbName); + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect((rows[0] as { dbExists: number }).dbExists).toBe(1); + }); + + it.skip("should drop an existing database", async () => { + // Drop the database + await db.exec(` + ALTER DATABASE [${testDbName}] SET SINGLE_USER WITH ROLLBACK IMMEDIATE; + DROP DATABASE [${testDbName}]; + `); + + // Verify the database no longer exists + const stmt = db.prepare( + "SELECT name FROM sys.databases WHERE name = ?", + ); + const rows = await stmt.all(testDbName); + expect(rows).toBeDefined(); + expect(rows.length).toBe(0); + }); +}); + describe("getTediousDataType", () => { it("should return NVarChar for null", () => { expect(getTediousDataType(null)).toBe(TYPES.NVarChar); From 4efde4e36f7900d4b0d14e64e89d48d462466efe Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 19:27:04 +0300 Subject: [PATCH 08/21] test updates --- test/connectors/mssql.test.ts | 90 +++++++++++++++++++++++++++++++++-- 1 file changed, 87 insertions(+), 3 deletions(-) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index f9a4ee1..1ae177c 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -74,6 +74,12 @@ describe.runIf( DROP PROCEDURE dbo.AddNumbers `; + // Drop procedure if it exists + await db.sql` + IF OBJECT_ID('dbo.ProcessUserData', 'P') IS NOT NULL + DROP PROCEDURE dbo.ProcessUserData + `; + // Create a simple stored procedure that returns user count await db.sql` CREATE PROCEDURE dbo.GetUserCount @@ -96,6 +102,21 @@ describe.runIf( SELECT (@a + @b) as result END `; + + // Create a stored procedure that accepts JSON data + await db.sql` + CREATE PROCEDURE dbo.ProcessUserData + @jsonData NVARCHAR(MAX) + AS + BEGIN + -- Parse JSON and return the data + SELECT + JSON_VALUE(@jsonData, '$.name') as name, + JSON_VALUE(@jsonData, '$.email') as email, + JSON_VALUE(@jsonData, '$.age') as age, + (SELECT * FROM OPENJSON(@jsonData, '$.hobbies') WITH (hobby NVARCHAR(100) '$') FOR JSON PATH) as hobbies + END + `; }); afterAll(async () => { @@ -108,11 +129,15 @@ describe.runIf( IF OBJECT_ID('dbo.AddNumbers', 'P') IS NOT NULL DROP PROCEDURE dbo.AddNumbers `; + await db.sql` + IF OBJECT_ID('dbo.ProcessUserData', 'P') IS NOT NULL + DROP PROCEDURE dbo.ProcessUserData + `; await db.dispose(); }); it("should call a stored procedure with parameters", async () => { - const stmt = db.prepare("EXEC dbo.GetUserCount ?"); + const stmt = db.prepare("EXEC dbo.GetUserCount @minAge = ?"); const rows = await stmt.all(30); expect(rows).toBeDefined(); expect(rows.length).toBe(1); @@ -121,7 +146,7 @@ describe.runIf( }); it("should call a stored procedure with multiple parameters", async () => { - const stmt = db.prepare("EXEC dbo.AddNumbers ?, ?"); + const stmt = db.prepare("EXEC dbo.AddNumbers @a = ?, @b = ?"); const rows = await stmt.all(10, 20); expect(rows).toBeDefined(); expect(rows.length).toBe(1); @@ -130,7 +155,7 @@ describe.runIf( }); it("should call a stored procedure using prepare", async () => { - const stmt = db.prepare("EXEC dbo.AddNumbers ?, ?"); + const stmt = db.prepare("EXEC dbo.AddNumbers @a = ?, @b = ?"); const rows = await stmt.all(5, 15); expect(rows).toBeDefined(); expect(rows.length).toBe(1); @@ -214,6 +239,65 @@ describe.runIf( phone: "555-1234", }); }); + + it("should call a stored procedure with JSON parameter", async () => { + const userData = { + name: "Alice Johnson", + email: "alice@example.com", + age: "28", + hobbies: ["reading", "hiking", "photography"], + }; + + const jsonString = JSON.stringify(userData); + const stmt = db.prepare("EXEC dbo.ProcessUserData @jsonData = ?"); + const rows = await stmt.all(jsonString); + + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + expect(rows[0]).toHaveProperty("name", "Alice Johnson"); + expect(rows[0]).toHaveProperty("email", "alice@example.com"); + expect(rows[0]).toHaveProperty("age", "28"); + expect(rows[0]).toHaveProperty("hobbies"); + + // Parse the hobbies JSON array + const hobbiesData = (rows[0] as Record).hobbies; + if (hobbiesData) { + const hobbies = JSON.parse(hobbiesData); + expect(Array.isArray(hobbies)).toBe(true); + expect(hobbies.length).toBe(3); + expect(hobbies[0]).toHaveProperty("hobby", "reading"); + expect(hobbies[1]).toHaveProperty("hobby", "hiking"); + expect(hobbies[2]).toHaveProperty("hobby", "photography"); + } + }); + + it("should call a stored procedure with complex JSON parameter", async () => { + const complexData = { + name: "Bob Smith", + email: "bob@example.com", + age: "35", + hobbies: ["gaming", "cooking"], + }; + + const jsonString = JSON.stringify(complexData); + const stmt = db.prepare("EXEC dbo.ProcessUserData @jsonData = ?"); + const rows = await stmt.all(jsonString); + + expect(rows).toBeDefined(); + expect(rows.length).toBe(1); + + const result = rows[0] as Record; + expect(result.name).toBe("Bob Smith"); + expect(result.email).toBe("bob@example.com"); + expect(result.age).toBe("35"); + + // Verify hobbies array + if (result.hobbies) { + const hobbies = JSON.parse(result.hobbies); + expect(Array.isArray(hobbies)).toBe(true); + expect(hobbies.length).toBe(2); + } + }); }); describe.runIf( From e7ac3e93bfdfdc262740aa1a54de3056453f890a Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 17:50:30 +0000 Subject: [PATCH 09/21] chore: apply automated updates --- test/connectors/mssql.test.ts | 37 ++++++++++++++++------------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 1ae177c..8cbc990 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -180,12 +180,12 @@ describe.runIf( const rows = await stmt.all(); expect(rows).toBeDefined(); expect(rows.length).toBeGreaterThan(0); - + // SQL Server returns JSON as a single column result // The JSON data is in the first column (usually named "JSON_F52E2B61-18A1-11d1-B105-00805F49916B") const jsonColumn = Object.keys(rows[0] as object)[0]; const jsonString = (rows[0] as Record)[jsonColumn]; - + expect(jsonString).toBeDefined(); const jsonData = JSON.parse(jsonString); expect(Array.isArray(jsonData)).toBe(true); @@ -220,19 +220,20 @@ describe.runIf( `); const rows = await stmt.all(); expect(rows).toBeDefined(); - + const jsonColumn = Object.keys(rows[0] as object)[0]; const jsonString = (rows[0] as Record)[jsonColumn]; const jsonData = JSON.parse(jsonString); - + expect(Array.isArray(jsonData)).toBe(true); expect(jsonData[0]).toHaveProperty("id", 1); expect(jsonData[0]).toHaveProperty("firstName", "John"); expect(jsonData[0]).toHaveProperty("contact"); - + // The nested contact is already a JSON string that needs to be parsed const contactData = jsonData[0].contact; - const contact = typeof contactData === "string" ? JSON.parse(contactData) : contactData; + const contact = + typeof contactData === "string" ? JSON.parse(contactData) : contactData; expect(Array.isArray(contact)).toBe(true); expect(contact[0]).toMatchObject({ email: "john@example.com", @@ -247,18 +248,18 @@ describe.runIf( age: "28", hobbies: ["reading", "hiking", "photography"], }; - + const jsonString = JSON.stringify(userData); const stmt = db.prepare("EXEC dbo.ProcessUserData @jsonData = ?"); const rows = await stmt.all(jsonString); - + expect(rows).toBeDefined(); expect(rows.length).toBe(1); expect(rows[0]).toHaveProperty("name", "Alice Johnson"); expect(rows[0]).toHaveProperty("email", "alice@example.com"); expect(rows[0]).toHaveProperty("age", "28"); expect(rows[0]).toHaveProperty("hobbies"); - + // Parse the hobbies JSON array const hobbiesData = (rows[0] as Record).hobbies; if (hobbiesData) { @@ -278,19 +279,19 @@ describe.runIf( age: "35", hobbies: ["gaming", "cooking"], }; - + const jsonString = JSON.stringify(complexData); const stmt = db.prepare("EXEC dbo.ProcessUserData @jsonData = ?"); const rows = await stmt.all(jsonString); - + expect(rows).toBeDefined(); expect(rows.length).toBe(1); - + const result = rows[0] as Record; expect(result.name).toBe("Bob Smith"); expect(result.email).toBe("bob@example.com"); expect(result.age).toBe("35"); - + // Verify hobbies array if (result.hobbies) { const hobbies = JSON.parse(result.hobbies); @@ -356,7 +357,7 @@ describe.runIf( DROP DATABASE [${testDbName}]; END `); - } catch (error) { + } catch { // Ignore errors if database doesn't exist } @@ -364,9 +365,7 @@ describe.runIf( await db.exec(`CREATE DATABASE [${testDbName}]`); // Verify the database exists - const stmt = db.prepare( - "SELECT name FROM sys.databases WHERE name = ?", - ); + const stmt = db.prepare("SELECT name FROM sys.databases WHERE name = ?"); const rows = await stmt.all(testDbName); expect(rows).toBeDefined(); expect(rows.length).toBe(1); @@ -395,9 +394,7 @@ describe.runIf( `); // Verify the database no longer exists - const stmt = db.prepare( - "SELECT name FROM sys.databases WHERE name = ?", - ); + const stmt = db.prepare("SELECT name FROM sys.databases WHERE name = ?"); const rows = await stmt.all(testDbName); expect(rows).toBeDefined(); expect(rows.length).toBe(0); From 4fe1fb6825dfabe0895c31d44aad88146176f225 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 21:11:18 +0300 Subject: [PATCH 10/21] Bug fix --- src/connectors/mssql.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index bb39d62..ed345a4 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -179,8 +179,17 @@ export function getTediousDataType(value: unknown): DataType { } // replace `?` placeholders with `@1`, `@2`, etc. -export function prepareSqlParameters(sql: string, parameters: unknown[]) { - const parameterIndexes = []; +export function prepareSqlParameters( + sql: string, + parameters: unknown[] = [], +): { + sql: string; + parameters: Record< + string, + { name: string; type: DataType; value: unknown } + >; +} { + const parameterIndexes: number[] = []; const tokens = [...sql]; // find all `?` placeholders in the SQL string @@ -190,7 +199,10 @@ export function prepareSqlParameters(sql: string, parameters: unknown[]) { } } - const namedParameters = {}; + const namedParameters: Record< + string, + { name: string; type: DataType; value: unknown } + > = {}; for (const [index, parameterIndex] of parameterIndexes.entries()) { const incrementedIndex = index + 1; // replace `?` placeholder with index-based parameter name From e83f6d7a250bafd261e43945a1d825770e02af05 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:14:25 +0000 Subject: [PATCH 11/21] chore: apply automated updates --- src/connectors/mssql.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index ed345a4..627646c 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -184,10 +184,7 @@ export function prepareSqlParameters( parameters: unknown[] = [], ): { sql: string; - parameters: Record< - string, - { name: string; type: DataType; value: unknown } - >; + parameters: Record; } { const parameterIndexes: number[] = []; const tokens = [...sql]; From 18425a6722bfa9ddbe84b9b25ffec3a32bd9e2fe Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 21:29:54 +0300 Subject: [PATCH 12/21] fixed docker-compose.yml --- docker-compose.yaml | 3 --- scripts/mssql-init.sh | 9 --------- 2 files changed, 12 deletions(-) delete mode 100755 scripts/mssql-init.sh diff --git a/docker-compose.yaml b/docker-compose.yaml index 9abee9a..8934a64 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -24,6 +24,3 @@ services: ACCEPT_EULA: "Y" MSSQL_SA_PASSWORD: "MyStrong!Passw0rd" MSSQL_PID: "Developer" - volumes: - - ./scripts/mssql-init.sh:/docker-entrypoint-initdb.d/mssql-init.sh - command: /bin/bash -c "/docker-entrypoint-initdb.d/mssql-init.sh & /opt/mssql/bin/sqlservr" diff --git a/scripts/mssql-init.sh b/scripts/mssql-init.sh deleted file mode 100755 index c36256a..0000000 --- a/scripts/mssql-init.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -# Wait for SQL Server to start -sleep 30s - -# Run the setup script to create the database -/opt/mssql-tools/bin/sqlcmd -S localhost -U sa -P MyStrong!Passw0rd -d master -Q "IF NOT EXISTS (SELECT * FROM sys.databases WHERE name = 'TestDB') CREATE DATABASE TestDB" - -echo "TestDB database created successfully" From 9f38ae2c3bc155da8b627b4f62ab0435ac3639bf Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 21:37:00 +0300 Subject: [PATCH 13/21] Fix TypeScript errors in MSSQL connector --- src/connectors/mssql.ts | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index 627646c..c3f6cba 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -5,9 +5,11 @@ import { type ConnectionConfiguration, TYPES, } from "tedious"; -import type { DataType } from "tedious/lib/data-type"; -import type { Connector, Statement } from "../types"; +import type { Connector, Statement, Primitive } from "db0"; + +// Type for tedious DataType +type DataType = (typeof TYPES)[keyof typeof TYPES]; export type ConnectorOptions = ConnectionConfiguration; @@ -28,7 +30,11 @@ export default function mssqlConnector(opts: ConnectorOptions) { _client = client; }); - client.on("connect", () => resolve(_client)); + client.on("connect", () => { + if (_client) { + resolve(_client); + } + }); client.on("error", reject); }); } @@ -64,7 +70,7 @@ export default function mssqlConnector(opts: ConnectorOptions) { const rows: unknown[] = []; request.on("row", (columns = []) => { - const currentRow = {}; + const currentRow: Record = {}; for (const column of columns) { const { value, metadata } = column; const { colName } = metadata; @@ -111,30 +117,31 @@ export default function mssqlConnector(opts: ConnectorOptions) { return _run(sql, []); }, prepare(sql: string) { - const statement = { - _sql: sql, - _params: [], - bind(...params) { + let _sql = sql; + let _params: Primitive[] = []; + + const statement: Statement = { + bind(...params: Primitive[]) { if (params.length > 0) { - this._params = params; + _params = params; } return statement; }, - async all(...params) { - const { rows } = await _run(this._sql, params || this._params); + async all(...params: Primitive[]) { + const { rows } = await _run(_sql, params.length > 0 ? params : _params); return rows; }, - async run(...params) { + async run(...params: Primitive[]) { const { success = false } = - (await _run(this._sql, params || this._params)) || {}; + (await _run(_sql, params.length > 0 ? params : _params)) || {}; return { success, }; }, - async get(...params) { + async get(...params: Primitive[]) { const { rows: [row], - } = await _run(this._sql, params || this._params); + } = await _run(_sql, params.length > 0 ? params : _params); return row; }, }; From ba0d00b3248c12c5fedeb94be027e1d1a243d950 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sat, 29 Nov 2025 18:37:38 +0000 Subject: [PATCH 14/21] chore: apply automated updates --- src/connectors/mssql.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/connectors/mssql.ts b/src/connectors/mssql.ts index c3f6cba..246c0c7 100644 --- a/src/connectors/mssql.ts +++ b/src/connectors/mssql.ts @@ -117,7 +117,7 @@ export default function mssqlConnector(opts: ConnectorOptions) { return _run(sql, []); }, prepare(sql: string) { - let _sql = sql; + const _sql = sql; let _params: Primitive[] = []; const statement: Statement = { @@ -128,7 +128,10 @@ export default function mssqlConnector(opts: ConnectorOptions) { return statement; }, async all(...params: Primitive[]) { - const { rows } = await _run(_sql, params.length > 0 ? params : _params); + const { rows } = await _run( + _sql, + params.length > 0 ? params : _params, + ); return rows; }, async run(...params: Primitive[]) { From 513c8e4bb7633118d580d6e874c4cbaab459581c Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sat, 29 Nov 2025 22:38:33 +0300 Subject: [PATCH 15/21] Trigger CI workflow From 79983512ec35e79228f2648d1c6fe1357da84bdd Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sun, 30 Nov 2025 08:16:44 +0300 Subject: [PATCH 16/21] Add Codecov coverage upload to CI workflow --- .github/workflows/ci.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bff6bca..b557209 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,3 +15,7 @@ jobs: - run: pnpm build - run: pnpm test:types - run: pnpm vitest + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v5 + with: + token: ${{ secrets.CODECOV_TOKEN }} From 5295f648c09e1ec93d0befd7c23eaed24170d61f Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Tue, 2 Dec 2025 15:31:21 +0300 Subject: [PATCH 17/21] Added more tests --- test/connectors/mssql.test.ts | 159 ++++++++++++++++++++++++++++++++-- 1 file changed, 151 insertions(+), 8 deletions(-) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 8cbc990..5e0549e 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -4,10 +4,10 @@ import { TYPES } from "tedious"; import { getTediousDataType, prepareSqlParameters, -} from "../../src/connectors/mssql"; -import connector from "../../src/connectors/mssql"; -import { testConnector } from "./_tests"; -import { createDatabase } from "../../src"; +} from "../../src/connectors/mssql.js"; +import connector from "../../src/connectors/mssql.js"; +import { testConnector } from "./_tests.js"; +import { createDatabase } from "../../src/index.js"; describe.runIf( process.env.MSSQL_HOST && @@ -80,6 +80,12 @@ describe.runIf( DROP PROCEDURE dbo.ProcessUserData `; + // Drop procedure if it exists + await db.sql` + IF OBJECT_ID('dbo.CalculateWithOutput', 'P') IS NOT NULL + DROP PROCEDURE dbo.CalculateWithOutput + `; + // Create a simple stored procedure that returns user count await db.sql` CREATE PROCEDURE dbo.GetUserCount @@ -117,6 +123,23 @@ describe.runIf( (SELECT * FROM OPENJSON(@jsonData, '$.hobbies') WITH (hobby NVARCHAR(100) '$') FOR JSON PATH) as hobbies END `; + + // Create a stored procedure with output parameters + await db.sql` + CREATE PROCEDURE dbo.CalculateWithOutput + @a INT, + @b INT, + @sum INT OUTPUT, + @product INT OUTPUT, + @difference INT OUTPUT + AS + BEGIN + SET @sum = @a + @b + SET @product = @a * @b + SET @difference = @a - @b + SELECT @sum as calculatedSum, @product as calculatedProduct, @difference as calculatedDifference + END + `; }); afterAll(async () => { @@ -133,6 +156,10 @@ describe.runIf( IF OBJECT_ID('dbo.ProcessUserData', 'P') IS NOT NULL DROP PROCEDURE dbo.ProcessUserData `; + await db.sql` + IF OBJECT_ID('dbo.CalculateWithOutput', 'P') IS NOT NULL + DROP PROCEDURE dbo.CalculateWithOutput + `; await db.dispose(); }); @@ -163,6 +190,84 @@ describe.runIf( expect((rows[0] as { result: number }).result).toBe(20); }); + it("should call a stored procedure with output parameters", async () => { + // Declare output variables and execute the stored procedure + const stmt = db.prepare(` + DECLARE @sum INT, @product INT, @difference INT; + EXEC dbo.CalculateWithOutput + @a = ?, + @b = ?, + @sum = @sum OUTPUT, + @product = @product OUTPUT, + @difference = @difference OUTPUT; + SELECT @sum as sum, @product as product, @difference as difference; + `); + + const rows = await stmt.all(10, 5); + expect(rows).toBeDefined(); + // The stored procedure returns a result set, plus we SELECT the output values + // So we get 2 rows: one from the procedure, one from the SELECT + expect(rows.length).toBe(2); + + // First row is from the stored procedure's SELECT statement + const procResult = rows[0] as { + calculatedSum: number; + calculatedProduct: number; + calculatedDifference: number; + }; + expect(procResult.calculatedSum).toBe(15); + expect(procResult.calculatedProduct).toBe(50); + expect(procResult.calculatedDifference).toBe(5); + + // Second row is from our SELECT of output parameters + const outputResult = rows[1] as { + sum: number; + product: number; + difference: number; + }; + expect(outputResult.sum).toBe(15); + expect(outputResult.product).toBe(50); + expect(outputResult.difference).toBe(5); + }); + + it("should call a stored procedure with output parameters and result set", async () => { + // Test that we can get both the result set and output parameters + const stmt = db.prepare(` + DECLARE @sum INT, @product INT, @difference INT; + EXEC dbo.CalculateWithOutput + @a = ?, + @b = ?, + @sum = @sum OUTPUT, + @product = @product OUTPUT, + @difference = @difference OUTPUT; + SELECT @sum as outputSum, @product as outputProduct, @difference as outputDifference; + `); + + const rows = await stmt.all(20, 8); + expect(rows).toBeDefined(); + expect(rows.length).toBe(2); + + // First row is from the stored procedure's internal SELECT + const procResult = rows[0] as { + calculatedSum: number; + calculatedProduct: number; + calculatedDifference: number; + }; + expect(procResult.calculatedSum).toBe(28); + expect(procResult.calculatedProduct).toBe(160); + expect(procResult.calculatedDifference).toBe(12); + + // Second row is from our SELECT of output parameters + const outputResult = rows[1] as { + outputSum: number; + outputProduct: number; + outputDifference: number; + }; + expect(outputResult.outputSum).toBe(28); + expect(outputResult.outputProduct).toBe(160); + expect(outputResult.outputDifference).toBe(12); + }); + it("should return JSON data using FOR JSON PATH", async () => { const stmt = db.prepare(` SELECT @@ -183,8 +288,8 @@ describe.runIf( // SQL Server returns JSON as a single column result // The JSON data is in the first column (usually named "JSON_F52E2B61-18A1-11d1-B105-00805F49916B") - const jsonColumn = Object.keys(rows[0] as object)[0]; - const jsonString = (rows[0] as Record)[jsonColumn]; + const jsonColumn = Object.keys(rows[0] as object)[0]!; + const jsonString = (rows[0] as Record)[jsonColumn]!; expect(jsonString).toBeDefined(); const jsonData = JSON.parse(jsonString); @@ -221,8 +326,8 @@ describe.runIf( const rows = await stmt.all(); expect(rows).toBeDefined(); - const jsonColumn = Object.keys(rows[0] as object)[0]; - const jsonString = (rows[0] as Record)[jsonColumn]; + const jsonColumn = Object.keys(rows[0] as object)[0]!; + const jsonString = (rows[0] as Record)[jsonColumn]!; const jsonData = JSON.parse(jsonString); expect(Array.isArray(jsonData)).toBe(true); @@ -241,6 +346,41 @@ describe.runIf( }); }); + it("should return single JSON object using FOR JSON PATH, WITHOUT_ARRAY_WRAPPER", async () => { + const stmt = db.prepare(` + SELECT + id, + firstName, + lastName, + email, + age + FROM ( + VALUES (1, 'John', 'Doe', 'john@example.com', 30) + ) AS Users(id, firstName, lastName, email, age) + FOR JSON PATH, WITHOUT_ARRAY_WRAPPER + `); + const rows = await stmt.all(); + expect(rows).toBeDefined(); + expect(rows.length).toBeGreaterThan(0); + + // SQL Server returns JSON as a single column result + const jsonColumn = Object.keys(rows[0] as object)[0]!; + const jsonString = (rows[0] as Record)[jsonColumn]!; + + expect(jsonString).toBeDefined(); + const jsonData = JSON.parse(jsonString); + + // WITHOUT_ARRAY_WRAPPER returns a single object, not an array + expect(Array.isArray(jsonData)).toBe(false); + expect(jsonData).toMatchObject({ + id: 1, + firstName: "John", + lastName: "Doe", + email: "john@example.com", + age: 30, + }); + }); + it("should call a stored procedure with JSON parameter", async () => { const userData = { name: "Alice Johnson", @@ -403,6 +543,7 @@ describe.runIf( describe("getTediousDataType", () => { it("should return NVarChar for null", () => { + // eslint-disable-next-line unicorn/no-null expect(getTediousDataType(null)).toBe(TYPES.NVarChar); }); @@ -479,12 +620,14 @@ describe("prepareSqlParameters", () => { it("should handle null and undefined parameters", () => { const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; + // eslint-disable-next-line unicorn/no-null const parameters = [null, undefined]; const result = prepareSqlParameters(sql, parameters); expect(result.sql).toBe( "SELECT * FROM users WHERE name = @1 AND email = @2", ); expect(result.parameters).toEqual({ + // eslint-disable-next-line unicorn/no-null "@1": { name: "1", type: TYPES.NVarChar, value: null }, "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, }); From 561fd7b2159e0798356060b810e145ffb279247f Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 2 Dec 2025 12:31:49 +0000 Subject: [PATCH 18/21] chore: apply automated updates --- test/connectors/mssql.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 5e0549e..6c91ae6 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -543,7 +543,6 @@ describe.runIf( describe("getTediousDataType", () => { it("should return NVarChar for null", () => { - // eslint-disable-next-line unicorn/no-null expect(getTediousDataType(null)).toBe(TYPES.NVarChar); }); @@ -620,14 +619,13 @@ describe("prepareSqlParameters", () => { it("should handle null and undefined parameters", () => { const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; - // eslint-disable-next-line unicorn/no-null + const parameters = [null, undefined]; const result = prepareSqlParameters(sql, parameters); expect(result.sql).toBe( "SELECT * FROM users WHERE name = @1 AND email = @2", ); expect(result.parameters).toEqual({ - // eslint-disable-next-line unicorn/no-null "@1": { name: "1", type: TYPES.NVarChar, value: null }, "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, }); From a527256d4e441545b7955d26934fe21826a414bc Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Tue, 2 Dec 2025 15:37:45 +0300 Subject: [PATCH 19/21] Trigger CI workflow to test coverage and autofix From 90077586b89b12de2b425618193dba526a120015 Mon Sep 17 00:00:00 2001 From: Veeran Puthumkara Date: Sun, 7 Dec 2025 21:45:08 +0300 Subject: [PATCH 20/21] Upgraded tedious to the latest version and added more tests --- package.json | 2 +- pnpm-lock.yaml | 15 +- test/connectors/mssql.test.ts | 579 ++++++++++++++++++++++++---------- vitest.config.ts | 2 +- 4 files changed, 431 insertions(+), 167 deletions(-) diff --git a/package.json b/package.json index 1c3be8b..09e458b 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "pg": "^8.16.3", "prettier": "^3.6.2", "scule": "^1.3.0", - "tedious": "^18.6.1", + "tedious": "^19.1.3", "typescript": "^5.9.3", "vitest": "^4.0.12", "wrangler": "^4.49.1" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a2ed0f2..310330b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -82,8 +82,8 @@ importers: specifier: ^1.3.0 version: 1.3.0 tedious: - specifier: ^18.6.1 - version: 18.6.2 + specifier: ^19.1.3 + version: 19.1.3 typescript: specifier: ^5.9.3 version: 5.9.3 @@ -3661,9 +3661,9 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} - tedious@18.6.2: - resolution: {integrity: sha512-g7jC56o3MzLkE3lHkaFe2ZdOVFBahq5bsB60/M4NYUbocw/MCrS89IOEQUFr+ba6pb8ZHczZ/VqCyYeYq0xBAg==} - engines: {node: '>=18'} + tedious@19.1.3: + resolution: {integrity: sha512-6O6efTeYtcnar3Cqf/ptqJs+U10fYYjp/SHRNm3VGuCTUDys+AUgIbxWbT2kzl4baXAzuy9byV3qCgOimrRfTA==} + engines: {node: '>=18.17'} timers-ext@0.1.8: resolution: {integrity: sha512-wFH7+SEAcKfJpfLPkrgMPvvwnEtj8W4IurvEyrKsDleXnKLCDw71w8jltvfLa8Rm4qQxxT4jmDBYbJG/z7qoww==} @@ -6241,6 +6241,7 @@ snapshots: iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 + optional: true iconv-lite@0.7.0: dependencies: @@ -7287,7 +7288,7 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 - tedious@18.6.2: + tedious@19.1.3: dependencies: '@azure/core-auth': 1.10.1 '@azure/identity': 4.13.0 @@ -7295,7 +7296,7 @@ snapshots: '@js-joda/core': 5.6.5 '@types/node': 24.10.1 bl: 6.1.5 - iconv-lite: 0.6.3 + iconv-lite: 0.7.0 js-md4: 0.3.2 native-duplexpair: 1.0.0 sprintf-js: 1.1.3 diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index 6c91ae6..aeadb69 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -9,6 +9,26 @@ import connector from "../../src/connectors/mssql.js"; import { testConnector } from "./_tests.js"; import { createDatabase } from "../../src/index.js"; +// Helper function to create connection configuration +function createConnectionConfig(database: string = process.env.MSSQL_DB_NAME!) { + return { + server: process.env.MSSQL_HOST!, + authentication: { + type: "default" as const, + options: { + userName: process.env.MSSQL_USERNAME!, + password: process.env.MSSQL_PASSWORD!, + }, + }, + options: { + database, + port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), + trustServerCertificate: true, + encrypt: false, + }, + }; +} + describe.runIf( process.env.MSSQL_HOST && process.env.MSSQL_DB_NAME && @@ -17,22 +37,7 @@ describe.runIf( )("connectors: mssql.test", () => { testConnector({ dialect: "mssql", - connector: connector({ - server: process.env.MSSQL_HOST!, - authentication: { - type: "default", - options: { - userName: process.env.MSSQL_USERNAME!, - password: process.env.MSSQL_PASSWORD!, - }, - }, - options: { - database: process.env.MSSQL_DB_NAME!, - port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), - trustServerCertificate: true, - encrypt: false, - }, - }), + connector: connector(createConnectionConfig()), }); }); @@ -42,24 +47,7 @@ describe.runIf( process.env.MSSQL_USERNAME && process.env.MSSQL_PASSWORD, )("callProcedure", () => { - const db = createDatabase( - connector({ - server: process.env.MSSQL_HOST!, - authentication: { - type: "default", - options: { - userName: process.env.MSSQL_USERNAME!, - password: process.env.MSSQL_PASSWORD!, - }, - }, - options: { - database: process.env.MSSQL_DB_NAME!, - port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), - trustServerCertificate: true, - encrypt: false, - }, - }), - ); + const db = createDatabase(connector(createConnectionConfig())); beforeAll(async () => { // Drop procedure if it exists @@ -80,12 +68,6 @@ describe.runIf( DROP PROCEDURE dbo.ProcessUserData `; - // Drop procedure if it exists - await db.sql` - IF OBJECT_ID('dbo.CalculateWithOutput', 'P') IS NOT NULL - DROP PROCEDURE dbo.CalculateWithOutput - `; - // Create a simple stored procedure that returns user count await db.sql` CREATE PROCEDURE dbo.GetUserCount @@ -123,23 +105,6 @@ describe.runIf( (SELECT * FROM OPENJSON(@jsonData, '$.hobbies') WITH (hobby NVARCHAR(100) '$') FOR JSON PATH) as hobbies END `; - - // Create a stored procedure with output parameters - await db.sql` - CREATE PROCEDURE dbo.CalculateWithOutput - @a INT, - @b INT, - @sum INT OUTPUT, - @product INT OUTPUT, - @difference INT OUTPUT - AS - BEGIN - SET @sum = @a + @b - SET @product = @a * @b - SET @difference = @a - @b - SELECT @sum as calculatedSum, @product as calculatedProduct, @difference as calculatedDifference - END - `; }); afterAll(async () => { @@ -156,10 +121,6 @@ describe.runIf( IF OBJECT_ID('dbo.ProcessUserData', 'P') IS NOT NULL DROP PROCEDURE dbo.ProcessUserData `; - await db.sql` - IF OBJECT_ID('dbo.CalculateWithOutput', 'P') IS NOT NULL - DROP PROCEDURE dbo.CalculateWithOutput - `; await db.dispose(); }); @@ -190,84 +151,6 @@ describe.runIf( expect((rows[0] as { result: number }).result).toBe(20); }); - it("should call a stored procedure with output parameters", async () => { - // Declare output variables and execute the stored procedure - const stmt = db.prepare(` - DECLARE @sum INT, @product INT, @difference INT; - EXEC dbo.CalculateWithOutput - @a = ?, - @b = ?, - @sum = @sum OUTPUT, - @product = @product OUTPUT, - @difference = @difference OUTPUT; - SELECT @sum as sum, @product as product, @difference as difference; - `); - - const rows = await stmt.all(10, 5); - expect(rows).toBeDefined(); - // The stored procedure returns a result set, plus we SELECT the output values - // So we get 2 rows: one from the procedure, one from the SELECT - expect(rows.length).toBe(2); - - // First row is from the stored procedure's SELECT statement - const procResult = rows[0] as { - calculatedSum: number; - calculatedProduct: number; - calculatedDifference: number; - }; - expect(procResult.calculatedSum).toBe(15); - expect(procResult.calculatedProduct).toBe(50); - expect(procResult.calculatedDifference).toBe(5); - - // Second row is from our SELECT of output parameters - const outputResult = rows[1] as { - sum: number; - product: number; - difference: number; - }; - expect(outputResult.sum).toBe(15); - expect(outputResult.product).toBe(50); - expect(outputResult.difference).toBe(5); - }); - - it("should call a stored procedure with output parameters and result set", async () => { - // Test that we can get both the result set and output parameters - const stmt = db.prepare(` - DECLARE @sum INT, @product INT, @difference INT; - EXEC dbo.CalculateWithOutput - @a = ?, - @b = ?, - @sum = @sum OUTPUT, - @product = @product OUTPUT, - @difference = @difference OUTPUT; - SELECT @sum as outputSum, @product as outputProduct, @difference as outputDifference; - `); - - const rows = await stmt.all(20, 8); - expect(rows).toBeDefined(); - expect(rows.length).toBe(2); - - // First row is from the stored procedure's internal SELECT - const procResult = rows[0] as { - calculatedSum: number; - calculatedProduct: number; - calculatedDifference: number; - }; - expect(procResult.calculatedSum).toBe(28); - expect(procResult.calculatedProduct).toBe(160); - expect(procResult.calculatedDifference).toBe(12); - - // Second row is from our SELECT of output parameters - const outputResult = rows[1] as { - outputSum: number; - outputProduct: number; - outputDifference: number; - }; - expect(outputResult.outputSum).toBe(28); - expect(outputResult.outputProduct).toBe(160); - expect(outputResult.outputDifference).toBe(12); - }); - it("should return JSON data using FOR JSON PATH", async () => { const stmt = db.prepare(` SELECT @@ -451,24 +334,7 @@ describe.runIf( beforeAll(() => { // Connect to master database to create/drop test database - db = createDatabase( - connector({ - server: process.env.MSSQL_HOST!, - authentication: { - type: "default", - options: { - userName: process.env.MSSQL_USERNAME!, - password: process.env.MSSQL_PASSWORD!, - }, - }, - options: { - database: "master", - port: Number.parseInt(process.env.MSSQL_PORT || "1433", 10), - trustServerCertificate: true, - encrypt: false, - }, - }), - ); + db = createDatabase(connector(createConnectionConfig("master"))); }); afterAll(async () => { @@ -543,6 +409,7 @@ describe.runIf( describe("getTediousDataType", () => { it("should return NVarChar for null", () => { + // eslint-disable-next-line unicorn/no-null expect(getTediousDataType(null)).toBe(TYPES.NVarChar); }); @@ -619,15 +486,411 @@ describe("prepareSqlParameters", () => { it("should handle null and undefined parameters", () => { const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; - + // eslint-disable-next-line unicorn/no-null const parameters = [null, undefined]; const result = prepareSqlParameters(sql, parameters); expect(result.sql).toBe( "SELECT * FROM users WHERE name = @1 AND email = @2", ); expect(result.parameters).toEqual({ + // eslint-disable-next-line unicorn/no-null "@1": { name: "1", type: TYPES.NVarChar, value: null }, "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, }); }); }); + +// Error Handling Tests +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("error handling", () => { + const db = createDatabase(connector(createConnectionConfig())); + + afterAll(async () => { + await db.dispose(); + }); + + it("should handle invalid SQL syntax", async () => { + await expect(async () => { + await db.exec("SELECT * FORM invalid_table"); + }).rejects.toThrow(); + }); + + it("should handle non-existent table", async () => { + await expect(async () => { + await db.sql`SELECT * FROM non_existent_table_12345`; + }).rejects.toThrow(); + }); + + it("should handle parameter count mismatch", async () => { + const stmt = db.prepare( + "INSERT INTO sys.tables (name, object_id) VALUES (?, ?)", + ); + // Providing only one parameter when two are expected - should fail + await expect(async () => { + await stmt.all("test"); + }).rejects.toThrow(); + }); + + it("should handle empty SQL query", async () => { + await expect(async () => { + await db.exec(""); + }).rejects.toThrow("SQL query must be provided"); + }); + + it("should provide error context with SQL and parameters", async () => { + try { + const stmt = db.prepare("SELECT * FROM invalid_table WHERE id = ?"); + await stmt.all(123); + expect.fail("Should have thrown an error"); + } catch (error: any) { + expect(error.sql).toBeDefined(); + expect(error.parameters).toBeDefined(); + } + }); + + it("should handle type conversion errors gracefully", async () => { + // Test that tedious converts string numbers to integers automatically + const stmt = db.prepare("SELECT CAST(? AS INT) as result"); + const rows = await stmt.all("42"); + expect(rows.length).toBe(1); + expect((rows[0] as any).result).toBe(42); + }); +}); + +// Transaction Tests +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("transactions", () => { + const db = createDatabase(connector(createConnectionConfig())); + + beforeAll(async () => { + await db.exec(` + IF OBJECT_ID('dbo.test_transactions', 'U') IS NOT NULL + DROP TABLE dbo.test_transactions; + CREATE TABLE dbo.test_transactions ( + id INT PRIMARY KEY, + value NVARCHAR(100) + ); + `); + }); + + afterAll(async () => { + await db.exec("DROP TABLE IF EXISTS dbo.test_transactions"); + await db.dispose(); + }); + + it("should commit a transaction successfully", async () => { + // Use single connection for transaction operations + await db.exec(` + BEGIN TRANSACTION; + INSERT INTO dbo.test_transactions (id, value) VALUES (1, 'test1'); + COMMIT TRANSACTION; + `); + + const stmt = db.prepare("SELECT * FROM dbo.test_transactions WHERE id = ?"); + const rows = await stmt.all(1); + expect(rows.length).toBe(1); + expect((rows[0] as any).value).toBe("test1"); + }); + + it("should rollback a transaction", async () => { + // Use single connection for transaction operations + await db.exec(` + BEGIN TRANSACTION; + INSERT INTO dbo.test_transactions (id, value) VALUES (2, 'test2'); + ROLLBACK TRANSACTION; + `); + + const stmt = db.prepare("SELECT * FROM dbo.test_transactions WHERE id = ?"); + const rows = await stmt.all(2); + expect(rows.length).toBe(0); + }); + + it("should handle nested transactions with savepoints", async () => { + await db.exec(` + BEGIN TRANSACTION; + INSERT INTO dbo.test_transactions (id, value) VALUES (3, 'outer'); + SAVE TRANSACTION savepoint1; + INSERT INTO dbo.test_transactions (id, value) VALUES (4, 'inner'); + ROLLBACK TRANSACTION savepoint1; + COMMIT TRANSACTION; + `); + + const stmt = db.prepare( + "SELECT * FROM dbo.test_transactions WHERE id IN (?, ?)", + ); + const rows = await stmt.all(3, 4); + + expect(rows.length).toBe(1); + expect((rows[0] as any).id).toBe(3); + }); + + it("should handle transaction isolation levels", async () => { + await db.exec(` + SET TRANSACTION ISOLATION LEVEL READ COMMITTED; + BEGIN TRANSACTION; + INSERT INTO dbo.test_transactions (id, value) VALUES (5, 'isolation_test'); + COMMIT TRANSACTION; + `); + + const stmt = db.prepare("SELECT * FROM dbo.test_transactions WHERE id = ?"); + const rows = await stmt.all(5); + expect(rows.length).toBe(1); + }); +}); + +// Batch Operations and Advanced Tests +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("batch operations", () => { + const db = createDatabase(connector(createConnectionConfig())); + + beforeAll(async () => { + await db.exec(` + IF OBJECT_ID('dbo.test_batch', 'U') IS NOT NULL + DROP TABLE dbo.test_batch; + CREATE TABLE dbo.test_batch ( + id INT PRIMARY KEY, + name NVARCHAR(100), + data VARBINARY(MAX) + ); + + IF OBJECT_ID('dbo.GetUserWithOutput', 'P') IS NOT NULL + DROP PROCEDURE dbo.GetUserWithOutput; + `); + + await db.exec(` + CREATE PROCEDURE dbo.GetUserWithOutput + @userId INT, + @userName NVARCHAR(100) OUTPUT + AS + BEGIN + SET @userName = 'User_' + CAST(@userId AS NVARCHAR); + SELECT @userId as id, @userName as name; + END + `); + }); + + afterAll(async () => { + await db.exec("DROP TABLE IF EXISTS dbo.test_batch"); + await db.exec("DROP PROCEDURE IF EXISTS dbo.GetUserWithOutput"); + await db.dispose(); + }); + + it("should handle multiple inserts in batch", async () => { + const stmt = db.prepare( + "INSERT INTO dbo.test_batch (id, name) VALUES (?, ?)", + ); + + await stmt.run(1, "Alice"); + await stmt.run(2, "Bob"); + await stmt.run(3, "Charlie"); + + const selectStmt = db.prepare( + "SELECT COUNT(*) as count FROM dbo.test_batch", + ); + const rows = await selectStmt.all(); + expect((rows[0] as any).count).toBe(3); + }); + + it("should handle binary data (BLOB)", async () => { + const binaryData = Buffer.from("Hello, World!", "utf8"); + const stmt = db.prepare( + "INSERT INTO dbo.test_batch (id, name, data) VALUES (?, ?, ?);", + ); + await stmt.run(10, "binary_test", binaryData as any); + + const selectStmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?"); + const rows = await selectStmt.all(10); + expect(rows.length).toBe(1); + expect(Buffer.isBuffer((rows[0] as any).data)).toBe(true); + expect((rows[0] as any).data.toString("utf8")).toBe("Hello, World!"); + }); + + it("should handle special characters in parameters", async () => { + const specialText = + "Test with 'quotes', \"double quotes\", and\nnewlines\ttabs"; + const stmt = db.prepare( + "INSERT INTO dbo.test_batch (id, name) VALUES (?, ?)", + ); + await stmt.run(20, specialText); + + const selectStmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?"); + const rows = await selectStmt.all(20); + expect((rows[0] as any).name).toBe(specialText); + }); + + it("should handle unicode characters", async () => { + const unicodeText = "Hello 世界 🌍 مرحبا"; + const stmt = db.prepare( + "INSERT INTO dbo.test_batch (id, name) VALUES (?, ?)", + ); + await stmt.run(30, unicodeText); + + const selectStmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?"); + const rows = await selectStmt.all(30); + expect((rows[0] as any).name).toBe(unicodeText); + }); + + it("should handle stored procedure with output parameters", async () => { + // Note: Current implementation doesn't support OUTPUT parameters directly + // This test calls the procedure and gets result set instead (returns 2 rows) + const stmt = db.prepare( + "DECLARE @name NVARCHAR(100); EXEC dbo.GetUserWithOutput @userId = ?, @userName = @name OUTPUT; SELECT @name as userName", + ); + const rows = await stmt.all(42); + // Procedure returns result set + our SELECT statement = 2 rows + expect(rows.length).toBeGreaterThanOrEqual(1); + // Check the last row for our output + const lastRow = rows.at(-1); + expect((lastRow as any).userName).toBe("User_42"); + }); + + it("should handle empty result sets", async () => { + const stmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?"); + const rows = await stmt.all(99_999); + expect(rows.length).toBe(0); + }); + + it("should handle NULL values correctly", async () => { + const stmt = db.prepare( + "INSERT INTO dbo.test_batch (id, name) VALUES (?, ?)", + ); + // eslint-disable-next-line unicorn/no-null + await stmt.run(40, null); + + const selectStmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?"); + const rows = await selectStmt.all(40); + expect(rows.length).toBe(1); + expect((rows[0] as any).name).toBeNull(); + }); +}); + +// Performance Tests +describe.runIf( + process.env.MSSQL_HOST && + process.env.MSSQL_DB_NAME && + process.env.MSSQL_USERNAME && + process.env.MSSQL_PASSWORD, +)("performance", () => { + const db = createDatabase(connector(createConnectionConfig())); + + beforeAll(async () => { + await db.exec(` + IF OBJECT_ID('dbo.test_performance', 'U') IS NOT NULL + DROP TABLE dbo.test_performance; + CREATE TABLE dbo.test_performance ( + id INT PRIMARY KEY, + data NVARCHAR(1000) + ); + `); + }); + + afterAll(async () => { + await db.exec("DROP TABLE IF EXISTS dbo.test_performance"); + await db.dispose(); + }); + + it("should handle multiple sequential queries efficiently", async () => { + const start = Date.now(); + const stmt = db.prepare("SELECT ?"); + + for (let i = 0; i < 10; i++) { + await stmt.all(i); + } + + const duration = Date.now() - start; + // Should complete 10 queries in reasonable time (less than 5 seconds) + expect(duration).toBeLessThan(5000); + }); + + it("should handle large result sets", async () => { + // Insert 100 rows + const insertStmt = db.prepare( + "INSERT INTO dbo.test_performance (id, data) VALUES (?, ?)", + ); + for (let i = 1; i <= 100; i++) { + await insertStmt.run(i, `Data for row ${i}`); + } + + const start = Date.now(); + const selectStmt = db.prepare("SELECT * FROM dbo.test_performance"); + const rows = await selectStmt.all(); + const duration = Date.now() - start; + + expect(rows.length).toBe(100); + // Should fetch 100 rows in reasonable time (less than 2 seconds) + expect(duration).toBeLessThan(2000); + }); + + it("should handle prepared statement reuse", async () => { + const stmt = db.prepare("SELECT * FROM dbo.test_performance WHERE id = ?"); + + const start = Date.now(); + for (let i = 1; i <= 20; i++) { + await stmt.all(i); + } + const duration = Date.now() - start; + + // Reusing prepared statement should be efficient (less than 3 seconds) + expect(duration).toBeLessThan(3000); + }); + + it("should handle concurrent query execution", async () => { + const stmt = db.prepare("SELECT ?"); + + const start = Date.now(); + // Note: Current implementation may execute these sequentially due to connection management + const promises = []; + for (let i = 0; i < 5; i++) { + promises.push(stmt.all(i)); + } + await Promise.all(promises); + const duration = Date.now() - start; + + // Should handle 5 queries efficiently + expect(duration).toBeLessThan(5000); + }); + + it("should handle large text data", async () => { + const largeText = "a".repeat(900); // Just under 1000 char limit + const stmt = db.prepare( + "INSERT INTO dbo.test_performance (id, data) VALUES (?, ?)", + ); + + const start = Date.now(); + await stmt.run(1000, largeText); + const duration = Date.now() - start; + + expect(duration).toBeLessThan(2000); + + const selectStmt = db.prepare( + "SELECT * FROM dbo.test_performance WHERE id = ?", + ); + const rows = await selectStmt.all(1000); + expect((rows[0] as any).data).toBe(largeText); + }); + + it("should handle query timeout scenarios", async () => { + // Test a long-running query + const stmt = db.prepare("WAITFOR DELAY '00:00:01'; SELECT 1 as result"); + + const start = Date.now(); + const rows = await stmt.all(); + const duration = Date.now() - start; + + expect(rows.length).toBe(1); + expect(duration).toBeGreaterThanOrEqual(1000); + expect(duration).toBeLessThan(3000); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts index cec357c..3570523 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -4,7 +4,7 @@ export default defineConfig({ test: { setupFiles: ["dotenv/config"], coverage: { - reporter: ["text", "clover", "json"], + reporter: ["text", "clover", "json", "html"], include: ["src/**/*.ts"], }, }, From a5fa0e02aebeec1631ee79a4f87074b1b92ce279 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Sun, 7 Dec 2025 18:45:41 +0000 Subject: [PATCH 21/21] chore: apply automated updates --- test/connectors/mssql.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/connectors/mssql.test.ts b/test/connectors/mssql.test.ts index aeadb69..1eec6d8 100644 --- a/test/connectors/mssql.test.ts +++ b/test/connectors/mssql.test.ts @@ -409,7 +409,6 @@ describe.runIf( describe("getTediousDataType", () => { it("should return NVarChar for null", () => { - // eslint-disable-next-line unicorn/no-null expect(getTediousDataType(null)).toBe(TYPES.NVarChar); }); @@ -486,14 +485,13 @@ describe("prepareSqlParameters", () => { it("should handle null and undefined parameters", () => { const sql = "SELECT * FROM users WHERE name = ? AND email = ?"; - // eslint-disable-next-line unicorn/no-null + const parameters = [null, undefined]; const result = prepareSqlParameters(sql, parameters); expect(result.sql).toBe( "SELECT * FROM users WHERE name = @1 AND email = @2", ); expect(result.parameters).toEqual({ - // eslint-disable-next-line unicorn/no-null "@1": { name: "1", type: TYPES.NVarChar, value: null }, "@2": { name: "2", type: TYPES.NVarChar, value: undefined }, }); @@ -766,7 +764,7 @@ describe.runIf( const stmt = db.prepare( "INSERT INTO dbo.test_batch (id, name) VALUES (?, ?)", ); - // eslint-disable-next-line unicorn/no-null + await stmt.run(40, null); const selectStmt = db.prepare("SELECT * FROM dbo.test_batch WHERE id = ?");