@mantiq/database 0.1.3 → 0.2.0

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 CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Query builder, Eloquent-style ORM, schema migrations, seeders, and factories for MantiqJS. Supports SQLite, Postgres, MySQL, and MongoDB.
4
4
 
5
- Part of [MantiqJS](https://github.com/abdullahkhan/mantiq) — a batteries-included TypeScript web framework for Bun.
5
+ Part of [MantiqJS](https://github.com/mantiqjs/mantiq) — a batteries-included TypeScript web framework for Bun.
6
6
 
7
7
  ## Installation
8
8
 
@@ -12,7 +12,7 @@ bun add @mantiq/database
12
12
 
13
13
  ## Documentation
14
14
 
15
- See the [MantiqJS repository](https://github.com/abdullahkhan/mantiq) for full documentation.
15
+ See the [MantiqJS repository](https://github.com/mantiqjs/mantiq) for full documentation.
16
16
 
17
17
  ## License
18
18
 
package/package.json CHANGED
@@ -1,18 +1,18 @@
1
1
  {
2
2
  "name": "@mantiq/database",
3
- "version": "0.1.3",
3
+ "version": "0.2.0",
4
4
  "description": "Query builder, ORM, migrations, seeders, factories — with SQLite, Postgres, MySQL and MongoDB support",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "Abdullah Khan",
8
- "homepage": "https://github.com/abdullahkhan/mantiq/tree/main/packages/database",
8
+ "homepage": "https://github.com/mantiqjs/mantiq/tree/main/packages/database",
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/abdullahkhan/mantiq.git",
11
+ "url": "https://github.com/mantiqjs/mantiq.git",
12
12
  "directory": "packages/database"
13
13
  },
14
14
  "bugs": {
15
- "url": "https://github.com/abdullahkhan/mantiq/issues"
15
+ "url": "https://github.com/mantiqjs/mantiq/issues"
16
16
  },
17
17
  "keywords": [
18
18
  "mantiq",
@@ -40,7 +40,7 @@
40
40
  "LICENSE"
41
41
  ],
42
42
  "scripts": {
43
- "build": "bun build ./src/index.ts --outdir ./dist --target bun",
43
+ "build": "bun build ./src/index.ts --outdir ./dist --target bun --packages=external",
44
44
  "test": "bun test",
45
45
  "typecheck": "tsc --noEmit",
46
46
  "clean": "rm -rf dist"
@@ -1,10 +1,23 @@
1
- import type { QueryBuilder } from '../query/Builder.ts'
1
+ import type { QueryBuilder, QueryState } from '../query/Builder.ts'
2
2
  import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
3
 
4
4
  export interface DatabaseConnection {
5
+ // ── Universal execution (works on ALL drivers) ──────────────────────────
6
+ executeSelect(state: QueryState): Promise<Record<string, any>[]>
7
+ executeInsert(table: string, data: Record<string, any>): Promise<number>
8
+ executeInsertGetId(table: string, data: Record<string, any>): Promise<number | string>
9
+ executeUpdate(table: string, state: QueryState, data: Record<string, any>): Promise<number>
10
+ executeDelete(table: string, state: QueryState): Promise<number>
11
+ executeTruncate(table: string): Promise<void>
12
+ executeAggregate(state: QueryState, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string): Promise<number>
13
+ executeExists(state: QueryState): Promise<boolean>
14
+
15
+ // ── Raw SQL escape hatch (throws DriverNotSupportedError on non-SQL) ───
5
16
  select(sql: string, bindings?: any[]): Promise<Record<string, any>[]>
6
17
  statement(sql: string, bindings?: any[]): Promise<number>
7
- insertGetId(sql: string, bindings?: any[]): Promise<number | bigint>
18
+ insertGetId(sql: string, bindings?: any[]): Promise<number | bigint | string>
19
+
20
+ // ── Shared ──────────────────────────────────────────────────────────────
8
21
  transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T>
9
22
  table(name: string): QueryBuilder
10
23
  schema(): SchemaBuilder
@@ -0,0 +1,111 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+ import type { Grammar } from '../contracts/Grammar.ts'
3
+ import type { QueryState } from '../query/Builder.ts'
4
+ import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
5
+ import { QueryBuilder } from '../query/Builder.ts'
6
+ import { Expression } from '../query/Expression.ts'
7
+
8
+ /**
9
+ * Abstract base for all SQL connections. Provides `executeXxx()` methods
10
+ * by compiling QueryState via Grammar and delegating to the raw SQL methods
11
+ * that each driver must implement.
12
+ */
13
+ export abstract class BaseSQLConnection implements DatabaseConnection {
14
+ abstract readonly _grammar: Grammar
15
+
16
+ // ── Subclasses implement these (raw SQL execution) ──────────────────────
17
+ abstract select(sql: string, bindings?: any[]): Promise<Record<string, any>[]>
18
+ abstract statement(sql: string, bindings?: any[]): Promise<number>
19
+ abstract insertGetId(sql: string, bindings?: any[]): Promise<number | bigint | string>
20
+ abstract transaction<T>(callback: (connection: DatabaseConnection) => Promise<T>): Promise<T>
21
+ abstract schema(): SchemaBuilder
22
+ abstract getDriverName(): string
23
+
24
+ getTablePrefix(): string {
25
+ return ''
26
+ }
27
+
28
+ table(name: string): QueryBuilder {
29
+ return new QueryBuilder(this, name)
30
+ }
31
+
32
+ // ── Universal executeXxx (compile via Grammar → run via raw methods) ────
33
+
34
+ async executeSelect(state: QueryState): Promise<Record<string, any>[]> {
35
+ const { sql, bindings } = this._grammar.compileSelect(state)
36
+ return this.select(sql, bindings)
37
+ }
38
+
39
+ async executeInsert(table: string, data: Record<string, any>): Promise<number> {
40
+ const { sql, bindings } = this._grammar.compileInsert(table, data)
41
+ return this.statement(sql, bindings)
42
+ }
43
+
44
+ async executeInsertGetId(table: string, data: Record<string, any>): Promise<number | string> {
45
+ const { sql, bindings } = this._grammar.compileInsertGetId(table, data)
46
+ const id = await this.insertGetId(sql, bindings)
47
+ // SQL drivers always return numeric IDs (bigint from SQLite, string from pg for BIGSERIAL)
48
+ return Number(id)
49
+ }
50
+
51
+ async executeUpdate(table: string, state: QueryState, data: Record<string, any>): Promise<number> {
52
+ const { sql, bindings } = this._grammar.compileUpdate(table, state, data)
53
+ return this.statement(sql, bindings)
54
+ }
55
+
56
+ async executeDelete(table: string, state: QueryState): Promise<number> {
57
+ const { sql, bindings } = this._grammar.compileDelete(table, state)
58
+ return this.statement(sql, bindings)
59
+ }
60
+
61
+ async executeTruncate(table: string): Promise<void> {
62
+ const sql = this._grammar.compileTruncate(table)
63
+ await this.statement(sql, [])
64
+ }
65
+
66
+ async executeAggregate(state: QueryState, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string): Promise<number> {
67
+ const aggState: QueryState = {
68
+ ...state,
69
+ columns: [new Expression(`${fn.toUpperCase()}(${column}) as aggregate`)],
70
+ orders: [], // aggregates don't need ORDER BY
71
+ }
72
+ const { sql, bindings } = this._grammar.compileSelect(aggState)
73
+ const rows = await this.select(sql, bindings)
74
+ return Number(rows[0]?.['aggregate'] ?? 0)
75
+ }
76
+
77
+ async executeExists(state: QueryState): Promise<boolean> {
78
+ const existsState: QueryState = {
79
+ ...state,
80
+ columns: [new Expression('1 as exists_check')],
81
+ limitValue: 1,
82
+ orders: [],
83
+ }
84
+ const { sql, bindings } = this._grammar.compileSelect(existsState)
85
+ const rows = await this.select(sql, bindings)
86
+ return rows.length > 0
87
+ }
88
+
89
+ /**
90
+ * Creates executeXxx methods for a transactional connection wrapper.
91
+ * Call this in transaction() to give the txConn the universal methods.
92
+ */
93
+ protected applyExecuteMethods(txConn: any): void {
94
+ txConn.executeSelect = (state: QueryState) =>
95
+ BaseSQLConnection.prototype.executeSelect.call({ ...txConn, _grammar: this._grammar }, state)
96
+ txConn.executeInsert = (table: string, data: Record<string, any>) =>
97
+ BaseSQLConnection.prototype.executeInsert.call({ ...txConn, _grammar: this._grammar }, table, data)
98
+ txConn.executeInsertGetId = (table: string, data: Record<string, any>) =>
99
+ BaseSQLConnection.prototype.executeInsertGetId.call({ ...txConn, _grammar: this._grammar }, table, data)
100
+ txConn.executeUpdate = (table: string, state: QueryState, data: Record<string, any>) =>
101
+ BaseSQLConnection.prototype.executeUpdate.call({ ...txConn, _grammar: this._grammar }, table, state, data)
102
+ txConn.executeDelete = (table: string, state: QueryState) =>
103
+ BaseSQLConnection.prototype.executeDelete.call({ ...txConn, _grammar: this._grammar }, table, state)
104
+ txConn.executeTruncate = (table: string) =>
105
+ BaseSQLConnection.prototype.executeTruncate.call({ ...txConn, _grammar: this._grammar }, table)
106
+ txConn.executeAggregate = (state: QueryState, fn: string, column: string) =>
107
+ BaseSQLConnection.prototype.executeAggregate.call({ ...txConn, _grammar: this._grammar }, state, fn, column)
108
+ txConn.executeExists = (state: QueryState) =>
109
+ BaseSQLConnection.prototype.executeExists.call({ ...txConn, _grammar: this._grammar }, state)
110
+ }
111
+ }
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseConnection } from '../contracts/Connection.ts'
2
2
  import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+ import { BaseSQLConnection } from './BaseSQLConnection.ts'
3
4
  import { QueryBuilder } from '../query/Builder.ts'
4
5
  import { MSSQLGrammar } from './MSSQLGrammar.ts'
5
6
  import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
@@ -17,12 +18,13 @@ export interface MSSQLConfig {
17
18
  pool?: { min?: number; max?: number }
18
19
  }
19
20
 
20
- export class MSSQLConnection implements DatabaseConnection {
21
+ export class MSSQLConnection extends BaseSQLConnection {
21
22
  readonly _grammar = new MSSQLGrammar()
22
23
  private pool: any = null
23
24
  private config: MSSQLConfig
24
25
 
25
26
  constructor(config: MSSQLConfig) {
27
+ super()
26
28
  this.config = config
27
29
  }
28
30
 
@@ -100,33 +102,33 @@ export class MSSQLConnection implements DatabaseConnection {
100
102
  const transaction = new sql.Transaction(pool)
101
103
  await transaction.begin()
102
104
  try {
103
- const txConn: DatabaseConnection = {
104
- select: async (s, b = []) => {
105
+ const txConn: any = {
106
+ _grammar: this._grammar,
107
+ select: async (s: string, b: any[] = []) => {
105
108
  const req = transaction.request()
106
109
  b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
107
110
  const r = await req.query(s)
108
111
  return r.recordset ?? []
109
112
  },
110
- statement: async (s, b = []) => {
113
+ statement: async (s: string, b: any[] = []) => {
111
114
  const req = transaction.request()
112
115
  b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
113
116
  const r = await req.query(s)
114
117
  return r.rowsAffected?.[0] ?? 0
115
118
  },
116
- insertGetId: async (s, b = []) => {
119
+ insertGetId: async (s: string, b: any[] = []) => {
117
120
  const req = transaction.request()
118
121
  b.forEach((val: any, i: number) => req.input(`p${i + 1}`, val))
119
122
  const r = await req.query(s)
120
123
  return r.recordset?.[0]?.id ?? 0
121
124
  },
122
- transaction: (cb) => cb(txConn),
123
- table: (name) => new QueryBuilder(txConn, name),
125
+ transaction: (cb: any) => cb(txConn),
126
+ table: (name: string) => new QueryBuilder(txConn, name),
124
127
  schema: () => new SchemaBuilderImpl(txConn),
125
128
  getDriverName: () => 'mssql',
126
129
  getTablePrefix: () => '',
127
130
  }
128
- // @ts-ignore — attach grammar
129
- txConn._grammar = this._grammar
131
+ this.applyExecuteMethods(txConn)
130
132
  const result = await callback(txConn)
131
133
  await transaction.commit()
132
134
  return result
@@ -136,10 +138,6 @@ export class MSSQLConnection implements DatabaseConnection {
136
138
  }
137
139
  }
138
140
 
139
- table(name: string): QueryBuilder {
140
- return new QueryBuilder(this, name)
141
- }
142
-
143
141
  schema(): SchemaBuilder {
144
142
  return new SchemaBuilderImpl(this)
145
143
  }
@@ -147,8 +145,4 @@ export class MSSQLConnection implements DatabaseConnection {
147
145
  getDriverName(): string {
148
146
  return 'mssql'
149
147
  }
150
-
151
- getTablePrefix(): string {
152
- return ''
153
- }
154
148
  }
@@ -1,5 +1,6 @@
1
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
2
+ import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
1
3
  import type {
2
- MongoDatabaseConnection,
3
4
  MongoCollectionContract,
4
5
  MongoFilter,
5
6
  MongoUpdateDoc,
@@ -10,9 +11,12 @@ import type {
10
11
  MongoDeleteResult,
11
12
  MongoQueryBuilder,
12
13
  } from '../contracts/MongoConnection.ts'
14
+ import type { QueryState, WhereClause } from '../query/Builder.ts'
15
+ import { QueryBuilder } from '../query/Builder.ts'
16
+ import { Expression } from '../query/Expression.ts'
13
17
  import { MongoQueryBuilderImpl } from './MongoQueryBuilderImpl.ts'
14
18
  import { ConnectionError } from '../errors/ConnectionError.ts'
15
- import { QueryError } from '../errors/QueryError.ts'
19
+ import { DriverNotSupportedError } from '../errors/DriverNotSupportedError.ts'
16
20
 
17
21
  export interface MongoConfig {
18
22
  uri: string
@@ -20,149 +24,146 @@ export interface MongoConfig {
20
24
  options?: Record<string, any>
21
25
  }
22
26
 
23
- class MongoCollectionImpl implements MongoCollectionContract {
24
- constructor(
25
- private readonly col: any,
26
- private readonly name: string,
27
- ) {}
27
+ // ── Operator translation map ──────────────────────────────────────────────────
28
28
 
29
- find(filter: MongoFilter = {}): MongoQueryBuilder {
30
- return new MongoQueryBuilderImpl(
31
- this.name,
32
- async (opts) => {
33
- let cursor = this.col.find(opts.filter ?? {})
34
- if (opts.projection) cursor = cursor.project(opts.projection)
35
- if (opts.sort) cursor = cursor.sort(opts.sort)
36
- if (opts.skip) cursor = cursor.skip(opts.skip)
37
- if (opts.limit) cursor = cursor.limit(opts.limit)
38
- return cursor.toArray()
39
- },
40
- async (f) => this.col.countDocuments(f),
41
- )
42
- }
29
+ const OPERATOR_MAP: Record<string, string> = {
30
+ '=': '$eq',
31
+ '!=': '$ne',
32
+ '<>': '$ne',
33
+ '>': '$gt',
34
+ '>=': '$gte',
35
+ '<': '$lt',
36
+ '<=': '$lte',
37
+ }
43
38
 
44
- async findOne(filter: MongoFilter = {}): Promise<Record<string, any> | null> {
45
- return this.col.findOne(filter)
46
- }
39
+ // ── MongoConnection implements the universal DatabaseConnection interface ──
47
40
 
48
- async findById(id: any): Promise<Record<string, any> | null> {
49
- const { ObjectId } = await import('mongodb')
50
- return this.col.findOne({ _id: typeof id === 'string' ? new ObjectId(id) : id })
51
- }
41
+ export class MongoConnection implements DatabaseConnection {
42
+ private client: any = null
43
+ private db: any = null
44
+ private config: MongoConfig
52
45
 
53
- async insertOne(doc: Record<string, any>): Promise<MongoInsertResult> {
54
- const result = await this.col.insertOne(doc)
55
- return { insertedId: result.insertedId, acknowledged: result.acknowledged }
46
+ constructor(config: MongoConfig) {
47
+ this.config = config
56
48
  }
57
49
 
58
- async insertMany(docs: Record<string, any>[]): Promise<MongoInsertManyResult> {
59
- const result = await this.col.insertMany(docs)
60
- return {
61
- insertedIds: Object.values(result.insertedIds),
62
- acknowledged: result.acknowledged,
63
- insertedCount: result.insertedCount,
50
+ private async getDb(): Promise<any> {
51
+ if (!this.db) {
52
+ try {
53
+ const { MongoClient } = await import('mongodb')
54
+ this.client = new MongoClient(this.config.uri, this.config.options ?? {})
55
+ await this.client.connect()
56
+ this.db = this.client.db(this.config.database)
57
+ } catch (e: any) {
58
+ throw new ConnectionError(`MongoDB connection failed: ${e.message}`, 'mongodb', e)
59
+ }
64
60
  }
61
+ return this.db
65
62
  }
66
63
 
67
- async updateOne(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
68
- const result = await this.col.updateOne(filter, update)
69
- return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
70
- }
64
+ // ── Universal executeXxx methods ──────────────────────────────────────────
71
65
 
72
- async updateMany(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
73
- const result = await this.col.updateMany(filter, update)
74
- return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
75
- }
66
+ async executeSelect(state: QueryState): Promise<Record<string, any>[]> {
67
+ this.guardNoJoins(state)
68
+ this.guardNoHavings(state)
76
69
 
77
- async replaceOne(filter: MongoFilter, replacement: Record<string, any>): Promise<MongoUpdateResult> {
78
- const result = await this.col.replaceOne(filter, replacement)
79
- return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
70
+ const db = await this.getDb()
71
+ const col = db.collection(state.table)
72
+ const filter = this.translateWheres(state.wheres)
73
+ const projection = this.translateColumns(state.columns)
74
+ const sort = this.translateOrders(state.orders)
75
+
76
+ let cursor = col.find(filter)
77
+ if (projection) cursor = cursor.project(projection)
78
+ if (sort) cursor = cursor.sort(sort)
79
+ if (state.offsetValue !== null) cursor = cursor.skip(state.offsetValue)
80
+ if (state.limitValue !== null) cursor = cursor.limit(state.limitValue)
81
+
82
+ return cursor.toArray()
80
83
  }
81
84
 
82
- async upsert(filter: MongoFilter, update: MongoUpdateDoc): Promise<MongoUpdateResult> {
83
- const result = await this.col.updateOne(filter, update, { upsert: true })
84
- return { matchedCount: result.matchedCount, modifiedCount: result.modifiedCount, acknowledged: result.acknowledged }
85
+ async executeInsert(table: string, data: Record<string, any>): Promise<number> {
86
+ const db = await this.getDb()
87
+ const result = await db.collection(table).insertOne(data)
88
+ return result.acknowledged ? 1 : 0
85
89
  }
86
90
 
87
- async deleteOne(filter: MongoFilter): Promise<MongoDeleteResult> {
88
- const result = await this.col.deleteOne(filter)
89
- return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
91
+ async executeInsertGetId(table: string, data: Record<string, any>): Promise<number | string> {
92
+ const db = await this.getDb()
93
+ const result = await db.collection(table).insertOne(data)
94
+ const id = result.insertedId
95
+ return typeof id === 'object' ? id.toString() : id
90
96
  }
91
97
 
92
- async deleteMany(filter: MongoFilter): Promise<MongoDeleteResult> {
93
- const result = await this.col.deleteMany(filter)
94
- return { deletedCount: result.deletedCount, acknowledged: result.acknowledged }
98
+ async executeUpdate(table: string, state: QueryState, data: Record<string, any>): Promise<number> {
99
+ const db = await this.getDb()
100
+ const filter = this.translateWheres(state.wheres)
101
+ const result = await db.collection(table).updateMany(filter, { $set: data })
102
+ return result.modifiedCount
95
103
  }
96
104
 
97
- async aggregate(pipeline: MongoPipelineStage[]): Promise<Record<string, any>[]> {
98
- return this.col.aggregate(pipeline).toArray()
105
+ async executeDelete(table: string, state: QueryState): Promise<number> {
106
+ const db = await this.getDb()
107
+ const filter = this.translateWheres(state.wheres)
108
+ const result = await db.collection(table).deleteMany(filter)
109
+ return result.deletedCount
99
110
  }
100
111
 
101
- async count(filter: MongoFilter = {}): Promise<number> {
102
- return this.col.countDocuments(filter)
112
+ async executeTruncate(table: string): Promise<void> {
113
+ const db = await this.getDb()
114
+ await db.collection(table).deleteMany({})
103
115
  }
104
116
 
105
- async createIndex(spec: Record<string, any>, options?: Record<string, any>): Promise<string> {
106
- return this.col.createIndex(spec, options)
117
+ async executeAggregate(state: QueryState, fn: 'count' | 'sum' | 'avg' | 'min' | 'max', column: string): Promise<number> {
118
+ const db = await this.getDb()
119
+ const filter = this.translateWheres(state.wheres)
120
+
121
+ if (fn === 'count') {
122
+ return db.collection(state.table).countDocuments(filter)
123
+ }
124
+
125
+ const aggOp = `$${fn}`
126
+ const aggField = column === '*' ? 1 : `$${column}`
127
+ const pipeline: any[] = [
128
+ { $match: filter },
129
+ { $group: { _id: null, result: { [aggOp]: aggField } } },
130
+ ]
131
+ const [row] = await db.collection(state.table).aggregate(pipeline).toArray()
132
+ return Number(row?.result ?? 0)
107
133
  }
108
134
 
109
- async drop(): Promise<boolean> {
110
- return this.col.drop()
135
+ async executeExists(state: QueryState): Promise<boolean> {
136
+ const db = await this.getDb()
137
+ const filter = this.translateWheres(state.wheres)
138
+ const count = await db.collection(state.table).countDocuments(filter, { limit: 1 })
139
+ return count > 0
111
140
  }
112
- }
113
141
 
114
- export class MongoConnection implements MongoDatabaseConnection {
115
- private client: any = null
116
- private db: any = null
117
- private config: MongoConfig
142
+ // ── Raw SQL methods — throw on MongoDB ────────────────────────────────────
118
143
 
119
- constructor(config: MongoConfig) {
120
- this.config = config
144
+ async select(sql: string, bindings?: any[]): Promise<Record<string, any>[]> {
145
+ throw new DriverNotSupportedError('mongodb', 'raw SQL queries')
121
146
  }
122
147
 
123
- private async getDb(): Promise<any> {
124
- if (!this.db) {
125
- try {
126
- const { MongoClient } = await import('mongodb')
127
- this.client = new MongoClient(this.config.uri, this.config.options ?? {})
128
- await this.client.connect()
129
- this.db = this.client.db(this.config.database)
130
- } catch (e: any) {
131
- throw new ConnectionError(`MongoDB connection failed: ${e.message}`, 'mongodb', e)
132
- }
133
- }
134
- return this.db
148
+ async statement(sql: string, bindings?: any[]): Promise<number> {
149
+ throw new DriverNotSupportedError('mongodb', 'raw SQL queries')
135
150
  }
136
151
 
137
- collection(name: string): MongoCollectionContract {
138
- // Lazily get collection actual DB ops will connect
139
- const self = this
140
- const col = {
141
- async getCol() {
142
- const db = await self.getDb()
143
- return db.collection(name)
144
- },
145
- }
146
-
147
- // We need to return a proxy that defers the actual collection resolution
148
- return new LazyMongoCollection(name, async () => {
149
- const db = await self.getDb()
150
- return db.collection(name)
151
- })
152
+ async insertGetId(sql: string, bindings?: any[]): Promise<number | bigint | string> {
153
+ throw new DriverNotSupportedError('mongodb', 'raw SQL queries')
152
154
  }
153
155
 
154
- async command(command: Record<string, any>): Promise<any> {
155
- const db = await this.getDb()
156
- return db.command(command)
156
+ // ── Shared interface ──────────────────────────────────────────────────────
157
+
158
+ table(name: string): QueryBuilder {
159
+ return new QueryBuilder(this, name)
157
160
  }
158
161
 
159
- async listCollections(): Promise<string[]> {
160
- const db = await this.getDb()
161
- const collections = await db.listCollections().toArray()
162
- return collections.map((c: any) => c.name)
162
+ schema(): SchemaBuilder {
163
+ throw new DriverNotSupportedError('mongodb', 'schema builder (MongoDB is schemaless — use native() for indexes)')
163
164
  }
164
165
 
165
- async transaction<T>(callback: (conn: MongoDatabaseConnection) => Promise<T>): Promise<T> {
166
+ async transaction<T>(callback: (conn: DatabaseConnection) => Promise<T>): Promise<T> {
166
167
  const client = await this.getClient()
167
168
  const session = client.startSession()
168
169
  try {
@@ -179,22 +180,194 @@ export class MongoConnection implements MongoDatabaseConnection {
179
180
  }
180
181
  }
181
182
 
182
- private async getClient(): Promise<any> {
183
- await this.getDb()
184
- return this.client
185
- }
186
-
187
183
  getDriverName(): string {
188
184
  return 'mongodb'
189
185
  }
190
186
 
187
+ getTablePrefix(): string {
188
+ return ''
189
+ }
190
+
191
+ // ── Native escape hatch — direct MongoDB access ───────────────────────────
192
+
193
+ /** Returns the native MongoDB collection for advanced operations. */
194
+ collection(name: string): MongoCollectionContract {
195
+ return new LazyMongoCollection(name, async () => {
196
+ const db = await this.getDb()
197
+ return db.collection(name)
198
+ })
199
+ }
200
+
201
+ /** Returns the underlying MongoDB Db instance. */
202
+ async native(): Promise<any> {
203
+ return this.getDb()
204
+ }
205
+
206
+ async command(command: Record<string, any>): Promise<any> {
207
+ const db = await this.getDb()
208
+ return db.command(command)
209
+ }
210
+
211
+ async listCollections(): Promise<string[]> {
212
+ const db = await this.getDb()
213
+ const collections = await db.listCollections().toArray()
214
+ return collections.map((c: any) => c.name)
215
+ }
216
+
191
217
  async disconnect(): Promise<void> {
192
218
  await this.client?.close()
193
219
  this.client = null
194
220
  this.db = null
195
221
  }
222
+
223
+ private async getClient(): Promise<any> {
224
+ await this.getDb()
225
+ return this.client
226
+ }
227
+
228
+ // ── QueryState → MongoDB translation ──────────────────────────────────────
229
+
230
+ private translateWheres(wheres: WhereClause[]): Record<string, any> {
231
+ if (wheres.length === 0) return {}
232
+
233
+ const andClauses: Record<string, any>[] = []
234
+ const orGroups: Record<string, any>[][] = []
235
+ let currentAnd: Record<string, any>[] = []
236
+
237
+ for (const w of wheres) {
238
+ const clause = this.translateSingleWhere(w)
239
+
240
+ if (w.boolean === 'or' && currentAnd.length > 0) {
241
+ // Push accumulated AND clauses as one OR branch
242
+ orGroups.push(currentAnd)
243
+ currentAnd = [clause]
244
+ } else {
245
+ currentAnd.push(clause)
246
+ }
247
+ }
248
+
249
+ // Final group
250
+ if (orGroups.length > 0) {
251
+ orGroups.push(currentAnd)
252
+ return { $or: orGroups.map((group) => group.length === 1 ? group[0] : { $and: group }) }
253
+ }
254
+
255
+ if (currentAnd.length === 1) return currentAnd[0]
256
+ return { $and: currentAnd }
257
+ }
258
+
259
+ private translateSingleWhere(w: WhereClause): Record<string, any> {
260
+ switch (w.type) {
261
+ case 'basic': {
262
+ const col = w.column!
263
+ const op = w.operator ?? '='
264
+ const val = w.value
265
+
266
+ if (op === '=' || op === '$eq') {
267
+ return { [col]: val }
268
+ }
269
+
270
+ if (op === 'like' || op === 'LIKE') {
271
+ return { [col]: { $regex: this.likeToRegex(val), $options: 'i' } }
272
+ }
273
+
274
+ if (op === 'not like' || op === 'NOT LIKE') {
275
+ return { [col]: { $not: { $regex: this.likeToRegex(val), $options: 'i' } } }
276
+ }
277
+
278
+ const mongoOp = OPERATOR_MAP[op]
279
+ if (mongoOp) {
280
+ return { [col]: { [mongoOp]: val } }
281
+ }
282
+
283
+ throw new DriverNotSupportedError('mongodb', `operator "${op}"`)
284
+ }
285
+
286
+ case 'in':
287
+ return { [w.column!]: { $in: w.values! } }
288
+
289
+ case 'notIn':
290
+ return { [w.column!]: { $nin: w.values! } }
291
+
292
+ case 'null':
293
+ return { [w.column!]: null }
294
+
295
+ case 'notNull':
296
+ return { [w.column!]: { $ne: null } }
297
+
298
+ case 'between':
299
+ return { [w.column!]: { $gte: w.range![0], $lte: w.range![1] } }
300
+
301
+ case 'nested':
302
+ return this.translateWheres(w.nested ?? [])
303
+
304
+ case 'raw':
305
+ throw new DriverNotSupportedError('mongodb', 'whereRaw (use standard where methods instead)')
306
+
307
+ case 'column':
308
+ throw new DriverNotSupportedError('mongodb', 'whereColumn (use $expr in native queries instead)')
309
+
310
+ default:
311
+ throw new DriverNotSupportedError('mongodb', `where type "${w.type}"`)
312
+ }
313
+ }
314
+
315
+ private translateColumns(columns: (string | Expression)[]): Record<string, 1> | undefined {
316
+ if (columns.length === 1 && columns[0] === '*') return undefined
317
+ if (columns.some((c) => c instanceof Expression)) {
318
+ // Allow expressions only if they're simple strings (column names)
319
+ // For actual SQL expressions, throw
320
+ const hasRealExpressions = columns.some((c) => c instanceof Expression && (c.value.includes('(') || c.value.includes(' ')))
321
+ if (hasRealExpressions) {
322
+ throw new DriverNotSupportedError('mongodb', 'selectRaw with SQL expressions')
323
+ }
324
+ }
325
+
326
+ const projection: Record<string, 1> = {}
327
+ for (const col of columns) {
328
+ const name = col instanceof Expression ? col.value : col
329
+ projection[name] = 1
330
+ }
331
+ return projection
332
+ }
333
+
334
+ private translateOrders(orders: Array<{ column: string | Expression; direction: 'asc' | 'desc' }>): Record<string, 1 | -1> | undefined {
335
+ if (orders.length === 0) return undefined
336
+ const sort: Record<string, 1 | -1> = {}
337
+ for (const o of orders) {
338
+ if (o.column instanceof Expression) {
339
+ throw new DriverNotSupportedError('mongodb', 'orderBy with raw Expression')
340
+ }
341
+ sort[o.column] = o.direction === 'asc' ? 1 : -1
342
+ }
343
+ return sort
344
+ }
345
+
346
+ /** Converts SQL LIKE pattern to regex: % → .*, _ → . */
347
+ private likeToRegex(pattern: string): string {
348
+ return pattern
349
+ .replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
350
+ .replace(/%/g, '.*')
351
+ .replace(/_/g, '.')
352
+ }
353
+
354
+ // ── Guards ────────────────────────────────────────────────────────────────
355
+
356
+ private guardNoJoins(state: QueryState): void {
357
+ if (state.joins.length > 0) {
358
+ throw new DriverNotSupportedError('mongodb', 'joins (use relationships or native $lookup instead)')
359
+ }
360
+ }
361
+
362
+ private guardNoHavings(state: QueryState): void {
363
+ if (state.havings.length > 0) {
364
+ throw new DriverNotSupportedError('mongodb', 'having (use native aggregation pipeline instead)')
365
+ }
366
+ }
196
367
  }
197
368
 
369
+ // ── LazyMongoCollection — deferred collection resolution ────────────────────
370
+
198
371
  class LazyMongoCollection implements MongoCollectionContract {
199
372
  private _col: any = null
200
373
 
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseConnection } from '../contracts/Connection.ts'
2
2
  import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+ import { BaseSQLConnection } from './BaseSQLConnection.ts'
3
4
  import { QueryBuilder } from '../query/Builder.ts'
4
5
  import { MySQLGrammar } from './MySQLGrammar.ts'
5
6
  import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
@@ -15,12 +16,13 @@ export interface MySQLConfig {
15
16
  pool?: { min?: number; max?: number }
16
17
  }
17
18
 
18
- export class MySQLConnection implements DatabaseConnection {
19
+ export class MySQLConnection extends BaseSQLConnection {
19
20
  readonly _grammar = new MySQLGrammar()
20
21
  private pool: any = null
21
22
  private config: MySQLConfig
22
23
 
23
24
  constructor(config: MySQLConfig) {
25
+ super()
24
26
  this.config = config
25
27
  }
26
28
 
@@ -79,18 +81,18 @@ export class MySQLConnection implements DatabaseConnection {
79
81
  const conn = await pool.getConnection()
80
82
  try {
81
83
  await conn.beginTransaction()
82
- const txConn: DatabaseConnection = {
83
- select: async (sql, b) => { const [rows] = await conn.query(sql, b); return rows as Record<string, any>[] },
84
- statement: async (sql, b) => { const [r] = await conn.query(sql, b); return (r as any).affectedRows ?? 0 },
85
- insertGetId: async (sql, b) => { const [r] = await conn.query(sql, b); return (r as any).insertId ?? 0 },
86
- transaction: (cb) => cb(txConn),
87
- table: (name) => new QueryBuilder(txConn, name),
84
+ const txConn: any = {
85
+ _grammar: this._grammar,
86
+ select: async (sql: string, b?: any[]) => { const [rows] = await conn.query(sql, b); return rows as Record<string, any>[] },
87
+ statement: async (sql: string, b?: any[]) => { const [r] = await conn.query(sql, b); return (r as any).affectedRows ?? 0 },
88
+ insertGetId: async (sql: string, b?: any[]) => { const [r] = await conn.query(sql, b); return (r as any).insertId ?? 0 },
89
+ transaction: (cb: any) => cb(txConn),
90
+ table: (name: string) => new QueryBuilder(txConn, name),
88
91
  schema: () => new SchemaBuilderImpl(txConn),
89
92
  getDriverName: () => 'mysql',
90
93
  getTablePrefix: () => '',
91
94
  }
92
- // @ts-ignore — attach grammar
93
- txConn._grammar = this._grammar
95
+ this.applyExecuteMethods(txConn)
94
96
  const result = await callback(txConn)
95
97
  await conn.commit()
96
98
  return result
@@ -102,10 +104,6 @@ export class MySQLConnection implements DatabaseConnection {
102
104
  }
103
105
  }
104
106
 
105
- table(name: string): QueryBuilder {
106
- return new QueryBuilder(this, name)
107
- }
108
-
109
107
  schema(): SchemaBuilder {
110
108
  return new SchemaBuilderImpl(this)
111
109
  }
@@ -113,8 +111,4 @@ export class MySQLConnection implements DatabaseConnection {
113
111
  getDriverName(): string {
114
112
  return 'mysql'
115
113
  }
116
-
117
- getTablePrefix(): string {
118
- return ''
119
- }
120
114
  }
@@ -1,5 +1,6 @@
1
1
  import type { DatabaseConnection } from '../contracts/Connection.ts'
2
2
  import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
+ import { BaseSQLConnection } from './BaseSQLConnection.ts'
3
4
  import { QueryBuilder } from '../query/Builder.ts'
4
5
  import { PostgresGrammar } from './PostgresGrammar.ts'
5
6
  import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
@@ -16,12 +17,13 @@ export interface PostgresConfig {
16
17
  pool?: { min?: number; max?: number }
17
18
  }
18
19
 
19
- export class PostgresConnection implements DatabaseConnection {
20
+ export class PostgresConnection extends BaseSQLConnection {
20
21
  readonly _grammar = new PostgresGrammar()
21
22
  private client: any = null
22
23
  private config: PostgresConfig
23
24
 
24
25
  constructor(config: PostgresConfig) {
26
+ super()
25
27
  this.config = config
26
28
  }
27
29
 
@@ -83,19 +85,18 @@ export class PostgresConnection implements DatabaseConnection {
83
85
  const client = await pool.connect()
84
86
  try {
85
87
  await client.query('BEGIN')
86
- // Create a transactional connection wrapper
87
- const txConn: DatabaseConnection = {
88
- select: async (sql, b) => { const r = await client.query(sql, b); return r.rows },
89
- statement: async (sql, b) => { const r = await client.query(sql, b); return r.rowCount ?? 0 },
90
- insertGetId: async (sql, b) => { const r = await client.query(sql, b); return r.rows[0]?.id ?? 0 },
91
- transaction: (cb) => cb(txConn),
92
- table: (name) => new QueryBuilder(txConn, name),
88
+ const txConn: any = {
89
+ _grammar: this._grammar,
90
+ select: async (sql: string, b?: any[]) => { const r = await client.query(sql, b); return r.rows },
91
+ statement: async (sql: string, b?: any[]) => { const r = await client.query(sql, b); return r.rowCount ?? 0 },
92
+ insertGetId: async (sql: string, b?: any[]) => { const r = await client.query(sql, b); return r.rows[0]?.id ?? 0 },
93
+ transaction: (cb: any) => cb(txConn),
94
+ table: (name: string) => new QueryBuilder(txConn, name),
93
95
  schema: () => new SchemaBuilderImpl(txConn),
94
96
  getDriverName: () => 'postgres',
95
97
  getTablePrefix: () => '',
96
98
  }
97
- // @ts-ignore — attach grammar for the builder
98
- txConn._grammar = this._grammar
99
+ this.applyExecuteMethods(txConn)
99
100
  const result = await callback(txConn)
100
101
  await client.query('COMMIT')
101
102
  return result
@@ -107,10 +108,6 @@ export class PostgresConnection implements DatabaseConnection {
107
108
  }
108
109
  }
109
110
 
110
- table(name: string): QueryBuilder {
111
- return new QueryBuilder(this, name)
112
- }
113
-
114
111
  schema(): SchemaBuilder {
115
112
  return new SchemaBuilderImpl(this)
116
113
  }
@@ -118,8 +115,4 @@ export class PostgresConnection implements DatabaseConnection {
118
115
  getDriverName(): string {
119
116
  return 'postgres'
120
117
  }
121
-
122
- getTablePrefix(): string {
123
- return ''
124
- }
125
118
  }
@@ -1,6 +1,7 @@
1
- import type { DatabaseConnection } from '../contracts/Connection.ts'
2
1
  import type { SchemaBuilder } from '../schema/SchemaBuilder.ts'
3
2
  import type { EventDispatcher } from '@mantiq/core'
3
+ import type { DatabaseConnection } from '../contracts/Connection.ts'
4
+ import { BaseSQLConnection } from './BaseSQLConnection.ts'
4
5
  import { QueryBuilder } from '../query/Builder.ts'
5
6
  import { SQLiteGrammar } from './SQLiteGrammar.ts'
6
7
  import { SchemaBuilderImpl } from '../schema/SchemaBuilder.ts'
@@ -12,7 +13,7 @@ export interface SQLiteConfig {
12
13
  database: string // ':memory:' or file path
13
14
  }
14
15
 
15
- export class SQLiteConnection implements DatabaseConnection {
16
+ export class SQLiteConnection extends BaseSQLConnection {
16
17
  readonly _grammar = new SQLiteGrammar()
17
18
  private db: import('bun:sqlite').Database | null = null
18
19
  private config: SQLiteConfig
@@ -21,6 +22,7 @@ export class SQLiteConnection implements DatabaseConnection {
21
22
  static _dispatcher: EventDispatcher | null = null
22
23
 
23
24
  constructor(config: SQLiteConfig) {
25
+ super()
24
26
  this.config = config
25
27
  }
26
28
 
@@ -102,10 +104,6 @@ export class SQLiteConnection implements DatabaseConnection {
102
104
  await SQLiteConnection._dispatcher?.emit(new QueryExecuted(sql, bindings, time, 'sqlite'))
103
105
  }
104
106
 
105
- table(name: string): QueryBuilder {
106
- return new QueryBuilder(this, name)
107
- }
108
-
109
107
  schema(): SchemaBuilder {
110
108
  return new SchemaBuilderImpl(this)
111
109
  }
@@ -114,10 +112,6 @@ export class SQLiteConnection implements DatabaseConnection {
114
112
  return 'sqlite'
115
113
  }
116
114
 
117
- getTablePrefix(): string {
118
- return ''
119
- }
120
-
121
115
  close(): void {
122
116
  this.db?.close()
123
117
  this.db = null
@@ -0,0 +1,6 @@
1
+ export class DriverNotSupportedError extends Error {
2
+ constructor(driver: string, feature: string) {
3
+ super(`"${feature}" is not supported by the ${driver} driver.`)
4
+ this.name = 'DriverNotSupportedError'
5
+ }
6
+ }
package/src/index.ts CHANGED
@@ -21,6 +21,7 @@ export type {
21
21
  export { QueryError } from './errors/QueryError.ts'
22
22
  export { ModelNotFoundError } from './errors/ModelNotFoundError.ts'
23
23
  export { ConnectionError } from './errors/ConnectionError.ts'
24
+ export { DriverNotSupportedError } from './errors/DriverNotSupportedError.ts'
24
25
 
25
26
  // ── Query Builder ─────────────────────────────────────────────────────────────
26
27
  export { QueryBuilder } from './query/Builder.ts'
@@ -34,6 +35,9 @@ export { PostgresGrammar } from './drivers/PostgresGrammar.ts'
34
35
  export { MySQLGrammar } from './drivers/MySQLGrammar.ts'
35
36
  export { MSSQLGrammar } from './drivers/MSSQLGrammar.ts'
36
37
 
38
+ // ── Base Connection ──────────────────────────────────────────────────────────
39
+ export { BaseSQLConnection } from './drivers/BaseSQLConnection.ts'
40
+
37
41
  // ── SQL Connections ───────────────────────────────────────────────────────────
38
42
  export { SQLiteConnection } from './drivers/SQLiteConnection.ts'
39
43
  export type { SQLiteConfig } from './drivers/SQLiteConnection.ts'
@@ -76,7 +80,8 @@ export type { ModelStatic } from './orm/Model.ts'
76
80
  export { ModelQueryBuilder } from './orm/ModelQueryBuilder.ts'
77
81
  export { Collection } from './orm/Collection.ts'
78
82
 
79
- // ── MongoDB Document ORM ──────────────────────────────────────────────────────
83
+ // ── MongoDB Document ORM (deprecated — use Model with MongoDB connection) ────
84
+ /** @deprecated Use Model with a MongoDB connection instead */
80
85
  export { Document } from './orm/Document.ts'
81
86
 
82
87
  // ── Seeders & Factories ───────────────────────────────────────────────────────
package/src/orm/Model.ts CHANGED
@@ -12,6 +12,8 @@ export interface ModelStatic<T extends Model> {
12
12
  connection: DatabaseConnection | null
13
13
  table: string
14
14
  primaryKey: string
15
+ incrementing: boolean
16
+ keyType: 'int' | 'string'
15
17
  fillable: string[]
16
18
  guarded: string[]
17
19
  hidden: string[]
@@ -56,6 +58,8 @@ export abstract class Model {
56
58
  static connection: DatabaseConnection | null = null
57
59
  static table: string = ''
58
60
  static primaryKey: string = 'id'
61
+ static incrementing = true
62
+ static keyType: 'int' | 'string' = 'int'
59
63
  static fillable: string[] = []
60
64
  static guarded: string[] = ['id']
61
65
  static hidden: string[] = []
@@ -470,7 +474,7 @@ export abstract class Model {
470
474
  }
471
475
 
472
476
  const id = await ctor.connection.table(table).insertGetId(this._attributes)
473
- this._attributes[ctor.primaryKey] = Number(id)
477
+ this._attributes[ctor.primaryKey] = ctor.incrementing ? Number(id) : id
474
478
  this._original = { ...this._attributes }
475
479
  this._exists = true
476
480
 
@@ -92,11 +92,10 @@ export class QueryBuilder {
92
92
  return this
93
93
  }
94
94
 
95
- // ── Where conditions ─────────────────────────────────────────────────────
95
+ // ── Where conditions ───────────────────────────────────────────────────────
96
96
 
97
97
  where(column: string | ((q: QueryBuilder) => void), operatorOrValue?: any, value?: any): this {
98
98
  if (typeof column === 'function') {
99
- const nested: WhereClause[] = []
100
99
  const sub = new QueryBuilder(this._connection, this.state.table)
101
100
  column(sub)
102
101
  this.state.wheres.push({ type: 'nested', boolean: 'and', nested: sub.state.wheres })
@@ -224,7 +223,7 @@ export class QueryBuilder {
224
223
  return this.whereRaw(`strftime('%H:%M:%S', ${column}) ${op} ?`, [val])
225
224
  }
226
225
 
227
- // ── Joins ─────────────────────────────────────────────────────────────────
226
+ // ── Joins ───────────────────────────────────────────────────────────────────
228
227
 
229
228
  join(table: string, first: string, operator: string, second: string): this {
230
229
  this.state.joins.push({ type: 'inner', table, first, operator, second })
@@ -241,7 +240,7 @@ export class QueryBuilder {
241
240
  return this
242
241
  }
243
242
 
244
- // ── Ordering / Grouping ───────────────────────────────────────────────────
243
+ // ── Ordering / Grouping ─────────────────────────────────────────────────────
245
244
 
246
245
  orderBy(column: string | Expression, direction: 'asc' | 'desc' = 'asc'): this {
247
246
  this.state.orders.push({ column, direction })
@@ -267,7 +266,7 @@ export class QueryBuilder {
267
266
  return this
268
267
  }
269
268
 
270
- // ── Pagination ────────────────────────────────────────────────────────────
269
+ // ── Pagination ──────────────────────────────────────────────────────────────
271
270
 
272
271
  limit(value: number): this {
273
272
  this.state.limitValue = value
@@ -282,11 +281,10 @@ export class QueryBuilder {
282
281
  take = this.limit
283
282
  skip = this.offset
284
283
 
285
- // ── Execution ─────────────────────────────────────────────────────────────
284
+ // ── Execution ───────────────────────────────────────────────────────────────
286
285
 
287
286
  async get(): Promise<Record<string, any>[]> {
288
- const { sql, bindings } = this.grammar().compileSelect(this.state)
289
- return this._connection.select(sql, bindings)
287
+ return this._connection.executeSelect(this.state)
290
288
  }
291
289
 
292
290
  async first(): Promise<Record<string, any> | null> {
@@ -315,8 +313,7 @@ export class QueryBuilder {
315
313
  }
316
314
 
317
315
  async exists(): Promise<boolean> {
318
- const row = await this.selectRaw('1 as exists_check').limit(1).first()
319
- return row !== null
316
+ return this._connection.executeExists(this.state)
320
317
  }
321
318
 
322
319
  async doesntExist(): Promise<boolean> {
@@ -333,52 +330,43 @@ export class QueryBuilder {
333
330
  return results[0]!
334
331
  }
335
332
 
336
- // ── Aggregates ────────────────────────────────────────────────────────────
333
+ // ── Aggregates ──────────────────────────────────────────────────────────────
337
334
 
338
335
  async count(column = '*'): Promise<number> {
339
- const row = await this.selectRaw(`COUNT(${column}) as aggregate`).first()
340
- return Number(row?.['aggregate'] ?? 0)
336
+ return this._connection.executeAggregate(this.state, 'count', column)
341
337
  }
342
338
 
343
339
  async sum(column: string): Promise<number> {
344
- const row = await this.selectRaw(`SUM(${column}) as aggregate`).first()
345
- return Number(row?.['aggregate'] ?? 0)
340
+ return this._connection.executeAggregate(this.state, 'sum', column)
346
341
  }
347
342
 
348
343
  async avg(column: string): Promise<number> {
349
- const row = await this.selectRaw(`AVG(${column}) as aggregate`).first()
350
- return Number(row?.['aggregate'] ?? 0)
344
+ return this._connection.executeAggregate(this.state, 'avg', column)
351
345
  }
352
346
 
353
347
  async min(column: string): Promise<any> {
354
- const row = await this.selectRaw(`MIN(${column}) as aggregate`).first()
355
- return row?.['aggregate']
348
+ return this._connection.executeAggregate(this.state, 'min', column)
356
349
  }
357
350
 
358
351
  async max(column: string): Promise<any> {
359
- const row = await this.selectRaw(`MAX(${column}) as aggregate`).first()
360
- return row?.['aggregate']
352
+ return this._connection.executeAggregate(this.state, 'max', column)
361
353
  }
362
354
 
363
- // ── Writes ────────────────────────────────────────────────────────────────
355
+ // ── Writes ──────────────────────────────────────────────────────────────────
364
356
 
365
357
  async insert(data: Record<string, any> | Record<string, any>[]): Promise<void> {
366
358
  const rows = Array.isArray(data) ? data : [data]
367
359
  for (const row of rows) {
368
- const { sql, bindings } = this.grammar().compileInsert(this.state.table, row)
369
- await this._connection.statement(sql, bindings)
360
+ await this._connection.executeInsert(this.state.table, row)
370
361
  }
371
362
  }
372
363
 
373
- async insertGetId(data: Record<string, any>): Promise<number> {
374
- const { sql, bindings } = this.grammar().compileInsertGetId(this.state.table, data)
375
- const id = await this._connection.insertGetId(sql, bindings)
376
- return Number(id)
364
+ async insertGetId(data: Record<string, any>): Promise<number | string> {
365
+ return this._connection.executeInsertGetId(this.state.table, data)
377
366
  }
378
367
 
379
368
  async update(data: Record<string, any>): Promise<number> {
380
- const { sql, bindings } = this.grammar().compileUpdate(this.state.table, this.state, data)
381
- return this._connection.statement(sql, bindings)
369
+ return this._connection.executeUpdate(this.state.table, this.state, data)
382
370
  }
383
371
 
384
372
  async updateOrInsert(
@@ -396,19 +384,19 @@ export class QueryBuilder {
396
384
  }
397
385
 
398
386
  async delete(): Promise<number> {
399
- const { sql, bindings } = this.grammar().compileDelete(this.state.table, this.state)
400
- return this._connection.statement(sql, bindings)
387
+ return this._connection.executeDelete(this.state.table, this.state)
401
388
  }
402
389
 
403
390
  async truncate(): Promise<void> {
404
- const sql = this.grammar().compileTruncate(this.state.table)
405
- await this._connection.statement(sql, [])
391
+ return this._connection.executeTruncate(this.state.table)
406
392
  }
407
393
 
408
- // ── Pagination ────────────────────────────────────────────────────────────
394
+ // ── Pagination ──────────────────────────────────────────────────────────────
409
395
 
410
396
  async paginate(page = 1, perPage = 15): Promise<PaginationResult> {
411
- const total = await this.clone().count()
397
+ const countQuery = this.clone()
398
+ countQuery.state.orders = []
399
+ const total = await countQuery.count()
412
400
  const lastPage = Math.max(1, Math.ceil(total / perPage))
413
401
  const currentPage = Math.min(page, lastPage)
414
402
  const data = await this.clone().limit(perPage).offset((currentPage - 1) * perPage).get()
@@ -417,14 +405,20 @@ export class QueryBuilder {
417
405
  return { data, total, perPage, currentPage, lastPage, from, to, hasMore: currentPage < lastPage }
418
406
  }
419
407
 
420
- // ── Utilities ─────────────────────────────────────────────────────────────
408
+ // ── Utilities ───────────────────────────────────────────────────────────────
421
409
 
410
+ /** Returns the SQL for this query. Only works on SQL connections. */
422
411
  toSql(): string {
423
- return this.grammar().compileSelect(this.state).sql
412
+ const grammar = this.getGrammar()
413
+ if (!grammar) throw new Error('toSql() is only available on SQL connections')
414
+ return grammar.compileSelect(this.state).sql
424
415
  }
425
416
 
417
+ /** Returns the bindings for this query. Only works on SQL connections. */
426
418
  getBindings(): any[] {
427
- return this.grammar().compileSelect(this.state).bindings
419
+ const grammar = this.getGrammar()
420
+ if (!grammar) throw new Error('getBindings() is only available on SQL connections')
421
+ return grammar.compileSelect(this.state).bindings
428
422
  }
429
423
 
430
424
  clone(): QueryBuilder {
@@ -445,10 +439,9 @@ export class QueryBuilder {
445
439
  return this.state
446
440
  }
447
441
 
448
- // ── Grammar (driver-specific SQL) ─────────────────────────────────────────
449
-
450
- protected grammar() {
451
- return (this._connection as any)._grammar as import('../contracts/Grammar.ts').Grammar
442
+ /** Returns the Grammar if this is a SQL connection, null otherwise. */
443
+ protected getGrammar(): import('../contracts/Grammar.ts').Grammar | null {
444
+ return (this._connection as any)._grammar ?? null
452
445
  }
453
446
  }
454
447