diff --git a/README.md b/README.md index a49fee9..ee0c29c 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,7 @@ npm install kysely pg npm install kysely mysql2 npm install kysely better-sqlite3 npm install @libsql/kysely-libsql +npm install kysely tedious tarn @tediousjs/connection-string ``` ## Generating type definitions @@ -36,6 +37,9 @@ DATABASE_URL=C:/Program Files/sqlite3/db # LibSQL DATABASE_URL=libsql://token@host:port/database + +# MSSQL +DATABASE_URL=Server=mssql;Database=database;User Id=user;Password=password ``` > If your URL contains a password with special characters, those characters may need to be [percent-encoded](https://en.wikipedia.org/wiki/Percent-encoding#Reserved_characters). diff --git a/package.json b/package.json index 6b2758f..5ccd6b6 100644 --- a/package.json +++ b/package.json @@ -55,6 +55,7 @@ "@types/minimist": "^1.2.5", "@types/node": "^20.11.16", "@types/pg": "^8.11.0", + "@types/tedious": "4.0.14", "@typescript-eslint/parser": "^6.20.0", "better-sqlite3": "^9.3.0", "eslint": "^8.56.0", @@ -73,16 +74,22 @@ }, "peerDependencies": { "@libsql/kysely-libsql": "^0.3.0", + "@tediousjs/connection-string": "^0.5.0", "better-sqlite3": ">=7.6.2", "kysely-bun-worker": "^0.5.3", - "kysely": ">=0.19.12", + "kysely": "^0.27.0", "mysql2": "^2.3.3 || ^3.0.0", - "pg": "^8.8.0" + "pg": "^8.8.0", + "tarn": "^3.0.0", + "tedious": "^16.6.0" }, "peerDependenciesMeta": { "@libsql/kysely-libsql": { "optional": true }, + "@tediousjs/connection-string": { + "optional": true + }, "better-sqlite3": { "optional": true }, @@ -97,6 +104,12 @@ }, "pg": { "optional": true + }, + "tarn": { + "optional": true + }, + "tedious": { + "optional": true } }, "eslintConfig": { diff --git a/src/cli/constants.ts b/src/cli/constants.ts index a56eb01..ac87c42 100644 --- a/src/cli/constants.ts +++ b/src/cli/constants.ts @@ -27,4 +27,5 @@ export const VALID_DIALECTS = [ 'sqlite', 'libsql', 'bun-sqlite', + 'mssql', ]; diff --git a/src/core/connection-string-parser.ts b/src/core/connection-string-parser.ts index 1a4dc91..9862563 100644 --- a/src/core/connection-string-parser.ts +++ b/src/core/connection-string-parser.ts @@ -41,6 +41,10 @@ export class ConnectionStringParser { return 'postgres'; } + if (connectionString.toLowerCase().includes('user id=')) { + return 'mssql'; + } + return 'sqlite'; } diff --git a/src/core/dialect-manager.ts b/src/core/dialect-manager.ts index 7f65cfb..f297fb3 100644 --- a/src/core/dialect-manager.ts +++ b/src/core/dialect-manager.ts @@ -1,6 +1,7 @@ import { BunSqliteDialect, LibsqlDialect, + MssqlDialect, MysqlDialect, PostgresDialect, SqliteDialect, @@ -12,7 +13,8 @@ export type DialectName = | 'libsql' | 'mysql' | 'postgres' - | 'sqlite'; + | 'sqlite' + | 'mssql'; /** * Returns a dialect instance for a pre-defined dialect name. @@ -28,6 +30,8 @@ export class DialectManager { return new MysqlDialect(); case 'postgres': return new PostgresDialect(); + case 'mssql': + return new MssqlDialect(); default: return new SqliteDialect(); } diff --git a/src/dialects/index.ts b/src/dialects/index.ts index 1a5923f..0c0ed8a 100644 --- a/src/dialects/index.ts +++ b/src/dialects/index.ts @@ -1,5 +1,6 @@ export * from './bun-sqlite'; export * from './libsql'; +export * from './mssql'; export * from './mysql'; export * from './postgres'; export * from './sqlite'; diff --git a/src/dialects/mssql/index.ts b/src/dialects/mssql/index.ts new file mode 100644 index 0000000..8f966e2 --- /dev/null +++ b/src/dialects/mssql/index.ts @@ -0,0 +1,2 @@ +export * from './mssql-adapter'; +export * from './mssql-dialect'; diff --git a/src/dialects/mssql/mssql-adapter.ts b/src/dialects/mssql/mssql-adapter.ts new file mode 100644 index 0000000..f8cfb4e --- /dev/null +++ b/src/dialects/mssql/mssql-adapter.ts @@ -0,0 +1,38 @@ +import { IdentifierNode } from '../../ast'; +import { Adapter } from '../../core'; + +export class MssqlAdapter extends Adapter { + // https://github.com/tediousjs/tedious/tree/master/src/data-types + override readonly scalars = { + bigint: new IdentifierNode('number'), + binary: new IdentifierNode('Buffer'), + bit: new IdentifierNode('Buffer'), + char: new IdentifierNode('string'), + date: new IdentifierNode('Date'), + datetime: new IdentifierNode('Date'), + datetime2: new IdentifierNode('Date'), + datetimeoffset: new IdentifierNode('Date'), + decimal: new IdentifierNode('number'), + number: new IdentifierNode('number'), + double: new IdentifierNode('number'), + float: new IdentifierNode('number'), + int: new IdentifierNode('number'), + image: new IdentifierNode('Buffer'), + money: new IdentifierNode('number'), + nchar: new IdentifierNode('string'), + ntext: new IdentifierNode('string'), + numeric: new IdentifierNode('number'), + nvarchar: new IdentifierNode('string'), + real: new IdentifierNode('number'), + smalldatetime: new IdentifierNode('Date'), + smallint: new IdentifierNode('number'), + smallmoney: new IdentifierNode('number'), + text: new IdentifierNode('string'), + time: new IdentifierNode('Date'), + tinyint: new IdentifierNode('number'), + tvp: new IdentifierNode('unknown'), + uniqueidentifier: new IdentifierNode('string'), + varbinary: new IdentifierNode('Buffer'), + varchar: new IdentifierNode('string'), + }; +} diff --git a/src/dialects/mssql/mssql-dialect.ts b/src/dialects/mssql/mssql-dialect.ts new file mode 100644 index 0000000..a87fa31 --- /dev/null +++ b/src/dialects/mssql/mssql-dialect.ts @@ -0,0 +1,61 @@ +import { MssqlDialect as KyselyMssqlDialect } from 'kysely'; +import { CreateKyselyDialectOptions, Dialect } from '../../core'; +import { MssqlAdapter } from './mssql-adapter'; +import { MssqlIntrospector } from './mssql-introspector'; + +export class MssqlDialect extends Dialect { + readonly adapter = new MssqlAdapter(); + readonly introspector = new MssqlIntrospector(); + + async createKyselyDialect(options: CreateKyselyDialectOptions) { + const tedious = await import('tedious'); + const tarn = await import('tarn'); + const { parseConnectionString } = await import( + '@tediousjs/connection-string' + ); + + let server = ''; + let port = 1433; + + const connectionParams: any = parseConnectionString( + options.connectionString, + ); + + server = connectionParams.server; + // https://www.connectionstrings.com/microsoft-data-sqlclient/using-a-non-standard-port/ + if (server.includes(',')) { + const [serverName, portString] = server.split(',') as [string, string]; + server = serverName; + port = parseInt(portString, 10); + } + + return new KyselyMssqlDialect({ + tarn: { + ...tarn, + options: { + min: 0, + max: 1, + }, + }, + tedious: { + ...tedious, + connectionFactory: () => + new tedious.Connection({ + authentication: { + type: 'default', + options: { + password: connectionParams.password, + userName: connectionParams['user id'], + }, + }, + options: { + database: connectionParams.database, + port, + trustServerCertificate: true, + }, + server, + }), + }, + }); + } +} diff --git a/src/dialects/mssql/mssql-introspector.ts b/src/dialects/mssql/mssql-introspector.ts new file mode 100644 index 0000000..0762d53 --- /dev/null +++ b/src/dialects/mssql/mssql-introspector.ts @@ -0,0 +1,10 @@ +import { DatabaseMetadata, EnumCollection } from '../../core'; +import { IntrospectOptions, Introspector } from '../../introspector'; + +export class MssqlIntrospector extends Introspector { + async introspect(options: IntrospectOptions) { + const tables = await this.getTables(options); + const enums = new EnumCollection(); + return new DatabaseMetadata(tables, enums); + } +}