@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.
- package/README.md +93 -71
- package/dist/index.d.ts +90 -23
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/package.json +4 -1
- package/src/adapters/mssql.ts +207 -0
- package/src/adapters/mysql.ts +73 -51
- package/src/adapters/postgres.ts +67 -42
- package/src/adapters/sqlite.ts +53 -34
- package/src/connection.ts +26 -16
- package/src/factory.ts +20 -16
- package/src/helpers.ts +78 -25
- package/src/index.ts +21 -10
- package/src/types.ts +36 -25
|
@@ -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()
|
package/src/adapters/mysql.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
110
|
+
.then(r => r.rows?.[0]?.name))
|
|
111
|
+
|
|
112
|
+
if (!dbName) {
|
|
113
|
+
return 0
|
|
114
|
+
}
|
|
103
115
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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<
|
|
129
|
+
async truncateTable(db: Kysely<any>, tableName: string): Promise<boolean> {
|
|
130
|
+
assertValidIdentifier(tableName, 'table name')
|
|
118
131
|
try {
|
|
119
|
-
|
|
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(
|
|
127
|
-
|
|
128
|
-
|
|
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()
|
package/src/adapters/postgres.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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(
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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<
|
|
114
|
+
async truncateTable(db: Kysely<any>, tableName: string): Promise<boolean> {
|
|
115
|
+
assertValidIdentifier(tableName, 'table name')
|
|
104
116
|
try {
|
|
105
|
-
await sql
|
|
106
|
-
|
|
107
|
-
|
|
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()
|