@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.
Files changed (43) hide show
  1. package/bin/uptimizr-db-migrate.js +4 -0
  2. package/bin/uptimizr-db-new-project.js +4 -0
  3. package/dist/cli/createProject.js +0 -0
  4. package/dist/cli/migrate.js +0 -0
  5. package/dist/env.d.ts +57 -2
  6. package/dist/env.d.ts.map +1 -1
  7. package/dist/env.js +31 -2
  8. package/dist/env.js.map +1 -1
  9. package/dist/index.d.ts +6 -2
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +5 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/query/aggregations.d.ts +4 -4
  14. package/dist/query/aggregations.d.ts.map +1 -1
  15. package/dist/query/aggregations.js +200 -143
  16. package/dist/query/aggregations.js.map +1 -1
  17. package/dist/query/clickhouseDialect.d.ts.map +1 -1
  18. package/dist/query/clickhouseDialect.js +7 -2
  19. package/dist/query/clickhouseDialect.js.map +1 -1
  20. package/dist/query/dialect.d.ts +55 -11
  21. package/dist/query/dialect.d.ts.map +1 -1
  22. package/dist/query/dialect.js +6 -4
  23. package/dist/query/dialect.js.map +1 -1
  24. package/dist/query/duckdbDialect.d.ts.map +1 -1
  25. package/dist/query/duckdbDialect.js +7 -2
  26. package/dist/query/duckdbDialect.js.map +1 -1
  27. package/dist/query/index.d.ts +3 -0
  28. package/dist/query/index.d.ts.map +1 -1
  29. package/dist/query/index.js +6 -0
  30. package/dist/query/index.js.map +1 -1
  31. package/dist/query/mssqlDialect.d.ts +99 -0
  32. package/dist/query/mssqlDialect.d.ts.map +1 -0
  33. package/dist/query/mssqlDialect.js +398 -0
  34. package/dist/query/mssqlDialect.js.map +1 -0
  35. package/dist/query/postgresDialect.d.ts +48 -0
  36. package/dist/query/postgresDialect.d.ts.map +1 -0
  37. package/dist/query/postgresDialect.js +167 -0
  38. package/dist/query/postgresDialect.js.map +1 -0
  39. package/dist/query/relational.d.ts +69 -0
  40. package/dist/query/relational.d.ts.map +1 -0
  41. package/dist/query/relational.js +94 -0
  42. package/dist/query/relational.js.map +1 -0
  43. 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.8.2",
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": "./dist/cli/migrate.js",
43
- "uptimizr-db-new-project": "./dist/cli/createProject.js"
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.6.1"
55
+ "@uptimizr/schema": "1.0.0"
55
56
  },
56
57
  "devDependencies": {
57
58
  "@types/node": "^26.4.0",