@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 +2 -2
- package/package.json +5 -5
- package/src/contracts/Connection.ts +15 -2
- package/src/drivers/BaseSQLConnection.ts +111 -0
- package/src/drivers/MSSQLConnection.ts +11 -17
- package/src/drivers/MongoConnection.ts +285 -112
- package/src/drivers/MySQLConnection.ts +11 -17
- package/src/drivers/PostgresConnection.ts +11 -18
- package/src/drivers/SQLiteConnection.ts +4 -10
- package/src/errors/DriverNotSupportedError.ts +6 -0
- package/src/index.ts +6 -1
- package/src/orm/Model.ts +5 -1
- package/src/query/Builder.ts +36 -43
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/
|
|
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/
|
|
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.
|
|
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/
|
|
8
|
+
"homepage": "https://github.com/mantiqjs/mantiq/tree/main/packages/database",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "https://github.com/
|
|
11
|
+
"url": "https://github.com/mantiqjs/mantiq.git",
|
|
12
12
|
"directory": "packages/database"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/
|
|
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
|
|
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:
|
|
104
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
24
|
-
constructor(
|
|
25
|
-
private readonly col: any,
|
|
26
|
-
private readonly name: string,
|
|
27
|
-
) {}
|
|
27
|
+
// ── Operator translation map ──────────────────────────────────────────────────
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
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
|
-
|
|
45
|
-
return this.col.findOne(filter)
|
|
46
|
-
}
|
|
39
|
+
// ── MongoConnection — implements the universal DatabaseConnection interface ──
|
|
47
40
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
41
|
+
export class MongoConnection implements DatabaseConnection {
|
|
42
|
+
private client: any = null
|
|
43
|
+
private db: any = null
|
|
44
|
+
private config: MongoConfig
|
|
52
45
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
return { insertedId: result.insertedId, acknowledged: result.acknowledged }
|
|
46
|
+
constructor(config: MongoConfig) {
|
|
47
|
+
this.config = config
|
|
56
48
|
}
|
|
57
49
|
|
|
58
|
-
async
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
66
|
+
async executeSelect(state: QueryState): Promise<Record<string, any>[]> {
|
|
67
|
+
this.guardNoJoins(state)
|
|
68
|
+
this.guardNoHavings(state)
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
|
83
|
-
const
|
|
84
|
-
|
|
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
|
|
88
|
-
const
|
|
89
|
-
|
|
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
|
|
93
|
-
const
|
|
94
|
-
|
|
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
|
|
98
|
-
|
|
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
|
|
102
|
-
|
|
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
|
|
106
|
-
|
|
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
|
|
110
|
-
|
|
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
|
-
|
|
115
|
-
private client: any = null
|
|
116
|
-
private db: any = null
|
|
117
|
-
private config: MongoConfig
|
|
142
|
+
// ── Raw SQL methods — throw on MongoDB ────────────────────────────────────
|
|
118
143
|
|
|
119
|
-
|
|
120
|
-
|
|
144
|
+
async select(sql: string, bindings?: any[]): Promise<Record<string, any>[]> {
|
|
145
|
+
throw new DriverNotSupportedError('mongodb', 'raw SQL queries')
|
|
121
146
|
}
|
|
122
147
|
|
|
123
|
-
|
|
124
|
-
|
|
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
|
-
|
|
138
|
-
|
|
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
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// ── Shared interface ──────────────────────────────────────────────────────
|
|
157
|
+
|
|
158
|
+
table(name: string): QueryBuilder {
|
|
159
|
+
return new QueryBuilder(this, name)
|
|
157
160
|
}
|
|
158
161
|
|
|
159
|
-
|
|
160
|
-
|
|
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:
|
|
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
|
|
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:
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
|
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
|
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
|
|
package/src/query/Builder.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
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
|
|