@voltro/plugin-duckdb 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,52 @@
1
+ import { AggregateQuery } from '@voltro/runtime';
2
+ import { AnalyticsBucket } from '@voltro/runtime';
3
+ import { AnalyticsEvent } from '@voltro/runtime';
4
+ import { AnalyticsSinkSpec } from '@voltro/runtime';
5
+ import { AnalyticsTopEntry } from '@voltro/runtime';
6
+ import { TimeseriesQuery } from '@voltro/runtime';
7
+ import { TopNQuery } from '@voltro/runtime';
8
+
9
+ export { AggregateQuery }
10
+
11
+ export { AnalyticsBucket }
12
+
13
+ export { AnalyticsEvent }
14
+
15
+ export { AnalyticsTopEntry }
16
+
17
+ /**
18
+ * Construct the spec. `build()` returns the sink wrapper; `initialize`
19
+ * opens the DuckDB connection + creates the events table; `dispose`
20
+ * closes the connection + instance at shutdown. All three share a
21
+ * `state` holder so methods can grab the connection once it's been
22
+ * filled in, and teardown can release it.
23
+ */
24
+ export declare const duckdbAnalytics: (options?: DuckdbAnalyticsOptions) => AnalyticsSinkSpec;
25
+
26
+ export declare interface DuckdbAnalyticsOptions extends DuckdbSinkOptions {
27
+ }
28
+
29
+ declare interface DuckdbSinkOptions {
30
+ readonly path?: string;
31
+ /**
32
+ * Reactive tables to CDC-mirror into DuckDB. Each table gets a
33
+ * `voltro_mirror_<table>` table (`{ id, data }`) so analytical
34
+ * queries can JOIN events against live user data:
35
+ *
36
+ * SELECT json_extract_string(m.data, '$.tier'), COUNT(*)
37
+ * FROM voltro_events e
38
+ * JOIN voltro_mirror_users m ON m.id = e.subject_id
39
+ * GROUP BY 1
40
+ *
41
+ * Empty / omitted → events-only.
42
+ */
43
+ readonly mirrorTables?: ReadonlyArray<string>;
44
+ /** Primary-key column on the mirrored source rows. Default `'id'`. */
45
+ readonly mirrorPrimaryKey?: string;
46
+ }
47
+
48
+ export { TimeseriesQuery }
49
+
50
+ export { TopNQuery }
51
+
52
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,203 @@
1
+ import { Effect as e, Option as t } from "effect";
2
+ import { AnalyticsError as n } from "@voltro/runtime";
3
+ //#region src/sink.ts
4
+ var r = "duckdb", i = "voltro_events", a = null, o = () => a === null ? e.tryPromise({
5
+ try: async () => {
6
+ let e = await import("@duckdb/node-api");
7
+ return a = e, e;
8
+ },
9
+ catch: (e) => new n({
10
+ provider: r,
11
+ operation: "loadNativeModule",
12
+ cause: "@duckdb/node-api could not be loaded — the native binary for this platform may be missing (it is an optionalDependency). " + (e instanceof Error ? `${e.name}: ${e.message}` : String(e))
13
+ })
14
+ }) : e.succeed(a), s = (e) => new a.DuckDBTimestampValue(BigInt(e.getTime()) * 1000n), c = (e) => `voltro_mirror_${e}`, l = /^[A-Za-z_][A-Za-z0-9_]*$/, u = (e, t, i) => {
15
+ if (!l.test(t)) throw new n({
16
+ provider: r,
17
+ operation: e,
18
+ cause: `invalid ${i} ${JSON.stringify(t)} — must match ${l.source} (letters, digits, underscore; no dots/quotes/spaces)`
19
+ });
20
+ return t;
21
+ }, d = /['\\]/, f = (e) => {
22
+ for (let t = 0; t < e.length; t++) if (e.charCodeAt(t) < 32) return !0;
23
+ return !1;
24
+ }, p = (e, t) => {
25
+ if (d.test(t) || f(t)) throw new n({
26
+ provider: r,
27
+ operation: e,
28
+ cause: `invalid property key ${JSON.stringify(t)} — must not contain quotes, backslashes, or control characters`
29
+ });
30
+ return t;
31
+ }, m = `
32
+ CREATE SEQUENCE IF NOT EXISTS voltro_events_id_seq;
33
+ CREATE TABLE IF NOT EXISTS ${i} (
34
+ id BIGINT PRIMARY KEY DEFAULT nextval('voltro_events_id_seq'),
35
+ name VARCHAR NOT NULL,
36
+ subject_id VARCHAR,
37
+ tenant_id VARCHAR,
38
+ properties JSON NOT NULL DEFAULT '{}',
39
+ occurred_at TIMESTAMP NOT NULL DEFAULT now()
40
+ );
41
+ `.trim(), h = (i) => (a) => a.pipe(e.catchSomeDefect((r) => r instanceof n ? t.some(e.fail(r)) : t.none()), e.mapError((e) => e instanceof n ? e : new n({
42
+ provider: r,
43
+ operation: i,
44
+ cause: e instanceof Error ? `${e.name}: ${e.message}` : String(e)
45
+ }))), g = (t) => t.conn === null ? e.fail(new n({
46
+ provider: r,
47
+ operation: "ensureConn",
48
+ cause: "connection not initialized — spec.initialize must run before queries"
49
+ })) : e.succeed(t.conn), _ = (e, t) => `date_trunc('${t}', ${e})`, v = (e, t) => `json_extract_string(properties, '$.${p(e, t)}')`, y = (e, t) => `TRY_CAST(json_extract_string(properties, '$.${p(e, t)}') AS DOUBLE)`, b = (e, t) => t === "count" ? "COUNT(*)" : t === "unique" ? "COUNT(DISTINCT subject_id)" : "sum" in t ? `COALESCE(SUM(${y(e, t.sum)}), 0)` : `AVG(${y(e, t.avg)})`, x = (e, t) => t === "count" ? "COUNT(*)" : `COALESCE(SUM(${y(e, t.sum)}), 0)`, S = (e, t, n, r, i) => {
50
+ let { VARCHAR: o, TIMESTAMP: c } = a, l = [], u = [], d = [];
51
+ if (d.push("name = ?"), l.push(t), u.push(o), d.push("occurred_at >= ?"), l.push(s(n.from)), u.push(c), d.push("occurred_at < ?"), l.push(s(n.to ?? /* @__PURE__ */ new Date())), u.push(c), i != null && (d.push("tenant_id = ?"), l.push(i), u.push(o)), r !== void 0) for (let [t, n] of Object.entries(r)) d.push(`${v(e, t)} = ?`), l.push(String(n)), u.push(o);
52
+ return {
53
+ sql: d.join(" AND "),
54
+ params: l,
55
+ types: u
56
+ };
57
+ }, C = (t, n, r) => {
58
+ let i = /* @__PURE__ */ new Set(), o = (n, r) => e.gen(function* () {
59
+ if (i.has(r)) return;
60
+ u(n, r, "mirror table name");
61
+ let a = yield* g(t), o = c(r);
62
+ yield* e.tryPromise({
63
+ try: () => a.run(`CREATE TABLE IF NOT EXISTS ${o} (id VARCHAR PRIMARY KEY, data JSON NOT NULL)`),
64
+ catch: (e) => e
65
+ }), i.add(r);
66
+ });
67
+ return {
68
+ tables: n,
69
+ primaryKey: r,
70
+ upsert: (n, i) => e.gen(function* () {
71
+ yield* o("mirror.upsert", n);
72
+ let s = yield* g(t), { VARCHAR: l } = a, u = c(n), d = String(i[r]), f = JSON.stringify(i);
73
+ yield* e.tryPromise({
74
+ try: () => s.run(`INSERT INTO ${u} (id, data) VALUES (?, ?::JSON) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data`, [d, f], [l, l]),
75
+ catch: (e) => e
76
+ });
77
+ }).pipe(h("mirror.upsert")),
78
+ remove: (n, r) => e.gen(function* () {
79
+ yield* o("mirror.remove", n);
80
+ let i = yield* g(t), { VARCHAR: s } = a, l = c(n), u = String(r);
81
+ yield* e.tryPromise({
82
+ try: () => i.run(`DELETE FROM ${l} WHERE id = ?`, [u], [s]),
83
+ catch: (e) => e
84
+ });
85
+ }).pipe(h("mirror.remove"))
86
+ };
87
+ }, w = (t, n) => {
88
+ let o = n.mirrorTables ?? [], c = o.length > 0 ? C(t, o, n.mirrorPrimaryKey ?? "id") : void 0;
89
+ return {
90
+ info: {
91
+ provider: r,
92
+ capabilities: {
93
+ track: !0,
94
+ aggregate: !0,
95
+ timeseries: !0,
96
+ topN: !0,
97
+ mirror: c !== void 0
98
+ },
99
+ detail: {
100
+ path: n.path ?? ":memory:",
101
+ ...o.length > 0 ? { mirrorTables: o } : {}
102
+ }
103
+ },
104
+ track: (n) => e.gen(function* () {
105
+ let r = yield* g(t), { VARCHAR: o, TIMESTAMP: c } = a, l = n.occurredAt ?? /* @__PURE__ */ new Date(), u = JSON.stringify(n.properties), d = n.subjectId ?? null, f = n.tenantId ?? null;
106
+ yield* e.tryPromise({
107
+ try: () => r.run(`INSERT INTO ${i} (name, subject_id, tenant_id, properties, occurred_at) VALUES (?, ?, ?, ?::JSON, ?)`, [
108
+ n.name,
109
+ d,
110
+ f,
111
+ u,
112
+ s(l)
113
+ ], [
114
+ o,
115
+ o,
116
+ o,
117
+ o,
118
+ c
119
+ ]),
120
+ catch: (e) => e
121
+ });
122
+ }).pipe(h("track")),
123
+ aggregate: (n) => e.gen(function* () {
124
+ let r = yield* g(t), a = S("aggregate", n.event, n.range, n.filter, n.tenantId), o = `SELECT ${b("aggregate", n.metric)} AS value FROM ${i} WHERE ${a.sql}`, s = (yield* e.tryPromise({
125
+ try: () => r.runAndReadAll(o, a.params, a.types),
126
+ catch: (e) => e
127
+ })).getRowObjectsJS()[0]?.value ?? 0;
128
+ return typeof s == "number" ? s : Number(s ?? 0);
129
+ }).pipe(h("aggregate")),
130
+ timeseries: (n) => e.gen(function* () {
131
+ let r = yield* g(t), a = S("timeseries", n.event, n.range, n.filter, n.tenantId), o = _("occurred_at", n.bucket), s = `
132
+ SELECT ${o} AS bucket_start, ${b("timeseries", n.metric)} AS value
133
+ FROM ${i}
134
+ WHERE ${a.sql}
135
+ GROUP BY ${o}
136
+ ORDER BY ${o} ASC
137
+ `;
138
+ return (yield* e.tryPromise({
139
+ try: () => r.runAndReadAll(s, a.params, a.types),
140
+ catch: (e) => e
141
+ })).getRowObjectsJS().map((e) => ({
142
+ bucketStart: e.bucket_start instanceof Date ? e.bucket_start : new Date(e.bucket_start),
143
+ value: typeof e.value == "number" ? e.value : Number(e.value ?? 0)
144
+ }));
145
+ }).pipe(h("timeseries")),
146
+ topN: (n) => e.gen(function* () {
147
+ let r = yield* g(t), a = S("topN", n.event, n.range, n.filter, n.tenantId), o = Math.max(1, Math.floor(n.n)), s = v("topN", n.groupBy), c = `
148
+ SELECT ${s} AS key, ${x("topN", n.metric)} AS value
149
+ FROM ${i}
150
+ WHERE ${a.sql}
151
+ GROUP BY ${s}
152
+ ORDER BY value DESC
153
+ LIMIT ${o}
154
+ `;
155
+ return (yield* e.tryPromise({
156
+ try: () => r.runAndReadAll(c, a.params, a.types),
157
+ catch: (e) => e
158
+ })).getRowObjectsJS().filter((e) => e.key !== null).map((e) => ({
159
+ key: e.key,
160
+ value: typeof e.value == "number" ? e.value : Number(e.value ?? 0)
161
+ }));
162
+ }).pipe(h("topN")),
163
+ ...c === void 0 ? {} : { mirror: c }
164
+ };
165
+ }, T = (t, i) => e.gen(function* () {
166
+ if (t.conn !== null) return;
167
+ let a = yield* o(), s = yield* e.tryPromise({
168
+ try: async () => {
169
+ let e = await a.DuckDBInstance.create(i.path ?? ":memory:"), t = await e.connect();
170
+ return await t.run(m), {
171
+ instance: e,
172
+ connection: t
173
+ };
174
+ },
175
+ catch: (e) => new n({
176
+ provider: r,
177
+ operation: "initialize",
178
+ cause: e instanceof Error ? `${e.name}: ${e.message}` : String(e)
179
+ })
180
+ });
181
+ t.instance = s.instance, t.conn = s.connection;
182
+ }), E = (t) => e.gen(function* () {
183
+ let { conn: n, instance: i } = t;
184
+ t.conn = null, t.instance = null, !(n === null && i === null) && (yield* e.try(() => {
185
+ n?.closeSync(), i?.closeSync();
186
+ }).pipe(e.catchAll((t) => e.logWarning("duckdb analytics sink teardown failed", {
187
+ provider: r,
188
+ err: t
189
+ }))));
190
+ }), D = (e = {}) => {
191
+ let t = {
192
+ conn: null,
193
+ instance: null
194
+ };
195
+ return {
196
+ kind: "duckdb",
197
+ build: (n) => w(t, e),
198
+ initialize: () => T(t, e),
199
+ dispose: () => E(t)
200
+ };
201
+ };
202
+ //#endregion
203
+ export { D as duckdbAnalytics };
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@voltro/plugin-duckdb",
3
+ "version": "0.1.0",
4
+ "description": "Embedded DuckDB AnalyticsSink. Runs DuckDB in-process for real OLAP performance without an external service. Events land in a DuckDB column-store table; aggregate / timeseries / topN queries execute vectorized.",
5
+ "keywords": [
6
+ "voltro",
7
+ "typescript",
8
+ "framework"
9
+ ],
10
+ "license": "SEE LICENSE IN LICENSE",
11
+ "homepage": "https://voltro.dev",
12
+ "bugs": {
13
+ "email": "support@voltro.dev"
14
+ },
15
+ "author": {
16
+ "name": "Voltro UG",
17
+ "url": "https://voltro.dev"
18
+ },
19
+ "type": "module",
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.ts",
23
+ "import": "./dist/index.js",
24
+ "default": "./dist/index.js"
25
+ }
26
+ },
27
+ "main": "./dist/index.js",
28
+ "module": "./dist/index.js",
29
+ "types": "./dist/index.d.ts",
30
+ "sideEffects": false,
31
+ "engines": {
32
+ "node": ">=24.0.0"
33
+ },
34
+ "dependencies": {
35
+ "@voltro/runtime": "0.1.0"
36
+ },
37
+ "optionalDependencies": {
38
+ "@duckdb/node-api": "1.5.4-r.1"
39
+ },
40
+ "peerDependencies": {
41
+ "effect": "^3.21.4"
42
+ },
43
+ "publishConfig": {
44
+ "access": "public"
45
+ }
46
+ }