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