@uptimizr/db 0.8.2 → 1.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/bin/uptimizr-db-migrate.js +4 -0
- package/bin/uptimizr-db-new-project.js +4 -0
- package/dist/cli/createProject.js +0 -0
- package/dist/cli/migrate.js +0 -0
- package/dist/env.d.ts +57 -2
- package/dist/env.d.ts.map +1 -1
- package/dist/env.js +31 -2
- package/dist/env.js.map +1 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -1
- package/dist/query/aggregations.d.ts +4 -4
- package/dist/query/aggregations.d.ts.map +1 -1
- package/dist/query/aggregations.js +200 -143
- package/dist/query/aggregations.js.map +1 -1
- package/dist/query/clickhouseDialect.d.ts.map +1 -1
- package/dist/query/clickhouseDialect.js +7 -2
- package/dist/query/clickhouseDialect.js.map +1 -1
- package/dist/query/dialect.d.ts +55 -11
- package/dist/query/dialect.d.ts.map +1 -1
- package/dist/query/dialect.js +6 -4
- package/dist/query/dialect.js.map +1 -1
- package/dist/query/duckdbDialect.d.ts.map +1 -1
- package/dist/query/duckdbDialect.js +7 -2
- package/dist/query/duckdbDialect.js.map +1 -1
- package/dist/query/index.d.ts +3 -0
- package/dist/query/index.d.ts.map +1 -1
- package/dist/query/index.js +6 -0
- package/dist/query/index.js.map +1 -1
- package/dist/query/mssqlDialect.d.ts +99 -0
- package/dist/query/mssqlDialect.d.ts.map +1 -0
- package/dist/query/mssqlDialect.js +398 -0
- package/dist/query/mssqlDialect.js.map +1 -0
- package/dist/query/postgresDialect.d.ts +48 -0
- package/dist/query/postgresDialect.d.ts.map +1 -0
- package/dist/query/postgresDialect.js +167 -0
- package/dist/query/postgresDialect.js.map +1 -0
- package/dist/query/relational.d.ts +69 -0
- package/dist/query/relational.d.ts.map +1 -0
- package/dist/query/relational.js +94 -0
- package/dist/query/relational.js.map +1 -0
- package/package.json +5 -4
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PostgreSQL dialect for the query layer (ADR 0020, #84).
|
|
3
|
+
*
|
|
4
|
+
* Postgres is the optional single-tenant *relational* store: a multi-writer
|
|
5
|
+
* server many self-hosters already operate, for shops that "only want Postgres"
|
|
6
|
+
* and have outgrown DuckDB's single read-write process. This dialect renders the
|
|
7
|
+
* engine-specific fragments of each shared aggregation (`aggregations.ts`) to
|
|
8
|
+
* Postgres SQL, so the bulk of every query stays shared with DuckDB/ClickHouse.
|
|
9
|
+
*
|
|
10
|
+
* Like the other dialects it is *single-tenant* — no `org_id`, no tenant
|
|
11
|
+
* isolation — which keeps it relocatable across the open-core boundary.
|
|
12
|
+
*
|
|
13
|
+
* Binding model: parameters are emitted as named `$name::type` placeholders
|
|
14
|
+
* (every logical {@link ParamType} carries an explicit cast so Postgres never has
|
|
15
|
+
* to infer a parameter type from context) and rewritten to positional `$1…$n`
|
|
16
|
+
* by `toPositionalParams` in the `@uptimizr/db-postgres` runner. Timestamp params
|
|
17
|
+
* are bound as naive-UTC strings (see {@link toPostgresTimestamp}) against
|
|
18
|
+
* `timestamp` (without time zone) columns, so ordering, bucketing, and `::date`
|
|
19
|
+
* truncation are wall-clock-UTC exactly as on DuckDB and ClickHouse, independent
|
|
20
|
+
* of the session `TimeZone`.
|
|
21
|
+
*
|
|
22
|
+
* Row-store fit gaps (issue #84) and how they are closed here:
|
|
23
|
+
* - **No `ASOF JOIN`.** `asofJoin` renders the shared nearest-row emulation
|
|
24
|
+
* (`renderNearestRowJoin`, `relational.ts`) as `JOIN LATERAL (… ORDER BY ts
|
|
25
|
+
* DESC LIMIT 1) ON TRUE`.
|
|
26
|
+
* - **No MergeTree rollups.** The daily rollups are plain views that recompute
|
|
27
|
+
* at query time (see the `@uptimizr/db-postgres` migrations), so the `-Merge`
|
|
28
|
+
* combinators reduce to pass-through aggregates, as on DuckDB.
|
|
29
|
+
* - **Arrays are 1-indexed** — the same convention DuckDB and ClickHouse use, so
|
|
30
|
+
* `position[1]` in the shared SQL needs no translation; `arrayLength` maps to
|
|
31
|
+
* `cardinality` and `vectorNorm` unnests the array.
|
|
32
|
+
* - **JSON** lives in a `jsonb` column; extraction is `#>>` with a text[] path,
|
|
33
|
+
* and the numeric extractors regex-guard the cast (Postgres has no `TRY_CAST`)
|
|
34
|
+
* so an absent / non-numeric key yields NULL exactly like DuckDB's `TRY_CAST`.
|
|
35
|
+
*/
|
|
36
|
+
import { renderNearestRowJoin } from "./relational.js";
|
|
37
|
+
/** Map a logical {@link ParamType} to the explicit Postgres cast it is bound with. */
|
|
38
|
+
function pgCast(type) {
|
|
39
|
+
switch (type) {
|
|
40
|
+
case "string":
|
|
41
|
+
return "text";
|
|
42
|
+
case "u32":
|
|
43
|
+
return "integer";
|
|
44
|
+
case "f64":
|
|
45
|
+
return "double precision";
|
|
46
|
+
case "timestamp":
|
|
47
|
+
return "timestamp";
|
|
48
|
+
case "date":
|
|
49
|
+
return "date";
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Format an epoch-millisecond timestamp as a naive-UTC `YYYY-MM-DD HH:MM:SS.mmm`
|
|
54
|
+
* string for binding to a Postgres `timestamp` (without time zone) column/param.
|
|
55
|
+
* Mirrors the DuckDB / ClickHouse literal format so all stores order and bucket
|
|
56
|
+
* time identically.
|
|
57
|
+
*/
|
|
58
|
+
export function toPostgresTimestamp(epochMs) {
|
|
59
|
+
const d = new Date(epochMs);
|
|
60
|
+
const p = (n, w = 2) => String(n).padStart(w, "0");
|
|
61
|
+
return (`${d.getUTCFullYear()}-${p(d.getUTCMonth() + 1)}-${p(d.getUTCDate())} ` +
|
|
62
|
+
`${p(d.getUTCHours())}:${p(d.getUTCMinutes())}:${p(d.getUTCSeconds())}.${p(d.getUTCMilliseconds(), 3)}`);
|
|
63
|
+
}
|
|
64
|
+
/** Join keywords for the Postgres flavour of the nearest-row ASOF emulation. */
|
|
65
|
+
export const POSTGRES_NEAREST_ROW_JOIN = {
|
|
66
|
+
inner: "INNER JOIN LATERAL",
|
|
67
|
+
left: "LEFT JOIN LATERAL",
|
|
68
|
+
onTrue: "ON TRUE",
|
|
69
|
+
selectPrefix: "SELECT",
|
|
70
|
+
selectSuffix: "LIMIT 1",
|
|
71
|
+
};
|
|
72
|
+
/** Render a trusted compile-time key path as a Postgres `text[]` literal. */
|
|
73
|
+
function jsonPath(path) {
|
|
74
|
+
return `'{${path.map((k) => `"${k}"`).join(",")}}'`;
|
|
75
|
+
}
|
|
76
|
+
/** `(column #>> '{path}')` — the JSON value at `path` as text, NULL when absent. */
|
|
77
|
+
function jsonAt(column, path) {
|
|
78
|
+
return `(${column} #>> ${jsonPath(path)})`;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Regex-guarded numeric cast of a JSON text extraction. Postgres has no
|
|
82
|
+
* `TRY_CAST`, so the value is only cast when it matches the numeric shape;
|
|
83
|
+
* anything else (absent key, non-numeric string) yields NULL — parity with
|
|
84
|
+
* DuckDB's `TRY_CAST(... AS BIGINT|DOUBLE)`.
|
|
85
|
+
*/
|
|
86
|
+
function guardedCast(extract, pattern, type) {
|
|
87
|
+
return `CASE WHEN ${extract} ~ '${pattern}' THEN ${extract}::${type} END`;
|
|
88
|
+
}
|
|
89
|
+
export const postgresDialect = {
|
|
90
|
+
name: "postgres",
|
|
91
|
+
placeholder(name, type) {
|
|
92
|
+
return `$${name}::${pgCast(type)}`;
|
|
93
|
+
},
|
|
94
|
+
timestampValue(epochMs) {
|
|
95
|
+
return toPostgresTimestamp(epochMs);
|
|
96
|
+
},
|
|
97
|
+
quantile(expr, q) {
|
|
98
|
+
// `percentile_cont` interpolates linearly between adjacent ranks (type-7),
|
|
99
|
+
// exactly like DuckDB's `quantile_cont` and ClickHouse's `quantile`; NULLs
|
|
100
|
+
// are ignored and an empty input yields NULL. Cast so integer-typed
|
|
101
|
+
// expressions (e.g. `long_frames` sums) are accepted.
|
|
102
|
+
return `percentile_cont(${q}) WITHIN GROUP (ORDER BY (${expr})::double precision)`;
|
|
103
|
+
},
|
|
104
|
+
vectorNorm(expr) {
|
|
105
|
+
// Postgres has no vector norm; unnest the array in a correlated scalar
|
|
106
|
+
// subquery. Works for any length (matches L2Norm / list_dot_product).
|
|
107
|
+
return `sqrt((SELECT sum(v * v) FROM unnest(${expr}) AS u(v)))`;
|
|
108
|
+
},
|
|
109
|
+
arrayLength(expr) {
|
|
110
|
+
// `cardinality` returns 0 for an empty array (`array_length` returns NULL).
|
|
111
|
+
return `cardinality(${expr})`;
|
|
112
|
+
},
|
|
113
|
+
avgIf(value, cond) {
|
|
114
|
+
return `avg(${value}) FILTER (WHERE ${cond})`;
|
|
115
|
+
},
|
|
116
|
+
anyValue(expr) {
|
|
117
|
+
// `min` is a valid "any" (the callers use it on values constant within the
|
|
118
|
+
// group), ignores NULLs like DuckDB's `any_value`, and — unlike PG 16's
|
|
119
|
+
// `any_value()` — works on every supported Postgres version.
|
|
120
|
+
return `min(${expr})`;
|
|
121
|
+
},
|
|
122
|
+
timeBucketMs(tsExpr, intervalPlaceholder) {
|
|
123
|
+
// Integer epoch-ms floored to the interval grid (same arithmetic as DuckDB /
|
|
124
|
+
// ClickHouse). Divide in double precision so the interval param's integer
|
|
125
|
+
// type can never turn `/` into a truncating integer division.
|
|
126
|
+
const ms = `(EXTRACT(EPOCH FROM ${tsExpr}) * 1000)::bigint`;
|
|
127
|
+
const bucket = `(${intervalPlaceholder} * 1000)`;
|
|
128
|
+
return `(floor(${ms}::double precision / ${bucket}) * ${bucket})::bigint`;
|
|
129
|
+
},
|
|
130
|
+
epochMs(tsExpr) {
|
|
131
|
+
// EXTRACT(EPOCH) of a naive timestamp is wall-clock UTC seconds; the store
|
|
132
|
+
// writes millisecond precision, so `* 1000` is exact before the cast.
|
|
133
|
+
return `(EXTRACT(EPOCH FROM ${tsExpr}) * 1000)::bigint`;
|
|
134
|
+
},
|
|
135
|
+
toDate(expr) {
|
|
136
|
+
return `CAST(${expr} AS DATE)`;
|
|
137
|
+
},
|
|
138
|
+
toText(expr) {
|
|
139
|
+
return `CAST(${expr} AS TEXT)`;
|
|
140
|
+
},
|
|
141
|
+
jsonText(column, ...path) {
|
|
142
|
+
return jsonAt(column, path);
|
|
143
|
+
},
|
|
144
|
+
jsonInt(column, ...path) {
|
|
145
|
+
return guardedCast(jsonAt(column, path), "^-?[0-9]+$", "bigint");
|
|
146
|
+
},
|
|
147
|
+
jsonFloat(column, ...path) {
|
|
148
|
+
// Numeric path components index a JSON array (0-based) natively in `#>>`.
|
|
149
|
+
return guardedCast(jsonAt(column, path), "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$", "double precision");
|
|
150
|
+
},
|
|
151
|
+
// The daily rollups are query-time views (pre-grouped by day), so each read
|
|
152
|
+
// GROUP BY sees exactly one source row per group and the "merge" of a single
|
|
153
|
+
// precomputed value is a plain pass-through aggregate — as on DuckDB.
|
|
154
|
+
countMerge(stateExpr) {
|
|
155
|
+
return `sum(${stateExpr})`;
|
|
156
|
+
},
|
|
157
|
+
avgMerge(stateExpr) {
|
|
158
|
+
return `avg(${stateExpr})`;
|
|
159
|
+
},
|
|
160
|
+
quantileMerge(stateExpr, q) {
|
|
161
|
+
return `percentile_cont(${q}) WITHIN GROUP (ORDER BY (${stateExpr})::double precision)`;
|
|
162
|
+
},
|
|
163
|
+
asofJoin(spec) {
|
|
164
|
+
return renderNearestRowJoin(spec, POSTGRES_NEAREST_ROW_JOIN);
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
//# sourceMappingURL=postgresDialect.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postgresDialect.js","sourceRoot":"","sources":["../../src/query/postgresDialect.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-neutral building blocks shared by the *relational* dialects
|
|
3
|
+
* (ADR 0020) — the pieces that are not Postgres-specific and that the SQL
|
|
4
|
+
* Server port (#85) is expected to reuse verbatim:
|
|
5
|
+
*
|
|
6
|
+
* 1. {@link renderNativeAsofJoin} — the `ASOF JOIN` clause for engines that
|
|
7
|
+
* have one (DuckDB, ClickHouse).
|
|
8
|
+
* 2. {@link renderNearestRowJoin} — the ASOF *emulation* for row stores that
|
|
9
|
+
* lack it: a correlated nearest-row subquery (`LATERAL … LIMIT 1` on
|
|
10
|
+
* Postgres, `CROSS/OUTER APPLY … TOP 1` on SQL Server). Only the join
|
|
11
|
+
* keywords differ, so they are injected as {@link NearestRowJoinTokens}.
|
|
12
|
+
* 3. {@link toPositionalParams} — rewrite the query layer's named `$name`
|
|
13
|
+
* placeholders to an engine's positional form (`$1` on Postgres, `@p1` on
|
|
14
|
+
* SQL Server) at execution time, so dialects stay pure string builders that
|
|
15
|
+
* never need to count parameters.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is pure (no I/O, no client coupling) and isomorphic.
|
|
18
|
+
*/
|
|
19
|
+
import type { AsofJoinSpec } from "./dialect.js";
|
|
20
|
+
/** Render `ASOF INNER|LEFT JOIN <right> AS <alias> ON <keys> AND <ts predicate>`. */
|
|
21
|
+
export declare function renderNativeAsofJoin(spec: AsofJoinSpec): string;
|
|
22
|
+
/**
|
|
23
|
+
* Engine keywords for {@link renderNearestRowJoin}. Postgres:
|
|
24
|
+
* `{ inner: "INNER JOIN LATERAL", left: "LEFT JOIN LATERAL", onTrue: "ON TRUE",
|
|
25
|
+
* selectPrefix: "SELECT", selectSuffix: "LIMIT 1" }`. SQL Server would use
|
|
26
|
+
* `CROSS APPLY` / `OUTER APPLY`, an empty `onTrue`, and `SELECT TOP 1` / `""`.
|
|
27
|
+
*/
|
|
28
|
+
export interface NearestRowJoinTokens {
|
|
29
|
+
/** Introducer for the inner (row-dropping) variant. */
|
|
30
|
+
readonly inner: string;
|
|
31
|
+
/** Introducer for the left (row-keeping) variant. */
|
|
32
|
+
readonly left: string;
|
|
33
|
+
/** Trailing join condition, e.g. `ON TRUE` (empty for `APPLY`). */
|
|
34
|
+
readonly onTrue: string;
|
|
35
|
+
/** `SELECT` keyword, optionally carrying a row cap (`SELECT TOP 1`). */
|
|
36
|
+
readonly selectPrefix: string;
|
|
37
|
+
/** Trailing row cap after `ORDER BY` (`LIMIT 1`), or empty. */
|
|
38
|
+
readonly selectSuffix: string;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Emulate an ASOF join on an engine without one: for every left row, a
|
|
42
|
+
* correlated subquery picks the single right row with equal keys and the
|
|
43
|
+
* nearest timestamp in the requested direction. Exactly the semantics of
|
|
44
|
+
* `ASOF JOIN` (ties on the right timestamp are broken arbitrarily on every
|
|
45
|
+
* engine).
|
|
46
|
+
*
|
|
47
|
+
* Trade-off (documented in `@uptimizr/db-postgres`): the planner evaluates the
|
|
48
|
+
* subquery once per left row — an index-backed nearest-row lookup — instead of
|
|
49
|
+
* DuckDB/ClickHouse's single merge pass. Fine at the self-host scale this store
|
|
50
|
+
* targets; the `(project_id, session_id, ts)` indexes exist for exactly this.
|
|
51
|
+
*/
|
|
52
|
+
export declare function renderNearestRowJoin(spec: AsofJoinSpec, t: NearestRowJoinTokens): string;
|
|
53
|
+
/** Result of {@link toPositionalParams}: the rewritten SQL and ordered values. */
|
|
54
|
+
export interface PositionalQuery {
|
|
55
|
+
readonly sql: string;
|
|
56
|
+
readonly values: unknown[];
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Rewrite named `$name` placeholders into positional ones. Each distinct name
|
|
60
|
+
* is assigned the next index on first sight (so a name bound several times maps
|
|
61
|
+
* to one value), and `render(index)` produces the engine's token (`$1`, `@p1`).
|
|
62
|
+
* A placeholder with no bound value is a programming error and throws.
|
|
63
|
+
*
|
|
64
|
+
* Only bare `$identifier` tokens are rewritten; the query layer never emits a
|
|
65
|
+
* `$` in any other position (no dollar-quoting, no JSON `$.path` on these
|
|
66
|
+
* dialects), so no literal-awareness is needed.
|
|
67
|
+
*/
|
|
68
|
+
export declare function toPositionalParams(sql: string, params: Readonly<Record<string, unknown>>, render: (index: number) => string): PositionalQuery;
|
|
69
|
+
//# sourceMappingURL=relational.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relational.d.ts","sourceRoot":"","sources":["../../src/query/relational.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAEjD,qFAAqF;AACrF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAK/D;AAED;;;;;GAKG;AACH,MAAM,WAAW,oBAAoB;IACnC,uDAAuD;IACvD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,qDAAqD;IACrD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,wEAAwE;IACxE,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,+DAA+D;IAC/D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAoBD;;;;;;;;;;;GAWG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,YAAY,EAAE,CAAC,EAAE,oBAAoB,GAAG,MAAM,CAYxF;AAED,kFAAkF;AAClF,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;CAC5B;AAED;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,EACzC,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,GAChC,eAAe,CAejB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Engine-neutral building blocks shared by the *relational* dialects
|
|
3
|
+
* (ADR 0020) — the pieces that are not Postgres-specific and that the SQL
|
|
4
|
+
* Server port (#85) is expected to reuse verbatim:
|
|
5
|
+
*
|
|
6
|
+
* 1. {@link renderNativeAsofJoin} — the `ASOF JOIN` clause for engines that
|
|
7
|
+
* have one (DuckDB, ClickHouse).
|
|
8
|
+
* 2. {@link renderNearestRowJoin} — the ASOF *emulation* for row stores that
|
|
9
|
+
* lack it: a correlated nearest-row subquery (`LATERAL … LIMIT 1` on
|
|
10
|
+
* Postgres, `CROSS/OUTER APPLY … TOP 1` on SQL Server). Only the join
|
|
11
|
+
* keywords differ, so they are injected as {@link NearestRowJoinTokens}.
|
|
12
|
+
* 3. {@link toPositionalParams} — rewrite the query layer's named `$name`
|
|
13
|
+
* placeholders to an engine's positional form (`$1` on Postgres, `@p1` on
|
|
14
|
+
* SQL Server) at execution time, so dialects stay pure string builders that
|
|
15
|
+
* never need to count parameters.
|
|
16
|
+
*
|
|
17
|
+
* Everything here is pure (no I/O, no client coupling) and isomorphic.
|
|
18
|
+
*/
|
|
19
|
+
/** Render `ASOF INNER|LEFT JOIN <right> AS <alias> ON <keys> AND <ts predicate>`. */
|
|
20
|
+
export function renderNativeAsofJoin(spec) {
|
|
21
|
+
const kind = spec.kind === "left" ? "ASOF LEFT JOIN" : "ASOF INNER JOIN";
|
|
22
|
+
const keys = spec.keys.map(([l, r]) => `${l} = ${spec.alias}.${r}`).join(" AND ");
|
|
23
|
+
return `${kind} ${spec.right} AS ${spec.alias}
|
|
24
|
+
ON ${keys} AND ${spec.leftTs} ${spec.op} ${spec.alias}.${spec.rightTs}`;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Flip a `leftTs <op> rightTs` comparison into the `rightTs <op'> leftTs` form a
|
|
28
|
+
* correlated subquery filters with, plus the sort direction that puts the
|
|
29
|
+
* nearest right row first.
|
|
30
|
+
*/
|
|
31
|
+
function nearestRowPredicate(op) {
|
|
32
|
+
switch (op) {
|
|
33
|
+
case ">=":
|
|
34
|
+
return { flipped: "<=", order: "DESC" };
|
|
35
|
+
case ">":
|
|
36
|
+
return { flipped: "<", order: "DESC" };
|
|
37
|
+
case "<=":
|
|
38
|
+
return { flipped: ">=", order: "ASC" };
|
|
39
|
+
case "<":
|
|
40
|
+
return { flipped: ">", order: "ASC" };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Emulate an ASOF join on an engine without one: for every left row, a
|
|
45
|
+
* correlated subquery picks the single right row with equal keys and the
|
|
46
|
+
* nearest timestamp in the requested direction. Exactly the semantics of
|
|
47
|
+
* `ASOF JOIN` (ties on the right timestamp are broken arbitrarily on every
|
|
48
|
+
* engine).
|
|
49
|
+
*
|
|
50
|
+
* Trade-off (documented in `@uptimizr/db-postgres`): the planner evaluates the
|
|
51
|
+
* subquery once per left row — an index-backed nearest-row lookup — instead of
|
|
52
|
+
* DuckDB/ClickHouse's single merge pass. Fine at the self-host scale this store
|
|
53
|
+
* targets; the `(project_id, session_id, ts)` indexes exist for exactly this.
|
|
54
|
+
*/
|
|
55
|
+
export function renderNearestRowJoin(spec, t) {
|
|
56
|
+
const { flipped, order } = nearestRowPredicate(spec.op);
|
|
57
|
+
const a = spec.alias;
|
|
58
|
+
const keys = spec.keys.map(([l, r]) => `${a}.${r} = ${l}`).join(" AND ");
|
|
59
|
+
const introducer = spec.kind === "left" ? t.left : t.inner;
|
|
60
|
+
const suffix = t.selectSuffix ? ` ${t.selectSuffix}` : "";
|
|
61
|
+
const onTrue = t.onTrue ? ` ${t.onTrue}` : "";
|
|
62
|
+
return `${introducer} (
|
|
63
|
+
${t.selectPrefix} * FROM ${spec.right} AS ${a}
|
|
64
|
+
WHERE ${keys} AND ${a}.${spec.rightTs} ${flipped} ${spec.leftTs}
|
|
65
|
+
ORDER BY ${a}.${spec.rightTs} ${order}${suffix}
|
|
66
|
+
) AS ${a}${onTrue}`;
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Rewrite named `$name` placeholders into positional ones. Each distinct name
|
|
70
|
+
* is assigned the next index on first sight (so a name bound several times maps
|
|
71
|
+
* to one value), and `render(index)` produces the engine's token (`$1`, `@p1`).
|
|
72
|
+
* A placeholder with no bound value is a programming error and throws.
|
|
73
|
+
*
|
|
74
|
+
* Only bare `$identifier` tokens are rewritten; the query layer never emits a
|
|
75
|
+
* `$` in any other position (no dollar-quoting, no JSON `$.path` on these
|
|
76
|
+
* dialects), so no literal-awareness is needed.
|
|
77
|
+
*/
|
|
78
|
+
export function toPositionalParams(sql, params, render) {
|
|
79
|
+
const indexByName = new Map();
|
|
80
|
+
const values = [];
|
|
81
|
+
const rewritten = sql.replace(/\$([A-Za-z_][A-Za-z0-9_]*)/g, (_match, name) => {
|
|
82
|
+
let index = indexByName.get(name);
|
|
83
|
+
if (index === undefined) {
|
|
84
|
+
if (!Object.prototype.hasOwnProperty.call(params, name)) {
|
|
85
|
+
throw new Error(`Unbound query parameter "$${name}"`);
|
|
86
|
+
}
|
|
87
|
+
index = values.push(params[name]);
|
|
88
|
+
indexByName.set(name, index);
|
|
89
|
+
}
|
|
90
|
+
return render(index);
|
|
91
|
+
});
|
|
92
|
+
return { sql: rewritten, values };
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=relational.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relational.js","sourceRoot":"","sources":["../../src/query/relational.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uptimizr/db",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "OSS storage contracts (dialect-agnostic query layer, neutral event/metadata types) and the single-file DuckDB store for Uptimizr.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"uptimizr",
|
|
@@ -39,11 +39,12 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"bin": {
|
|
42
|
-
"uptimizr-db-migrate": "./
|
|
43
|
-
"uptimizr-db-new-project": "./
|
|
42
|
+
"uptimizr-db-migrate": "./bin/uptimizr-db-migrate.js",
|
|
43
|
+
"uptimizr-db-new-project": "./bin/uptimizr-db-new-project.js"
|
|
44
44
|
},
|
|
45
45
|
"files": [
|
|
46
46
|
"dist",
|
|
47
|
+
"bin",
|
|
47
48
|
"README.md",
|
|
48
49
|
"LICENSE",
|
|
49
50
|
"AGENTS.md",
|
|
@@ -51,7 +52,7 @@
|
|
|
51
52
|
],
|
|
52
53
|
"dependencies": {
|
|
53
54
|
"@duckdb/node-api": "^1.5.5-r.4",
|
|
54
|
-
"@uptimizr/schema": "0.
|
|
55
|
+
"@uptimizr/schema": "1.0.0"
|
|
55
56
|
},
|
|
56
57
|
"devDependencies": {
|
|
57
58
|
"@types/node": "^26.4.0",
|