@xnetjs/sqlite 0.0.2
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/LICENSE +21 -0
- package/README.md +220 -0
- package/dist/adapter-I7yAV6iu.d.ts +166 -0
- package/dist/adapters/electron.d.ts +106 -0
- package/dist/adapters/electron.js +336 -0
- package/dist/adapters/expo.d.ts +65 -0
- package/dist/adapters/expo.js +217 -0
- package/dist/adapters/memory.d.ts +46 -0
- package/dist/adapters/memory.js +176 -0
- package/dist/adapters/web-proxy.d.ts +75 -0
- package/dist/adapters/web-proxy.js +154 -0
- package/dist/adapters/web-worker.d.ts +32 -0
- package/dist/adapters/web-worker.js +81 -0
- package/dist/adapters/web.d.ts +64 -0
- package/dist/adapters/web.js +9 -0
- package/dist/browser-support.d.ts +67 -0
- package/dist/browser-support.js +8 -0
- package/dist/chunk-BXYZU3OL.js +245 -0
- package/dist/chunk-HIREU5S5.js +193 -0
- package/dist/chunk-ZRR5D2OD.js +140 -0
- package/dist/index.d.ts +207 -0
- package/dist/index.js +353 -0
- package/dist/schema-CjkXTqxn.d.ts +35 -0
- package/dist/types-C_aHfRDF.d.ts +42 -0
- package/package.json +90 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
import { S as SQLValue } from './types-C_aHfRDF.js';
|
|
2
|
+
export { R as RunResult, a as SQLRow, b as SQLiteConfig, c as SchemaVersion } from './types-C_aHfRDF.js';
|
|
3
|
+
import { S as SQLiteAdapter } from './adapter-I7yAV6iu.js';
|
|
4
|
+
export { P as PreparedStatement } from './adapter-I7yAV6iu.js';
|
|
5
|
+
export { a as SCHEMA_DDL, b as SCHEMA_DDL_CORE, c as SCHEMA_DDL_FTS, d as SCHEMA_MIGRATIONS, S as SCHEMA_VERSION, g as getMigrationSQL } from './schema-CjkXTqxn.js';
|
|
6
|
+
export { BrowserSupport, checkBrowserSupport, showUnsupportedBrowserMessage } from './browser-support.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* @xnetjs/sqlite - SQL query building helpers
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Build a parameterized INSERT statement.
|
|
13
|
+
*/
|
|
14
|
+
declare function buildInsert(table: string, columns: string[], options?: {
|
|
15
|
+
orReplace?: boolean;
|
|
16
|
+
orIgnore?: boolean;
|
|
17
|
+
}): {
|
|
18
|
+
sql: string;
|
|
19
|
+
placeholders: string;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Build a parameterized UPDATE statement.
|
|
23
|
+
*/
|
|
24
|
+
declare function buildUpdate(table: string, columns: string[], whereColumns: string[]): string;
|
|
25
|
+
/**
|
|
26
|
+
* Build a parameterized SELECT statement with optional filters.
|
|
27
|
+
*/
|
|
28
|
+
declare function buildSelect(table: string, columns?: string[], options?: {
|
|
29
|
+
where?: string[];
|
|
30
|
+
orderBy?: string;
|
|
31
|
+
limit?: number;
|
|
32
|
+
offset?: number;
|
|
33
|
+
}): string;
|
|
34
|
+
/**
|
|
35
|
+
* Escape a string for use in LIKE patterns.
|
|
36
|
+
*/
|
|
37
|
+
declare function escapeLike(value: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Build a batch INSERT statement for multiple rows.
|
|
40
|
+
*/
|
|
41
|
+
declare function buildBatchInsert(table: string, columns: string[], rowCount: number): string;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* @xnetjs/sqlite - Full-Text Search (FTS5) helpers
|
|
45
|
+
*
|
|
46
|
+
* These functions manage the FTS5 index for searchable node content.
|
|
47
|
+
* FTS5 is not supported by sql.js, so these functions are no-ops
|
|
48
|
+
* when using MemorySQLiteAdapter.
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
interface FTSSearchResult {
|
|
52
|
+
nodeId: string;
|
|
53
|
+
rank: number;
|
|
54
|
+
snippet?: string;
|
|
55
|
+
}
|
|
56
|
+
interface FTSSearchOptions {
|
|
57
|
+
/** Maximum number of results */
|
|
58
|
+
limit?: number;
|
|
59
|
+
/** Offset for pagination */
|
|
60
|
+
offset?: number;
|
|
61
|
+
/** Include snippets in results */
|
|
62
|
+
includeSnippets?: boolean;
|
|
63
|
+
/** Highlight markers for snippets */
|
|
64
|
+
highlightMarkers?: {
|
|
65
|
+
start: string;
|
|
66
|
+
end: string;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Update the FTS index for a node.
|
|
71
|
+
* Call this when a node's title or content changes.
|
|
72
|
+
*
|
|
73
|
+
* @param db - SQLite adapter
|
|
74
|
+
* @param nodeId - ID of the node
|
|
75
|
+
* @param title - Node title (can be null)
|
|
76
|
+
* @param content - Searchable content (can be null)
|
|
77
|
+
*/
|
|
78
|
+
declare function updateNodeFTS(db: SQLiteAdapter, nodeId: string, title: string | null, content: string | null): Promise<void>;
|
|
79
|
+
/**
|
|
80
|
+
* Delete a node from the FTS index.
|
|
81
|
+
*
|
|
82
|
+
* @param db - SQLite adapter
|
|
83
|
+
* @param nodeId - ID of the node to remove
|
|
84
|
+
*/
|
|
85
|
+
declare function deleteNodeFTS(db: SQLiteAdapter, nodeId: string): Promise<void>;
|
|
86
|
+
/**
|
|
87
|
+
* Search nodes using FTS5.
|
|
88
|
+
*
|
|
89
|
+
* @param db - SQLite adapter
|
|
90
|
+
* @param query - FTS5 match query (e.g., "hello world", "title:project")
|
|
91
|
+
* @param options - Search options
|
|
92
|
+
* @returns Array of matching node IDs with rank
|
|
93
|
+
*/
|
|
94
|
+
declare function searchNodes(db: SQLiteAdapter, query: string, options?: FTSSearchOptions): Promise<FTSSearchResult[]>;
|
|
95
|
+
/**
|
|
96
|
+
* Rebuild the entire FTS index from node data.
|
|
97
|
+
* Use this after data imports or to fix index corruption.
|
|
98
|
+
*
|
|
99
|
+
* @param db - SQLite adapter
|
|
100
|
+
* @param getNodeContent - Function to get title and content for a node
|
|
101
|
+
*/
|
|
102
|
+
declare function rebuildFTS(db: SQLiteAdapter, getNodeContent: (nodeId: string) => Promise<{
|
|
103
|
+
title: string | null;
|
|
104
|
+
content: string | null;
|
|
105
|
+
}>): Promise<number>;
|
|
106
|
+
/**
|
|
107
|
+
* Optimize the FTS index.
|
|
108
|
+
* Call this periodically or after large batch operations.
|
|
109
|
+
*/
|
|
110
|
+
declare function optimizeFTS(db: SQLiteAdapter): Promise<void>;
|
|
111
|
+
/**
|
|
112
|
+
* Extract searchable text from TipTap JSON content.
|
|
113
|
+
*/
|
|
114
|
+
declare function extractTextFromTipTap(node: TipTapNode): string;
|
|
115
|
+
/**
|
|
116
|
+
* Extract searchable text from node properties.
|
|
117
|
+
* Handles common property types (string, TipTap JSON).
|
|
118
|
+
*/
|
|
119
|
+
declare function extractSearchableContent(properties: Record<string, unknown>): string | null;
|
|
120
|
+
interface TipTapNode {
|
|
121
|
+
type: string;
|
|
122
|
+
content?: TipTapNode[];
|
|
123
|
+
text?: string;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* @xnetjs/sqlite - Database diagnostics and analysis utilities
|
|
128
|
+
*
|
|
129
|
+
* These functions help debug performance issues, analyze query plans,
|
|
130
|
+
* and inspect database state.
|
|
131
|
+
*/
|
|
132
|
+
|
|
133
|
+
interface IndexInfo {
|
|
134
|
+
name: string;
|
|
135
|
+
tableName: string;
|
|
136
|
+
unique: boolean;
|
|
137
|
+
columns: string[];
|
|
138
|
+
partial: boolean;
|
|
139
|
+
}
|
|
140
|
+
interface TableStats {
|
|
141
|
+
name: string;
|
|
142
|
+
rowCount: number;
|
|
143
|
+
pageCount: number;
|
|
144
|
+
unusedBytes: number;
|
|
145
|
+
}
|
|
146
|
+
interface QueryPlanStep {
|
|
147
|
+
id: number;
|
|
148
|
+
parent: number;
|
|
149
|
+
detail: string;
|
|
150
|
+
}
|
|
151
|
+
interface DatabaseStats {
|
|
152
|
+
pageSize: number;
|
|
153
|
+
pageCount: number;
|
|
154
|
+
freePageCount: number;
|
|
155
|
+
schemaVersion: number;
|
|
156
|
+
walMode: boolean;
|
|
157
|
+
foreignKeys: boolean;
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Get information about all indexes in the database.
|
|
161
|
+
*/
|
|
162
|
+
declare function getIndexInfo(db: SQLiteAdapter): Promise<IndexInfo[]>;
|
|
163
|
+
/**
|
|
164
|
+
* Check which indexes are being used for a query.
|
|
165
|
+
*/
|
|
166
|
+
declare function analyzeQuery(db: SQLiteAdapter, sql: string, params?: SQLValue[]): Promise<{
|
|
167
|
+
plan: QueryPlanStep[];
|
|
168
|
+
usedIndexes: string[];
|
|
169
|
+
fullTableScan: boolean;
|
|
170
|
+
}>;
|
|
171
|
+
/**
|
|
172
|
+
* Get statistics about a table.
|
|
173
|
+
*/
|
|
174
|
+
declare function analyzeTable(db: SQLiteAdapter, tableName: string): Promise<TableStats>;
|
|
175
|
+
/**
|
|
176
|
+
* Get statistics about all tables.
|
|
177
|
+
*/
|
|
178
|
+
declare function getAllTableStats(db: SQLiteAdapter): Promise<TableStats[]>;
|
|
179
|
+
/**
|
|
180
|
+
* Get overall database statistics.
|
|
181
|
+
*/
|
|
182
|
+
declare function getDatabaseStats(db: SQLiteAdapter): Promise<DatabaseStats>;
|
|
183
|
+
/**
|
|
184
|
+
* Run ANALYZE to update query planner statistics.
|
|
185
|
+
*/
|
|
186
|
+
declare function runAnalyze(db: SQLiteAdapter, tableName?: string): Promise<void>;
|
|
187
|
+
/**
|
|
188
|
+
* Check database integrity.
|
|
189
|
+
*/
|
|
190
|
+
declare function checkIntegrity(db: SQLiteAdapter): Promise<{
|
|
191
|
+
ok: boolean;
|
|
192
|
+
errors: string[];
|
|
193
|
+
}>;
|
|
194
|
+
/**
|
|
195
|
+
* Get the full EXPLAIN output for a query (bytecode).
|
|
196
|
+
*/
|
|
197
|
+
declare function explainQuery(db: SQLiteAdapter, sql: string, params?: SQLValue[]): Promise<string[]>;
|
|
198
|
+
/**
|
|
199
|
+
* Time a query and return the result with timing info.
|
|
200
|
+
*/
|
|
201
|
+
declare function timeQuery<T>(db: SQLiteAdapter, sql: string, params?: SQLValue[]): Promise<{
|
|
202
|
+
result: T[];
|
|
203
|
+
durationMs: number;
|
|
204
|
+
rowCount: number;
|
|
205
|
+
}>;
|
|
206
|
+
|
|
207
|
+
export { type DatabaseStats, type FTSSearchOptions, type FTSSearchResult, type IndexInfo, type QueryPlanStep, SQLValue, SQLiteAdapter, type TableStats, analyzeQuery, analyzeTable, buildBatchInsert, buildInsert, buildSelect, buildUpdate, checkIntegrity, deleteNodeFTS, escapeLike, explainQuery, extractSearchableContent, extractTextFromTipTap, getAllTableStats, getDatabaseStats, getIndexInfo, optimizeFTS, rebuildFTS, runAnalyze, searchNodes, timeQuery, updateNodeFTS };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import {
|
|
2
|
+
SCHEMA_DDL,
|
|
3
|
+
SCHEMA_DDL_CORE,
|
|
4
|
+
SCHEMA_DDL_FTS,
|
|
5
|
+
SCHEMA_MIGRATIONS,
|
|
6
|
+
SCHEMA_VERSION,
|
|
7
|
+
getMigrationSQL
|
|
8
|
+
} from "./chunk-HIREU5S5.js";
|
|
9
|
+
import {
|
|
10
|
+
checkBrowserSupport,
|
|
11
|
+
showUnsupportedBrowserMessage
|
|
12
|
+
} from "./chunk-ZRR5D2OD.js";
|
|
13
|
+
|
|
14
|
+
// src/query-builder.ts
|
|
15
|
+
function buildInsert(table, columns, options) {
|
|
16
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
17
|
+
const columnList = columns.join(", ");
|
|
18
|
+
let prefix = "INSERT";
|
|
19
|
+
if (options?.orReplace) prefix = "INSERT OR REPLACE";
|
|
20
|
+
if (options?.orIgnore) prefix = "INSERT OR IGNORE";
|
|
21
|
+
return {
|
|
22
|
+
sql: `${prefix} INTO ${table} (${columnList}) VALUES (${placeholders})`,
|
|
23
|
+
placeholders
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function buildUpdate(table, columns, whereColumns) {
|
|
27
|
+
const setClause = columns.map((c) => `${c} = ?`).join(", ");
|
|
28
|
+
const whereClause = whereColumns.map((c) => `${c} = ?`).join(" AND ");
|
|
29
|
+
return `UPDATE ${table} SET ${setClause} WHERE ${whereClause}`;
|
|
30
|
+
}
|
|
31
|
+
function buildSelect(table, columns = ["*"], options) {
|
|
32
|
+
let sql = `SELECT ${columns.join(", ")} FROM ${table}`;
|
|
33
|
+
if (options?.where && options.where.length > 0) {
|
|
34
|
+
sql += ` WHERE ${options.where.map((c) => `${c} = ?`).join(" AND ")}`;
|
|
35
|
+
}
|
|
36
|
+
if (options?.orderBy) {
|
|
37
|
+
sql += ` ORDER BY ${options.orderBy}`;
|
|
38
|
+
}
|
|
39
|
+
if (options?.limit !== void 0) {
|
|
40
|
+
sql += ` LIMIT ${options.limit}`;
|
|
41
|
+
}
|
|
42
|
+
if (options?.offset !== void 0) {
|
|
43
|
+
sql += ` OFFSET ${options.offset}`;
|
|
44
|
+
}
|
|
45
|
+
return sql;
|
|
46
|
+
}
|
|
47
|
+
function escapeLike(value) {
|
|
48
|
+
return value.replace(/[%_\\]/g, "\\$&");
|
|
49
|
+
}
|
|
50
|
+
function buildBatchInsert(table, columns, rowCount) {
|
|
51
|
+
const placeholders = columns.map(() => "?").join(", ");
|
|
52
|
+
const valuesList = Array(rowCount).fill(`(${placeholders})`).join(", ");
|
|
53
|
+
const columnList = columns.join(", ");
|
|
54
|
+
return `INSERT INTO ${table} (${columnList}) VALUES ${valuesList}`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/fts.ts
|
|
58
|
+
async function updateNodeFTS(db, nodeId, title, content) {
|
|
59
|
+
const hasFTS = await checkFTSSupport(db);
|
|
60
|
+
if (!hasFTS) return;
|
|
61
|
+
await db.run("DELETE FROM nodes_fts WHERE node_id = ?", [nodeId]);
|
|
62
|
+
if (title || content) {
|
|
63
|
+
await db.run("INSERT INTO nodes_fts (node_id, title, content) VALUES (?, ?, ?)", [
|
|
64
|
+
nodeId,
|
|
65
|
+
title ?? "",
|
|
66
|
+
content ?? ""
|
|
67
|
+
]);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function deleteNodeFTS(db, nodeId) {
|
|
71
|
+
const hasFTS = await checkFTSSupport(db);
|
|
72
|
+
if (!hasFTS) return;
|
|
73
|
+
await db.run("DELETE FROM nodes_fts WHERE node_id = ?", [nodeId]);
|
|
74
|
+
}
|
|
75
|
+
async function searchNodes(db, query, options = {}) {
|
|
76
|
+
const hasFTS = await checkFTSSupport(db);
|
|
77
|
+
if (!hasFTS) return [];
|
|
78
|
+
const { limit = 50, offset = 0, includeSnippets = false, highlightMarkers } = options;
|
|
79
|
+
const escapedQuery = escapeFTSQuery(query);
|
|
80
|
+
if (!escapedQuery) return [];
|
|
81
|
+
let sql;
|
|
82
|
+
if (includeSnippets) {
|
|
83
|
+
const startMark = highlightMarkers?.start ?? "<mark>";
|
|
84
|
+
const endMark = highlightMarkers?.end ?? "</mark>";
|
|
85
|
+
sql = `
|
|
86
|
+
SELECT
|
|
87
|
+
node_id,
|
|
88
|
+
rank,
|
|
89
|
+
snippet(nodes_fts, 2, '${startMark}', '${endMark}', '...', 32) as snippet
|
|
90
|
+
FROM nodes_fts
|
|
91
|
+
WHERE nodes_fts MATCH ?
|
|
92
|
+
ORDER BY rank
|
|
93
|
+
LIMIT ? OFFSET ?
|
|
94
|
+
`;
|
|
95
|
+
} else {
|
|
96
|
+
sql = `
|
|
97
|
+
SELECT node_id, rank
|
|
98
|
+
FROM nodes_fts
|
|
99
|
+
WHERE nodes_fts MATCH ?
|
|
100
|
+
ORDER BY rank
|
|
101
|
+
LIMIT ? OFFSET ?
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
const rows = await db.query(sql, [escapedQuery, limit, offset]);
|
|
105
|
+
return rows.map((row) => ({
|
|
106
|
+
nodeId: row.node_id,
|
|
107
|
+
rank: row.rank,
|
|
108
|
+
snippet: row.snippet ?? void 0
|
|
109
|
+
}));
|
|
110
|
+
}
|
|
111
|
+
async function rebuildFTS(db, getNodeContent) {
|
|
112
|
+
const hasFTS = await checkFTSSupport(db);
|
|
113
|
+
if (!hasFTS) return 0;
|
|
114
|
+
const nodes = await db.query("SELECT id FROM nodes WHERE deleted_at IS NULL");
|
|
115
|
+
await db.run("DELETE FROM nodes_fts");
|
|
116
|
+
let indexed = 0;
|
|
117
|
+
const batchSize = 100;
|
|
118
|
+
for (let i = 0; i < nodes.length; i += batchSize) {
|
|
119
|
+
const batch = nodes.slice(i, i + batchSize);
|
|
120
|
+
await db.transaction(async () => {
|
|
121
|
+
for (const node of batch) {
|
|
122
|
+
const { title, content } = await getNodeContent(node.id);
|
|
123
|
+
if (title || content) {
|
|
124
|
+
await db.run("INSERT INTO nodes_fts (node_id, title, content) VALUES (?, ?, ?)", [
|
|
125
|
+
node.id,
|
|
126
|
+
title ?? "",
|
|
127
|
+
content ?? ""
|
|
128
|
+
]);
|
|
129
|
+
indexed++;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return indexed;
|
|
135
|
+
}
|
|
136
|
+
async function optimizeFTS(db) {
|
|
137
|
+
const hasFTS = await checkFTSSupport(db);
|
|
138
|
+
if (!hasFTS) return;
|
|
139
|
+
await db.run("INSERT INTO nodes_fts(nodes_fts) VALUES('optimize')");
|
|
140
|
+
}
|
|
141
|
+
function extractTextFromTipTap(node) {
|
|
142
|
+
const parts = [];
|
|
143
|
+
if (node.text) {
|
|
144
|
+
parts.push(node.text);
|
|
145
|
+
}
|
|
146
|
+
if (node.content) {
|
|
147
|
+
for (const child of node.content) {
|
|
148
|
+
parts.push(extractTextFromTipTap(child));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return parts.join(" ").trim();
|
|
152
|
+
}
|
|
153
|
+
function extractSearchableContent(properties) {
|
|
154
|
+
const parts = [];
|
|
155
|
+
const content = properties.content;
|
|
156
|
+
if (content) {
|
|
157
|
+
if (typeof content === "string") {
|
|
158
|
+
parts.push(content);
|
|
159
|
+
} else if (typeof content === "object" && "type" in content) {
|
|
160
|
+
parts.push(extractTextFromTipTap(content));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const description = properties.description;
|
|
164
|
+
if (typeof description === "string") {
|
|
165
|
+
parts.push(description);
|
|
166
|
+
}
|
|
167
|
+
const body = properties.body;
|
|
168
|
+
if (typeof body === "string") {
|
|
169
|
+
parts.push(body);
|
|
170
|
+
}
|
|
171
|
+
return parts.length > 0 ? parts.join(" ") : null;
|
|
172
|
+
}
|
|
173
|
+
var ftsSupport = /* @__PURE__ */ new WeakMap();
|
|
174
|
+
async function checkFTSSupport(db) {
|
|
175
|
+
if (ftsSupport.has(db)) {
|
|
176
|
+
return ftsSupport.get(db);
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
const result = await db.queryOne(
|
|
180
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name='nodes_fts'"
|
|
181
|
+
);
|
|
182
|
+
const supported = result !== null;
|
|
183
|
+
ftsSupport.set(db, supported);
|
|
184
|
+
return supported;
|
|
185
|
+
} catch {
|
|
186
|
+
ftsSupport.set(db, false);
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function escapeFTSQuery(query) {
|
|
191
|
+
query = query.trim();
|
|
192
|
+
if (!query) return "";
|
|
193
|
+
query = query.replace(/"/g, '""');
|
|
194
|
+
if (/[*():^~-]/.test(query)) {
|
|
195
|
+
return `"${query}"`;
|
|
196
|
+
}
|
|
197
|
+
return query;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/diagnostics.ts
|
|
201
|
+
async function getIndexInfo(db) {
|
|
202
|
+
const indexes = await db.query(
|
|
203
|
+
"SELECT name, tbl_name, sql FROM sqlite_master WHERE type='index' AND name NOT LIKE 'sqlite_%'"
|
|
204
|
+
);
|
|
205
|
+
const result = [];
|
|
206
|
+
for (const idx of indexes) {
|
|
207
|
+
const columns = await db.query(`PRAGMA index_info('${idx.name}')`);
|
|
208
|
+
result.push({
|
|
209
|
+
name: idx.name,
|
|
210
|
+
tableName: idx.tbl_name,
|
|
211
|
+
unique: idx.sql?.includes("UNIQUE") ?? false,
|
|
212
|
+
columns: columns.map((c) => c.name),
|
|
213
|
+
partial: idx.sql?.includes("WHERE") ?? false
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return result;
|
|
217
|
+
}
|
|
218
|
+
async function analyzeQuery(db, sql, params) {
|
|
219
|
+
const plan = await db.query(`EXPLAIN QUERY PLAN ${sql}`, params);
|
|
220
|
+
const steps = plan.map((row) => ({
|
|
221
|
+
id: row.id,
|
|
222
|
+
parent: row.parent,
|
|
223
|
+
detail: row.detail
|
|
224
|
+
}));
|
|
225
|
+
const usedIndexes = [];
|
|
226
|
+
let fullTableScan = false;
|
|
227
|
+
for (const step of steps) {
|
|
228
|
+
const detail = step.detail.toUpperCase();
|
|
229
|
+
const indexMatch = step.detail.match(/USING (?:COVERING )?INDEX (\w+)/i);
|
|
230
|
+
if (indexMatch) {
|
|
231
|
+
usedIndexes.push(indexMatch[1]);
|
|
232
|
+
}
|
|
233
|
+
if (detail.includes("SCAN TABLE") && !detail.includes("USING")) {
|
|
234
|
+
fullTableScan = true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { plan: steps, usedIndexes, fullTableScan };
|
|
238
|
+
}
|
|
239
|
+
async function analyzeTable(db, tableName) {
|
|
240
|
+
const countResult = await db.queryOne(`SELECT COUNT(*) as count FROM ${tableName}`);
|
|
241
|
+
const rowCount = countResult?.count ?? 0;
|
|
242
|
+
let pageCount = 0;
|
|
243
|
+
let unusedBytes = 0;
|
|
244
|
+
try {
|
|
245
|
+
const pages = await db.query(`SELECT pageno, unused FROM dbstat WHERE name = ?`, [
|
|
246
|
+
tableName
|
|
247
|
+
]);
|
|
248
|
+
pageCount = pages.length;
|
|
249
|
+
unusedBytes = pages.reduce((sum, p) => sum + p.unused, 0);
|
|
250
|
+
} catch {
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
name: tableName,
|
|
254
|
+
rowCount,
|
|
255
|
+
pageCount,
|
|
256
|
+
unusedBytes
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
async function getAllTableStats(db) {
|
|
260
|
+
const tables = await db.query(
|
|
261
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'"
|
|
262
|
+
);
|
|
263
|
+
const stats = [];
|
|
264
|
+
for (const table of tables) {
|
|
265
|
+
stats.push(await analyzeTable(db, table.name));
|
|
266
|
+
}
|
|
267
|
+
return stats.sort((a, b) => b.rowCount - a.rowCount);
|
|
268
|
+
}
|
|
269
|
+
async function getDatabaseStats(db) {
|
|
270
|
+
const [pageSize, pageCount, freePageCount, schemaVersion, walMode, foreignKeys] = await Promise.all([
|
|
271
|
+
db.queryOne("PRAGMA page_size"),
|
|
272
|
+
db.queryOne("PRAGMA page_count"),
|
|
273
|
+
db.queryOne("PRAGMA freelist_count"),
|
|
274
|
+
db.queryOne("PRAGMA schema_version"),
|
|
275
|
+
db.queryOne("PRAGMA journal_mode"),
|
|
276
|
+
db.queryOne("PRAGMA foreign_keys")
|
|
277
|
+
]);
|
|
278
|
+
return {
|
|
279
|
+
pageSize: Number(pageSize?.page_size ?? 4096),
|
|
280
|
+
pageCount: Number(pageCount?.page_count ?? 0),
|
|
281
|
+
freePageCount: Number(freePageCount?.freelist_count ?? 0),
|
|
282
|
+
schemaVersion: Number(schemaVersion?.schema_version ?? 0),
|
|
283
|
+
walMode: String(walMode?.journal_mode ?? "").toLowerCase() === "wal",
|
|
284
|
+
foreignKeys: Boolean(foreignKeys?.foreign_keys)
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
async function runAnalyze(db, tableName) {
|
|
288
|
+
if (tableName) {
|
|
289
|
+
await db.exec(`ANALYZE ${tableName}`);
|
|
290
|
+
} else {
|
|
291
|
+
await db.exec("ANALYZE");
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
async function checkIntegrity(db) {
|
|
295
|
+
const results = await db.query("PRAGMA integrity_check(100)");
|
|
296
|
+
const errors = [];
|
|
297
|
+
for (const row of results) {
|
|
298
|
+
if (row.integrity_check !== "ok") {
|
|
299
|
+
errors.push(row.integrity_check);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
ok: errors.length === 0,
|
|
304
|
+
errors
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
async function explainQuery(db, sql, params) {
|
|
308
|
+
const rows = await db.query(`EXPLAIN ${sql}`, params);
|
|
309
|
+
return rows.map(
|
|
310
|
+
(row) => `${row.opcode.padEnd(15)} ${String(row.p1).padStart(4)} ${String(row.p2).padStart(4)} ${String(row.p3).padStart(4)} ${row.p4 || ""}`
|
|
311
|
+
);
|
|
312
|
+
}
|
|
313
|
+
async function timeQuery(db, sql, params) {
|
|
314
|
+
const start = performance.now();
|
|
315
|
+
const result = await db.query(sql, params);
|
|
316
|
+
const durationMs = performance.now() - start;
|
|
317
|
+
return {
|
|
318
|
+
result,
|
|
319
|
+
durationMs,
|
|
320
|
+
rowCount: result.length
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
export {
|
|
324
|
+
SCHEMA_DDL,
|
|
325
|
+
SCHEMA_DDL_CORE,
|
|
326
|
+
SCHEMA_DDL_FTS,
|
|
327
|
+
SCHEMA_MIGRATIONS,
|
|
328
|
+
SCHEMA_VERSION,
|
|
329
|
+
analyzeQuery,
|
|
330
|
+
analyzeTable,
|
|
331
|
+
buildBatchInsert,
|
|
332
|
+
buildInsert,
|
|
333
|
+
buildSelect,
|
|
334
|
+
buildUpdate,
|
|
335
|
+
checkBrowserSupport,
|
|
336
|
+
checkIntegrity,
|
|
337
|
+
deleteNodeFTS,
|
|
338
|
+
escapeLike,
|
|
339
|
+
explainQuery,
|
|
340
|
+
extractSearchableContent,
|
|
341
|
+
extractTextFromTipTap,
|
|
342
|
+
getAllTableStats,
|
|
343
|
+
getDatabaseStats,
|
|
344
|
+
getIndexInfo,
|
|
345
|
+
getMigrationSQL,
|
|
346
|
+
optimizeFTS,
|
|
347
|
+
rebuildFTS,
|
|
348
|
+
runAnalyze,
|
|
349
|
+
searchNodes,
|
|
350
|
+
showUnsupportedBrowserMessage,
|
|
351
|
+
timeQuery,
|
|
352
|
+
updateNodeFTS
|
|
353
|
+
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xnetjs/sqlite - Unified SQLite schema for xNet
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* Current schema version.
|
|
6
|
+
* Increment this when making schema changes.
|
|
7
|
+
*/
|
|
8
|
+
declare const SCHEMA_VERSION = 1;
|
|
9
|
+
/**
|
|
10
|
+
* Core SQLite schema for xNet (without FTS5).
|
|
11
|
+
* This schema works on all platforms including sql.js.
|
|
12
|
+
*/
|
|
13
|
+
declare const SCHEMA_DDL_CORE = "\n-- ============================================\n-- Schema Version Tracking\n-- ============================================\n\nCREATE TABLE IF NOT EXISTS _schema_version (\n version INTEGER PRIMARY KEY,\n applied_at INTEGER NOT NULL\n);\n\n-- ============================================\n-- Core Tables\n-- ============================================\n\n-- All nodes (Pages, Databases, Rows, Comments, etc.)\nCREATE TABLE IF NOT EXISTS nodes (\n id TEXT PRIMARY KEY,\n schema_id TEXT NOT NULL,\n created_at INTEGER NOT NULL,\n updated_at INTEGER NOT NULL,\n created_by TEXT NOT NULL,\n deleted_at INTEGER\n);\n\n-- Node properties (LWW per-property)\nCREATE TABLE IF NOT EXISTS node_properties (\n node_id TEXT NOT NULL,\n property_key TEXT NOT NULL,\n value BLOB,\n lamport_time INTEGER NOT NULL,\n updated_by TEXT NOT NULL,\n updated_at INTEGER NOT NULL,\n\n PRIMARY KEY (node_id, property_key),\n FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE\n);\n\n-- Change log (event sourcing)\nCREATE TABLE IF NOT EXISTS changes (\n hash TEXT PRIMARY KEY,\n node_id TEXT NOT NULL,\n payload BLOB NOT NULL,\n lamport_time INTEGER NOT NULL,\n lamport_peer TEXT NOT NULL,\n wall_time INTEGER NOT NULL,\n author TEXT NOT NULL,\n parent_hash TEXT,\n batch_id TEXT,\n signature BLOB NOT NULL,\n\n FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE\n);\n\n-- Y.Doc binary state (for nodes with collaborative content)\nCREATE TABLE IF NOT EXISTS yjs_state (\n node_id TEXT PRIMARY KEY,\n state BLOB NOT NULL,\n updated_at INTEGER NOT NULL,\n\n FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE\n);\n\n-- Y.Doc incremental updates (for sync)\nCREATE TABLE IF NOT EXISTS yjs_updates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n node_id TEXT NOT NULL,\n update_data BLOB NOT NULL,\n timestamp INTEGER NOT NULL,\n origin TEXT,\n\n FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE\n);\n\n-- Yjs snapshots (for document time travel)\nCREATE TABLE IF NOT EXISTS yjs_snapshots (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n node_id TEXT NOT NULL,\n timestamp INTEGER NOT NULL,\n snapshot BLOB NOT NULL,\n doc_state BLOB NOT NULL,\n byte_size INTEGER NOT NULL,\n\n FOREIGN KEY (node_id) REFERENCES nodes(id) ON DELETE CASCADE\n);\n\n-- Blobs (content-addressed)\nCREATE TABLE IF NOT EXISTS blobs (\n cid TEXT PRIMARY KEY,\n data BLOB NOT NULL,\n mime_type TEXT,\n size INTEGER NOT NULL,\n created_at INTEGER NOT NULL,\n reference_count INTEGER DEFAULT 1\n);\n\n-- Documents (for @xnetjs/storage compatibility)\nCREATE TABLE IF NOT EXISTS documents (\n id TEXT PRIMARY KEY,\n content BLOB NOT NULL,\n metadata TEXT NOT NULL,\n version INTEGER NOT NULL DEFAULT 1\n);\n\n-- Signed updates (for @xnetjs/storage compatibility)\nCREATE TABLE IF NOT EXISTS updates (\n id INTEGER PRIMARY KEY AUTOINCREMENT,\n doc_id TEXT NOT NULL,\n update_hash TEXT NOT NULL,\n update_data TEXT NOT NULL,\n created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000),\n UNIQUE(doc_id, update_hash)\n);\n\n-- Snapshots (for @xnetjs/storage compatibility)\nCREATE TABLE IF NOT EXISTS snapshots (\n doc_id TEXT PRIMARY KEY,\n snapshot_data TEXT NOT NULL,\n created_at INTEGER DEFAULT (strftime('%s', 'now') * 1000)\n);\n\n-- Sync metadata\nCREATE TABLE IF NOT EXISTS sync_state (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL\n);\n\n-- ============================================\n-- Indexes\n-- ============================================\n\nCREATE INDEX IF NOT EXISTS idx_nodes_schema ON nodes(schema_id);\nCREATE INDEX IF NOT EXISTS idx_nodes_updated ON nodes(updated_at);\nCREATE INDEX IF NOT EXISTS idx_nodes_created_by ON nodes(created_by);\nCREATE INDEX IF NOT EXISTS idx_nodes_deleted ON nodes(deleted_at);\n\nCREATE INDEX IF NOT EXISTS idx_properties_node ON node_properties(node_id);\nCREATE INDEX IF NOT EXISTS idx_properties_lamport ON node_properties(lamport_time);\n\nCREATE INDEX IF NOT EXISTS idx_changes_node ON changes(node_id);\nCREATE INDEX IF NOT EXISTS idx_changes_lamport ON changes(lamport_time);\nCREATE INDEX IF NOT EXISTS idx_changes_wall_time ON changes(wall_time);\nCREATE INDEX IF NOT EXISTS idx_changes_batch ON changes(batch_id);\n\nCREATE INDEX IF NOT EXISTS idx_yjs_state_updated ON yjs_state(updated_at);\nCREATE INDEX IF NOT EXISTS idx_yjs_updates_node ON yjs_updates(node_id);\nCREATE INDEX IF NOT EXISTS idx_yjs_snapshots_node ON yjs_snapshots(node_id);\nCREATE INDEX IF NOT EXISTS idx_yjs_snapshots_timestamp ON yjs_snapshots(node_id, timestamp);\n\nCREATE INDEX IF NOT EXISTS idx_updates_doc ON updates(doc_id);\nCREATE INDEX IF NOT EXISTS idx_updates_created ON updates(created_at);\n";
|
|
14
|
+
/**
|
|
15
|
+
* FTS5 schema for full-text search.
|
|
16
|
+
* This is applied separately because sql.js doesn't support FTS5.
|
|
17
|
+
*/
|
|
18
|
+
declare const SCHEMA_DDL_FTS = "\n-- ============================================\n-- Full-Text Search (FTS5)\n-- ============================================\n\n-- FTS index for searchable node content\nCREATE VIRTUAL TABLE IF NOT EXISTS nodes_fts USING fts5(\n node_id,\n title,\n content,\n tokenize='porter unicode61'\n);\n\n-- Triggers to keep FTS in sync will be managed by application layer\n-- since the searchable content is derived from node properties\n";
|
|
19
|
+
/**
|
|
20
|
+
* Unified SQLite schema for xNet.
|
|
21
|
+
* This schema is shared across all platforms (with FTS5).
|
|
22
|
+
* Use SCHEMA_DDL_CORE for platforms without FTS5 support (sql.js).
|
|
23
|
+
*/
|
|
24
|
+
declare const SCHEMA_DDL: string;
|
|
25
|
+
/**
|
|
26
|
+
* Schema for future versions (migrations).
|
|
27
|
+
* Each key is the version number, value is the upgrade SQL.
|
|
28
|
+
*/
|
|
29
|
+
declare const SCHEMA_MIGRATIONS: Record<number, string>;
|
|
30
|
+
/**
|
|
31
|
+
* Get the SQL to upgrade from one version to another.
|
|
32
|
+
*/
|
|
33
|
+
declare function getMigrationSQL(fromVersion: number, toVersion: number): string;
|
|
34
|
+
|
|
35
|
+
export { SCHEMA_VERSION as S, SCHEMA_DDL as a, SCHEMA_DDL_CORE as b, SCHEMA_DDL_FTS as c, SCHEMA_MIGRATIONS as d, getMigrationSQL as g };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @xnetjs/sqlite - Type definitions for unified SQLite adapter
|
|
3
|
+
*/
|
|
4
|
+
/**
|
|
5
|
+
* SQL parameter types that can be bound to statements.
|
|
6
|
+
*/
|
|
7
|
+
type SQLValue = string | number | bigint | Uint8Array | null;
|
|
8
|
+
/**
|
|
9
|
+
* Row type for query results.
|
|
10
|
+
*/
|
|
11
|
+
type SQLRow = Record<string, SQLValue>;
|
|
12
|
+
/**
|
|
13
|
+
* Result of a mutation query (INSERT, UPDATE, DELETE).
|
|
14
|
+
*/
|
|
15
|
+
interface RunResult {
|
|
16
|
+
/** Number of rows affected by the query */
|
|
17
|
+
changes: number;
|
|
18
|
+
/** Last inserted row ID (for INSERT with AUTOINCREMENT) */
|
|
19
|
+
lastInsertRowid: bigint;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Configuration options for SQLite database.
|
|
23
|
+
*/
|
|
24
|
+
interface SQLiteConfig {
|
|
25
|
+
/** Database file path or name */
|
|
26
|
+
path: string;
|
|
27
|
+
/** Enable WAL mode (default: true) */
|
|
28
|
+
walMode?: boolean;
|
|
29
|
+
/** Enable foreign keys (default: true) */
|
|
30
|
+
foreignKeys?: boolean;
|
|
31
|
+
/** Busy timeout in milliseconds (default: 5000) */
|
|
32
|
+
busyTimeout?: number;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Schema version information.
|
|
36
|
+
*/
|
|
37
|
+
interface SchemaVersion {
|
|
38
|
+
version: number;
|
|
39
|
+
appliedAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type { RunResult as R, SQLValue as S, SQLRow as a, SQLiteConfig as b, SchemaVersion as c };
|