@mantiq/database 0.0.1

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.
Files changed (42) hide show
  1. package/README.md +19 -0
  2. package/package.json +77 -0
  3. package/src/DatabaseManager.ts +115 -0
  4. package/src/DatabaseServiceProvider.ts +39 -0
  5. package/src/contracts/Connection.ts +13 -0
  6. package/src/contracts/Grammar.ts +16 -0
  7. package/src/contracts/MongoConnection.ts +122 -0
  8. package/src/contracts/Paginator.ts +10 -0
  9. package/src/drivers/BaseGrammar.ts +220 -0
  10. package/src/drivers/MSSQLConnection.ts +154 -0
  11. package/src/drivers/MSSQLGrammar.ts +106 -0
  12. package/src/drivers/MongoConnection.ts +298 -0
  13. package/src/drivers/MongoQueryBuilderImpl.ts +77 -0
  14. package/src/drivers/MySQLConnection.ts +120 -0
  15. package/src/drivers/MySQLGrammar.ts +19 -0
  16. package/src/drivers/PostgresConnection.ts +125 -0
  17. package/src/drivers/PostgresGrammar.ts +24 -0
  18. package/src/drivers/SQLiteConnection.ts +125 -0
  19. package/src/drivers/SQLiteGrammar.ts +19 -0
  20. package/src/errors/ConnectionError.ts +10 -0
  21. package/src/errors/ModelNotFoundError.ts +14 -0
  22. package/src/errors/QueryError.ts +11 -0
  23. package/src/events/DatabaseEvents.ts +101 -0
  24. package/src/factories/Factory.ts +170 -0
  25. package/src/factories/Faker.ts +382 -0
  26. package/src/helpers/db.ts +37 -0
  27. package/src/index.ts +100 -0
  28. package/src/migrations/Migration.ts +12 -0
  29. package/src/migrations/MigrationRepository.ts +50 -0
  30. package/src/migrations/Migrator.ts +201 -0
  31. package/src/orm/Collection.ts +236 -0
  32. package/src/orm/Document.ts +202 -0
  33. package/src/orm/Model.ts +775 -0
  34. package/src/orm/ModelQueryBuilder.ts +415 -0
  35. package/src/orm/Scope.ts +39 -0
  36. package/src/orm/eagerLoad.ts +300 -0
  37. package/src/query/Builder.ts +456 -0
  38. package/src/query/Expression.ts +18 -0
  39. package/src/schema/Blueprint.ts +196 -0
  40. package/src/schema/ColumnDefinition.ts +93 -0
  41. package/src/schema/SchemaBuilder.ts +376 -0
  42. package/src/seeders/Seeder.ts +28 -0
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+ # @mantiq/database
2
+
3
+ Query builder, Eloquent-style ORM, schema migrations, seeders, and factories for MantiqJS. Supports SQLite, Postgres, MySQL, and MongoDB.
4
+
5
+ Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @mantiq/database
11
+ ```
12
+
13
+ ## Documentation
14
+
15
+ See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
16
+
17
+ ## License
18
+
19
+ MIT
package/package.json ADDED
@@ -0,0 +1,77 @@
1
+ {
2
+ "name": "@mantiq/database",
3
+ "version": "0.0.1",
4
+ "description": "Query builder, ORM, migrations, seeders, factories — with SQLite, Postgres, MySQL and MongoDB support",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Abdullah Khan",
8
+ "homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/database",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "https://github.com/abdullahkhan/mantiq.git",
12
+ "directory": "packages/database"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/abdullahkhan/mantiq/issues"
16
+ },
17
+ "keywords": [
18
+ "mantiq",
19
+ "mantiqjs",
20
+ "bun",
21
+ "typescript",
22
+ "framework",
23
+ "database"
24
+ ],
25
+ "engines": {
26
+ "bun": ">=1.1.0"
27
+ },
28
+ "main": "./src/index.ts",
29
+ "types": "./src/index.ts",
30
+ "exports": {
31
+ ".": {
32
+ "bun": "./src/index.ts",
33
+ "default": "./src/index.ts"
34
+ }
35
+ },
36
+ "files": [
37
+ "src/",
38
+ "package.json",
39
+ "README.md",
40
+ "LICENSE"
41
+ ],
42
+ "scripts": {
43
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun",
44
+ "test": "bun test",
45
+ "typecheck": "tsc --noEmit",
46
+ "clean": "rm -rf dist"
47
+ },
48
+ "dependencies": {},
49
+ "devDependencies": {
50
+ "@types/pg": "^8.18.0",
51
+ "bun-types": "latest",
52
+ "mssql": "^11.0.0",
53
+ "mysql2": "^3.14.1",
54
+ "pg": "^8.13.3",
55
+ "typescript": "^5.7.0"
56
+ },
57
+ "peerDependencies": {
58
+ "pg": "",
59
+ "mysql2": "",
60
+ "mssql": ">=10",
61
+ "mongodb": ">=6"
62
+ },
63
+ "peerDependenciesMeta": {
64
+ "pg": {
65
+ "optional": true
66
+ },
67
+ "mysql2": {
68
+ "optional": true
69
+ },
70
+ "mssql": {
71
+ "optional": true
72
+ },
73
+ "mongodb": {
74
+ "optional": true
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,115 @@
1
+ import type { DatabaseConnection } from './contracts/Connection.ts'
2
+ import type { MongoDatabaseConnection } from './contracts/MongoConnection.ts'
3
+ import { SQLiteConnection } from './drivers/SQLiteConnection.ts'
4
+ import { PostgresConnection } from './drivers/PostgresConnection.ts'
5
+ import { MySQLConnection } from './drivers/MySQLConnection.ts'
6
+ import { MongoConnection } from './drivers/MongoConnection.ts'
7
+ import { ConnectionError } from './errors/ConnectionError.ts'
8
+
9
+ export interface SQLConfig {
10
+ driver: 'sqlite' | 'postgres' | 'mysql'
11
+ database: string
12
+ host?: string
13
+ port?: number
14
+ user?: string
15
+ password?: string
16
+ ssl?: boolean
17
+ pool?: { min?: number; max?: number }
18
+ }
19
+
20
+ export interface MongoConfig {
21
+ driver: 'mongodb'
22
+ uri: string
23
+ database: string
24
+ options?: Record<string, any>
25
+ }
26
+
27
+ export type ConnectionConfig = SQLConfig | MongoConfig
28
+
29
+ export interface DatabaseConfig {
30
+ default?: string
31
+ connections: Record<string, ConnectionConfig>
32
+ }
33
+
34
+ export class DatabaseManager {
35
+ private sqlConnections = new Map<string, DatabaseConnection>()
36
+ private mongoConnections = new Map<string, MongoDatabaseConnection>()
37
+
38
+ constructor(private readonly config: DatabaseConfig) {}
39
+
40
+ /** Get a SQL DatabaseConnection by name */
41
+ connection(name?: string): DatabaseConnection {
42
+ const connName = name ?? this.config.default ?? 'default'
43
+ if (this.sqlConnections.has(connName)) return this.sqlConnections.get(connName)!
44
+
45
+ const cfg = this.config.connections[connName]
46
+ if (!cfg) throw new ConnectionError(`Connection "${connName}" not configured`, connName)
47
+
48
+ const conn = this.makeConnection(cfg)
49
+ this.sqlConnections.set(connName, conn)
50
+ return conn
51
+ }
52
+
53
+ /** Get a MongoDB connection by name */
54
+ mongo(name?: string): MongoDatabaseConnection {
55
+ const connName = name ?? this.config.default ?? 'default'
56
+ if (this.mongoConnections.has(connName)) return this.mongoConnections.get(connName)!
57
+
58
+ const cfg = this.config.connections[connName]
59
+ if (!cfg) throw new ConnectionError(`Connection "${connName}" not configured`, connName)
60
+
61
+ if (cfg.driver !== 'mongodb') {
62
+ throw new ConnectionError(`Connection "${connName}" is not a MongoDB connection`, connName)
63
+ }
64
+
65
+ const conn = new MongoConnection({ uri: cfg.uri, database: cfg.database, options: cfg.options })
66
+ this.mongoConnections.set(connName, conn)
67
+ return conn
68
+ }
69
+
70
+ /** Shorthand for the default SQL connection's table() method */
71
+ table(name: string) {
72
+ return this.connection().table(name)
73
+ }
74
+
75
+ /** Shorthand for the default SQL connection's schema() method */
76
+ schema() {
77
+ return this.connection().schema()
78
+ }
79
+
80
+ /** Shorthand for MongoDB collection */
81
+ collection(name: string) {
82
+ return this.mongo().collection(name)
83
+ }
84
+
85
+ private makeConnection(cfg: ConnectionConfig): DatabaseConnection {
86
+ switch (cfg.driver) {
87
+ case 'sqlite':
88
+ return new SQLiteConnection({ database: cfg.database })
89
+ case 'postgres':
90
+ return new PostgresConnection({
91
+ database: cfg.database,
92
+ host: cfg.host,
93
+ port: cfg.port,
94
+ user: cfg.user,
95
+ password: cfg.password,
96
+ ssl: cfg.ssl,
97
+ pool: cfg.pool,
98
+ })
99
+ case 'mysql':
100
+ return new MySQLConnection({
101
+ database: cfg.database,
102
+ host: cfg.host,
103
+ port: cfg.port,
104
+ user: cfg.user,
105
+ password: cfg.password,
106
+ pool: cfg.pool,
107
+ })
108
+ case 'mongodb':
109
+ // MongoDB is handled separately via mongo()
110
+ throw new ConnectionError(`Use .mongo() to access MongoDB connections`, cfg.driver)
111
+ default:
112
+ throw new ConnectionError(`Unknown driver "${(cfg as any).driver}"`, (cfg as any).driver)
113
+ }
114
+ }
115
+ }
@@ -0,0 +1,39 @@
1
+ import { DatabaseManager } from './DatabaseManager.ts'
2
+ import { Model } from './orm/Model.ts'
3
+
4
+ export const DATABASE_MANAGER = Symbol('DatabaseManager')
5
+
6
+ /**
7
+ * Minimal service provider integration — provides a factory function
8
+ * so @mantiq/database can be used without @mantiq/core if needed.
9
+ *
10
+ * When using with @mantiq/core, extend ServiceProvider and register
11
+ * DatabaseManager as a singleton with your application's config.
12
+ *
13
+ * @example — with @mantiq/core:
14
+ * ```ts
15
+ * import { ServiceProvider } from '@mantiq/core'
16
+ * import { DatabaseManager, Model } from '@mantiq/database'
17
+ *
18
+ * export class DatabaseServiceProvider extends ServiceProvider {
19
+ * register(): void {
20
+ * this.app.singleton(DatabaseManager, () => {
21
+ * const config = this.app.make('config').get('database')
22
+ * const manager = new DatabaseManager(config)
23
+ * Model.setConnection(manager.connection())
24
+ * return manager
25
+ * })
26
+ * }
27
+ * }
28
+ * ```
29
+ */
30
+ export function createDatabaseManager(config: {
31
+ default?: string
32
+ connections: Record<string, any>
33
+ }): DatabaseManager {
34
+ return new DatabaseManager(config)
35
+ }
36
+
37
+ export function setupModels(manager: DatabaseManager, connectionName?: string): void {
38
+ Model.setConnection(manager.connection(connectionName))
39
+ }
@@ -0,0 +1,13 @@
1
+ import type { QueryBuilder } from '../query/Builder.ts'
2
+ import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+
4
+ export interface DatabaseConnection {
5
+ select(sql: string, bindings?: any[]): Promise<Record<string, any>[]>
6
+ statement(sql: string, bindings?: any[]): Promise<number>
7
+ insertGetId(sql: string, bindings?: any[]): Promise<number | bigint>
8
+ transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T>
9
+ table(name: string): QueryBuilder
10
+ schema(): SchemaBuilder
11
+ getDriverName(): string
12
+ getTablePrefix(): string
13
+ }
@@ -0,0 +1,16 @@
1
+ import type { QueryState } from '../query/Builder.ts'
2
+
3
+ export interface Grammar {
4
+ /** Quote a column or table identifier */
5
+ quoteIdentifier(name: string): string
6
+
7
+ compileSelect(state: QueryState): { sql: string; bindings: any[] }
8
+ compileInsert(table: string, data: Record<string, any>): { sql: string; bindings: any[] }
9
+ compileInsertGetId(table: string, data: Record<string, any>): { sql: string; bindings: any[] }
10
+ compileUpdate(table: string, state: QueryState, data: Record<string, any>): { sql: string; bindings: any[] }
11
+ compileDelete(table: string, state: QueryState): { sql: string; bindings: any[] }
12
+ compileTruncate(table: string): string
13
+
14
+ /** Placeholder token for parameterised queries: '?' for SQLite/MySQL, '$1' for Postgres */
15
+ placeholder(index: number): string
16
+ }
@@ -0,0 +1,122 @@
1
+ export interface MongoFilter {
2
+ [key: string]: any
3
+ }
4
+
5
+ export interface MongoProjection {
6
+ [key: string]: 0 | 1
7
+ }
8
+
9
+ export interface MongoUpdateDoc {
10
+ $set?: Record<string, any>
11
+ $unset?: Record<string, any>
12
+ $inc?: Record<string, any>
13
+ $push?: Record<string, any>
14
+ $pull?: Record<string, any>
15
+ $addToSet?: Record<string, any>
16
+ [key: string]: any
17
+ }
18
+
19
+ export interface MongoSortDoc {
20
+ [key: string]: 1 | -1
21
+ }
22
+
23
+ export interface MongoPipelineStage {
24
+ $match?: MongoFilter
25
+ $project?: MongoProjection
26
+ $sort?: MongoSortDoc
27
+ $limit?: number
28
+ $skip?: number
29
+ $group?: Record<string, any>
30
+ $lookup?: Record<string, any>
31
+ $unwind?: string | Record<string, any>
32
+ $addFields?: Record<string, any>
33
+ [key: string]: any
34
+ }
35
+
36
+ export interface MongoInsertResult {
37
+ insertedId: any
38
+ acknowledged: boolean
39
+ }
40
+
41
+ export interface MongoInsertManyResult {
42
+ insertedIds: any[]
43
+ acknowledged: boolean
44
+ insertedCount: number
45
+ }
46
+
47
+ export interface MongoUpdateResult {
48
+ matchedCount: number
49
+ modifiedCount: number
50
+ acknowledged: boolean
51
+ }
52
+
53
+ export interface MongoDeleteResult {
54
+ deletedCount: number
55
+ acknowledged: boolean
56
+ }
57
+
58
+ export interface MongoCollectionContract {
59
+ /** Find documents matching filter */
60
+ find(filter?: MongoFilter): MongoQueryBuilder
61
+ /** Find a single document */
62
+ findOne(filter?: MongoFilter): Promise<Record<string, any> | null>
63
+ /** Find by _id */
64
+ findById(id: any): Promise<Record<string, any> | null>
65
+ /** Insert a single document */
66
+ insertOne(doc: Record<string, any>): Promise<MongoInsertResult>
67
+ /** Insert multiple documents */
68
+ insertMany(docs: Record<string, any>[]): Promise<MongoInsertManyResult>
69
+ /** Update first matching document */
70
+ updateOne(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult>
71
+ /** Update all matching documents */
72
+ updateMany(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult>
73
+ /** Replace a document by _id */
74
+ replaceOne(filter: MongoFilter, replacement: Record<string, any>): Promise<MongoUpdateResult>
75
+ /** Upsert: update or insert */
76
+ upsert(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult>
77
+ /** Delete first matching document */
78
+ deleteOne(filter: MongoFilter): Promise<MongoDeleteResult>
79
+ /** Delete all matching documents */
80
+ deleteMany(filter: MongoFilter): Promise<MongoDeleteResult>
81
+ /** Run an aggregation pipeline */
82
+ aggregate(pipeline: MongoPipelineStage[]): Promise<Record<string, any>[]>
83
+ /** Count documents */
84
+ count(filter?: MongoFilter): Promise<number>
85
+ /** Create an index */
86
+ createIndex(spec: Record<string, any>, options?: Record<string, any>): Promise<string>
87
+ /** Drop the collection */
88
+ drop(): Promise<boolean>
89
+ }
90
+
91
+ export interface MongoQueryBuilder {
92
+ /** Filter documents */
93
+ where(filter: MongoFilter): MongoQueryBuilder
94
+ /** Projection — include/exclude fields */
95
+ select(projection: MongoProjection): MongoQueryBuilder
96
+ /** Sort results */
97
+ sort(sort: MongoSortDoc): MongoQueryBuilder
98
+ /** Limit results */
99
+ limit(n: number): MongoQueryBuilder
100
+ /** Skip results */
101
+ skip(n: number): MongoQueryBuilder
102
+ /** Execute and return all results */
103
+ get(): Promise<Record<string, any>[]>
104
+ /** Execute and return first result */
105
+ first(): Promise<Record<string, any> | null>
106
+ /** Execute and return first or throw */
107
+ firstOrFail(): Promise<Record<string, any>>
108
+ /** Count matching documents */
109
+ count(): Promise<number>
110
+ }
111
+
112
+ export interface MongoDatabaseConnection {
113
+ /** Get a collection query interface */
114
+ collection(name: string): MongoCollectionContract
115
+ /** Run a raw command */
116
+ command(command: Record<string, any>): Promise<any>
117
+ /** List all collection names */
118
+ listCollections(): Promise<string[]>
119
+ /** Start a transaction session */
120
+ transaction<T>(callback: (conn: MongoDatabaseConnection) => Promise<T>): Promise<T>
121
+ getDriverName(): string
122
+ }
@@ -0,0 +1,10 @@
1
+ export interface PaginationResult<T = Record<string, any>> {
2
+ data: T[]
3
+ total: number
4
+ perPage: number
5
+ currentPage: number
6
+ lastPage: number
7
+ from: number
8
+ to: number
9
+ hasMore: boolean
10
+ }
@@ -0,0 +1,220 @@
1
+ import { Expression } from '../query/Expression.ts'
2
+ import type { WhereClause, QueryState, JoinClause, OrderClause } from '../query/Builder.ts'
3
+ import type { Grammar } from '../contracts/Grammar.ts'
4
+
5
+ export abstract class BaseGrammar implements Grammar {
6
+ abstract quoteIdentifier(name: string): string
7
+ abstract placeholder(index: number): string
8
+
9
+ // ── SELECT ────────────────────────────────────────────────────────────────
10
+
11
+ compileSelect(state: QueryState): { sql: string; bindings: any[] } {
12
+ const bindings: any[] = []
13
+ const parts: string[] = []
14
+
15
+ // Collect raw expression bindings from columns first (they come first in the final bindings)
16
+ const colBindings: any[] = []
17
+ for (const c of state.columns) {
18
+ if (c instanceof Expression) colBindings.push(...c.bindings)
19
+ }
20
+
21
+ const cols = state.columns.map((c) => {
22
+ if (c instanceof Expression) return c.value
23
+ const s = c as string
24
+ // Don't quote wildcards or already-qualified expressions
25
+ if (s === '*' || s.endsWith('.*')) return s
26
+ return this.quoteIdentifier(s)
27
+ }).join(', ')
28
+
29
+ parts.push(`SELECT ${state.distinct ? 'DISTINCT ' : ''}${cols}`)
30
+ parts.push(`FROM ${this.quoteIdentifier(state.table)}`)
31
+
32
+ if (state.joins.length) {
33
+ for (const j of state.joins) {
34
+ parts.push(this.compileJoin(j))
35
+ }
36
+ }
37
+
38
+ if (state.wheres.length) {
39
+ const { sql: whereSql, bindings: wb } = this.compileWheres(state.wheres, colBindings.length + 1)
40
+ parts.push(`WHERE ${whereSql}`)
41
+ bindings.push(...wb)
42
+ }
43
+
44
+ if (state.groups.length) {
45
+ parts.push(`GROUP BY ${state.groups.map((g) => this.quoteIdentifier(g)).join(', ')}`)
46
+ }
47
+
48
+ if (state.havings.length) {
49
+ const havingStartIdx = colBindings.length + bindings.length + 1
50
+ const { sql: havingSql, bindings: hb } = this.compileWheres(state.havings, havingStartIdx)
51
+ parts.push(`HAVING ${havingSql}`)
52
+ bindings.push(...hb)
53
+ }
54
+
55
+ if (state.orders.length) {
56
+ const orderStr = state.orders.map((o) => {
57
+ const col = o.column instanceof Expression
58
+ ? o.column.value
59
+ : this.quoteIdentifier(o.column as string)
60
+ return `${col} ${o.direction.toUpperCase()}`
61
+ }).join(', ')
62
+ parts.push(`ORDER BY ${orderStr}`)
63
+ }
64
+
65
+ if (state.limitValue !== null) {
66
+ parts.push(`LIMIT ${state.limitValue}`)
67
+ }
68
+
69
+ if (state.offsetValue !== null) {
70
+ parts.push(`OFFSET ${state.offsetValue}`)
71
+ }
72
+
73
+ return { sql: parts.join(' '), bindings: [...colBindings, ...bindings] }
74
+ }
75
+
76
+ // ── INSERT ────────────────────────────────────────────────────────────────
77
+
78
+ compileInsert(table: string, data: Record<string, any>): { sql: string; bindings: any[] } {
79
+ const keys = Object.keys(data)
80
+ const cols = keys.map((k) => this.quoteIdentifier(k)).join(', ')
81
+ const placeholders = keys.map((_, i) => this.placeholder(i + 1)).join(', ')
82
+ return {
83
+ sql: `INSERT INTO ${this.quoteIdentifier(table)} (${cols}) VALUES (${placeholders})`,
84
+ bindings: Object.values(data),
85
+ }
86
+ }
87
+
88
+ compileInsertGetId(table: string, data: Record<string, any>): { sql: string; bindings: any[] } {
89
+ return this.compileInsert(table, data)
90
+ }
91
+
92
+ // ── UPDATE ────────────────────────────────────────────────────────────────
93
+
94
+ compileUpdate(
95
+ table: string,
96
+ state: QueryState,
97
+ data: Record<string, any>,
98
+ ): { sql: string; bindings: any[] } {
99
+ const bindings: any[] = []
100
+ const keys = Object.keys(data)
101
+ let setIndex = 1
102
+ const sets = keys.map((k) => {
103
+ const val = data[k]
104
+ if (val instanceof Expression) return `${this.quoteIdentifier(k)} = ${val.value}`
105
+ bindings.push(val)
106
+ return `${this.quoteIdentifier(k)} = ${this.placeholder(setIndex++)}`
107
+ }).join(', ')
108
+
109
+ let sql = `UPDATE ${this.quoteIdentifier(table)} SET ${sets}`
110
+
111
+ if (state.wheres.length) {
112
+ // Pass current binding count so WHERE $n continues from where SET left off
113
+ const { sql: whereSql, bindings: wb } = this.compileWheres(state.wheres, bindings.length + 1)
114
+ sql += ` WHERE ${whereSql}`
115
+ bindings.push(...wb)
116
+ }
117
+
118
+ return { sql, bindings }
119
+ }
120
+
121
+ // ── DELETE ────────────────────────────────────────────────────────────────
122
+
123
+ compileDelete(table: string, state: QueryState): { sql: string; bindings: any[] } {
124
+ const bindings: any[] = []
125
+ let sql = `DELETE FROM ${this.quoteIdentifier(table)}`
126
+
127
+ if (state.wheres.length) {
128
+ const { sql: whereSql, bindings: wb } = this.compileWheres(state.wheres)
129
+ sql += ` WHERE ${whereSql}`
130
+ bindings.push(...wb)
131
+ }
132
+
133
+ return { sql, bindings }
134
+ }
135
+
136
+ compileTruncate(table: string): string {
137
+ return `DELETE FROM ${this.quoteIdentifier(table)}`
138
+ }
139
+
140
+ // ── Private helpers ───────────────────────────────────────────────────────
141
+
142
+ private compileJoin(j: JoinClause): string {
143
+ const type = j.type.toUpperCase()
144
+ return `${type} JOIN ${this.quoteIdentifier(j.table)} ON ${j.first} ${j.operator} ${j.second}`
145
+ }
146
+
147
+ protected compileWheres(wheres: WhereClause[], startIndex = 1): { sql: string; bindings: any[] } {
148
+ const parts: string[] = []
149
+ const bindings: any[] = []
150
+ let bindingIndex = startIndex
151
+
152
+ for (let i = 0; i < wheres.length; i++) {
153
+ const w = wheres[i]!
154
+ const bool = i === 0 ? '' : w.boolean.toUpperCase() + ' '
155
+
156
+ if (w.type === 'raw') {
157
+ parts.push(`${bool}${w.sql}`)
158
+ const rawBindings = w.bindings ?? []
159
+ bindings.push(...rawBindings)
160
+ bindingIndex += rawBindings.length
161
+ continue
162
+ }
163
+
164
+ if (w.type === 'nested') {
165
+ const { sql: nestedSql, bindings: nb } = this.compileWheres(w.nested ?? [], bindingIndex)
166
+ parts.push(`${bool}(${nestedSql})`)
167
+ bindings.push(...nb)
168
+ bindingIndex += nb.length
169
+ continue
170
+ }
171
+
172
+ if (w.type === 'null') {
173
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} IS NULL`)
174
+ continue
175
+ }
176
+
177
+ if (w.type === 'notNull') {
178
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} IS NOT NULL`)
179
+ continue
180
+ }
181
+
182
+ if (w.type === 'in') {
183
+ const placeholders = (w.values ?? []).map((_, i) => this.placeholder(bindingIndex++)).join(', ')
184
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} IN (${placeholders})`)
185
+ bindings.push(...(w.values ?? []))
186
+ continue
187
+ }
188
+
189
+ if (w.type === 'notIn') {
190
+ const placeholders = (w.values ?? []).map((_, i) => this.placeholder(bindingIndex++)).join(', ')
191
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} NOT IN (${placeholders})`)
192
+ bindings.push(...(w.values ?? []))
193
+ continue
194
+ }
195
+
196
+ if (w.type === 'between') {
197
+ const p1 = this.placeholder(bindingIndex++)
198
+ const p2 = this.placeholder(bindingIndex++)
199
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} BETWEEN ${p1} AND ${p2}`)
200
+ bindings.push(...(w.range ?? []))
201
+ continue
202
+ }
203
+
204
+ if (w.type === 'column') {
205
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} ${w.operator} ${this.quoteIdentifier(w.secondColumn!)}`)
206
+ continue
207
+ }
208
+
209
+ // basic
210
+ if (w.value instanceof Expression) {
211
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} ${w.operator} ${w.value.value}`)
212
+ } else {
213
+ parts.push(`${bool}${this.quoteIdentifier(w.column!)} ${w.operator} ${this.placeholder(bindingIndex++)}`)
214
+ bindings.push(w.value)
215
+ }
216
+ }
217
+
218
+ return { sql: parts.join(' '), bindings }
219
+ }
220
+ }