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 +12 -0
- package/README.md +106 -0
- package/package.json +40 -0
- package/src/lex-gql-duckdb.d.ts +121 -0
- package/src/lex-gql-duckdb.js +525 -0
- package/test/lex-gql-duckdb.test.js +720 -0
- package/tsconfig.json +12 -0
package/CHANGELOG.md
ADDED
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';
|