lex-gql-duckdb 0.2.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,12 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 349ddb3: Add aggregate enhancements, actorHandle filtering, and DuckDB adapter
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [349ddb3]
12
+ - lex-gql@0.2.0
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # lex-gql-duckdb
2
+
3
+ DuckDB adapter for [lex-gql](../lex-gql) - optimized for analytics-heavy workloads.
4
+
5
+ ## Why DuckDB?
6
+
7
+ DuckDB is a columnar database designed for analytical queries. Compared to SQLite:
8
+
9
+ - **~17x faster aggregates** on large datasets (tested with 660K+ records)
10
+ - Better suited for GROUP BY, COUNT, and time-series queries
11
+ - Same embeddable, serverless model as SQLite
12
+
13
+ Use this adapter when your workload involves heavy aggregate queries (top tracks, leaderboards, time-based analytics).
14
+
15
+ ## Installation
16
+
17
+ ```bash
18
+ npm install lex-gql-duckdb duckdb
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ ```javascript
24
+ import { createAdapter, parseLexicon } from 'lex-gql';
25
+ import { createDuckDB, setupSchema, createWriter, createDuckDBAdapter } from 'lex-gql-duckdb';
26
+
27
+ // 1. Create DuckDB connection
28
+ const db = await createDuckDB('./data/records.duckdb');
29
+ await setupSchema(db);
30
+
31
+ // 2. Create writer for inserting records
32
+ const writer = createWriter(db);
33
+
34
+ await writer.insertRecord({
35
+ uri: 'at://did:plc:xyz/app.example.post/abc',
36
+ cid: 'bafyrei...',
37
+ record: { text: 'Hello world', createdAt: new Date().toISOString() }
38
+ });
39
+
40
+ await writer.upsertActor('did:plc:xyz', 'alice.bsky.social');
41
+
42
+ // 3. Create lex-gql adapter
43
+ const query = createDuckDBAdapter(db);
44
+ const adapter = createAdapter(lexicons, { query });
45
+
46
+ // 4. Use adapter.schema with your GraphQL server
47
+ ```
48
+
49
+ ## API
50
+
51
+ ### `createDuckDB(dbPath)`
52
+
53
+ Creates a promisified DuckDB connection.
54
+
55
+ - `dbPath` - Path to database file, or `':memory:'` for in-memory database
56
+ - Returns `Promise<DuckDBConnection>`
57
+
58
+ ### `setupSchema(conn)`
59
+
60
+ Creates the required tables and indexes.
61
+
62
+ ### `createWriter(conn)`
63
+
64
+ Returns an object with:
65
+
66
+ - `insertRecord({ uri, cid, record, indexedAt? })` - Insert or update a record
67
+ - `deleteRecord(uri)` - Delete a record by URI
68
+ - `upsertActor(did, handle)` - Insert or update an actor
69
+
70
+ ### `createDuckDBAdapter(conn)`
71
+
72
+ Creates a query adapter compatible with lex-gql's `createAdapter()`.
73
+
74
+ Supports:
75
+ - `findMany` - Paginated queries with filtering and sorting
76
+ - `aggregate` - GROUP BY queries with COUNT
77
+
78
+ ## Date Handling
79
+
80
+ The adapter handles both ISO strings and Unix timestamps (seconds or milliseconds) for date fields. Date interval suffixes work automatically:
81
+
82
+ ```graphql
83
+ query {
84
+ myRecordAggregate(groupBy: [createdAt_day]) {
85
+ groups {
86
+ createdAt_day # Returns "2025-01-15"
87
+ count
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ Supported suffixes: `_day`, `_week`, `_month`
94
+
95
+ ## Comparison with lex-gql-sqlite
96
+
97
+ | Feature | lex-gql-sqlite | lex-gql-duckdb |
98
+ |---------|----------------|----------------|
99
+ | Best for | Simple apps, small datasets | Analytics, large datasets |
100
+ | Aggregate speed | ~1500ms (660K rows) | ~85ms (660K rows) |
101
+ | Write speed | Fast | Fast (batch inserts) |
102
+ | Maturity | Battle-tested | Newer, but solid |
103
+
104
+ ## License
105
+
106
+ MIT
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "lex-gql-duckdb",
3
+ "version": "0.2.0",
4
+ "description": "DuckDB adapter for lex-gql - optimized for analytics queries",
5
+ "type": "module",
6
+ "main": "src/lex-gql-duckdb.js",
7
+ "types": "src/lex-gql-duckdb.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./src/lex-gql-duckdb.d.ts",
11
+ "default": "./src/lex-gql-duckdb.js"
12
+ }
13
+ },
14
+ "scripts": {
15
+ "build": "tsc",
16
+ "test": "vitest run",
17
+ "test:watch": "vitest",
18
+ "typecheck": "tsc --noEmit"
19
+ },
20
+ "keywords": [
21
+ "graphql",
22
+ "atproto",
23
+ "lexicon",
24
+ "duckdb",
25
+ "analytics",
26
+ "adapter"
27
+ ],
28
+ "license": "MIT",
29
+ "peerDependencies": {
30
+ "duckdb": ">=1.0.0",
31
+ "lex-gql": ">=0.2.0"
32
+ },
33
+ "devDependencies": {
34
+ "@types/node": "^22.10.0",
35
+ "duckdb": "^1.4.3",
36
+ "lex-gql": "workspace:*",
37
+ "typescript": "^5.7.2",
38
+ "vitest": "^1.6.1"
39
+ }
40
+ }
@@ -0,0 +1,121 @@
1
+ /**
2
+ * @typedef {Object} DuckDBConnection
3
+ * @property {duckdb.Database} db - The DuckDB database instance
4
+ * @property {duckdb.Connection} conn - The connection instance
5
+ * @property {(sql: string, ...params: any[]) => Promise<void>} run - Execute a statement
6
+ * @property {(sql: string, ...params: any[]) => Promise<any[]>} all - Query all rows
7
+ * @property {(sql: string, ...params: any[]) => Promise<any>} get - Query single row
8
+ */
9
+ /**
10
+ * Create a promisified DuckDB connection
11
+ * @param {string} dbPath - Path to database file (use ':memory:' for in-memory)
12
+ * @returns {Promise<DuckDBConnection>}
13
+ */
14
+ export function createDuckDB(dbPath: string): Promise<DuckDBConnection>;
15
+ /**
16
+ * Set up the required database schema for lex-gql-duckdb
17
+ * @param {DuckDBConnection} conn
18
+ */
19
+ export function setupSchema(conn: DuckDBConnection): Promise<void>;
20
+ /**
21
+ * @typedef {Object} RecordInput
22
+ * @property {string} uri - Record URI (at://did/collection/rkey)
23
+ * @property {string} [cid] - Record CID
24
+ * @property {object} record - Record data (will be JSON stringified)
25
+ * @property {string} [indexedAt] - Timestamp (defaults to now)
26
+ */
27
+ /**
28
+ * @typedef {Object} Writer
29
+ * @property {(record: RecordInput) => Promise<void>} insertRecord - Insert or replace a record
30
+ * @property {(uri: string) => Promise<void>} deleteRecord - Delete a record by URI
31
+ * @property {(did: string, handle: string) => Promise<void>} upsertActor - Insert or replace an actor
32
+ */
33
+ /**
34
+ * Create a writer with methods for efficient writes
35
+ * @param {DuckDBConnection} conn
36
+ * @returns {Writer}
37
+ */
38
+ export function createWriter(conn: DuckDBConnection): Writer;
39
+ /**
40
+ * Build SQL WHERE clause from lex-gql where conditions
41
+ * @param {import('lex-gql').WhereClause[]} where
42
+ * @returns {{ sql: string, params: any[] }}
43
+ */
44
+ export function buildWhere(where: import("lex-gql").WhereClause[]): {
45
+ sql: string;
46
+ params: any[];
47
+ };
48
+ /**
49
+ * Build SQL ORDER BY clause from lex-gql sort conditions
50
+ * @param {Array<{field: string, dir?: string}>} sort
51
+ * @returns {string}
52
+ */
53
+ export function buildOrderBy(sort: Array<{
54
+ field: string;
55
+ dir?: string;
56
+ }>): string;
57
+ /**
58
+ * Create a DuckDB query adapter for lex-gql
59
+ * @param {DuckDBConnection} conn - DuckDB connection from createDuckDB()
60
+ * @returns {(op: import('lex-gql').Operation) => Promise<any>}
61
+ */
62
+ export function createDuckDBAdapter(conn: DuckDBConnection): (op: import("lex-gql").Operation) => Promise<any>;
63
+ /**
64
+ * SQL schema for lex-gql records
65
+ */
66
+ export const SCHEMA_SQL: "\n CREATE SEQUENCE IF NOT EXISTS records_id_seq;\n\n CREATE TABLE IF NOT EXISTS records (\n id INTEGER DEFAULT nextval('records_id_seq'),\n uri TEXT UNIQUE NOT NULL,\n did TEXT NOT NULL,\n collection TEXT NOT NULL,\n rkey TEXT NOT NULL,\n cid TEXT,\n record JSON NOT NULL,\n indexed_at TIMESTAMP NOT NULL\n );\n\n CREATE INDEX IF NOT EXISTS idx_records_collection ON records(collection);\n CREATE INDEX IF NOT EXISTS idx_records_did ON records(did);\n\n CREATE TABLE IF NOT EXISTS actors (\n did TEXT PRIMARY KEY,\n handle TEXT NOT NULL\n );\n";
67
+ export type DuckDBConnection = {
68
+ /**
69
+ * - The DuckDB database instance
70
+ */
71
+ db: duckdb.Database;
72
+ /**
73
+ * - The connection instance
74
+ */
75
+ conn: duckdb.Connection;
76
+ /**
77
+ * - Execute a statement
78
+ */
79
+ run: (sql: string, ...params: any[]) => Promise<void>;
80
+ /**
81
+ * - Query all rows
82
+ */
83
+ all: (sql: string, ...params: any[]) => Promise<any[]>;
84
+ /**
85
+ * - Query single row
86
+ */
87
+ get: (sql: string, ...params: any[]) => Promise<any>;
88
+ };
89
+ export type RecordInput = {
90
+ /**
91
+ * - Record URI (at://did/collection/rkey)
92
+ */
93
+ uri: string;
94
+ /**
95
+ * - Record CID
96
+ */
97
+ cid?: string | undefined;
98
+ /**
99
+ * - Record data (will be JSON stringified)
100
+ */
101
+ record: object;
102
+ /**
103
+ * - Timestamp (defaults to now)
104
+ */
105
+ indexedAt?: string | undefined;
106
+ };
107
+ export type Writer = {
108
+ /**
109
+ * - Insert or replace a record
110
+ */
111
+ insertRecord: (record: RecordInput) => Promise<void>;
112
+ /**
113
+ * - Delete a record by URI
114
+ */
115
+ deleteRecord: (uri: string) => Promise<void>;
116
+ /**
117
+ * - Insert or replace an actor
118
+ */
119
+ upsertActor: (did: string, handle: string) => Promise<void>;
120
+ };
121
+ import duckdb from 'duckdb';