@avelonjs/postgres 0.1.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/LICENSE +21 -0
- package/README.md +197 -0
- package/package.json +51 -0
- package/src/compile.ts +177 -0
- package/src/driver.ts +386 -0
- package/src/errors.ts +100 -0
- package/src/fixtures.ts +59 -0
- package/src/index.ts +19 -0
- package/src/migrations.ts +123 -0
- package/src/normalize.ts +50 -0
- package/src/schema.ts +123 -0
- package/src/validate.ts +236 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Ryan Yannelli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
# @avelonjs/postgres
|
|
2
|
+
|
|
3
|
+
`@avelonjs/postgres` is the Postgres database driver for Avelon. It compiles the frozen `QueryIR` into parameterized SQL, runs it through Bun's SQL client, and maps vendor failures into the framework error taxonomy. Reach for this package when your application needs transactions, deep relation loads, upserts with returning, or an independent Postgres connection beside Supabase.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
bun add @avelonjs/postgres
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Set a connection URL in the environment or pass one when constructing the driver:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
export POSTGRES_URL=postgresql://postgres:avelon@127.0.0.1:5432/avelon
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { createPostgresDatabase } from '@avelonjs/postgres'
|
|
21
|
+
import type { QueryIR } from '@avelonjs/core'
|
|
22
|
+
|
|
23
|
+
const db = createPostgresDatabase({
|
|
24
|
+
url: process.env.POSTGRES_URL,
|
|
25
|
+
instance: 'primary',
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
const latest: QueryIR = {
|
|
29
|
+
table: 'posts',
|
|
30
|
+
mode: 'select',
|
|
31
|
+
select: ['id', 'title'],
|
|
32
|
+
where: [{ kind: 'null', column: 'published_at', negated: true }],
|
|
33
|
+
relations: [],
|
|
34
|
+
order: [{ column: 'published_at', direction: 'desc' }],
|
|
35
|
+
limit: 10,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const result = await db.execute<{ id: string; title: string }>(latest)
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Capabilities
|
|
42
|
+
|
|
43
|
+
| Capability | Value | Notes |
|
|
44
|
+
|---|---|---|
|
|
45
|
+
| `transactions` | `true` | Interactive transactions through `db.transaction()` |
|
|
46
|
+
| `rowSecurity` | `false` | Ward-to-RLS compilation belongs to `@avelonjs/supabase` |
|
|
47
|
+
| `maxRelationDepth` | `8` | Application-side nested loads; measured against nested assay fixtures |
|
|
48
|
+
| `fullTextSearch` | `false` | No portable `search()` surface in v1 |
|
|
49
|
+
| `upsert` | `true` | Requires explicit conflict target and update list |
|
|
50
|
+
| `returning` | `true` | Write queries may project rows |
|
|
51
|
+
| `windowFunctions` | `true` | Informational; available through `raw()` |
|
|
52
|
+
| `jsonOperators` | `true` | Informational; available through `raw()` |
|
|
53
|
+
|
|
54
|
+
## Query Compilation
|
|
55
|
+
|
|
56
|
+
The driver validates IR shape, rejects unknown public-schema identifiers, normalizes predicates, and compiles positional SQL. Empty AND is true, empty OR is false, empty IN matches nothing unless negated, and a constant-false ward or where short-circuits without a database round trip.
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import { compilePostgres, combinedPredicate } from '@avelonjs/postgres'
|
|
60
|
+
import type { QueryIR } from '@avelonjs/core'
|
|
61
|
+
|
|
62
|
+
const query: QueryIR = {
|
|
63
|
+
table: 'assay_users',
|
|
64
|
+
mode: 'select',
|
|
65
|
+
select: ['id'],
|
|
66
|
+
where: [{ kind: 'compare', column: 'age', op: '>=', value: 18 }],
|
|
67
|
+
ward: { kind: 'compare', column: 'age', op: '<', value: 30 },
|
|
68
|
+
relations: [],
|
|
69
|
+
order: [],
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const compiled = compilePostgres(query, combinedPredicate(query))
|
|
73
|
+
// compiled.text binds age thresholds as $1 and $2
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Transactions
|
|
77
|
+
|
|
78
|
+
```ts
|
|
79
|
+
import { createPostgresDatabase } from '@avelonjs/postgres'
|
|
80
|
+
|
|
81
|
+
const db = createPostgresDatabase()
|
|
82
|
+
|
|
83
|
+
await db.transaction(async (tx) => {
|
|
84
|
+
await tx.execute({
|
|
85
|
+
table: 'assay_users',
|
|
86
|
+
mode: 'insert',
|
|
87
|
+
select: [],
|
|
88
|
+
where: [],
|
|
89
|
+
relations: [],
|
|
90
|
+
order: [],
|
|
91
|
+
values: { id: 'u1', email: 'one@example.test', name: 'One', age: 20, nickname: null },
|
|
92
|
+
})
|
|
93
|
+
})
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Migrations
|
|
97
|
+
|
|
98
|
+
Migrations are driver-owned SQL pairs registered on the driver instance.
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
import { createPostgresDatabase } from '@avelonjs/postgres'
|
|
102
|
+
|
|
103
|
+
const db = createPostgresDatabase({
|
|
104
|
+
migrations: [
|
|
105
|
+
{
|
|
106
|
+
id: '20260804_create_posts',
|
|
107
|
+
up: [
|
|
108
|
+
'CREATE TABLE posts (id text PRIMARY KEY, title text NOT NULL)',
|
|
109
|
+
],
|
|
110
|
+
down: ['DROP TABLE posts'],
|
|
111
|
+
},
|
|
112
|
+
],
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
await db.plan()
|
|
116
|
+
await db.apply()
|
|
117
|
+
await db.status()
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Error Mapping
|
|
121
|
+
|
|
122
|
+
Vendor SQLSTATE values never leave the driver. Unique violations become `Conflict`, unknown tables/columns/routines become `Invalid`, and connection failures become `Unavailable`. Every other code becomes `DriverFault`.
|
|
123
|
+
|
|
124
|
+
| SQLSTATE | Framework error | Meaning |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `23505` | `Conflict` | unique_violation |
|
|
127
|
+
| `42P01` | `Invalid` | undefined_table |
|
|
128
|
+
| `42703` | `Invalid` | undefined_column |
|
|
129
|
+
| `42883` | `Invalid` | undefined_function |
|
|
130
|
+
| `08006` | `Unavailable` | connection_failure |
|
|
131
|
+
|
|
132
|
+
## Live Conformance
|
|
133
|
+
|
|
134
|
+
Fixture provisioning is owned by this package. Reset the assay schema, then run the shared database suite against a live Postgres:
|
|
135
|
+
|
|
136
|
+
```sh
|
|
137
|
+
export POSTGRES_URL=postgresql://postgres:avelon@127.0.0.1:5432/avelon_test
|
|
138
|
+
bun run fixtures:reset
|
|
139
|
+
bun test
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
## Method Reference
|
|
143
|
+
|
|
144
|
+
| Method | Signature | Description |
|
|
145
|
+
|---|---|---|
|
|
146
|
+
| `createPostgresDatabase` | `(options?: PostgresDatabaseOptions) => PostgresDatabase` | Constructs a driver from options or `POSTGRES_URL` / `DATABASE_URL`. |
|
|
147
|
+
| `postgresDatabaseCapabilities` | `{ transactions: true, rowSecurity: false, maxRelationDepth: 8, fullTextSearch: false, upsert: true, returning: true, windowFunctions: true, jsonOperators: true }` | Exact capability declaration for the Postgres driver. |
|
|
148
|
+
| `PostgresDatabaseOptions` | `interface` | Connection URL, instance name, and optional migrations. |
|
|
149
|
+
| `PostgresDatabase.execute` | `(query: QueryIR) => Promise<QueryResult>` | Validates, compiles, and executes one query IR operation. |
|
|
150
|
+
| `PostgresDatabase.rpc` | `(routine: string, args: Readonly<Record<string, unknown>>) => Promise<T>` | Invokes a Postgres routine; missing routines raise `Invalid`. |
|
|
151
|
+
| `PostgresDatabase.transaction` | `(callback: (tx: DatabaseTransaction) => Promise<T>) => Promise<T>` | Runs the callback atomically and returns its result. |
|
|
152
|
+
| `PostgresDatabase.plan` | `() => Promise<MigrationPlan>` | Returns pending migration identifiers and SQL steps. |
|
|
153
|
+
| `PostgresDatabase.apply` | `() => Promise<readonly MigrationStatus[]>` | Applies pending migrations inside transactions. |
|
|
154
|
+
| `PostgresDatabase.rollback` | `(steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
155
|
+
| `PostgresDatabase.status` | `() => Promise<readonly MigrationStatus[]>` | Lists applied and pending migration states. |
|
|
156
|
+
| `PostgresDatabase.raw` | `() => SQL` | Returns the Bun SQL client at the vendor boundary. |
|
|
157
|
+
| `PostgresDatabase.resetFixtures` | `() => Promise<void>` | Recreates assay tables and `assay_echo` for live conformance. |
|
|
158
|
+
| `PostgresDatabase.close` | `() => Promise<void>` | Closes the underlying SQL client pool. |
|
|
159
|
+
| `compilePostgres` | `(ir: QueryIR, predicate?: Predicate) => CompiledSql` | Compiles IR to parameterized SQL without executing it. |
|
|
160
|
+
| `compileSqlPredicate` | `(predicate: Predicate, bind: (value: unknown) => string) => string` | Compiles a normalized predicate to a SQL boolean expression. |
|
|
161
|
+
| `CompiledSql` | `interface` | Parameterized `text` plus positional `parameters`. |
|
|
162
|
+
| `normalizePredicate` | `(predicate: Predicate) => Predicate` | Applies empty-list and constant identities. |
|
|
163
|
+
| `combinedPredicate` | `(ir: Pick<QueryIR, 'where' \| 'ward'>) => Predicate` | ANDs where and ward, then normalizes. |
|
|
164
|
+
| `mapPostgresError` | `(error: unknown, operation: string) => never` | Maps vendor failures into framework errors. |
|
|
165
|
+
| `POSTGRES_ERROR_MAP` | `readonly { sqlstate, framework, meaning }[]` | SQLSTATE values this driver maps into the taxonomy. |
|
|
166
|
+
| `validateQueryIR` | `(ir: QueryIR, maxRelationDepth: number) => void` | Rejects malformed IR before compilation. |
|
|
167
|
+
| `assertIdentifier` | `(value: unknown, path: string) => asserts value is string` | Rejects identifiers that are not simple SQL names. |
|
|
168
|
+
| `assertQueryAgainstSchema` | `(cache: SchemaCache, query: QueryIR) => void` | Rejects unknown public-schema tables and columns. |
|
|
169
|
+
| `loadSchemaCache` | `(sql: SQL) => Promise<SchemaCache>` | Loads public base tables and their columns. |
|
|
170
|
+
| `SchemaCache` | `type` | `Map` of table name to column set. |
|
|
171
|
+
| `planMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<MigrationPlan>` | Builds a pending plan from registered migrations and history. |
|
|
172
|
+
| `applyMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<readonly MigrationStatus[]>` | Applies pending migrations inside transactions. |
|
|
173
|
+
| `rollbackMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[], steps?: number) => Promise<readonly MigrationStatus[]>` | Rolls back the newest applied migration batches. |
|
|
174
|
+
| `statusMigrations` | `(sql: SQL, migrations: readonly PostgresMigration[]) => Promise<readonly MigrationStatus[]>` | Returns applied/pending status for every registered migration. |
|
|
175
|
+
| `PostgresMigration` | `interface` | Driver-owned `id`, `up`, and `down` SQL pair. |
|
|
176
|
+
| `resetAssayFixtures` | `(sql: SQL) => Promise<void>` | Provisions empty assay fixtures on a SQL client. |
|
|
177
|
+
| `ASSAY_FIXTURE_SQL` | `string` | SQL that drops and recreates the database conformance fixtures. |
|
|
178
|
+
|
|
179
|
+
## Testing
|
|
180
|
+
|
|
181
|
+
Use the shared database conformance suite with a live database. The package ships fixture reset helpers so tests do not rely on hand-maintained schema.
|
|
182
|
+
|
|
183
|
+
```ts
|
|
184
|
+
import { databaseSuite } from '@avelonjs/conformance/suites'
|
|
185
|
+
import { createPostgresDatabase } from '@avelonjs/postgres'
|
|
186
|
+
|
|
187
|
+
databaseSuite({
|
|
188
|
+
name: 'live postgres',
|
|
189
|
+
create: async () => {
|
|
190
|
+
const driver = createPostgresDatabase()
|
|
191
|
+
await driver.resetFixtures()
|
|
192
|
+
return driver
|
|
193
|
+
},
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Unit tests cover SQL compilation and error mapping without a network round trip. Live tests require Postgres and fail closed when the database is unreachable.
|
package/package.json
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@avelonjs/postgres",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "Postgres database driver that compiles Avelon QueryIR to SQL.",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Ryan Yannelli <ryanyannelli@gmail.com>",
|
|
8
|
+
"homepage": "https://github.com/yannelli/avelon",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/yannelli/avelon.git",
|
|
12
|
+
"directory": "packages/postgres"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/yannelli/avelon/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"avelon",
|
|
19
|
+
"typescript",
|
|
20
|
+
"postgres",
|
|
21
|
+
"database"
|
|
22
|
+
],
|
|
23
|
+
"type": "module",
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public"
|
|
26
|
+
},
|
|
27
|
+
"files": [
|
|
28
|
+
"src",
|
|
29
|
+
"README.md",
|
|
30
|
+
"LICENSE"
|
|
31
|
+
],
|
|
32
|
+
"exports": {
|
|
33
|
+
".": "./src/index.ts"
|
|
34
|
+
},
|
|
35
|
+
"scripts": {
|
|
36
|
+
"test": "bun test",
|
|
37
|
+
"typecheck": "tsc --noEmit",
|
|
38
|
+
"fixtures:reset": "bun ./scripts/reset-fixtures.ts"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"@avelonjs/core": "workspace:*"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@avelonjs/conformance": "workspace:*",
|
|
45
|
+
"@types/bun": "1.3.14",
|
|
46
|
+
"typescript": "5.9.3"
|
|
47
|
+
},
|
|
48
|
+
"engines": {
|
|
49
|
+
"bun": ">=1.3.14"
|
|
50
|
+
}
|
|
51
|
+
}
|
package/src/compile.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import { Invalid, type CompareOp, type OrderTerm, type Predicate, type QueryIR } from '@avelonjs/core'
|
|
2
|
+
import { combinedPredicate } from './normalize'
|
|
3
|
+
import { assertIdentifier } from './validate'
|
|
4
|
+
|
|
5
|
+
/** Compiled SQL text with positional parameters. */
|
|
6
|
+
export interface CompiledSql {
|
|
7
|
+
/** Parameterized SQL statement. */
|
|
8
|
+
text: string
|
|
9
|
+
/** Values bound to `$1..$n` placeholders. */
|
|
10
|
+
parameters: unknown[]
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type Row = Record<string, unknown>
|
|
14
|
+
|
|
15
|
+
const SQL_OPERATOR: Record<CompareOp, string> = {
|
|
16
|
+
'=': '=',
|
|
17
|
+
'!=': '<>',
|
|
18
|
+
'<': '<',
|
|
19
|
+
'<=': '<=',
|
|
20
|
+
'>': '>',
|
|
21
|
+
'>=': '>=',
|
|
22
|
+
like: 'LIKE',
|
|
23
|
+
ilike: 'ILIKE',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function invalid(message: string, field: string, detail: string): never {
|
|
27
|
+
throw new Invalid(message, { metadata: { fields: { [field]: [detail] } } })
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function quoteIdentifier(identifier: string): string {
|
|
31
|
+
assertIdentifier(identifier, 'identifier')
|
|
32
|
+
return `"${identifier}"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sqlProjection(projection: string[] | '*'): string {
|
|
36
|
+
return projection === '*' ? '*' : projection.map(quoteIdentifier).join(', ')
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function sqlReturning(returning: string[] | '*' | undefined): string {
|
|
40
|
+
return returning === undefined ? '' : ` RETURNING ${sqlProjection(returning)}`
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function writeRows(ir: QueryIR): { rows: Row[]; columns: string[] } {
|
|
44
|
+
const rows = (Array.isArray(ir.values) ? ir.values : [ir.values]) as Row[]
|
|
45
|
+
const columns = Object.keys(rows[0] as Row)
|
|
46
|
+
return { rows, columns }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function ensureUnscopedWritePredicate(predicate: Predicate, mode: 'insert' | 'upsert'): void {
|
|
50
|
+
if (predicate.kind !== 'const' || !predicate.value) {
|
|
51
|
+
invalid(
|
|
52
|
+
`${mode} with a non-constant where or ward has no documented row scope.`,
|
|
53
|
+
'where',
|
|
54
|
+
`${mode} predicates are not documented`,
|
|
55
|
+
)
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Compiles a normalized predicate into a SQL boolean expression. */
|
|
60
|
+
export function compileSqlPredicate(
|
|
61
|
+
predicate: Predicate,
|
|
62
|
+
bind: (value: unknown) => string,
|
|
63
|
+
): string {
|
|
64
|
+
switch (predicate.kind) {
|
|
65
|
+
case 'const':
|
|
66
|
+
return predicate.value ? 'TRUE' : 'FALSE'
|
|
67
|
+
case 'compare':
|
|
68
|
+
return `${quoteIdentifier(predicate.column)} ${SQL_OPERATOR[predicate.op]} ${bind(predicate.value)}`
|
|
69
|
+
case 'null':
|
|
70
|
+
return `${quoteIdentifier(predicate.column)} IS ${predicate.negated ? 'NOT ' : ''}NULL`
|
|
71
|
+
case 'in':
|
|
72
|
+
if (predicate.values.length === 0) return predicate.negated ? 'TRUE' : 'FALSE'
|
|
73
|
+
return `${quoteIdentifier(predicate.column)} ${predicate.negated ? 'NOT ' : ''}IN (${predicate.values.map(bind).join(', ')})`
|
|
74
|
+
case 'and':
|
|
75
|
+
return `(${predicate.predicates.map((child) => compileSqlPredicate(child, bind)).join(' AND ')})`
|
|
76
|
+
case 'or':
|
|
77
|
+
return `(${predicate.predicates.map((child) => compileSqlPredicate(child, bind)).join(' OR ')})`
|
|
78
|
+
case 'not':
|
|
79
|
+
return `NOT (${compileSqlPredicate(predicate.predicate, bind)})`
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function compileSqlOrder(order: readonly OrderTerm[]): string {
|
|
84
|
+
if (order.length === 0) return ''
|
|
85
|
+
return ` ORDER BY ${order
|
|
86
|
+
.map(
|
|
87
|
+
(term) =>
|
|
88
|
+
`${quoteIdentifier(term.column)} ${term.direction.toUpperCase()}${
|
|
89
|
+
term.nulls === undefined ? '' : ` NULLS ${term.nulls.toUpperCase()}`
|
|
90
|
+
}`,
|
|
91
|
+
)
|
|
92
|
+
.join(', ')}`
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Compiles a validated query IR into parameterized Postgres SQL.
|
|
97
|
+
*
|
|
98
|
+
* Pass an already-normalized predicate when the caller has short-circuit logic of its own.
|
|
99
|
+
*/
|
|
100
|
+
export function compilePostgres(ir: QueryIR, predicate = combinedPredicate(ir)): CompiledSql {
|
|
101
|
+
const parameters: unknown[] = []
|
|
102
|
+
const bind = (value: unknown): string => {
|
|
103
|
+
parameters.push(value)
|
|
104
|
+
return `$${parameters.length}`
|
|
105
|
+
}
|
|
106
|
+
const where = compileSqlPredicate(predicate, bind)
|
|
107
|
+
const whereClause = where === 'TRUE' ? '' : ` WHERE ${where}`
|
|
108
|
+
const order = compileSqlOrder(ir.order)
|
|
109
|
+
const limit = ir.limit === undefined ? '' : ` LIMIT ${bind(ir.limit)}`
|
|
110
|
+
const offset = ir.offset === undefined ? '' : ` OFFSET ${bind(ir.offset)}`
|
|
111
|
+
|
|
112
|
+
switch (ir.mode) {
|
|
113
|
+
case 'select':
|
|
114
|
+
return {
|
|
115
|
+
text: `SELECT ${sqlProjection(ir.select)} FROM ${quoteIdentifier(ir.table)}${whereClause}${order}${limit}${offset}`,
|
|
116
|
+
parameters,
|
|
117
|
+
}
|
|
118
|
+
case 'count':
|
|
119
|
+
return {
|
|
120
|
+
text: `SELECT COUNT(*)::int AS count FROM ${quoteIdentifier(ir.table)}${whereClause}`,
|
|
121
|
+
parameters,
|
|
122
|
+
}
|
|
123
|
+
case 'insert': {
|
|
124
|
+
ensureUnscopedWritePredicate(predicate, 'insert')
|
|
125
|
+
const { rows, columns } = writeRows(ir)
|
|
126
|
+
const values = rows
|
|
127
|
+
.map((row) => `(${columns.map((column) => bind(row[column])).join(', ')})`)
|
|
128
|
+
.join(', ')
|
|
129
|
+
return {
|
|
130
|
+
text: `INSERT INTO ${quoteIdentifier(ir.table)} (${columns.map(quoteIdentifier).join(', ')}) VALUES ${values}${sqlReturning(ir.returning)}`,
|
|
131
|
+
parameters,
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
case 'update': {
|
|
135
|
+
const row = writeRows(ir).rows[0] as Row
|
|
136
|
+
const assignments = Object.keys(row).map(
|
|
137
|
+
(column) => `${quoteIdentifier(column)} = ${bind(row[column])}`,
|
|
138
|
+
)
|
|
139
|
+
return {
|
|
140
|
+
text: `UPDATE ${quoteIdentifier(ir.table)} SET ${assignments.join(', ')}${whereClause}${sqlReturning(ir.returning)}`,
|
|
141
|
+
parameters,
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
case 'delete':
|
|
145
|
+
return {
|
|
146
|
+
text: `DELETE FROM ${quoteIdentifier(ir.table)}${whereClause}${sqlReturning(ir.returning)}`,
|
|
147
|
+
parameters,
|
|
148
|
+
}
|
|
149
|
+
case 'upsert': {
|
|
150
|
+
ensureUnscopedWritePredicate(predicate, 'upsert')
|
|
151
|
+
const conflict = ir.conflict
|
|
152
|
+
if (conflict === undefined) invalid('upsert requires conflict.', 'conflict', 'Required for upsert')
|
|
153
|
+
const { rows, columns } = writeRows(ir)
|
|
154
|
+
const values = rows
|
|
155
|
+
.map((row) => `(${columns.map((column) => bind(row[column])).join(', ')})`)
|
|
156
|
+
.join(', ')
|
|
157
|
+
const updateColumns =
|
|
158
|
+
conflict.update === '*'
|
|
159
|
+
? columns.filter((column) => !conflict.columns.includes(column))
|
|
160
|
+
: [...conflict.update]
|
|
161
|
+
if (updateColumns.length === 0) {
|
|
162
|
+
invalid(
|
|
163
|
+
'upsert update list resolved to no columns.',
|
|
164
|
+
'conflict',
|
|
165
|
+
'Provide at least one update column',
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
const assignments = updateColumns.map(
|
|
169
|
+
(column) => `${quoteIdentifier(column)} = EXCLUDED.${quoteIdentifier(column)}`,
|
|
170
|
+
)
|
|
171
|
+
return {
|
|
172
|
+
text: `INSERT INTO ${quoteIdentifier(ir.table)} (${columns.map(quoteIdentifier).join(', ')}) VALUES ${values} ON CONFLICT (${conflict.columns.map(quoteIdentifier).join(', ')}) DO UPDATE SET ${assignments.join(', ')}${sqlReturning(ir.returning)}`,
|
|
173
|
+
parameters,
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|