@zmdb/sql 1.0.0-beta.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/LICENSE +674 -0
- package/README.md +35 -0
- package/dist/aggregations/index.d.ts +30 -0
- package/dist/aggregations/index.d.ts.map +1 -0
- package/dist/aggregations/index.js +52 -0
- package/dist/aggregations/index.js.map +1 -0
- package/dist/clauses.d.ts +112 -0
- package/dist/clauses.d.ts.map +1 -0
- package/dist/clauses.js +267 -0
- package/dist/clauses.js.map +1 -0
- package/dist/comments/index.d.ts +26 -0
- package/dist/comments/index.d.ts.map +1 -0
- package/dist/comments/index.js +28 -0
- package/dist/comments/index.js.map +1 -0
- package/dist/compiled-query.d.ts +12 -0
- package/dist/compiled-query.d.ts.map +1 -0
- package/dist/compiled-query.js +2 -0
- package/dist/compiled-query.js.map +1 -0
- package/dist/dialects/index.d.ts +19 -0
- package/dist/dialects/index.d.ts.map +1 -0
- package/dist/dialects/index.js +43 -0
- package/dist/dialects/index.js.map +1 -0
- package/dist/dialects/protocol.d.ts +230 -0
- package/dist/dialects/protocol.d.ts.map +1 -0
- package/dist/dialects/protocol.js +230 -0
- package/dist/dialects/protocol.js.map +1 -0
- package/dist/errors.d.ts +9 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +17 -0
- package/dist/errors.js.map +1 -0
- package/dist/expressions/index.d.ts +50 -0
- package/dist/expressions/index.d.ts.map +1 -0
- package/dist/expressions/index.js +77 -0
- package/dist/expressions/index.js.map +1 -0
- package/dist/extensions/index.d.ts +85 -0
- package/dist/extensions/index.d.ts.map +1 -0
- package/dist/extensions/index.js +120 -0
- package/dist/extensions/index.js.map +1 -0
- package/dist/fts/index.d.ts +28 -0
- package/dist/fts/index.d.ts.map +1 -0
- package/dist/fts/index.js +110 -0
- package/dist/fts/index.js.map +1 -0
- package/dist/index.d.ts +128 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +364 -0
- package/dist/index.js.map +1 -0
- package/dist/introspect/types.d.ts +58 -0
- package/dist/introspect/types.d.ts.map +1 -0
- package/dist/introspect/types.js +2 -0
- package/dist/introspect/types.js.map +1 -0
- package/dist/joins/index.d.ts +20 -0
- package/dist/joins/index.d.ts.map +1 -0
- package/dist/joins/index.js +24 -0
- package/dist/joins/index.js.map +1 -0
- package/dist/migrations/types.d.ts +100 -0
- package/dist/migrations/types.d.ts.map +1 -0
- package/dist/migrations/types.js +2 -0
- package/dist/migrations/types.js.map +1 -0
- package/dist/quoting.d.ts +40 -0
- package/dist/quoting.d.ts.map +1 -0
- package/dist/quoting.js +98 -0
- package/dist/quoting.js.map +1 -0
- package/dist/schema-objects/extensions.d.ts +5 -0
- package/dist/schema-objects/extensions.d.ts.map +1 -0
- package/dist/schema-objects/extensions.js +9 -0
- package/dist/schema-objects/extensions.js.map +1 -0
- package/dist/schema-objects/index.d.ts +25 -0
- package/dist/schema-objects/index.d.ts.map +1 -0
- package/dist/schema-objects/index.js +86 -0
- package/dist/schema-objects/index.js.map +1 -0
- package/dist/schema-objects/types.d.ts +62 -0
- package/dist/schema-objects/types.d.ts.map +1 -0
- package/dist/schema-objects/types.js +2 -0
- package/dist/schema-objects/types.js.map +1 -0
- package/dist/set-ops/index.d.ts +16 -0
- package/dist/set-ops/index.d.ts.map +1 -0
- package/dist/set-ops/index.js +41 -0
- package/dist/set-ops/index.js.map +1 -0
- package/package.json +70 -0
- package/src/aggregations/index.ts +127 -0
- package/src/clauses.ts +387 -0
- package/src/comments/index.ts +49 -0
- package/src/compiled-query.ts +12 -0
- package/src/dialects/index.ts +103 -0
- package/src/dialects/protocol.ts +478 -0
- package/src/errors.ts +18 -0
- package/src/expressions/index.ts +127 -0
- package/src/extensions/index.ts +235 -0
- package/src/fts/index.ts +157 -0
- package/src/index.ts +685 -0
- package/src/introspect/types.ts +60 -0
- package/src/joins/index.ts +69 -0
- package/src/migrations/types.ts +93 -0
- package/src/quoting.ts +111 -0
- package/src/schema-objects/extensions.ts +12 -0
- package/src/schema-objects/index.ts +125 -0
- package/src/schema-objects/types.ts +73 -0
- package/src/set-ops/index.ts +48 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ColumnSnapshot, SchemaSnapshot, TableSnapshot } from '../migrations/types.js';
|
|
2
|
+
|
|
3
|
+
export type ReferentialAction = 'no action' | 'restrict' | 'cascade' | 'set null' | 'set default';
|
|
4
|
+
|
|
5
|
+
export type CatalogIndexColumn =
|
|
6
|
+
| string
|
|
7
|
+
| { readonly column: string; readonly opclass?: string }
|
|
8
|
+
| { readonly expr: string; readonly opclass?: string };
|
|
9
|
+
|
|
10
|
+
export interface CatalogForeignKeySnapshot {
|
|
11
|
+
readonly name: string;
|
|
12
|
+
readonly columns: readonly string[];
|
|
13
|
+
readonly targetTable: string;
|
|
14
|
+
readonly targetColumns: readonly string[];
|
|
15
|
+
readonly onDelete: ReferentialAction;
|
|
16
|
+
readonly onUpdate: ReferentialAction;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface CatalogIndexSnapshot {
|
|
20
|
+
readonly name: string;
|
|
21
|
+
readonly columns: readonly CatalogIndexColumn[];
|
|
22
|
+
readonly unique: boolean;
|
|
23
|
+
readonly method?: string;
|
|
24
|
+
readonly where?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface CatalogColumnSnapshot extends ColumnSnapshot {
|
|
28
|
+
readonly catalogType: string;
|
|
29
|
+
readonly default?: string;
|
|
30
|
+
/** Dialect catalog evidence for a generated column. Drift comparison may ignore it. */
|
|
31
|
+
readonly generated?: {
|
|
32
|
+
readonly expression: string;
|
|
33
|
+
readonly stored: boolean;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface CatalogWarning {
|
|
38
|
+
readonly table: string;
|
|
39
|
+
readonly column?: string;
|
|
40
|
+
readonly reason: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface CatalogTableSnapshot extends Omit<TableSnapshot, 'columns'> {
|
|
44
|
+
readonly columns: readonly CatalogColumnSnapshot[];
|
|
45
|
+
readonly primaryKey: readonly string[];
|
|
46
|
+
readonly foreignKeys: readonly CatalogForeignKeySnapshot[];
|
|
47
|
+
readonly indexes: readonly CatalogIndexSnapshot[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface CatalogSchemaSnapshot extends Omit<SchemaSnapshot, 'tables'> {
|
|
51
|
+
readonly tables: readonly CatalogTableSnapshot[];
|
|
52
|
+
readonly extensions: readonly { readonly name: string; readonly schema?: string }[];
|
|
53
|
+
readonly warnings: readonly CatalogWarning[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface CatalogSelection {
|
|
57
|
+
readonly schemas?: readonly string[];
|
|
58
|
+
readonly include?: readonly string[];
|
|
59
|
+
readonly exclude?: readonly string[];
|
|
60
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type ComparisonPredicate,
|
|
3
|
+
type JoinCondition,
|
|
4
|
+
type JoinSpec,
|
|
5
|
+
type Predicate,
|
|
6
|
+
frozenQuery,
|
|
7
|
+
joinClauses,
|
|
8
|
+
joinMethods,
|
|
9
|
+
queryTelemetry,
|
|
10
|
+
tailClause,
|
|
11
|
+
tailMethods,
|
|
12
|
+
whereClause,
|
|
13
|
+
} from '../clauses.js';
|
|
14
|
+
import type { DialectTarget } from '../dialects/index.js';
|
|
15
|
+
import { type CompiledQuery, type QueryCompilerOptions } from '../index.js';
|
|
16
|
+
import { quoteTable } from '../quoting.js';
|
|
17
|
+
|
|
18
|
+
export type { JoinCondition, JoinKind } from '../clauses.js';
|
|
19
|
+
|
|
20
|
+
interface State {
|
|
21
|
+
readonly table: string;
|
|
22
|
+
readonly joins: readonly JoinSpec[];
|
|
23
|
+
readonly wheres: readonly Predicate[];
|
|
24
|
+
readonly orderBys: readonly { col: string; dir: 'asc' | 'desc' }[];
|
|
25
|
+
readonly limitN?: number;
|
|
26
|
+
readonly offsetN?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface JoinableSelect {
|
|
30
|
+
innerJoin(target: string, leftCol: string, rightCol: string, on?: readonly Predicate[]): JoinableSelect;
|
|
31
|
+
innerJoin(target: string, conditions: readonly JoinCondition[], on?: readonly Predicate[]): JoinableSelect;
|
|
32
|
+
leftJoin(target: string, leftCol: string, rightCol: string, on?: readonly Predicate[]): JoinableSelect;
|
|
33
|
+
leftJoin(target: string, conditions: readonly JoinCondition[], on?: readonly Predicate[]): JoinableSelect;
|
|
34
|
+
rightJoin(target: string, leftCol: string, rightCol: string, on?: readonly Predicate[]): JoinableSelect;
|
|
35
|
+
rightJoin(target: string, conditions: readonly JoinCondition[], on?: readonly Predicate[]): JoinableSelect;
|
|
36
|
+
where(col: string, op: string, value: unknown): JoinableSelect;
|
|
37
|
+
whereGroup(predicates: readonly ComparisonPredicate[]): JoinableSelect;
|
|
38
|
+
orderBy(col: string, dir: 'asc' | 'desc'): JoinableSelect;
|
|
39
|
+
limit(n: number): JoinableSelect;
|
|
40
|
+
offset(n: number): JoinableSelect;
|
|
41
|
+
compile(): CompiledQuery;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function make(d: DialectTarget, s: State, telemetry: boolean): JoinableSelect {
|
|
45
|
+
const next = (patch: Partial<State>): JoinableSelect => make(d, { ...s, ...patch }, telemetry);
|
|
46
|
+
return {
|
|
47
|
+
...joinMethods(s.joins, next),
|
|
48
|
+
...tailMethods(s, next),
|
|
49
|
+
where: (col, op, value) => next({ wheres: [...s.wheres, { col, op, value }] }),
|
|
50
|
+
whereGroup: predicates => next({ wheres: [...s.wheres, { kind: 'group', predicates, connector: 'AND' }] }),
|
|
51
|
+
compile: () => {
|
|
52
|
+
const params: unknown[] = [];
|
|
53
|
+
const text =
|
|
54
|
+
`SELECT * FROM ${quoteTable(d, s.table)}` +
|
|
55
|
+
joinClauses(d, s.joins, params) +
|
|
56
|
+
whereClause(d, s.wheres, params) +
|
|
57
|
+
tailClause(d, s);
|
|
58
|
+
return frozenQuery(text, params, queryTelemetry(d, 'SELECT', s.table, telemetry));
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function joinableSelectFrom(
|
|
64
|
+
table: string,
|
|
65
|
+
dialect: DialectTarget,
|
|
66
|
+
options?: QueryCompilerOptions,
|
|
67
|
+
): JoinableSelect {
|
|
68
|
+
return make(dialect, { table, joins: [], wheres: [], orderBys: [] }, options?.telemetry === true);
|
|
69
|
+
}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
export interface ExtensionType {
|
|
2
|
+
readonly extension: string;
|
|
3
|
+
readonly name: string;
|
|
4
|
+
readonly args?: readonly (string | number)[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ColumnSnapshot {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
/**
|
|
10
|
+
* The **abstract** column type — `'timestamp'`, not `'TIMESTAMPTZ'`.
|
|
11
|
+
*
|
|
12
|
+
* A snapshot is a record of what the schema says, so it must not name a dialect: the
|
|
13
|
+
* same snapshot is diffed and then emitted for Postgres, MySQL and SQLite. `ddlType`
|
|
14
|
+
* is where it becomes a real one.
|
|
15
|
+
*/
|
|
16
|
+
readonly type: string | ExtensionType;
|
|
17
|
+
readonly nullable: boolean;
|
|
18
|
+
readonly primaryKey: boolean;
|
|
19
|
+
/** `varchar(255)` → `255`. MySQL rejects a `VARCHAR` with no length. */
|
|
20
|
+
readonly length?: number | undefined;
|
|
21
|
+
/** Carried so dialect-specific DDL can validate or emit the declaration. */
|
|
22
|
+
readonly unique?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface TableOptions {
|
|
26
|
+
readonly shardKey?: readonly string[];
|
|
27
|
+
readonly sortKey?: readonly string[];
|
|
28
|
+
readonly rowstore?: true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ReferentialAction = 'cascade' | 'restrict' | 'set null' | 'set default' | 'no action';
|
|
32
|
+
|
|
33
|
+
export interface ForeignKeySnapshot {
|
|
34
|
+
readonly name: string;
|
|
35
|
+
readonly columns: readonly string[];
|
|
36
|
+
readonly targetTable: string;
|
|
37
|
+
readonly targetColumns: readonly string[];
|
|
38
|
+
readonly onDelete: ReferentialAction;
|
|
39
|
+
readonly onUpdate: ReferentialAction;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface TableSnapshot {
|
|
43
|
+
readonly name: string;
|
|
44
|
+
readonly columns: readonly ColumnSnapshot[];
|
|
45
|
+
/** Ordered by declaration, independently of the deterministically sorted columns. */
|
|
46
|
+
readonly primaryKey: readonly string[];
|
|
47
|
+
readonly foreignKeys: readonly ForeignKeySnapshot[];
|
|
48
|
+
readonly tableOptions?: TableOptions;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SchemaSnapshot {
|
|
52
|
+
readonly version: 1;
|
|
53
|
+
readonly tables: readonly TableSnapshot[];
|
|
54
|
+
readonly extensions: readonly ExtensionSnapshot[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ExtensionSnapshot {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly schema?: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ChangeOp =
|
|
63
|
+
| { readonly kind: 'create_extension'; readonly name: string; readonly schema?: string }
|
|
64
|
+
| {
|
|
65
|
+
readonly kind: 'create_table';
|
|
66
|
+
readonly table: string;
|
|
67
|
+
readonly columns: readonly ColumnSnapshot[];
|
|
68
|
+
readonly primaryKey: readonly string[];
|
|
69
|
+
readonly foreignKeys: readonly ForeignKeySnapshot[];
|
|
70
|
+
readonly tableOptions?: TableOptions;
|
|
71
|
+
}
|
|
72
|
+
| { readonly kind: 'drop_table'; readonly table: string }
|
|
73
|
+
| { readonly kind: 'add_column'; readonly table: string; readonly column: ColumnSnapshot }
|
|
74
|
+
| { readonly kind: 'drop_column'; readonly table: string; readonly column: string }
|
|
75
|
+
| {
|
|
76
|
+
readonly kind: 'alter_column_type';
|
|
77
|
+
readonly table: string;
|
|
78
|
+
readonly column: string;
|
|
79
|
+
readonly from: string | ExtensionType;
|
|
80
|
+
readonly to: string | ExtensionType;
|
|
81
|
+
/** Required by dialects whose ALTER COLUMN restates nullability. */
|
|
82
|
+
readonly fromNullable?: boolean;
|
|
83
|
+
/** Required by dialects whose ALTER COLUMN restates nullability. */
|
|
84
|
+
readonly toNullable?: boolean;
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
readonly kind: 'alter_primary_key';
|
|
88
|
+
readonly table: string;
|
|
89
|
+
readonly from: readonly string[];
|
|
90
|
+
readonly to: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
| { readonly kind: 'add_foreign_key'; readonly table: string; readonly fk: ForeignKeySnapshot }
|
|
93
|
+
| { readonly kind: 'drop_foreign_key'; readonly table: string; readonly name: string };
|
package/src/quoting.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { dialectTraits, type DialectTarget } from './dialects/index.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safely quote a single SQL identifier (table name, column name, alias, etc.)
|
|
5
|
+
* according to the target database dialect, escaping internal quote characters.
|
|
6
|
+
*
|
|
7
|
+
* PostgreSQL & SQLite: double quotes (`"`), escaping internal double quotes as `""`.
|
|
8
|
+
* MySQL: backticks (`` ` ``), escaping internal backticks as ``` `` ```.
|
|
9
|
+
*/
|
|
10
|
+
export function quoteIdentifier(dialect: DialectTarget, identifier: string): string {
|
|
11
|
+
const [open, close] = dialectTraits(dialect).quote;
|
|
12
|
+
const escaped = identifier.replaceAll(close, close + close);
|
|
13
|
+
return `${open}${escaped}${close}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Safely quote a column reference that may be dot-qualified (e.g. `schema.table.column` or `table.*`).
|
|
18
|
+
* Each identifier segment is quoted individually, while wildcard `*` segments remain unquoted.
|
|
19
|
+
*/
|
|
20
|
+
export function quoteColumn(dialect: DialectTarget, col: string): string {
|
|
21
|
+
return col
|
|
22
|
+
.split('.')
|
|
23
|
+
.map(segment => (segment === '*' ? '*' : quoteIdentifier(dialect, segment)))
|
|
24
|
+
.join('.');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isWhitespace(ch: string | undefined): boolean {
|
|
28
|
+
return ch !== undefined && /\s/.test(ch);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface TableSpec {
|
|
32
|
+
readonly table: string;
|
|
33
|
+
readonly alias?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function splitTableSpec(tableSpec: string): TableSpec {
|
|
37
|
+
const trimmed = tableSpec.trim();
|
|
38
|
+
const lower = trimmed.toLowerCase();
|
|
39
|
+
let searchIndex = 0;
|
|
40
|
+
while (searchIndex < lower.length) {
|
|
41
|
+
const asIndex = lower.indexOf('as', searchIndex);
|
|
42
|
+
if (asIndex === -1) {
|
|
43
|
+
break;
|
|
44
|
+
}
|
|
45
|
+
const hasSpaceBefore = isWhitespace(trimmed[asIndex - 1]);
|
|
46
|
+
const hasSpaceAfter = isWhitespace(trimmed[asIndex + 2]);
|
|
47
|
+
|
|
48
|
+
if (hasSpaceBefore && hasSpaceAfter) {
|
|
49
|
+
const table = trimmed.slice(0, asIndex).trim();
|
|
50
|
+
const alias = trimmed.slice(asIndex + 2).trim();
|
|
51
|
+
if (table.length > 0 && alias.length > 0) {
|
|
52
|
+
return { table, alias };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
searchIndex = asIndex + 2;
|
|
56
|
+
}
|
|
57
|
+
return { table: trimmed };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** The primary table named by a table specification, without an optional alias. */
|
|
61
|
+
export function unaliasedTable(tableSpec: string): string {
|
|
62
|
+
return splitTableSpec(tableSpec).table;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Safely quote a table specification, which may be a table name (optionally dot-qualified)
|
|
67
|
+
* or a table alias expression (e.g. `table as alias` or `schema.table AS alias`).
|
|
68
|
+
*
|
|
69
|
+
* Uses manual scanning for `AS` boundaries to avoid polynomial regex backtracking (ReDoS)
|
|
70
|
+
* on arbitrary table specification inputs.
|
|
71
|
+
*/
|
|
72
|
+
export function quoteTable(dialect: DialectTarget, tableSpec: string): string {
|
|
73
|
+
const { table, alias } = splitTableSpec(tableSpec);
|
|
74
|
+
const tablePart = quoteColumn(dialect, table);
|
|
75
|
+
return alias === undefined ? tablePart : `${tablePart} AS ${quoteIdentifier(dialect, alias)}`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generates dialect-specific 1-based parameter placeholders:
|
|
80
|
+
* - PostgreSQL uses numbered sequential indices (`$1`, `$2`, ...)
|
|
81
|
+
* - MySQL and SQLite use positional tokens (`?`)
|
|
82
|
+
*/
|
|
83
|
+
export function formatPlaceholder(dialect: DialectTarget, index: number): string {
|
|
84
|
+
switch (dialectTraits(dialect).placeholder) {
|
|
85
|
+
case 'numbered':
|
|
86
|
+
return `$${index}`;
|
|
87
|
+
case 'positional':
|
|
88
|
+
return '?';
|
|
89
|
+
case 'named':
|
|
90
|
+
return `@p${index}`;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Renumbers numbered (`$n`) or named (`@pN`) placeholders by adding an offset.
|
|
96
|
+
* Positional `?` placeholders are already ordered by the parameter array and are unchanged.
|
|
97
|
+
*
|
|
98
|
+
* Note: Designed for compiler-generated SQL where literal values are always
|
|
99
|
+
* parameterized into placeholders. Does not parse raw SQL string literals or comments
|
|
100
|
+
* where a generated placeholder pattern might appear as literal text.
|
|
101
|
+
*/
|
|
102
|
+
export function renumberPlaceholders(text: string, offset: number, dialect: DialectTarget): string {
|
|
103
|
+
switch (dialectTraits(dialect).placeholder) {
|
|
104
|
+
case 'numbered':
|
|
105
|
+
return text.replace(/\$(\d+)/g, (_match, n: string) => `$${offset + Number(n)}`);
|
|
106
|
+
case 'named':
|
|
107
|
+
return text.replace(/@p(\d+)/g, (_match, n: string) => `@p${offset + Number(n)}`);
|
|
108
|
+
case 'positional':
|
|
109
|
+
return text;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type SqlDialect } from '../index.js';
|
|
2
|
+
import type { ExtensionDef } from './types.js';
|
|
3
|
+
|
|
4
|
+
export type { ExtensionDef } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function createExtensionDdl(definition: ExtensionDef, dialect: SqlDialect): string {
|
|
7
|
+
const statements = dialect.migrations.emitSchemaObject({ kind: 'create_extension', definition });
|
|
8
|
+
if (statements.length !== 1 || statements[0] === undefined) {
|
|
9
|
+
throw new TypeError(`${dialect.name} extension emission must return exactly one statement`);
|
|
10
|
+
}
|
|
11
|
+
return statements[0];
|
|
12
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { DialectSqlType, MigrationDialect, SchemaObjectOperation, SqlDialect } from '../dialects/index.js';
|
|
2
|
+
import { UnsupportedFeatureError } from '../errors.js';
|
|
3
|
+
import type { ColumnSnapshot } from '../migrations/types.js';
|
|
4
|
+
import { quoteIdentifier } from '../quoting.js';
|
|
5
|
+
import type { GeneratedColumn, IndexDef, RlsPolicy, RoutineDef, SequenceDef, ViewDef } from './types.js';
|
|
6
|
+
|
|
7
|
+
export { UnsupportedFeatureError };
|
|
8
|
+
export { createExtensionDdl, type ExtensionDef } from './extensions.js';
|
|
9
|
+
export type {
|
|
10
|
+
GeneratedColumn,
|
|
11
|
+
IndexColumn,
|
|
12
|
+
IndexDef,
|
|
13
|
+
IndexMethod,
|
|
14
|
+
RlsPolicy,
|
|
15
|
+
RoutineDef,
|
|
16
|
+
RoutineSqlType,
|
|
17
|
+
SequenceDef,
|
|
18
|
+
ViewDef,
|
|
19
|
+
} from './types.js';
|
|
20
|
+
|
|
21
|
+
export type DdlSqlType = DialectSqlType;
|
|
22
|
+
|
|
23
|
+
function migrationsOf(dialect: SqlDialect | MigrationDialect): MigrationDialect {
|
|
24
|
+
return 'migrations' in dialect ? dialect.migrations : dialect;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function exactlyOne(dialect: SqlDialect, operation: SchemaObjectOperation): string {
|
|
28
|
+
const statements = dialect.migrations.emitSchemaObject(operation);
|
|
29
|
+
if (statements.length !== 1 || statements[0] === undefined) {
|
|
30
|
+
throw new TypeError(`${dialect.name} schema-object emission must return exactly one statement`);
|
|
31
|
+
}
|
|
32
|
+
return statements[0];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ddlType(dialect: SqlDialect | MigrationDialect, column: ColumnSnapshot): string {
|
|
36
|
+
return migrationsOf(dialect).ddlType(column);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function quoteId(dialect: SqlDialect, id: string): string {
|
|
40
|
+
return quoteIdentifier(dialect, id);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function createIndexDdl(definition: IndexDef, dialect: SqlDialect): string {
|
|
44
|
+
return exactlyOne(dialect, { kind: 'create_index', definition });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function checkConstraintDdl(table: string, name: string, expression: string, dialect: SqlDialect): string {
|
|
48
|
+
return exactlyOne(dialect, { kind: 'check_constraint', table, name, expression });
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createViewDdl(definition: ViewDef, dialect: SqlDialect): string {
|
|
52
|
+
return exactlyOne(dialect, { kind: 'create_view', definition });
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function dropViewDdl(name: string, dialect: SqlDialect, materialized?: boolean): string {
|
|
56
|
+
return exactlyOne(dialect, {
|
|
57
|
+
kind: 'drop_view',
|
|
58
|
+
name,
|
|
59
|
+
...(materialized === undefined ? {} : { materialized }),
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function createSequenceDdl(definition: SequenceDef, dialect: SqlDialect): string {
|
|
64
|
+
return exactlyOne(dialect, { kind: 'create_sequence', definition });
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function generatedColumnDdl(definition: GeneratedColumn, dialect: SqlDialect): string {
|
|
68
|
+
return exactlyOne(dialect, { kind: 'generated_column', definition });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function createSchemaDdl(name: string, dialect: SqlDialect): string {
|
|
72
|
+
return exactlyOne(dialect, { kind: 'create_schema', name });
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function qualify(schema: string, object: string, dialect: SqlDialect): string {
|
|
76
|
+
return `${quoteId(dialect, schema)}.${quoteId(dialect, object)}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function enableRlsDdl(table: string, dialect: SqlDialect): string {
|
|
80
|
+
return exactlyOne(dialect, { kind: 'enable_rls', table });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function createPolicyDdl(definition: RlsPolicy, dialect: SqlDialect): string {
|
|
84
|
+
return exactlyOne(dialect, { kind: 'create_policy', definition });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createRoutineDdl(definition: RoutineDef, dialect: SqlDialect): string {
|
|
88
|
+
return exactlyOne(dialect, { kind: 'create_routine', definition });
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function dropRoutineDdl(definition: RoutineDef, dialect: SqlDialect): string {
|
|
92
|
+
return exactlyOne(dialect, { kind: 'drop_routine', definition });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function replaceRoutineStatements(
|
|
96
|
+
previous: RoutineDef | undefined,
|
|
97
|
+
next: RoutineDef,
|
|
98
|
+
dialect: SqlDialect,
|
|
99
|
+
): readonly string[] {
|
|
100
|
+
return dialect.migrations.emitSchemaObject({
|
|
101
|
+
kind: 'replace_routine',
|
|
102
|
+
...(previous === undefined ? {} : { previous }),
|
|
103
|
+
next,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function normalizedRoutineBody(body: string): string {
|
|
108
|
+
return body
|
|
109
|
+
.split('\n')
|
|
110
|
+
.map(line => line.replace(/\s+$/u, ''))
|
|
111
|
+
.join('\n')
|
|
112
|
+
.replace(/\n+$/u, '');
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function routineFingerprint(definition: RoutineDef): string {
|
|
116
|
+
return JSON.stringify([
|
|
117
|
+
definition.kind,
|
|
118
|
+
definition.name,
|
|
119
|
+
definition.params.map(parameter => [parameter.name, parameter.type, parameter.mode ?? null]),
|
|
120
|
+
definition.returns === undefined ? null : [definition.returns.type, definition.returns.setof ?? null],
|
|
121
|
+
definition.language ?? null,
|
|
122
|
+
definition.deterministic ?? null,
|
|
123
|
+
normalizedRoutineBody(definition.body),
|
|
124
|
+
]);
|
|
125
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export type IndexMethod = 'btree' | 'hash' | 'gin' | 'gist' | 'brin' | 'ivfflat' | 'hnsw';
|
|
2
|
+
export type IndexColumn =
|
|
3
|
+
| string
|
|
4
|
+
| { readonly column: string; readonly opclass?: string }
|
|
5
|
+
| { readonly expr: string; readonly opclass?: string };
|
|
6
|
+
|
|
7
|
+
export interface IndexDef {
|
|
8
|
+
name: string;
|
|
9
|
+
table: string;
|
|
10
|
+
columns: readonly IndexColumn[];
|
|
11
|
+
unique?: boolean;
|
|
12
|
+
where?: string;
|
|
13
|
+
method?: IndexMethod;
|
|
14
|
+
with?: Readonly<Record<string, number>>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ViewDef {
|
|
18
|
+
name: string;
|
|
19
|
+
select: string;
|
|
20
|
+
materialized?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SequenceDef {
|
|
24
|
+
name: string;
|
|
25
|
+
start?: number;
|
|
26
|
+
increment?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface GeneratedColumn {
|
|
30
|
+
name: string;
|
|
31
|
+
type: string;
|
|
32
|
+
expression: string;
|
|
33
|
+
stored?: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface RlsPolicy {
|
|
37
|
+
name: string;
|
|
38
|
+
table: string;
|
|
39
|
+
using: string;
|
|
40
|
+
command?: 'ALL' | 'SELECT' | 'INSERT' | 'UPDATE' | 'DELETE';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type RoutineSqlType =
|
|
44
|
+
| 'serial'
|
|
45
|
+
| 'integer'
|
|
46
|
+
| 'bigint'
|
|
47
|
+
| 'numeric'
|
|
48
|
+
| 'text'
|
|
49
|
+
| 'varchar'
|
|
50
|
+
| 'boolean'
|
|
51
|
+
| 'timestamp'
|
|
52
|
+
| 'json'
|
|
53
|
+
| 'jsonEnum';
|
|
54
|
+
|
|
55
|
+
export interface RoutineDef {
|
|
56
|
+
readonly kind: 'function' | 'procedure';
|
|
57
|
+
readonly name: string;
|
|
58
|
+
readonly params: readonly {
|
|
59
|
+
readonly name: string;
|
|
60
|
+
readonly type: RoutineSqlType;
|
|
61
|
+
readonly mode?: 'in' | 'out' | 'inout';
|
|
62
|
+
}[];
|
|
63
|
+
readonly returns?: { readonly type: RoutineSqlType | 'void'; readonly setof?: boolean };
|
|
64
|
+
readonly language?: string;
|
|
65
|
+
readonly deterministic?: boolean;
|
|
66
|
+
readonly body: string;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ExtensionDef {
|
|
70
|
+
readonly name: string;
|
|
71
|
+
readonly schema?: string;
|
|
72
|
+
readonly version?: string;
|
|
73
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// Set operations (UNION/INTERSECT/EXCEPT) + Batch — see ./SPEC.md.
|
|
2
|
+
import type { DialectTarget } from '../dialects/index.js';
|
|
3
|
+
import { type CompiledQuery } from '../index.js';
|
|
4
|
+
import { renumberPlaceholders } from '../quoting.js';
|
|
5
|
+
|
|
6
|
+
export type SetOp = 'union' | 'unionAll' | 'intersect' | 'except';
|
|
7
|
+
|
|
8
|
+
export const SET_KEYWORD: Record<SetOp, string> = {
|
|
9
|
+
union: 'UNION',
|
|
10
|
+
unionAll: 'UNION ALL',
|
|
11
|
+
intersect: 'INTERSECT',
|
|
12
|
+
except: 'EXCEPT',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Combine compiled queries with a set operator. Numbered and named placeholders
|
|
17
|
+
* continue across fragments; positional `?` placeholders keep parameter-array order.
|
|
18
|
+
* Single query ⇒ passthrough; empty ⇒ throw.
|
|
19
|
+
*/
|
|
20
|
+
export function setOperation(op: SetOp, queries: readonly CompiledQuery[], dialect: DialectTarget): CompiledQuery {
|
|
21
|
+
const [first] = queries;
|
|
22
|
+
if (!first) throw new Error('setOperation requires at least one query');
|
|
23
|
+
if (queries.length === 1) return first;
|
|
24
|
+
const params: unknown[] = [];
|
|
25
|
+
const fragments = queries.map(q => {
|
|
26
|
+
const offset = params.length;
|
|
27
|
+
const text = renumberPlaceholders(q.text, offset, dialect);
|
|
28
|
+
for (const p of q.parameters) params.push(p);
|
|
29
|
+
return text;
|
|
30
|
+
});
|
|
31
|
+
const text = fragments.join(` ${SET_KEYWORD[op]} `);
|
|
32
|
+
return Object.freeze({ text, parameters: Object.freeze(params) });
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// ---- Batch (§2) ----
|
|
36
|
+
export interface BatchHandle {
|
|
37
|
+
readonly statements: readonly CompiledQuery[];
|
|
38
|
+
execute<R>(runner: (stmts: readonly CompiledQuery[]) => Promise<readonly R[]>): Promise<readonly R[]>;
|
|
39
|
+
}
|
|
40
|
+
export function batch(statements: readonly CompiledQuery[]): BatchHandle {
|
|
41
|
+
return {
|
|
42
|
+
statements,
|
|
43
|
+
async execute(runner) {
|
|
44
|
+
if (statements.length === 0) return [];
|
|
45
|
+
return runner(statements);
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|