@kysera/dialects 0.7.3

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 ADDED
@@ -0,0 +1,1015 @@
1
+ # @kysera/dialects
2
+
3
+ > Dialect-specific utilities for Kysely - PostgreSQL, MySQL, and SQLite support with unified adapter interface.
4
+
5
+ [![Version](https://img.shields.io/npm/v/@kysera/dialects.svg)](https://www.npmjs.com/package/@kysera/dialects)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+ [![TypeScript](https://img.shields.io/badge/TypeScript-5.9-blue)](https://www.typescriptlang.org/)
8
+
9
+ ## 🎯 Features
10
+
11
+ - ✅ **Zero Runtime Dependencies** - Only peer dependency on Kysely
12
+ - ✅ **Unified Adapter Interface** - Consistent API across all dialects
13
+ - ✅ **Multi-Database Support** - PostgreSQL, MySQL, and SQLite
14
+ - ✅ **Error Detection** - Detect unique, foreign key, and not-null constraint violations
15
+ - ✅ **Connection Utilities** - Parse and build connection URLs
16
+ - ✅ **Schema Introspection** - Table existence checks, column enumeration, database size
17
+ - ✅ **Testing Helpers** - Truncate tables, clean databases
18
+ - ✅ **100% Type Safe** - Full TypeScript support with strict mode
19
+ - ✅ **Cross-Runtime** - Works on Node.js, Bun, and Deno
20
+
21
+ ## 📦 Related Packages
22
+
23
+ - **[@kysera/core](../core)** - Error handling, pagination, types, logger interface
24
+ - **[@kysera/executor](../executor)** - Unified Execution Layer with plugin interception
25
+ - **[@kysera/dal](../dal)** - Functional Data Access Layer with composable queries
26
+ - **[@kysera/repository](../repository)** - Repository pattern with plugin support
27
+ - **[@kysera/testing](../testing)** - Testing utilities, factories, database cleanup
28
+
29
+ ## 📥 Installation
30
+
31
+ ```bash
32
+ # npm
33
+ npm install @kysera/dialects kysely
34
+
35
+ # pnpm
36
+ pnpm add @kysera/dialects kysely
37
+
38
+ # bun
39
+ bun add @kysera/dialects kysely
40
+
41
+ # deno
42
+ import * as dialects from "npm:@kysera/dialects"
43
+ ```
44
+
45
+ ## 🚀 Quick Start
46
+
47
+ ### Using Adapter Interface
48
+
49
+ ```typescript
50
+ import { Kysely, PostgresDialect } from 'kysely'
51
+ import { Pool } from 'pg'
52
+ import { getAdapter } from '@kysera/dialects'
53
+
54
+ // Create database connection
55
+ const db = new Kysely({
56
+ dialect: new PostgresDialect({ pool: new Pool({ /* config */ }) })
57
+ })
58
+
59
+ // Get dialect adapter
60
+ const adapter = getAdapter('postgres')
61
+
62
+ // Check if table exists
63
+ const exists = await adapter.tableExists(db, 'users')
64
+
65
+ // Get table columns
66
+ const columns = await adapter.getTableColumns(db, 'users')
67
+ // ['id', 'email', 'name', 'created_at', 'updated_at']
68
+
69
+ // Get all tables
70
+ const tables = await adapter.getTables(db)
71
+
72
+ // Get database size
73
+ const sizeBytes = await adapter.getDatabaseSize(db, 'mydb')
74
+ ```
75
+
76
+ ### Using Helper Functions
77
+
78
+ ```typescript
79
+ import {
80
+ tableExists,
81
+ getTableColumns,
82
+ escapeIdentifier,
83
+ isUniqueConstraintError,
84
+ isForeignKeyError,
85
+ isNotNullError
86
+ } from '@kysera/dialects'
87
+
88
+ // Standalone helpers (backward compatible)
89
+ const exists = await tableExists(db, 'users', 'postgres')
90
+ const columns = await getTableColumns(db, 'users', 'postgres')
91
+
92
+ // Escape identifiers
93
+ const escaped = escapeIdentifier('user-data', 'mysql') // `user-data`
94
+ const pgEscaped = escapeIdentifier('user-data', 'postgres') // "user-data"
95
+
96
+ // Error detection
97
+ try {
98
+ await db.insertInto('users').values({ email: 'duplicate@example.com' }).execute()
99
+ } catch (error) {
100
+ if (isUniqueConstraintError(error, 'postgres')) {
101
+ console.error('Email already exists')
102
+ }
103
+ }
104
+ ```
105
+
106
+ ### Connection URL Utilities
107
+
108
+ ```typescript
109
+ import { parseConnectionUrl, buildConnectionUrl, getDefaultPort } from '@kysera/dialects'
110
+
111
+ // Parse connection URL
112
+ const config = parseConnectionUrl('postgresql://user:pass@localhost:5432/mydb?ssl=true')
113
+ // {
114
+ // host: 'localhost',
115
+ // port: 5432,
116
+ // database: 'mydb',
117
+ // user: 'user',
118
+ // password: 'pass',
119
+ // ssl: true
120
+ // }
121
+
122
+ // Build connection URL
123
+ const url = buildConnectionUrl('postgres', {
124
+ host: 'localhost',
125
+ database: 'mydb',
126
+ user: 'admin',
127
+ password: 'secret'
128
+ })
129
+ // 'postgresql://admin:secret@localhost:5432/mydb'
130
+
131
+ // Get default ports
132
+ getDefaultPort('postgres') // 5432
133
+ getDefaultPort('mysql') // 3306
134
+ getDefaultPort('sqlite') // null
135
+ ```
136
+
137
+ ---
138
+
139
+ ## 📚 Table of Contents
140
+
141
+ 1. [Adapter Interface](#-adapter-interface)
142
+ - [PostgreSQL Adapter](#postgresql-adapter)
143
+ - [MySQL Adapter](#mysql-adapter)
144
+ - [SQLite Adapter](#sqlite-adapter)
145
+ 2. [Connection Utilities](#-connection-utilities)
146
+ - [Parse Connection URL](#parse-connection-url)
147
+ - [Build Connection URL](#build-connection-url)
148
+ - [Get Default Port](#get-default-port)
149
+ 3. [Error Detection](#-error-detection)
150
+ - [Unique Constraint Errors](#unique-constraint-errors)
151
+ - [Foreign Key Errors](#foreign-key-errors)
152
+ - [Not-Null Errors](#not-null-errors)
153
+ 4. [Helper Functions](#-helper-functions)
154
+ - [Schema Introspection](#schema-introspection)
155
+ - [Identifier Escaping](#identifier-escaping)
156
+ - [Timestamp Utilities](#timestamp-utilities)
157
+ - [Database Management](#database-management)
158
+ 5. [API Reference](#-api-reference)
159
+ 6. [Best Practices](#-best-practices)
160
+
161
+ ---
162
+
163
+ ## 🔌 Adapter Interface
164
+
165
+ The adapter pattern provides a unified interface for dialect-specific operations. Each dialect has its own adapter implementing the `DialectAdapter` interface.
166
+
167
+ ### Getting Adapters
168
+
169
+ ```typescript
170
+ import { getAdapter, createDialectAdapter } from '@kysera/dialects'
171
+
172
+ // Get singleton adapter (recommended)
173
+ const adapter = getAdapter('postgres')
174
+
175
+ // Create new adapter instance
176
+ const newAdapter = createDialectAdapter('mysql')
177
+ ```
178
+
179
+ ### PostgreSQL Adapter
180
+
181
+ ```typescript
182
+ import { PostgresAdapter, postgresAdapter } from '@kysera/dialects'
183
+
184
+ // Use singleton
185
+ const adapter = postgresAdapter
186
+
187
+ // Or create instance
188
+ const adapter = new PostgresAdapter()
189
+
190
+ // Adapter methods
191
+ adapter.getDefaultPort() // 5432
192
+ adapter.getCurrentTimestamp() // 'CURRENT_TIMESTAMP'
193
+ adapter.escapeIdentifier('col') // '"col"'
194
+ adapter.formatDate(new Date()) // ISO 8601 string
195
+
196
+ // PostgreSQL error detection
197
+ adapter.isUniqueConstraintError(error) // Code: 23505
198
+ adapter.isForeignKeyError(error) // Code: 23503
199
+ adapter.isNotNullError(error) // Code: 23502
200
+ ```
201
+
202
+ **PostgreSQL-specific features:**
203
+ - Uses `information_schema.tables` for schema introspection
204
+ - Filters by `table_schema = 'public'` by default
205
+ - Supports `pg_database_size()` for database size queries
206
+ - Error detection via PostgreSQL error codes (23xxx series)
207
+
208
+ ### MySQL Adapter
209
+
210
+ ```typescript
211
+ import { MySQLAdapter, mysqlAdapter } from '@kysera/dialects'
212
+
213
+ const adapter = mysqlAdapter
214
+
215
+ adapter.getDefaultPort() // 3306
216
+ adapter.getCurrentTimestamp() // 'CURRENT_TIMESTAMP'
217
+ adapter.escapeIdentifier('col') // '`col`'
218
+ adapter.formatDate(new Date()) // ISO 8601 string
219
+
220
+ // MySQL error detection
221
+ adapter.isUniqueConstraintError(error) // ER_DUP_ENTRY, ER_DUP_KEY
222
+ adapter.isForeignKeyError(error) // ER_NO_REFERENCED_ROW, ER_ROW_IS_REFERENCED
223
+ adapter.isNotNullError(error) // ER_BAD_NULL_ERROR, ER_NO_DEFAULT_FOR_FIELD
224
+ ```
225
+
226
+ **MySQL-specific features:**
227
+ - Uses `information_schema.tables` with `table_schema = database()`
228
+ - Supports backtick identifier escaping
229
+ - Error detection via MySQL error codes (ER_* constants)
230
+ - Database size queries via `information_schema.tables`
231
+
232
+ ### SQLite Adapter
233
+
234
+ ```typescript
235
+ import { SQLiteAdapter, sqliteAdapter } from '@kysera/dialects'
236
+
237
+ const adapter = sqliteAdapter
238
+
239
+ adapter.getDefaultPort() // null (file-based)
240
+ adapter.getCurrentTimestamp() // "datetime('now')"
241
+ adapter.escapeIdentifier('col') // '"col"'
242
+ adapter.formatDate(new Date()) // ISO 8601 string
243
+
244
+ // SQLite error detection (message-based)
245
+ adapter.isUniqueConstraintError(error) // "UNIQUE constraint failed"
246
+ adapter.isForeignKeyError(error) // "FOREIGN KEY constraint failed"
247
+ adapter.isNotNullError(error) // "NOT NULL constraint failed"
248
+ ```
249
+
250
+ **SQLite-specific features:**
251
+ - Uses `sqlite_master` for schema introspection
252
+ - No default port (file-based database)
253
+ - Error detection via message parsing
254
+ - Lightweight database size calculation via `page_count * page_size`
255
+
256
+ ### Custom Adapter Registration
257
+
258
+ ```typescript
259
+ import { registerAdapter, type DialectAdapter } from '@kysera/dialects'
260
+
261
+ class CustomDialectAdapter implements DialectAdapter {
262
+ readonly dialect = 'custom' as any
263
+
264
+ getDefaultPort() { return 9999 }
265
+ getCurrentTimestamp() { return 'NOW()' }
266
+ // ... implement all required methods
267
+ }
268
+
269
+ // Register custom adapter
270
+ registerAdapter(new CustomDialectAdapter())
271
+
272
+ // Now available via getAdapter
273
+ const adapter = getAdapter('custom' as any)
274
+ ```
275
+
276
+ ---
277
+
278
+ ## 🔗 Connection Utilities
279
+
280
+ ### Parse Connection URL
281
+
282
+ Parse a database connection URL into a structured configuration object.
283
+
284
+ ```typescript
285
+ import { parseConnectionUrl } from '@kysera/dialects'
286
+
287
+ // PostgreSQL
288
+ const config = parseConnectionUrl('postgresql://user:pass@localhost:5432/mydb?ssl=true')
289
+ // {
290
+ // host: 'localhost',
291
+ // port: 5432,
292
+ // database: 'mydb',
293
+ // user: 'user',
294
+ // password: 'pass',
295
+ // ssl: true
296
+ // }
297
+
298
+ // MySQL
299
+ const mysqlConfig = parseConnectionUrl('mysql://admin:secret@db.example.com:3306/production')
300
+ // {
301
+ // host: 'db.example.com',
302
+ // port: 3306,
303
+ // database: 'production',
304
+ // user: 'admin',
305
+ // password: 'secret',
306
+ // ssl: false
307
+ // }
308
+
309
+ // SQLite (file path)
310
+ const sqliteConfig = parseConnectionUrl('sqlite:///path/to/database.db')
311
+ // {
312
+ // host: '',
313
+ // port: undefined,
314
+ // database: '/path/to/database.db',
315
+ // user: undefined,
316
+ // password: undefined,
317
+ // ssl: false
318
+ // }
319
+ ```
320
+
321
+ **Supported URL formats:**
322
+ - `postgresql://[user[:password]@][host][:port]/database[?ssl=true]`
323
+ - `mysql://[user[:password]@][host][:port]/database[?ssl=true]`
324
+ - `sqlite:///path/to/file.db`
325
+
326
+ ### Build Connection URL
327
+
328
+ Build a connection URL from a configuration object.
329
+
330
+ ```typescript
331
+ import { buildConnectionUrl } from '@kysera/dialects'
332
+
333
+ // Basic URL
334
+ const url = buildConnectionUrl('postgres', {
335
+ host: 'localhost',
336
+ database: 'mydb'
337
+ })
338
+ // 'postgresql://localhost:5432/mydb'
339
+
340
+ // With authentication
341
+ const authUrl = buildConnectionUrl('mysql', {
342
+ host: 'db.example.com',
343
+ database: 'production',
344
+ user: 'admin',
345
+ password: 'secret',
346
+ port: 3306
347
+ })
348
+ // 'mysql://admin:secret@db.example.com:3306/production'
349
+
350
+ // With SSL
351
+ const sslUrl = buildConnectionUrl('postgres', {
352
+ host: 'secure.db.com',
353
+ database: 'app',
354
+ user: 'readonly',
355
+ ssl: true
356
+ })
357
+ // 'postgresql://readonly@secure.db.com:5432/app?ssl=true'
358
+ ```
359
+
360
+ **Default ports:**
361
+ - PostgreSQL: 5432
362
+ - MySQL: 3306
363
+ - SQLite: null (file-based)
364
+
365
+ ### Get Default Port
366
+
367
+ ```typescript
368
+ import { getDefaultPort } from '@kysera/dialects'
369
+
370
+ getDefaultPort('postgres') // 5432
371
+ getDefaultPort('mysql') // 3306
372
+ getDefaultPort('sqlite') // null
373
+ ```
374
+
375
+ ---
376
+
377
+ ## 🚨 Error Detection
378
+
379
+ Detect database constraint violations across different dialects with a unified API.
380
+
381
+ ### Unique Constraint Errors
382
+
383
+ ```typescript
384
+ import { isUniqueConstraintError } from '@kysera/dialects'
385
+
386
+ try {
387
+ await db
388
+ .insertInto('users')
389
+ .values({ email: 'existing@example.com', name: 'John' })
390
+ .execute()
391
+ } catch (error) {
392
+ if (isUniqueConstraintError(error, 'postgres')) {
393
+ console.error('Email already exists')
394
+ // Handle duplicate error
395
+ }
396
+ }
397
+ ```
398
+
399
+ **Detection criteria:**
400
+
401
+ | Dialect | Detection Method |
402
+ |---------|------------------|
403
+ | PostgreSQL | Error code `23505` or message contains "unique constraint" |
404
+ | MySQL | Error code `ER_DUP_ENTRY` or `ER_DUP_KEY` |
405
+ | SQLite | Message contains "UNIQUE constraint failed" |
406
+
407
+ ### Foreign Key Errors
408
+
409
+ ```typescript
410
+ import { isForeignKeyError } from '@kysera/dialects'
411
+
412
+ try {
413
+ await db
414
+ .insertInto('posts')
415
+ .values({ user_id: 999, title: 'Post', content: '...' })
416
+ .execute()
417
+ } catch (error) {
418
+ if (isForeignKeyError(error, 'postgres')) {
419
+ console.error('User does not exist')
420
+ // Handle foreign key violation
421
+ }
422
+ }
423
+ ```
424
+
425
+ **Detection criteria:**
426
+
427
+ | Dialect | Detection Method |
428
+ |---------|------------------|
429
+ | PostgreSQL | Error code `23503` or message contains "foreign key constraint" |
430
+ | MySQL | Error code `ER_NO_REFERENCED_ROW` or `ER_ROW_IS_REFERENCED` |
431
+ | SQLite | Message contains "FOREIGN KEY constraint failed" |
432
+
433
+ ### Not-Null Errors
434
+
435
+ ```typescript
436
+ import { isNotNullError } from '@kysera/dialects'
437
+
438
+ try {
439
+ await db
440
+ .insertInto('users')
441
+ .values({ name: 'John' }) // Missing required email
442
+ .execute()
443
+ } catch (error) {
444
+ if (isNotNullError(error, 'postgres')) {
445
+ console.error('Missing required field')
446
+ // Handle not-null violation
447
+ }
448
+ }
449
+ ```
450
+
451
+ **Detection criteria:**
452
+
453
+ | Dialect | Detection Method |
454
+ |---------|------------------|
455
+ | PostgreSQL | Error code `23502` or message contains "not-null constraint" |
456
+ | MySQL | Error code `ER_BAD_NULL_ERROR` or `ER_NO_DEFAULT_FOR_FIELD` |
457
+ | SQLite | Message contains "NOT NULL constraint failed" |
458
+
459
+ ### Adapter-based Error Detection
460
+
461
+ ```typescript
462
+ import { getAdapter } from '@kysera/dialects'
463
+
464
+ const adapter = getAdapter('postgres')
465
+
466
+ try {
467
+ await db.insertInto('users').values(data).execute()
468
+ } catch (error) {
469
+ if (adapter.isUniqueConstraintError(error)) {
470
+ // Handle unique violation
471
+ } else if (adapter.isForeignKeyError(error)) {
472
+ // Handle foreign key violation
473
+ } else if (adapter.isNotNullError(error)) {
474
+ // Handle not-null violation
475
+ }
476
+ }
477
+ ```
478
+
479
+ ---
480
+
481
+ ## 🛠 Helper Functions
482
+
483
+ ### Schema Introspection
484
+
485
+ #### Check Table Existence
486
+
487
+ ```typescript
488
+ import { tableExists } from '@kysera/dialects'
489
+
490
+ const exists = await tableExists(db, 'users', 'postgres')
491
+
492
+ if (!exists) {
493
+ console.log('Users table does not exist')
494
+ }
495
+ ```
496
+
497
+ #### Get Table Columns
498
+
499
+ ```typescript
500
+ import { getTableColumns } from '@kysera/dialects'
501
+
502
+ const columns = await getTableColumns(db, 'users', 'postgres')
503
+ // ['id', 'email', 'name', 'created_at', 'updated_at']
504
+
505
+ // Check if column exists
506
+ if (columns.includes('deleted_at')) {
507
+ console.log('Table has soft-delete support')
508
+ }
509
+ ```
510
+
511
+ #### Get All Tables
512
+
513
+ ```typescript
514
+ import { getTables } from '@kysera/dialects'
515
+
516
+ const tables = await getTables(db, 'postgres')
517
+ // ['users', 'posts', 'comments', 'tags']
518
+
519
+ console.log(`Database has ${tables.length} tables`)
520
+ ```
521
+
522
+ ### Identifier Escaping
523
+
524
+ ```typescript
525
+ import { escapeIdentifier } from '@kysera/dialects'
526
+
527
+ // PostgreSQL (double quotes)
528
+ escapeIdentifier('user-data', 'postgres') // "user-data"
529
+ escapeIdentifier('select', 'postgres') // "select"
530
+
531
+ // MySQL (backticks)
532
+ escapeIdentifier('user-data', 'mysql') // `user-data`
533
+ escapeIdentifier('order', 'mysql') // `order`
534
+
535
+ // SQLite (double quotes)
536
+ escapeIdentifier('user-data', 'sqlite') // "user-data"
537
+
538
+ // Handles quotes in identifiers
539
+ escapeIdentifier('user"data', 'postgres') // "user""data"
540
+ escapeIdentifier('user`data', 'mysql') // `user``data`
541
+ ```
542
+
543
+ ### Timestamp Utilities
544
+
545
+ #### Get Current Timestamp
546
+
547
+ ```typescript
548
+ import { getCurrentTimestamp } from '@kysera/dialects'
549
+
550
+ // PostgreSQL
551
+ getCurrentTimestamp('postgres') // 'CURRENT_TIMESTAMP'
552
+
553
+ // MySQL
554
+ getCurrentTimestamp('mysql') // 'CURRENT_TIMESTAMP'
555
+
556
+ // SQLite
557
+ getCurrentTimestamp('sqlite') // "datetime('now')"
558
+
559
+ // Usage in queries
560
+ const timestamp = getCurrentTimestamp('postgres')
561
+ await db
562
+ .insertInto('logs')
563
+ .values({ message: 'Event', created_at: sql`${sql.raw(timestamp)}` })
564
+ .execute()
565
+ ```
566
+
567
+ #### Format Date
568
+
569
+ ```typescript
570
+ import { formatDate } from '@kysera/dialects'
571
+
572
+ const date = new Date('2024-01-15T10:30:00Z')
573
+
574
+ // All dialects return ISO 8601 format
575
+ formatDate(date, 'postgres') // '2024-01-15T10:30:00.000Z'
576
+ formatDate(date, 'mysql') // '2024-01-15T10:30:00.000Z'
577
+ formatDate(date, 'sqlite') // '2024-01-15T10:30:00.000Z'
578
+ ```
579
+
580
+ ### Database Management
581
+
582
+ #### Get Database Size
583
+
584
+ ```typescript
585
+ import { getDatabaseSize } from '@kysera/dialects'
586
+
587
+ // PostgreSQL
588
+ const size = await getDatabaseSize(db, 'mydb', 'postgres')
589
+ console.log(`Database size: ${(size / 1024 / 1024).toFixed(2)} MB`)
590
+
591
+ // MySQL
592
+ const mysqlSize = await getDatabaseSize(db, 'production', 'mysql')
593
+
594
+ // SQLite
595
+ const sqliteSize = await getDatabaseSize(db, undefined, 'sqlite')
596
+ ```
597
+
598
+ **Returns:** Size in bytes
599
+
600
+ #### Truncate All Tables
601
+
602
+ ```typescript
603
+ import { truncateAllTables } from '@kysera/dialects'
604
+
605
+ // Truncate all tables (for testing)
606
+ await truncateAllTables(db, 'postgres')
607
+
608
+ // Exclude specific tables
609
+ await truncateAllTables(db, 'postgres', ['migrations', 'schema_version'])
610
+ ```
611
+
612
+ **Warning:** This permanently deletes all data. Use only in test environments.
613
+
614
+ **Behavior:**
615
+ - PostgreSQL: `TRUNCATE TABLE ... CASCADE`
616
+ - MySQL: `TRUNCATE TABLE ...`
617
+ - SQLite: `DELETE FROM ...` (no TRUNCATE support)
618
+
619
+ ---
620
+
621
+ ## 📖 API Reference
622
+
623
+ ### Factory Functions
624
+
625
+ #### `getAdapter(dialect: DatabaseDialect): DialectAdapter`
626
+
627
+ Get singleton adapter for specified dialect.
628
+
629
+ **Parameters:**
630
+ - `dialect` - `'postgres' | 'mysql' | 'sqlite'`
631
+
632
+ **Returns:** Dialect adapter instance
633
+
634
+ **Throws:** Error if dialect is unknown
635
+
636
+ ---
637
+
638
+ #### `createDialectAdapter(dialect: DatabaseDialect): DialectAdapter`
639
+
640
+ Create new adapter instance.
641
+
642
+ **Parameters:**
643
+ - `dialect` - `'postgres' | 'mysql' | 'sqlite'`
644
+
645
+ **Returns:** New dialect adapter instance
646
+
647
+ **Use Case:** When you need multiple adapter instances with different configurations.
648
+
649
+ ---
650
+
651
+ #### `registerAdapter(adapter: DialectAdapter): void`
652
+
653
+ Register custom dialect adapter.
654
+
655
+ **Parameters:**
656
+ - `adapter` - Custom adapter implementing `DialectAdapter` interface
657
+
658
+ **Use Case:** Extend with custom database support.
659
+
660
+ ---
661
+
662
+ ### Adapter Interface
663
+
664
+ #### `DialectAdapter`
665
+
666
+ Interface for dialect-specific operations.
667
+
668
+ **Properties:**
669
+ - `dialect: DatabaseDialect` - The dialect this adapter handles
670
+
671
+ **Methods:**
672
+ - `getDefaultPort(): number | null` - Get default port for this dialect
673
+ - `getCurrentTimestamp(): string` - Get SQL expression for current timestamp
674
+ - `escapeIdentifier(identifier: string): string` - Escape identifier for this dialect
675
+ - `formatDate(date: Date): string` - Format date for this dialect
676
+ - `isUniqueConstraintError(error: unknown): boolean` - Check for unique constraint violation
677
+ - `isForeignKeyError(error: unknown): boolean` - Check for foreign key violation
678
+ - `isNotNullError(error: unknown): boolean` - Check for not-null violation
679
+ - `tableExists(db: Kysely<any>, tableName: string): Promise<boolean>` - Check if table exists
680
+ - `getTableColumns(db: Kysely<any>, tableName: string): Promise<string[]>` - Get table columns
681
+ - `getTables(db: Kysely<any>): Promise<string[]>` - Get all tables
682
+ - `getDatabaseSize(db: Kysely<any>, databaseName?: string): Promise<number>` - Get database size in bytes
683
+ - `truncateTable(db: Kysely<any>, tableName: string): Promise<void>` - Truncate a table
684
+ - `truncateAllTables(db: Kysely<any>, exclude?: string[]): Promise<void>` - Truncate all tables
685
+
686
+ ---
687
+
688
+ ### Connection Utilities
689
+
690
+ #### `parseConnectionUrl(url: string): ConnectionConfig`
691
+
692
+ Parse connection URL into configuration object.
693
+
694
+ **Parameters:**
695
+ - `url` - Database connection URL
696
+
697
+ **Returns:** `ConnectionConfig` object
698
+
699
+ ---
700
+
701
+ #### `buildConnectionUrl(dialect: DatabaseDialect, config: ConnectionConfig): string`
702
+
703
+ Build connection URL from configuration.
704
+
705
+ **Parameters:**
706
+ - `dialect` - Database dialect
707
+ - `config` - Connection configuration
708
+
709
+ **Returns:** Connection URL string
710
+
711
+ ---
712
+
713
+ #### `getDefaultPort(dialect: DatabaseDialect): number | null`
714
+
715
+ Get default port for dialect.
716
+
717
+ **Parameters:**
718
+ - `dialect` - Database dialect
719
+
720
+ **Returns:** Port number or null for SQLite
721
+
722
+ ---
723
+
724
+ ### Helper Functions
725
+
726
+ #### `tableExists(db: Kysely<any>, tableName: string, dialect: DatabaseDialect): Promise<boolean>`
727
+
728
+ Check if table exists.
729
+
730
+ ---
731
+
732
+ #### `getTableColumns(db: Kysely<any>, tableName: string, dialect: DatabaseDialect): Promise<string[]>`
733
+
734
+ Get column names for a table.
735
+
736
+ ---
737
+
738
+ #### `getTables(db: Kysely<any>, dialect: DatabaseDialect): Promise<string[]>`
739
+
740
+ Get all tables in database.
741
+
742
+ ---
743
+
744
+ #### `escapeIdentifier(identifier: string, dialect: DatabaseDialect): string`
745
+
746
+ Escape identifier for SQL.
747
+
748
+ ---
749
+
750
+ #### `getCurrentTimestamp(dialect: DatabaseDialect): string`
751
+
752
+ Get SQL expression for current timestamp.
753
+
754
+ ---
755
+
756
+ #### `formatDate(date: Date, dialect: DatabaseDialect): string`
757
+
758
+ Format date for SQL.
759
+
760
+ ---
761
+
762
+ #### `isUniqueConstraintError(error: unknown, dialect: DatabaseDialect): boolean`
763
+
764
+ Check if error is unique constraint violation.
765
+
766
+ ---
767
+
768
+ #### `isForeignKeyError(error: unknown, dialect: DatabaseDialect): boolean`
769
+
770
+ Check if error is foreign key violation.
771
+
772
+ ---
773
+
774
+ #### `isNotNullError(error: unknown, dialect: DatabaseDialect): boolean`
775
+
776
+ Check if error is not-null violation.
777
+
778
+ ---
779
+
780
+ #### `getDatabaseSize(db: Kysely<any>, databaseName: string | undefined, dialect: DatabaseDialect): Promise<number>`
781
+
782
+ Get database size in bytes.
783
+
784
+ ---
785
+
786
+ #### `truncateAllTables(db: Kysely<any>, dialect: DatabaseDialect, exclude?: string[]): Promise<void>`
787
+
788
+ Truncate all tables in database.
789
+
790
+ ---
791
+
792
+ ### Types
793
+
794
+ #### `type DatabaseDialect = 'postgres' | 'mysql' | 'sqlite'`
795
+
796
+ Supported database dialects.
797
+
798
+ ---
799
+
800
+ #### `interface ConnectionConfig`
801
+
802
+ Database connection configuration.
803
+
804
+ ```typescript
805
+ interface ConnectionConfig {
806
+ host?: string | undefined
807
+ port?: number | undefined
808
+ database: string
809
+ user?: string | undefined
810
+ password?: string | undefined
811
+ ssl?: boolean | undefined
812
+ }
813
+ ```
814
+
815
+ ---
816
+
817
+ #### `interface DatabaseErrorLike`
818
+
819
+ Error object shape for database error detection.
820
+
821
+ ```typescript
822
+ interface DatabaseErrorLike {
823
+ message?: string
824
+ code?: string
825
+ }
826
+ ```
827
+
828
+ ---
829
+
830
+ ## ✨ Best Practices
831
+
832
+ ### 1. Use Adapter Interface for Dialect-Agnostic Code
833
+
834
+ ```typescript
835
+ // ✅ Good: Works with any dialect
836
+ import { getAdapter } from '@kysera/dialects'
837
+
838
+ function checkSchema(db: Kysely<any>, dialect: DatabaseDialect) {
839
+ const adapter = getAdapter(dialect)
840
+ return adapter.tableExists(db, 'users')
841
+ }
842
+
843
+ // ❌ Bad: Hard-coded dialect logic
844
+ function checkSchema(db: Kysely<any>) {
845
+ return db.selectFrom('information_schema.tables')
846
+ .where('table_name', '=', 'users')
847
+ .executeTakeFirst()
848
+ }
849
+ ```
850
+
851
+ ### 2. Use Helper Functions for Simple Operations
852
+
853
+ ```typescript
854
+ // ✅ Good: Simple and readable
855
+ const exists = await tableExists(db, 'users', 'postgres')
856
+
857
+ // ❌ Unnecessary: Adapter for single operation
858
+ const adapter = getAdapter('postgres')
859
+ const exists = await adapter.tableExists(db, 'users')
860
+ ```
861
+
862
+ ### 3. Centralize Error Handling
863
+
864
+ ```typescript
865
+ import { getAdapter } from '@kysera/dialects'
866
+ import { parseDatabaseError } from '@kysera/core'
867
+
868
+ async function handleDatabaseError(error: unknown, dialect: DatabaseDialect) {
869
+ const adapter = getAdapter(dialect)
870
+
871
+ if (adapter.isUniqueConstraintError(error)) {
872
+ return { type: 'duplicate', message: 'Record already exists' }
873
+ }
874
+
875
+ if (adapter.isForeignKeyError(error)) {
876
+ return { type: 'reference', message: 'Referenced record not found' }
877
+ }
878
+
879
+ if (adapter.isNotNullError(error)) {
880
+ return { type: 'required', message: 'Required field missing' }
881
+ }
882
+
883
+ // Use @kysera/core for detailed error parsing
884
+ const dbError = parseDatabaseError(error, dialect)
885
+ return { type: 'database', message: dbError.message }
886
+ }
887
+ ```
888
+
889
+ ### 4. Parse Connection URLs in Configuration
890
+
891
+ ```typescript
892
+ import { parseConnectionUrl } from '@kysera/dialects'
893
+
894
+ // ✅ Good: Parse from environment variable
895
+ const config = parseConnectionUrl(process.env.DATABASE_URL!)
896
+
897
+ const db = new Kysely({
898
+ dialect: new PostgresDialect({
899
+ pool: new Pool({
900
+ host: config.host,
901
+ port: config.port,
902
+ database: config.database,
903
+ user: config.user,
904
+ password: config.password,
905
+ ssl: config.ssl
906
+ })
907
+ })
908
+ })
909
+ ```
910
+
911
+ ### 5. Use Type Guards for Error Detection
912
+
913
+ ```typescript
914
+ import { getAdapter } from '@kysera/dialects'
915
+
916
+ const adapter = getAdapter(dialect)
917
+
918
+ try {
919
+ await db.insertInto('users').values(data).execute()
920
+ } catch (error) {
921
+ // ✅ Good: Type-safe error detection
922
+ if (adapter.isUniqueConstraintError(error)) {
923
+ throw new Error('Email already registered')
924
+ }
925
+ throw error
926
+ }
927
+ ```
928
+
929
+ ### 6. Escape Dynamic Identifiers
930
+
931
+ ```typescript
932
+ import { escapeIdentifier } from '@kysera/dialects'
933
+ import { sql } from 'kysely'
934
+
935
+ // ✅ Good: Escape dynamic table/column names
936
+ function selectFromTable(tableName: string, dialect: DatabaseDialect) {
937
+ const escaped = escapeIdentifier(tableName, dialect)
938
+ return db.selectFrom(sql.raw(escaped)).selectAll()
939
+ }
940
+
941
+ // ❌ Bad: SQL injection risk
942
+ function selectFromTable(tableName: string) {
943
+ return db.selectFrom(sql.raw(tableName)).selectAll()
944
+ }
945
+ ```
946
+
947
+ ### 7. Use Truncate for Test Cleanup
948
+
949
+ ```typescript
950
+ import { truncateAllTables } from '@kysera/dialects'
951
+
952
+ describe('User tests', () => {
953
+ afterEach(async () => {
954
+ // ✅ Fast cleanup for tests
955
+ await truncateAllTables(db, 'postgres', ['migrations'])
956
+ })
957
+
958
+ // ❌ Slow: Individual deletes
959
+ afterEach(async () => {
960
+ await db.deleteFrom('users').execute()
961
+ await db.deleteFrom('posts').execute()
962
+ // ...
963
+ })
964
+ })
965
+ ```
966
+
967
+ ### 8. Cache Adapters in Long-Running Applications
968
+
969
+ ```typescript
970
+ // ✅ Good: Cache adapter instance
971
+ const adapter = getAdapter(dialect)
972
+
973
+ for (const table of tables) {
974
+ await adapter.tableExists(db, table)
975
+ }
976
+
977
+ // ❌ Unnecessary: Re-fetch adapter each time
978
+ for (const table of tables) {
979
+ const adapter = getAdapter(dialect)
980
+ await adapter.tableExists(db, table)
981
+ }
982
+ ```
983
+
984
+ ---
985
+
986
+ ## 🤝 Contributing
987
+
988
+ Contributions are welcome! This package follows strict development principles:
989
+
990
+ - ✅ **Zero runtime dependencies** (peer deps only)
991
+ - ✅ **100% type safe** (TypeScript strict mode)
992
+ - ✅ **Comprehensive test coverage** (95%+ coverage)
993
+ - ✅ **Cross-database compatible** (PostgreSQL, MySQL, SQLite)
994
+ - ✅ **ESM only** (no CommonJS)
995
+
996
+ See [CLAUDE.md](../../CLAUDE.md) for development guidelines.
997
+
998
+ ---
999
+
1000
+ ## 📄 License
1001
+
1002
+ MIT © Kysera
1003
+
1004
+ ---
1005
+
1006
+ ## 🔗 Links
1007
+
1008
+ - [GitHub Repository](https://github.com/kysera-dev/kysera)
1009
+ - [Kysely Documentation](https://kysely.dev)
1010
+ - [Issue Tracker](https://github.com/kysera-dev/kysera/issues)
1011
+ - [Changelog](../../CHANGELOG.md)
1012
+
1013
+ ---
1014
+
1015
+ **Built with ❤️ for production TypeScript applications**