@kysera/dialects 0.7.3 → 0.7.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Microsoft SQL Server Dialect Adapter
3
+ *
4
+ * Supports SQL Server 2017+, Azure SQL Database, and Azure SQL Edge
5
+ */
6
+
7
+ import type { Kysely } from 'kysely'
8
+ import { sql } from 'kysely'
9
+ import type { DialectAdapter, DatabaseErrorLike } from '../types.js'
10
+ import { assertValidIdentifier } from '../helpers.js'
11
+
12
+ export class MSSQLAdapter implements DialectAdapter {
13
+ readonly dialect = 'mssql' as const
14
+
15
+ getDefaultPort(): number {
16
+ return 1433
17
+ }
18
+
19
+ getCurrentTimestamp(): string {
20
+ return 'GETDATE()'
21
+ }
22
+
23
+ escapeIdentifier(identifier: string): string {
24
+ // MSSQL uses square brackets for escaping
25
+ return '[' + identifier.replace(/\]/g, ']]') + ']'
26
+ }
27
+
28
+ formatDate(date: Date): string {
29
+ // MSSQL datetime format: YYYY-MM-DD HH:MM:SS.mmm
30
+ return date.toISOString().replace('T', ' ').replace('Z', '')
31
+ }
32
+
33
+ isUniqueConstraintError(error: unknown): boolean {
34
+ const e = error as DatabaseErrorLike
35
+ const message = e.message?.toLowerCase() || ''
36
+ const code = e.code || ''
37
+ // MSSQL error 2627: Violation of PRIMARY KEY/UNIQUE constraint
38
+ // MSSQL error 2601: Cannot insert duplicate key row
39
+ return (
40
+ code === '2627' ||
41
+ code === '2601' ||
42
+ message.includes('violation of unique key constraint') ||
43
+ message.includes('cannot insert duplicate key') ||
44
+ message.includes('unique constraint')
45
+ )
46
+ }
47
+
48
+ isForeignKeyError(error: unknown): boolean {
49
+ const e = error as DatabaseErrorLike
50
+ const message = e.message?.toLowerCase() || ''
51
+ const code = e.code || ''
52
+ // MSSQL error 547: FOREIGN KEY constraint violation
53
+ return (
54
+ code === '547' ||
55
+ message.includes('foreign key constraint') ||
56
+ message.includes('conflicted with the foreign key')
57
+ )
58
+ }
59
+
60
+ isNotNullError(error: unknown): boolean {
61
+ const e = error as DatabaseErrorLike
62
+ const message = e.message?.toLowerCase() || ''
63
+ const code = e.code || ''
64
+ // MSSQL error 515: Cannot insert NULL value
65
+ return (
66
+ code === '515' ||
67
+ message.includes('cannot insert the value null') ||
68
+ message.includes('does not allow nulls')
69
+ )
70
+ }
71
+
72
+ async tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {
73
+ assertValidIdentifier(tableName, 'table name')
74
+ try {
75
+ const result = await db
76
+ .selectFrom('INFORMATION_SCHEMA.TABLES')
77
+ .select('TABLE_NAME')
78
+ .where('TABLE_NAME', '=', tableName)
79
+ .where('TABLE_TYPE', '=', 'BASE TABLE')
80
+ .executeTakeFirst()
81
+ return !!result
82
+ } catch {
83
+ return false
84
+ }
85
+ }
86
+
87
+ async getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]> {
88
+ assertValidIdentifier(tableName, 'table name')
89
+ try {
90
+ const results = await db
91
+ .selectFrom('INFORMATION_SCHEMA.COLUMNS')
92
+ .select('COLUMN_NAME')
93
+ .where('TABLE_NAME', '=', tableName)
94
+ .execute()
95
+ return results.map(r => (r as { COLUMN_NAME: string }).COLUMN_NAME)
96
+ } catch {
97
+ return []
98
+ }
99
+ }
100
+
101
+ async getTables(db: Kysely<any>): Promise<string[]> {
102
+ try {
103
+ const results = await db
104
+ .selectFrom('INFORMATION_SCHEMA.TABLES')
105
+ .select('TABLE_NAME')
106
+ .where('TABLE_TYPE', '=', 'BASE TABLE')
107
+ .where('TABLE_SCHEMA', '=', 'dbo')
108
+ .execute()
109
+ return results.map(r => (r as { TABLE_NAME: string }).TABLE_NAME)
110
+ } catch {
111
+ return []
112
+ }
113
+ }
114
+
115
+ async getDatabaseSize(db: Kysely<any>, _databaseName?: string): Promise<number> {
116
+ try {
117
+ // MSSQL: Get database size using sys.database_files
118
+ // Note: _databaseName is ignored as MSSQL uses the current database context
119
+ const result = await sql<{ size: number }>`
120
+ SELECT SUM(size * 8 * 1024) as size
121
+ FROM sys.database_files
122
+ WHERE type = 0
123
+ `.execute(db)
124
+ return (result.rows?.[0] as { size?: number })?.size || 0
125
+ } catch {
126
+ return 0
127
+ }
128
+ }
129
+
130
+ async truncateTable(db: Kysely<any>, tableName: string): Promise<boolean> {
131
+ assertValidIdentifier(tableName, 'table name')
132
+ try {
133
+ // MSSQL: First try TRUNCATE, fall back to DELETE if FK constraints exist
134
+ try {
135
+ await sql.raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)}`).execute(db)
136
+ } catch (truncateError) {
137
+ // If truncate fails due to FK, use DELETE
138
+ const errorMsg = String(truncateError)
139
+ if (errorMsg.includes('FOREIGN KEY') || errorMsg.includes('Cannot truncate')) {
140
+ await sql.raw(`DELETE FROM ${this.escapeIdentifier(tableName)}`).execute(db)
141
+ // Reset identity if table has one
142
+ try {
143
+ // Use escaped identifier for SQL injection prevention
144
+ const escapedTableName = this.escapeIdentifier(tableName)
145
+ await sql
146
+ .raw(`DBCC CHECKIDENT (${escapedTableName}, RESEED, 0)`)
147
+ .execute(db)
148
+ } catch {
149
+ // Ignore if table doesn't have identity column
150
+ }
151
+ } else {
152
+ throw truncateError
153
+ }
154
+ }
155
+ return true
156
+ } catch (error) {
157
+ const errorMessage = String(error)
158
+ if (
159
+ errorMessage.includes('Invalid object name') ||
160
+ errorMessage.includes('does not exist')
161
+ ) {
162
+ return false
163
+ }
164
+ // Re-throw with context (logging should be handled by caller)
165
+ throw new Error(`Failed to truncate MSSQL table "${tableName}": ${String(error)}`)
166
+ }
167
+ }
168
+
169
+ async truncateAllTables(db: Kysely<any>, exclude: string[] = []): Promise<void> {
170
+ const tables = await this.getTables(db)
171
+
172
+ // MSSQL: Disable all FK constraints first
173
+ for (const table of tables) {
174
+ if (!exclude.includes(table)) {
175
+ try {
176
+ await sql
177
+ .raw(`ALTER TABLE ${this.escapeIdentifier(table)} NOCHECK CONSTRAINT ALL`)
178
+ .execute(db)
179
+ } catch {
180
+ // Ignore errors for tables without constraints
181
+ }
182
+ }
183
+ }
184
+
185
+ // Truncate all tables
186
+ for (const table of tables) {
187
+ if (!exclude.includes(table)) {
188
+ await this.truncateTable(db, table)
189
+ }
190
+ }
191
+
192
+ // Re-enable all FK constraints
193
+ for (const table of tables) {
194
+ if (!exclude.includes(table)) {
195
+ try {
196
+ await sql
197
+ .raw(`ALTER TABLE ${this.escapeIdentifier(table)} CHECK CONSTRAINT ALL`)
198
+ .execute(db)
199
+ } catch {
200
+ // Ignore errors
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+
207
+ export const mssqlAdapter = new MSSQLAdapter()
@@ -2,79 +2,88 @@
2
2
  * MySQL Dialect Adapter
3
3
  */
4
4
 
5
- import type { Kysely } from 'kysely';
6
- import { sql } from 'kysely';
7
- import type { DialectAdapter, DatabaseErrorLike } from '../types.js';
5
+ import type { Kysely } from 'kysely'
6
+ import { sql } from 'kysely'
7
+ import { silentLogger, type KyseraLogger } from '@kysera/core'
8
+ import type { DialectAdapter, DatabaseErrorLike } from '../types.js'
9
+ import { assertValidIdentifier } from '../helpers.js'
8
10
 
9
11
  export class MySQLAdapter implements DialectAdapter {
10
- readonly dialect = 'mysql' as const;
12
+ readonly dialect = 'mysql' as const
13
+ private logger: KyseraLogger
14
+
15
+ constructor(logger: KyseraLogger = silentLogger) {
16
+ this.logger = logger
17
+ }
11
18
 
12
19
  getDefaultPort(): number {
13
- return 3306;
20
+ return 3306
14
21
  }
15
22
 
16
23
  getCurrentTimestamp(): string {
17
- return 'CURRENT_TIMESTAMP';
24
+ return 'CURRENT_TIMESTAMP'
18
25
  }
19
26
 
20
27
  escapeIdentifier(identifier: string): string {
21
- return `\`${identifier.replace(/`/g, '``')}\``;
28
+ return '`' + identifier.replace(/`/g, '``') + '`'
22
29
  }
23
30
 
24
31
  formatDate(date: Date): string {
25
32
  // MySQL datetime format: YYYY-MM-DD HH:MM:SS
26
- return date.toISOString().slice(0, 19).replace('T', ' ');
33
+ return date.toISOString().slice(0, 19).replace('T', ' ')
27
34
  }
28
35
 
29
36
  isUniqueConstraintError(error: unknown): boolean {
30
- const e = error as DatabaseErrorLike;
31
- const message = e.message?.toLowerCase() || '';
32
- const code = e.code || '';
33
- return code === 'ER_DUP_ENTRY' || code === '1062' || message.includes('duplicate entry');
37
+ const e = error as DatabaseErrorLike
38
+ const message = e.message?.toLowerCase() || ''
39
+ const code = e.code || ''
40
+ return code === 'ER_DUP_ENTRY' || code === '1062' || message.includes('duplicate entry')
34
41
  }
35
42
 
36
43
  isForeignKeyError(error: unknown): boolean {
37
- const e = error as DatabaseErrorLike;
38
- const code = e.code || '';
44
+ const e = error as DatabaseErrorLike
45
+ const code = e.code || ''
39
46
  return (
40
47
  code === 'ER_ROW_IS_REFERENCED' ||
41
48
  code === '1451' ||
42
49
  code === 'ER_NO_REFERENCED_ROW' ||
43
50
  code === '1452'
44
- );
51
+ )
45
52
  }
46
53
 
47
54
  isNotNullError(error: unknown): boolean {
48
- const e = error as DatabaseErrorLike;
49
- const code = e.code || '';
50
- return code === 'ER_BAD_NULL_ERROR' || code === '1048';
55
+ const e = error as DatabaseErrorLike
56
+ const code = e.code || ''
57
+ return code === 'ER_BAD_NULL_ERROR' || code === '1048'
51
58
  }
52
59
 
53
60
  async tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {
61
+ assertValidIdentifier(tableName, 'table name')
54
62
  try {
55
63
  const result = await db
56
64
  .selectFrom('information_schema.tables')
57
65
  .select('table_name')
58
66
  .where('table_name', '=', tableName)
59
67
  .where('table_schema', '=', sql`DATABASE()`)
60
- .executeTakeFirst();
61
- return !!result;
68
+ .executeTakeFirst()
69
+ return !!result
62
70
  } catch {
63
- return false;
71
+ return false
64
72
  }
65
73
  }
66
74
 
67
75
  async getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]> {
76
+ assertValidIdentifier(tableName, 'table name')
68
77
  try {
69
78
  const results = await db
70
79
  .selectFrom('information_schema.columns')
71
80
  .select('column_name')
72
81
  .where('table_name', '=', tableName)
73
82
  .where('table_schema', '=', sql`DATABASE()`)
74
- .execute();
75
- return results.map((r) => r.column_name as string);
83
+ .execute()
84
+ return results.map(r => r.column_name as string)
76
85
  } catch {
77
- return [];
86
+ return []
78
87
  }
79
88
  }
80
89
 
@@ -85,10 +94,10 @@ export class MySQLAdapter implements DialectAdapter {
85
94
  .select('table_name')
86
95
  .where('table_schema', '=', sql`DATABASE()`)
87
96
  .where('table_type', '=', 'BASE TABLE')
88
- .execute();
89
- return results.map((r) => r.table_name as string);
97
+ .execute()
98
+ return results.map(r => r.table_name as string)
90
99
  } catch {
91
- return [];
100
+ return []
92
101
  }
93
102
  }
94
103
 
@@ -96,48 +105,61 @@ export class MySQLAdapter implements DialectAdapter {
96
105
  try {
97
106
  const dbName =
98
107
  databaseName ||
99
- (await sql
100
- .raw('SELECT DATABASE() as name')
108
+ (await sql<{ name: string }>`SELECT DATABASE() as name`
101
109
  .execute(db)
102
- .then((r) => (r.rows?.[0] as { name?: string })?.name));
110
+ .then(r => r.rows?.[0]?.name))
111
+
112
+ if (!dbName) {
113
+ return 0
114
+ }
103
115
 
104
- const result = await sql
105
- .raw(
106
- `SELECT SUM(data_length + index_length) as size FROM information_schema.tables WHERE table_schema = '${dbName}'`
107
- )
108
- .execute(db)
109
- .then((r) => r.rows?.[0]);
116
+ // Use parameterized query to prevent SQL injection
117
+ const result = await sql<{ size: number }>`
118
+ SELECT SUM(data_length + index_length) as size
119
+ FROM information_schema.tables
120
+ WHERE table_schema = ${dbName}
121
+ `.execute(db)
110
122
 
111
- return (result as { size?: number })?.size || 0;
123
+ return (result.rows?.[0] as { size?: number })?.size || 0
112
124
  } catch {
113
- return 0;
125
+ return 0
114
126
  }
115
127
  }
116
128
 
117
- async truncateTable(db: Kysely<any>, tableName: string): Promise<void> {
129
+ async truncateTable(db: Kysely<any>, tableName: string): Promise<boolean> {
130
+ assertValidIdentifier(tableName, 'table name')
118
131
  try {
119
- // Temporarily disable foreign key checks
120
- await sql.raw('SET FOREIGN_KEY_CHECKS = 0').execute(db);
121
- await sql.raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)}`).execute(db);
122
- await sql.raw('SET FOREIGN_KEY_CHECKS = 1').execute(db);
123
- } catch {
124
- // Re-enable foreign key checks even on error
132
+ await sql.raw('SET FOREIGN_KEY_CHECKS = 0').execute(db)
125
133
  try {
126
- await sql.raw('SET FOREIGN_KEY_CHECKS = 1').execute(db);
127
- } catch {
128
- // Ignore
134
+ await sql.raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)}`).execute(db)
135
+ return true
136
+ } finally {
137
+ // Always try to re-enable FK checks
138
+ try {
139
+ await sql.raw('SET FOREIGN_KEY_CHECKS = 1').execute(db)
140
+ } catch (fkError) {
141
+ this.logger.error('Failed to re-enable foreign key checks:', fkError)
142
+ }
143
+ }
144
+ } catch (error) {
145
+ const errorMessage = String(error)
146
+ if (errorMessage.includes("doesn't exist") || errorMessage.includes('Unknown table')) {
147
+ return false
129
148
  }
149
+ // Log and rethrow unexpected errors
150
+ this.logger.error(`Failed to truncate table "${tableName}":`, error)
151
+ throw error
130
152
  }
131
153
  }
132
154
 
133
155
  async truncateAllTables(db: Kysely<any>, exclude: string[] = []): Promise<void> {
134
- const tables = await this.getTables(db);
156
+ const tables = await this.getTables(db)
135
157
  for (const table of tables) {
136
158
  if (!exclude.includes(table)) {
137
- await this.truncateTable(db, table);
159
+ await this.truncateTable(db, table)
138
160
  }
139
161
  }
140
162
  }
141
163
  }
142
164
 
143
- export const mysqlAdapter = new MySQLAdapter();
165
+ export const mysqlAdapter = new MySQLAdapter()
@@ -2,75 +2,84 @@
2
2
  * PostgreSQL Dialect Adapter
3
3
  */
4
4
 
5
- import type { Kysely } from 'kysely';
6
- import { sql } from 'kysely';
7
- import type { DialectAdapter, DatabaseErrorLike } from '../types.js';
5
+ import type { Kysely } from 'kysely'
6
+ import { sql } from 'kysely'
7
+ import { silentLogger, type KyseraLogger } from '@kysera/core'
8
+ import type { DialectAdapter, DatabaseErrorLike } from '../types.js'
9
+ import { assertValidIdentifier } from '../helpers.js'
8
10
 
9
11
  export class PostgresAdapter implements DialectAdapter {
10
- readonly dialect = 'postgres' as const;
12
+ readonly dialect = 'postgres' as const
13
+ private logger: KyseraLogger
14
+
15
+ constructor(logger: KyseraLogger = silentLogger) {
16
+ this.logger = logger
17
+ }
11
18
 
12
19
  getDefaultPort(): number {
13
- return 5432;
20
+ return 5432
14
21
  }
15
22
 
16
23
  getCurrentTimestamp(): string {
17
- return 'CURRENT_TIMESTAMP';
24
+ return 'CURRENT_TIMESTAMP'
18
25
  }
19
26
 
20
27
  escapeIdentifier(identifier: string): string {
21
- return `"${identifier.replace(/"/g, '""')}"`;
28
+ return '"' + identifier.replace(/"/g, '""') + '"'
22
29
  }
23
30
 
24
31
  formatDate(date: Date): string {
25
- return date.toISOString();
32
+ return date.toISOString()
26
33
  }
27
34
 
28
35
  isUniqueConstraintError(error: unknown): boolean {
29
- const e = error as DatabaseErrorLike;
30
- const message = e.message?.toLowerCase() || '';
31
- const code = e.code || '';
32
- return code === '23505' || message.includes('unique constraint');
36
+ const e = error as DatabaseErrorLike
37
+ const message = e.message?.toLowerCase() || ''
38
+ const code = e.code || ''
39
+ return code === '23505' || message.includes('unique constraint')
33
40
  }
34
41
 
35
42
  isForeignKeyError(error: unknown): boolean {
36
- const e = error as DatabaseErrorLike;
37
- const message = e.message?.toLowerCase() || '';
38
- const code = e.code || '';
39
- return code === '23503' || message.includes('foreign key constraint');
43
+ const e = error as DatabaseErrorLike
44
+ const message = e.message?.toLowerCase() || ''
45
+ const code = e.code || ''
46
+ return code === '23503' || message.includes('foreign key constraint')
40
47
  }
41
48
 
42
49
  isNotNullError(error: unknown): boolean {
43
- const e = error as DatabaseErrorLike;
44
- const message = e.message?.toLowerCase() || '';
45
- const code = e.code || '';
46
- return code === '23502' || message.includes('not-null constraint');
50
+ const e = error as DatabaseErrorLike
51
+ const message = e.message?.toLowerCase() || ''
52
+ const code = e.code || ''
53
+ return code === '23502' || message.includes('not-null constraint')
47
54
  }
48
55
 
49
56
  async tableExists(db: Kysely<any>, tableName: string): Promise<boolean> {
57
+ assertValidIdentifier(tableName, 'table name')
50
58
  try {
51
59
  const result = await db
52
60
  .selectFrom('information_schema.tables')
53
61
  .select('table_name')
54
62
  .where('table_name', '=', tableName)
55
63
  .where('table_schema', '=', 'public')
56
- .executeTakeFirst();
57
- return !!result;
64
+ .executeTakeFirst()
65
+ return !!result
58
66
  } catch {
59
- return false;
67
+ return false
60
68
  }
61
69
  }
62
70
 
63
71
  async getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]> {
72
+ assertValidIdentifier(tableName, 'table name')
64
73
  try {
65
74
  const results = await db
66
75
  .selectFrom('information_schema.columns')
67
76
  .select('column_name')
68
77
  .where('table_name', '=', tableName)
69
78
  .where('table_schema', '=', 'public')
70
- .execute();
71
- return results.map((r) => r.column_name as string);
79
+ .execute()
80
+ return results.map(r => r.column_name as string)
72
81
  } catch {
73
- return [];
82
+ return []
74
83
  }
75
84
  }
76
85
 
@@ -81,41 +90,57 @@ export class PostgresAdapter implements DialectAdapter {
81
90
  .select('table_name')
82
91
  .where('table_schema', '=', 'public')
83
92
  .where('table_type', '=', 'BASE TABLE')
84
- .execute();
85
- return results.map((r) => r.table_name as string);
93
+ .execute()
94
+ return results.map(r => r.table_name as string)
86
95
  } catch {
87
- return [];
96
+ return []
88
97
  }
89
98
  }
90
99
 
91
100
  async getDatabaseSize(db: Kysely<any>, databaseName?: string): Promise<number> {
92
101
  try {
93
- const result = await sql
94
- .raw(`SELECT pg_database_size(${databaseName ? `'${databaseName}'` : 'current_database()'}) as size`)
95
- .execute(db)
96
- .then((r) => r.rows?.[0]);
97
- return (result as { size?: number })?.size || 0;
102
+ // Use parameterized query to prevent SQL injection
103
+ const result = databaseName
104
+ ? await sql<{ size: number }>`SELECT pg_database_size(${databaseName}) as size`.execute(db)
105
+ : await sql<{ size: number }>`SELECT pg_database_size(current_database()) as size`.execute(
106
+ db
107
+ )
108
+ return (result.rows?.[0] as { size?: number })?.size || 0
98
109
  } catch {
99
- return 0;
110
+ return 0
100
111
  }
101
112
  }
102
113
 
103
- async truncateTable(db: Kysely<any>, tableName: string): Promise<void> {
114
+ async truncateTable(db: Kysely<any>, tableName: string): Promise<boolean> {
115
+ assertValidIdentifier(tableName, 'table name')
104
116
  try {
105
- await sql.raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)} RESTART IDENTITY CASCADE`).execute(db);
106
- } catch {
107
- // Ignore errors for tables that might not exist or have constraints
117
+ await sql
118
+ .raw(`TRUNCATE TABLE ${this.escapeIdentifier(tableName)} RESTART IDENTITY CASCADE`)
119
+ .execute(db)
120
+ return true
121
+ } catch (error) {
122
+ // Only ignore "table does not exist" errors
123
+ const errorMessage = String(error)
124
+ if (
125
+ errorMessage.includes('does not exist') ||
126
+ (errorMessage.includes('relation') && errorMessage.includes('not exist'))
127
+ ) {
128
+ return false
129
+ }
130
+ // Log and rethrow unexpected errors
131
+ this.logger.error(`Failed to truncate table "${tableName}":`, error)
132
+ throw error
108
133
  }
109
134
  }
110
135
 
111
136
  async truncateAllTables(db: Kysely<any>, exclude: string[] = []): Promise<void> {
112
- const tables = await this.getTables(db);
137
+ const tables = await this.getTables(db)
113
138
  for (const table of tables) {
114
139
  if (!exclude.includes(table)) {
115
- await this.truncateTable(db, table);
140
+ await this.truncateTable(db, table)
116
141
  }
117
142
  }
118
143
  }
119
144
  }
120
145
 
121
- export const postgresAdapter = new PostgresAdapter();
146
+ export const postgresAdapter = new PostgresAdapter()