Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -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
Comment thread
vkuttyp marked this conversation as resolved.
4 changes: 4 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
8 changes: 8 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,11 @@ 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"
1 change: 1 addition & 0 deletions docs/2.connectors/1.index.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
30 changes: 30 additions & 0 deletions docs/2.connectors/mssql.md
Original file line number Diff line number Diff line change
@@ -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"}
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"pg": "^8.20.0",
"prettier": "^3.8.1",
"scule": "^1.3.0",
"tedious": "^19.1.3",
"typescript": "^5.9.3",
"vitest": "^4.1.0",
"wrangler": "^4.74.0"
Expand All @@ -79,7 +80,8 @@
"drizzle-orm": "*",
"kysely": "*",
"mysql2": "*",
"sqlite3": "*"
"sqlite3": "*",
"tedious": "*"
},
"peerDependenciesMeta": {
"@libsql/client": {
Expand All @@ -97,6 +99,9 @@
"mysql2": {
"optional": true
},
"tedious": {
"optional": true
},
"@electric-sql/pglite": {
"optional": true
},
Expand Down
2,612 changes: 1,379 additions & 1,233 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion scripts/gen-connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ const connectors: {
optionsTName?: string;
}[] = [];

const connectorOptionsNameAliases: Record<string, string> = {
"mssql": "MSSQL"
};

for (const entry of connectorEntries) {
const pathName = entry.replace(/\.ts$/, "");
const name = pathName.replace(/[/\\]/g, "-");
Expand All @@ -65,7 +69,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,
Expand Down
5 changes: 4 additions & 1 deletion src/_connectors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@ 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";
import type { ConnectorOptions as PlanetscaleOptions } from "db0/connectors/planetscale";
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;
Expand All @@ -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 */
Expand All @@ -56,6 +58,7 @@ export const connectors: Record<ConnectorName, string> = 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 */
Expand Down
229 changes: 229 additions & 0 deletions src/connectors/mssql.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import {
Connection,
Request,
Connection as TediousConnection,
type ConnectionConfiguration,
TYPES,
} from "tedious";

import type { Connector, Statement, Primitive } from "db0";

// Type for tedious DataType
type DataType = (typeof TYPES)[keyof typeof TYPES];

export type ConnectorOptions = ConnectionConfiguration;

export default function mssqlConnector(opts: ConnectorOptions) {
let _client: undefined | TediousConnection;
async function getClient(): Promise<TediousConnection> {
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;
});
Comment thread
vkuttyp marked this conversation as resolved.

client.on("connect", () => {
if (_client) {
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: Record<string, unknown> = {};
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);
});
Comment thread
vkuttyp marked this conversation as resolved.

connection.execSql(request);
},
);

try {
const { rows, success } = await query;

return {
rows,
success,
};
} catch (error: any) {
error.sql = _sql;
error.parameters = parameters;
throw error;
}
}

return <Connector<TediousConnection>>{
name: "mssql",
dialect: "mssql",
getInstance: () => getClient(),
exec(sql: string) {
return _run(sql, []);
},
prepare(sql: string) {
const _sql = sql;
let _params: Primitive[] = [];

const statement: Statement = {
bind(...params: Primitive[]) {
if (params.length > 0) {
_params = params;
}
return statement;
},
async all(...params: Primitive[]) {
const { rows } = await _run(
_sql,
params.length > 0 ? params : _params,
);
return rows;
},
async run(...params: Primitive[]) {
const { success = false } =
(await _run(_sql, params.length > 0 ? params : _params)) || {};
return {
success,
};
},
async get(...params: Primitive[]) {
const {
rows: [row],
} = await _run(_sql, params.length > 0 ? params : _params);
return row;
},
};

return statement;
},
};
}
Comment thread
vkuttyp marked this conversation as resolved.

// 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[] = [],
): {
sql: string;
parameters: Record<string, { name: string; type: DataType; value: unknown }>;
} {
const parameterIndexes: number[] = [];
const tokens = [...sql];

// find all `?` placeholders in the SQL string
for (const [i, token] of tokens.entries()) {
if (token === "?") {
parameterIndexes.push(i);
}
}
Comment thread
vkuttyp marked this conversation as resolved.

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
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,
};
}
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
/**
Expand Down
Loading