@prisma-next/sql-builder 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 +68 -0
- package/package.json +66 -0
- package/src/exports/types.ts +8 -0
- package/src/expression.ts +158 -0
- package/src/index.ts +8 -0
- package/src/resolve.ts +14 -0
- package/src/runtime/builder-base.ts +369 -0
- package/src/runtime/expression-impl.ts +23 -0
- package/src/runtime/field-proxy.ts +37 -0
- package/src/runtime/functions.ts +190 -0
- package/src/runtime/index.ts +4 -0
- package/src/runtime/joined-tables-impl.ts +238 -0
- package/src/runtime/mutation-impl.ts +317 -0
- package/src/runtime/query-impl.ts +254 -0
- package/src/runtime/sql.ts +31 -0
- package/src/runtime/table-proxy-impl.ts +173 -0
- package/src/scope.ts +83 -0
- package/src/types/db.ts +13 -0
- package/src/types/grouped-query.ts +68 -0
- package/src/types/joined-tables.ts +6 -0
- package/src/types/mutation-query.ts +73 -0
- package/src/types/select-query.ts +64 -0
- package/src/types/shared.ts +121 -0
- package/src/types/table-proxy.ts +52 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @prisma-next/sql-builder
|
|
2
|
+
|
|
3
|
+
Type-safe SQL query builder for Prisma Next with runtime execution.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```typescript
|
|
8
|
+
import { sql } from '@prisma-next/sql-builder/runtime';
|
|
9
|
+
|
|
10
|
+
const db = sql({ context, runtime });
|
|
11
|
+
|
|
12
|
+
// SELECT with WHERE
|
|
13
|
+
const user = await db.users
|
|
14
|
+
.select('id', 'email')
|
|
15
|
+
.where((f, fns) => fns.eq(f.id, 1))
|
|
16
|
+
.first();
|
|
17
|
+
|
|
18
|
+
// Aliased expression select
|
|
19
|
+
const rows = await db.users
|
|
20
|
+
.select('id')
|
|
21
|
+
.select('userName', (f) => f.name)
|
|
22
|
+
.all();
|
|
23
|
+
|
|
24
|
+
// JOIN
|
|
25
|
+
const rows = await db.users
|
|
26
|
+
.innerJoin(db.posts, (f, fns) => fns.eq(f.users.id, f.posts.user_id))
|
|
27
|
+
.select('name', 'title')
|
|
28
|
+
.all();
|
|
29
|
+
|
|
30
|
+
// Self-join via .as()
|
|
31
|
+
const rows = await db.users
|
|
32
|
+
.as('invitee')
|
|
33
|
+
.innerJoin(db.users.as('inviter'), (f, fns) =>
|
|
34
|
+
fns.eq(f.invitee.invited_by_id, f.inviter.id),
|
|
35
|
+
)
|
|
36
|
+
.select('name')
|
|
37
|
+
.all();
|
|
38
|
+
|
|
39
|
+
// Subquery as join source
|
|
40
|
+
const sub = db.posts.select('user_id', 'title').as('sub');
|
|
41
|
+
const rows = await db.users
|
|
42
|
+
.innerJoin(sub, (f, fns) => fns.eq(f.users.id, f.sub.user_id))
|
|
43
|
+
.select('name', 'title')
|
|
44
|
+
.all();
|
|
45
|
+
|
|
46
|
+
// GROUP BY with aggregate
|
|
47
|
+
const counts = await db.posts
|
|
48
|
+
.select('user_id')
|
|
49
|
+
.select('cnt', (_f, fns) => fns.count())
|
|
50
|
+
.groupBy('user_id')
|
|
51
|
+
.having((_f, fns) => fns.gt(fns.count(), 1))
|
|
52
|
+
.all();
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Dependencies
|
|
56
|
+
|
|
57
|
+
- `@prisma-next/sql-relational-core` — AST nodes, execution context, query operation registry
|
|
58
|
+
- `@prisma-next/sql-runtime` — Runtime type for query execution
|
|
59
|
+
|
|
60
|
+
## Architecture
|
|
61
|
+
|
|
62
|
+
- **Domain:** SQL
|
|
63
|
+
- **Layer:** Lanes
|
|
64
|
+
- **Plane:** Runtime
|
|
65
|
+
|
|
66
|
+
## Status
|
|
67
|
+
|
|
68
|
+
See [STATUS.md](./STATUS.md) for covered clauses and known gaps.
|
package/package.json
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@prisma-next/sql-builder",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"sideEffects": false,
|
|
6
|
+
"description": "SQL builder lane for Prisma Next",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsdown",
|
|
9
|
+
"preemit": "pnpm --filter @prisma-next/cli... build",
|
|
10
|
+
"emit": "node ../../../1-framework/3-tooling/cli/dist/cli.js contract emit --config test/fixtures/prisma-next.config.ts",
|
|
11
|
+
"emit:check": "pnpm emit && git diff --exit-code test/fixtures/generated/",
|
|
12
|
+
"test": "vitest run --passWithNoTests",
|
|
13
|
+
"~test:coverage": "vitest run --coverage --passWithNoTests",
|
|
14
|
+
"typecheck": "tsc --noEmit",
|
|
15
|
+
"lint": "biome check . --error-on-warnings",
|
|
16
|
+
"lint:fix": "biome check --write .",
|
|
17
|
+
"lint:fix:unsafe": "biome check --write --unsafe .",
|
|
18
|
+
"clean": "rm -rf dist dist-tsc dist-tsc-prod coverage .tmp-output"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@prisma-next/sql-relational-core": "workspace:*"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@prisma-next/adapter-postgres": "workspace:*",
|
|
25
|
+
"@prisma-next/cli": "workspace:*",
|
|
26
|
+
"@prisma-next/contract": "workspace:*",
|
|
27
|
+
"@prisma-next/core-execution-plane": "workspace:*",
|
|
28
|
+
"@prisma-next/driver-postgres": "workspace:*",
|
|
29
|
+
"@prisma-next/extension-pgvector": "workspace:*",
|
|
30
|
+
"@prisma-next/family-sql": "workspace:*",
|
|
31
|
+
"@prisma-next/ids": "^0.0.1",
|
|
32
|
+
"@prisma-next/sql-contract": "workspace:*",
|
|
33
|
+
"@prisma-next/sql-runtime": "workspace:*",
|
|
34
|
+
"@prisma-next/sql-contract-ts": "workspace:*",
|
|
35
|
+
"@prisma-next/target-postgres": "workspace:*",
|
|
36
|
+
"@prisma-next/test-utils": "workspace:*",
|
|
37
|
+
"@prisma-next/tsconfig": "workspace:*",
|
|
38
|
+
"@prisma-next/tsdown": "workspace:*",
|
|
39
|
+
"@types/pg": "catalog:",
|
|
40
|
+
"pg": "catalog:",
|
|
41
|
+
"tsdown": "catalog:",
|
|
42
|
+
"typescript": "catalog:",
|
|
43
|
+
"vitest": "catalog:"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist",
|
|
47
|
+
"src"
|
|
48
|
+
],
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20"
|
|
51
|
+
},
|
|
52
|
+
"exports": {
|
|
53
|
+
".": "./dist/index.mjs",
|
|
54
|
+
"./types": "./dist/exports/types.mjs",
|
|
55
|
+
"./runtime": "./dist/runtime/index.mjs",
|
|
56
|
+
"./package.json": "./package.json"
|
|
57
|
+
},
|
|
58
|
+
"main": "./dist/index.mjs",
|
|
59
|
+
"module": "./dist/index.mjs",
|
|
60
|
+
"types": "./dist/index.d.mts",
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "https://github.com/prisma/prisma-next.git",
|
|
64
|
+
"directory": "packages/2-sql/4-lanes/sql-builder"
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { AggregateFunctions, Expression, Functions } from '../expression';
|
|
2
|
+
export type { ResolveRow } from '../resolve';
|
|
3
|
+
export type { GatedMethod, QueryContext, Scope, ScopeField, Subquery } from '../scope';
|
|
4
|
+
export type { Db } from '../types/db';
|
|
5
|
+
export type { GroupedQuery } from '../types/grouped-query';
|
|
6
|
+
export type { DeleteQuery, InsertQuery, UpdateQuery } from '../types/mutation-query';
|
|
7
|
+
export type { SelectQuery } from '../types/select-query';
|
|
8
|
+
export type { TableProxy } from '../types/table-proxy';
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import type { QueryOperationTypesBase } from '@prisma-next/sql-contract/types';
|
|
2
|
+
import type {
|
|
3
|
+
Expand,
|
|
4
|
+
ExpressionType,
|
|
5
|
+
QueryContext,
|
|
6
|
+
Scope,
|
|
7
|
+
ScopeField,
|
|
8
|
+
ScopeTable,
|
|
9
|
+
Subquery,
|
|
10
|
+
} from './scope';
|
|
11
|
+
|
|
12
|
+
export type Expression<T extends ScopeField> = {
|
|
13
|
+
[ExpressionType]: T;
|
|
14
|
+
buildAst(): import('@prisma-next/sql-relational-core/ast').AnyExpression;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type WithField<Source, Field extends ScopeField, Alias extends string> = Expand<
|
|
18
|
+
Source & { [K in Alias]: Field }
|
|
19
|
+
>;
|
|
20
|
+
|
|
21
|
+
export type WithFields<
|
|
22
|
+
Source,
|
|
23
|
+
FromScope extends ScopeTable,
|
|
24
|
+
Columns extends readonly (keyof FromScope)[],
|
|
25
|
+
> = Expand<Source & Pick<FromScope, Columns[number]>>;
|
|
26
|
+
|
|
27
|
+
export type ExtractScopeFields<T extends Record<string, Expression<ScopeField>>> = {
|
|
28
|
+
[K in keyof T]: T[K] extends Expression<infer F extends ScopeField> ? F : never;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type FieldProxy<AvailableScope extends Scope> = {
|
|
32
|
+
[K in keyof AvailableScope['topLevel']]: Expression<AvailableScope['topLevel'][K]>;
|
|
33
|
+
} & {
|
|
34
|
+
[TableName in keyof AvailableScope['namespaces']]: {
|
|
35
|
+
[K in keyof AvailableScope['namespaces'][TableName]]: Expression<
|
|
36
|
+
AvailableScope['namespaces'][TableName][K]
|
|
37
|
+
>;
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ExpressionOrValue<
|
|
42
|
+
T extends ScopeField,
|
|
43
|
+
CT extends Record<string, { readonly input: unknown }>,
|
|
44
|
+
> = Expression<T> | (T['codecId'] extends keyof CT ? CT[T['codecId']]['input'] : never);
|
|
45
|
+
|
|
46
|
+
export type BooleanCodecType = { codecId: 'pg/bool@1'; nullable: boolean };
|
|
47
|
+
|
|
48
|
+
export type ExpressionBuilder<AvailableScope extends Scope, QC extends QueryContext> = (
|
|
49
|
+
fields: FieldProxy<AvailableScope>,
|
|
50
|
+
fns: Functions<QC>,
|
|
51
|
+
) => Expression<BooleanCodecType>;
|
|
52
|
+
|
|
53
|
+
export type OrderByDirection = 'asc' | 'desc';
|
|
54
|
+
export type OrderByNulls = 'first' | 'last';
|
|
55
|
+
|
|
56
|
+
export type OrderByOptions = {
|
|
57
|
+
direction?: OrderByDirection;
|
|
58
|
+
nulls?: OrderByNulls;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export type OrderByScope<
|
|
62
|
+
AvailableScope extends Scope,
|
|
63
|
+
RowType extends Record<string, ScopeField>,
|
|
64
|
+
> = {
|
|
65
|
+
topLevel: Expand<AvailableScope['topLevel'] & RowType>;
|
|
66
|
+
namespaces: AvailableScope['namespaces'];
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
type ExtensionFunctionArgs<
|
|
70
|
+
Args extends readonly ScopeField[],
|
|
71
|
+
CT extends Record<string, { readonly input: unknown }>,
|
|
72
|
+
> = { [I in keyof Args]: ExpressionOrValue<Args[I], CT> };
|
|
73
|
+
|
|
74
|
+
type DeriveExtFunctions<
|
|
75
|
+
OT extends QueryOperationTypesBase,
|
|
76
|
+
CT extends Record<string, { readonly input: unknown }>,
|
|
77
|
+
> = {
|
|
78
|
+
[K in keyof OT]: (
|
|
79
|
+
...args: ExtensionFunctionArgs<OT[K]['args'], CT>
|
|
80
|
+
) => Expression<OT[K]['returns']>;
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
export type BuiltinFunctions<CT extends Record<string, { readonly input: unknown }>> = {
|
|
84
|
+
eq: <CodecId extends string>(
|
|
85
|
+
a: ExpressionOrValue<{ codecId: CodecId; nullable: boolean }, CT> | null,
|
|
86
|
+
b: ExpressionOrValue<{ codecId: CodecId; nullable: boolean }, CT> | null,
|
|
87
|
+
) => Expression<BooleanCodecType>;
|
|
88
|
+
ne: <T extends ScopeField>(
|
|
89
|
+
a: ExpressionOrValue<T, CT> | null,
|
|
90
|
+
b: ExpressionOrValue<T, CT> | null,
|
|
91
|
+
) => Expression<BooleanCodecType>;
|
|
92
|
+
gt: <T extends ScopeField>(
|
|
93
|
+
a: ExpressionOrValue<T, CT>,
|
|
94
|
+
b: ExpressionOrValue<T, CT>,
|
|
95
|
+
) => Expression<BooleanCodecType>;
|
|
96
|
+
gte: <T extends ScopeField>(
|
|
97
|
+
a: ExpressionOrValue<T, CT>,
|
|
98
|
+
b: ExpressionOrValue<T, CT>,
|
|
99
|
+
) => Expression<BooleanCodecType>;
|
|
100
|
+
lt: <T extends ScopeField>(
|
|
101
|
+
a: ExpressionOrValue<T, CT>,
|
|
102
|
+
b: ExpressionOrValue<T, CT>,
|
|
103
|
+
) => Expression<BooleanCodecType>;
|
|
104
|
+
lte: <T extends ScopeField>(
|
|
105
|
+
a: ExpressionOrValue<T, CT>,
|
|
106
|
+
b: ExpressionOrValue<T, CT>,
|
|
107
|
+
) => Expression<BooleanCodecType>;
|
|
108
|
+
and: (...ands: ExpressionOrValue<BooleanCodecType, CT>[]) => Expression<BooleanCodecType>;
|
|
109
|
+
or: (...ors: ExpressionOrValue<BooleanCodecType, CT>[]) => Expression<BooleanCodecType>;
|
|
110
|
+
|
|
111
|
+
exists: (subquery: Subquery<Record<string, ScopeField>>) => Expression<BooleanCodecType>;
|
|
112
|
+
notExists: (subquery: Subquery<Record<string, ScopeField>>) => Expression<BooleanCodecType>;
|
|
113
|
+
|
|
114
|
+
in: {
|
|
115
|
+
<CodecId extends string>(
|
|
116
|
+
expr: Expression<{ codecId: CodecId; nullable: boolean }>,
|
|
117
|
+
subquery: Subquery<Record<string, { codecId: CodecId; nullable: boolean }>>,
|
|
118
|
+
): Expression<BooleanCodecType>;
|
|
119
|
+
<CodecId extends string>(
|
|
120
|
+
expr: Expression<{ codecId: CodecId; nullable: boolean }>,
|
|
121
|
+
values: Array<ExpressionOrValue<{ codecId: CodecId; nullable: boolean }, CT>>,
|
|
122
|
+
): Expression<BooleanCodecType>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
notIn: {
|
|
126
|
+
<CodecId extends string>(
|
|
127
|
+
expr: Expression<{ codecId: CodecId; nullable: boolean }>,
|
|
128
|
+
subquery: Subquery<Record<string, { codecId: CodecId; nullable: boolean }>>,
|
|
129
|
+
): Expression<BooleanCodecType>;
|
|
130
|
+
<CodecId extends string>(
|
|
131
|
+
expr: Expression<{ codecId: CodecId; nullable: boolean }>,
|
|
132
|
+
values: Array<ExpressionOrValue<{ codecId: CodecId; nullable: boolean }, CT>>,
|
|
133
|
+
): Expression<BooleanCodecType>;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export type Functions<QC extends QueryContext> = BuiltinFunctions<QC['codecTypes']> &
|
|
138
|
+
DeriveExtFunctions<QC['queryOperationTypes'], QC['codecTypes']>;
|
|
139
|
+
|
|
140
|
+
export type CountField = { codecId: 'pg/int8@1'; nullable: false };
|
|
141
|
+
|
|
142
|
+
export type AggregateOnlyFunctions = {
|
|
143
|
+
count: (expr?: Expression<ScopeField>) => Expression<CountField>;
|
|
144
|
+
sum: <T extends ScopeField>(
|
|
145
|
+
expr: Expression<T>,
|
|
146
|
+
) => Expression<{ codecId: T['codecId']; nullable: true }>;
|
|
147
|
+
avg: <T extends ScopeField>(
|
|
148
|
+
expr: Expression<T>,
|
|
149
|
+
) => Expression<{ codecId: T['codecId']; nullable: true }>;
|
|
150
|
+
min: <T extends ScopeField>(
|
|
151
|
+
expr: Expression<T>,
|
|
152
|
+
) => Expression<{ codecId: T['codecId']; nullable: true }>;
|
|
153
|
+
max: <T extends ScopeField>(
|
|
154
|
+
expr: Expression<T>,
|
|
155
|
+
) => Expression<{ codecId: T['codecId']; nullable: true }>;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
export type AggregateFunctions<QC extends QueryContext> = Functions<QC> & AggregateOnlyFunctions;
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { AggregateFunctions, Expression, Functions } from './expression';
|
|
2
|
+
export type { ResolveRow } from './resolve';
|
|
3
|
+
export type { GatedMethod, QueryContext, Scope, ScopeField, Subquery } from './scope';
|
|
4
|
+
export type { Db } from './types/db';
|
|
5
|
+
export type { GroupedQuery } from './types/grouped-query';
|
|
6
|
+
export type { DeleteQuery, InsertQuery, UpdateQuery } from './types/mutation-query';
|
|
7
|
+
export type { SelectQuery } from './types/select-query';
|
|
8
|
+
export type { TableProxy } from './types/table-proxy';
|
package/src/resolve.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Expand, ScopeField } from './scope';
|
|
2
|
+
|
|
3
|
+
/// Given a row type of { <fieldName>: { codecId: <codecId>, nullable: <nullable> } }, return a record of { <fieldName>: <codecOutputType> }
|
|
4
|
+
/// Also resolves nullability of the field.
|
|
5
|
+
export type ResolveRow<
|
|
6
|
+
Row extends Record<string, ScopeField>,
|
|
7
|
+
CodecTypes extends Record<string, { readonly output: unknown }>,
|
|
8
|
+
> = Expand<{
|
|
9
|
+
-readonly [K in keyof Row]: Row[K]['codecId'] extends keyof CodecTypes
|
|
10
|
+
? Row[K]['nullable'] extends true
|
|
11
|
+
? CodecTypes[Row[K]['codecId']]['output'] | null
|
|
12
|
+
: CodecTypes[Row[K]['codecId']]['output']
|
|
13
|
+
: unknown;
|
|
14
|
+
}>;
|
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
import type { PlanMeta } from '@prisma-next/contract/types';
|
|
2
|
+
import type { StorageTable } from '@prisma-next/sql-contract/types';
|
|
3
|
+
import {
|
|
4
|
+
AndExpr,
|
|
5
|
+
type AnyExpression as AstExpression,
|
|
6
|
+
IdentifierRef,
|
|
7
|
+
OrderByItem,
|
|
8
|
+
ProjectionItem,
|
|
9
|
+
SelectAst,
|
|
10
|
+
type TableSource,
|
|
11
|
+
} from '@prisma-next/sql-relational-core/ast';
|
|
12
|
+
import type { SqlQueryPlan } from '@prisma-next/sql-relational-core/plan';
|
|
13
|
+
import type {
|
|
14
|
+
AppliedMutationDefault,
|
|
15
|
+
MutationDefaultsOptions,
|
|
16
|
+
} from '@prisma-next/sql-relational-core/query-lane-context';
|
|
17
|
+
import type { QueryOperationEntry } from '@prisma-next/sql-relational-core/query-operations';
|
|
18
|
+
import type {
|
|
19
|
+
AggregateFunctions,
|
|
20
|
+
Expression,
|
|
21
|
+
FieldProxy,
|
|
22
|
+
OrderByOptions,
|
|
23
|
+
OrderByScope,
|
|
24
|
+
} from '../expression';
|
|
25
|
+
import type {
|
|
26
|
+
GatedMethod,
|
|
27
|
+
MergeScopes,
|
|
28
|
+
NullableScope,
|
|
29
|
+
QueryContext,
|
|
30
|
+
Scope,
|
|
31
|
+
ScopeField,
|
|
32
|
+
ScopeTable,
|
|
33
|
+
} from '../scope';
|
|
34
|
+
import type { ExpressionImpl } from './expression-impl';
|
|
35
|
+
import { createFieldProxy } from './field-proxy';
|
|
36
|
+
import { createAggregateFunctions, createFunctions } from './functions';
|
|
37
|
+
|
|
38
|
+
export type ExprCallback = (fields: FieldProxy<Scope>, fns: unknown) => Expression<ScopeField>;
|
|
39
|
+
|
|
40
|
+
export class BuilderBase<Capabilities = unknown> {
|
|
41
|
+
protected readonly ctx: BuilderContext;
|
|
42
|
+
|
|
43
|
+
constructor(ctx: BuilderContext) {
|
|
44
|
+
this.ctx = ctx;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
protected _gate<Req extends Record<string, Record<string, boolean>>, Args extends unknown[], R>(
|
|
48
|
+
required: Req,
|
|
49
|
+
methodName: string,
|
|
50
|
+
method: (...args: Args) => R,
|
|
51
|
+
): GatedMethod<Capabilities, Req, (...args: Args) => R> {
|
|
52
|
+
return ((...args: Args): R => {
|
|
53
|
+
assertCapability(this.ctx, required, methodName);
|
|
54
|
+
return method(...args);
|
|
55
|
+
}) as GatedMethod<Capabilities, Req, (...args: Args) => R>;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface BuilderState {
|
|
60
|
+
readonly from: TableSource;
|
|
61
|
+
readonly joins: readonly import('@prisma-next/sql-relational-core/ast').JoinAst[];
|
|
62
|
+
readonly projections: readonly ProjectionItem[];
|
|
63
|
+
readonly where: readonly AstExpression[];
|
|
64
|
+
readonly orderBy: readonly OrderByItem[];
|
|
65
|
+
readonly groupBy: readonly AstExpression[];
|
|
66
|
+
readonly having: AstExpression | undefined;
|
|
67
|
+
readonly limit: number | undefined;
|
|
68
|
+
readonly offset: number | undefined;
|
|
69
|
+
readonly distinct: true | undefined;
|
|
70
|
+
readonly distinctOn: readonly AstExpression[] | undefined;
|
|
71
|
+
readonly scope: Scope;
|
|
72
|
+
readonly rowFields: Record<string, ScopeField>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export interface BuilderContext {
|
|
76
|
+
readonly capabilities: Record<string, Record<string, boolean>>;
|
|
77
|
+
readonly queryOperationTypes: Readonly<Record<string, QueryOperationEntry>>;
|
|
78
|
+
readonly target: string;
|
|
79
|
+
readonly storageHash: string;
|
|
80
|
+
readonly applyMutationDefaults: (
|
|
81
|
+
options: MutationDefaultsOptions,
|
|
82
|
+
) => ReadonlyArray<AppliedMutationDefault>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function emptyState(from: TableSource, scope: Scope): BuilderState {
|
|
86
|
+
return {
|
|
87
|
+
from,
|
|
88
|
+
joins: [],
|
|
89
|
+
projections: [],
|
|
90
|
+
where: [],
|
|
91
|
+
orderBy: [],
|
|
92
|
+
groupBy: [],
|
|
93
|
+
having: undefined,
|
|
94
|
+
limit: undefined,
|
|
95
|
+
offset: undefined,
|
|
96
|
+
distinct: undefined,
|
|
97
|
+
distinctOn: undefined,
|
|
98
|
+
scope,
|
|
99
|
+
rowFields: {},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export function cloneState(state: BuilderState, overrides: Partial<BuilderState>): BuilderState {
|
|
104
|
+
return { ...state, ...overrides };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function combineWhereExprs(exprs: readonly AstExpression[]): AstExpression | undefined {
|
|
108
|
+
if (exprs.length === 0) return undefined;
|
|
109
|
+
if (exprs.length === 1) return exprs[0];
|
|
110
|
+
return AndExpr.of(exprs);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function buildSelectAst(state: BuilderState): SelectAst {
|
|
114
|
+
const where = combineWhereExprs(state.where);
|
|
115
|
+
return new SelectAst({
|
|
116
|
+
from: state.from,
|
|
117
|
+
joins: state.joins.length > 0 ? state.joins : undefined,
|
|
118
|
+
projection: state.projections,
|
|
119
|
+
where,
|
|
120
|
+
orderBy: state.orderBy.length > 0 ? state.orderBy : undefined,
|
|
121
|
+
distinct: state.distinct,
|
|
122
|
+
distinctOn: state.distinctOn && state.distinctOn.length > 0 ? state.distinctOn : undefined,
|
|
123
|
+
groupBy: state.groupBy.length > 0 ? state.groupBy : undefined,
|
|
124
|
+
having: state.having,
|
|
125
|
+
limit: state.limit,
|
|
126
|
+
offset: state.offset,
|
|
127
|
+
selectAllIntent: undefined,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function buildQueryPlan<Row = unknown>(
|
|
132
|
+
ast: import('@prisma-next/sql-relational-core/ast').AnyQueryAst,
|
|
133
|
+
rowFields: Record<string, ScopeField>,
|
|
134
|
+
ctx: BuilderContext,
|
|
135
|
+
): SqlQueryPlan<Row> {
|
|
136
|
+
const projectionTypes: Record<string, string> = {};
|
|
137
|
+
const codecs: Record<string, string> = {};
|
|
138
|
+
for (const [alias, field] of Object.entries(rowFields)) {
|
|
139
|
+
projectionTypes[alias] = field.codecId;
|
|
140
|
+
codecs[alias] = field.codecId;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const paramRefs = ast.collectParamRefs();
|
|
144
|
+
const seen = new Set<import('@prisma-next/sql-relational-core/ast').ParamRef>();
|
|
145
|
+
const uniqueRefs: import('@prisma-next/sql-relational-core/ast').ParamRef[] = [];
|
|
146
|
+
for (const ref of paramRefs) {
|
|
147
|
+
if (!seen.has(ref)) {
|
|
148
|
+
seen.add(ref);
|
|
149
|
+
uniqueRefs.push(ref);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const paramValues = uniqueRefs.map((r) => r.value);
|
|
153
|
+
const paramDescriptors = uniqueRefs.map((ref, i) => ({
|
|
154
|
+
index: i + 1,
|
|
155
|
+
source: 'dsl' as const,
|
|
156
|
+
...(ref.codecId ? { codecId: ref.codecId } : {}),
|
|
157
|
+
}));
|
|
158
|
+
|
|
159
|
+
for (const [i, ref] of uniqueRefs.entries()) {
|
|
160
|
+
if (ref.codecId) codecs[`$${i + 1}`] = ref.codecId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const hasProjectionTypes = Object.keys(projectionTypes).length > 0;
|
|
164
|
+
const hasCodecs = Object.keys(codecs).length > 0;
|
|
165
|
+
|
|
166
|
+
const meta: PlanMeta = Object.freeze({
|
|
167
|
+
target: ctx.target,
|
|
168
|
+
storageHash: ctx.storageHash,
|
|
169
|
+
lane: 'dsl',
|
|
170
|
+
paramDescriptors,
|
|
171
|
+
...(hasProjectionTypes ? { projectionTypes } : {}),
|
|
172
|
+
...(hasCodecs ? { annotations: Object.freeze({ codecs: Object.freeze(codecs) }) } : {}),
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return Object.freeze({ ast, params: paramValues, meta });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildPlan<Row = unknown>(
|
|
179
|
+
state: BuilderState,
|
|
180
|
+
ctx: BuilderContext,
|
|
181
|
+
): SqlQueryPlan<Row> {
|
|
182
|
+
return buildQueryPlan<Row>(buildSelectAst(state), state.rowFields, ctx);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function tableToScope(name: string, table: StorageTable): Scope {
|
|
186
|
+
const fields: ScopeTable = {};
|
|
187
|
+
for (const [colName, col] of Object.entries(table.columns)) {
|
|
188
|
+
fields[colName] = { codecId: col.codecId, nullable: col.nullable };
|
|
189
|
+
}
|
|
190
|
+
return { topLevel: { ...fields }, namespaces: { [name]: fields } };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
export function mergeScopes<A extends Scope, B extends Scope>(a: A, b: B): MergeScopes<A, B> {
|
|
194
|
+
const topLevel: ScopeTable = {};
|
|
195
|
+
for (const [k, v] of Object.entries(a.topLevel)) {
|
|
196
|
+
if (!(k in b.topLevel)) topLevel[k] = v;
|
|
197
|
+
}
|
|
198
|
+
for (const [k, v] of Object.entries(b.topLevel)) {
|
|
199
|
+
if (!(k in a.topLevel)) topLevel[k] = v;
|
|
200
|
+
}
|
|
201
|
+
return {
|
|
202
|
+
topLevel,
|
|
203
|
+
namespaces: { ...a.namespaces, ...b.namespaces },
|
|
204
|
+
} as MergeScopes<A, B>;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function nullableScope<S extends Scope>(scope: S): NullableScope<S> {
|
|
208
|
+
const mkNullable = (tbl: ScopeTable): ScopeTable => {
|
|
209
|
+
const result: ScopeTable = {};
|
|
210
|
+
for (const [k, v] of Object.entries(tbl)) {
|
|
211
|
+
result[k] = { codecId: v.codecId, nullable: true };
|
|
212
|
+
}
|
|
213
|
+
return result;
|
|
214
|
+
};
|
|
215
|
+
const namespaces: Record<string, ScopeTable> = {};
|
|
216
|
+
for (const [k, v] of Object.entries(scope.namespaces)) {
|
|
217
|
+
namespaces[k] = mkNullable(v);
|
|
218
|
+
}
|
|
219
|
+
return { topLevel: mkNullable(scope.topLevel), namespaces } as NullableScope<S>;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function orderByScopeOf<S extends Scope, R extends Record<string, ScopeField>>(
|
|
223
|
+
scope: S,
|
|
224
|
+
rowFields: R,
|
|
225
|
+
): OrderByScope<S, R> {
|
|
226
|
+
return {
|
|
227
|
+
topLevel: { ...scope.topLevel, ...rowFields },
|
|
228
|
+
namespaces: scope.namespaces,
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
export function assertCapability(
|
|
233
|
+
ctx: BuilderContext,
|
|
234
|
+
required: Record<string, Record<string, boolean>>,
|
|
235
|
+
methodName: string,
|
|
236
|
+
): void {
|
|
237
|
+
for (const [ns, keys] of Object.entries(required)) {
|
|
238
|
+
for (const key of Object.keys(keys)) {
|
|
239
|
+
if (!ctx.capabilities[ns]?.[key]) {
|
|
240
|
+
throw new Error(`${methodName}() requires capability ${ns}.${key}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export function resolveSelectArgs(
|
|
247
|
+
args: unknown[],
|
|
248
|
+
scope: Scope,
|
|
249
|
+
ctx: BuilderContext,
|
|
250
|
+
): { projections: ProjectionItem[]; newRowFields: Record<string, ScopeField> } {
|
|
251
|
+
const projections: ProjectionItem[] = [];
|
|
252
|
+
const newRowFields: Record<string, ScopeField> = {};
|
|
253
|
+
|
|
254
|
+
if (args.length === 0) return { projections, newRowFields };
|
|
255
|
+
|
|
256
|
+
if (typeof args[0] === 'string' && (args.length === 1 || typeof args[1] !== 'function')) {
|
|
257
|
+
for (const colName of args as string[]) {
|
|
258
|
+
const field = scope.topLevel[colName];
|
|
259
|
+
if (!field) throw new Error(`Column "${colName}" not found in scope`);
|
|
260
|
+
projections.push(ProjectionItem.of(colName, IdentifierRef.of(colName)));
|
|
261
|
+
newRowFields[colName] = field;
|
|
262
|
+
}
|
|
263
|
+
return { projections, newRowFields };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (typeof args[0] === 'string' && typeof args[1] === 'function') {
|
|
267
|
+
const alias = args[0] as string;
|
|
268
|
+
const exprFn = args[1] as (
|
|
269
|
+
f: FieldProxy<Scope>,
|
|
270
|
+
fns: AggregateFunctions<QueryContext>,
|
|
271
|
+
) => Expression<ScopeField>;
|
|
272
|
+
const fns = createAggregateFunctions(ctx.queryOperationTypes);
|
|
273
|
+
const result = exprFn(createFieldProxy(scope), fns);
|
|
274
|
+
projections.push(ProjectionItem.of(alias, result.buildAst()));
|
|
275
|
+
newRowFields[alias] = (result as ExpressionImpl).field;
|
|
276
|
+
return { projections, newRowFields };
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
if (typeof args[0] === 'function') {
|
|
280
|
+
const callbackFn = args[0] as (
|
|
281
|
+
f: FieldProxy<Scope>,
|
|
282
|
+
fns: AggregateFunctions<QueryContext>,
|
|
283
|
+
) => Record<string, Expression<ScopeField>>;
|
|
284
|
+
const fns = createAggregateFunctions(ctx.queryOperationTypes);
|
|
285
|
+
const record = callbackFn(createFieldProxy(scope), fns);
|
|
286
|
+
for (const [key, expr] of Object.entries(record)) {
|
|
287
|
+
projections.push(ProjectionItem.of(key, expr.buildAst()));
|
|
288
|
+
newRowFields[key] = (expr as ExpressionImpl).field;
|
|
289
|
+
}
|
|
290
|
+
return { projections, newRowFields };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
throw new Error('Invalid .select() arguments');
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function resolveOrderBy(
|
|
297
|
+
arg: unknown,
|
|
298
|
+
options: OrderByOptions | undefined,
|
|
299
|
+
scope: Scope,
|
|
300
|
+
rowFields: Record<string, ScopeField>,
|
|
301
|
+
ctx: BuilderContext,
|
|
302
|
+
useAggregateFns: boolean,
|
|
303
|
+
): OrderByItem {
|
|
304
|
+
const dir = options?.direction ?? 'asc';
|
|
305
|
+
|
|
306
|
+
if (typeof arg === 'string') {
|
|
307
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
308
|
+
if (!(arg in combined.topLevel))
|
|
309
|
+
throw new Error(`Column "${arg}" not found in scope for orderBy`);
|
|
310
|
+
const expr = IdentifierRef.of(arg);
|
|
311
|
+
return dir === 'asc' ? OrderByItem.asc(expr) : OrderByItem.desc(expr);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof arg === 'function') {
|
|
315
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
316
|
+
const fns = useAggregateFns
|
|
317
|
+
? createAggregateFunctions(ctx.queryOperationTypes)
|
|
318
|
+
: createFunctions(ctx.queryOperationTypes);
|
|
319
|
+
const result = (arg as ExprCallback)(createFieldProxy(combined), fns);
|
|
320
|
+
return dir === 'asc' ? OrderByItem.asc(result.buildAst()) : OrderByItem.desc(result.buildAst());
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
throw new Error('Invalid orderBy argument');
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
export function resolveGroupBy(
|
|
327
|
+
args: unknown[],
|
|
328
|
+
scope: Scope,
|
|
329
|
+
rowFields: Record<string, ScopeField>,
|
|
330
|
+
ctx: BuilderContext,
|
|
331
|
+
): AstExpression[] {
|
|
332
|
+
if (typeof args[0] === 'string') {
|
|
333
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
334
|
+
return (args as string[]).map((colName) => {
|
|
335
|
+
if (!(colName in combined.topLevel))
|
|
336
|
+
throw new Error(`Column "${colName}" not found in scope for groupBy`);
|
|
337
|
+
return IdentifierRef.of(colName);
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (typeof args[0] === 'function') {
|
|
342
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
343
|
+
const fns = createFunctions(ctx.queryOperationTypes);
|
|
344
|
+
const result = (args[0] as ExprCallback)(createFieldProxy(combined), fns);
|
|
345
|
+
return [result.buildAst()];
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
throw new Error('Invalid groupBy arguments');
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export function resolveDistinctOn(
|
|
352
|
+
args: unknown[],
|
|
353
|
+
scope: Scope,
|
|
354
|
+
rowFields: Record<string, ScopeField>,
|
|
355
|
+
ctx: BuilderContext,
|
|
356
|
+
): AstExpression[] {
|
|
357
|
+
if (args.length === 1 && typeof args[0] === 'function') {
|
|
358
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
359
|
+
const fns = createFunctions(ctx.queryOperationTypes);
|
|
360
|
+
const result = (args[0] as ExprCallback)(createFieldProxy(combined), fns);
|
|
361
|
+
return [result.buildAst()];
|
|
362
|
+
}
|
|
363
|
+
const combined = orderByScopeOf(scope, rowFields);
|
|
364
|
+
return (args as string[]).map((colName) => {
|
|
365
|
+
if (!(colName in combined.topLevel))
|
|
366
|
+
throw new Error(`Column "${colName}" not found in scope for distinctOn`);
|
|
367
|
+
return IdentifierRef.of(colName);
|
|
368
|
+
});
|
|
369
|
+
}
|