sensemaking 0.18.2 → 0.18.4
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/dist/cjs/cli/status.js +32 -31
- package/dist/cjs/cli/status.js.map +1 -1
- package/dist/cjs/commands/map.js +50 -67
- package/dist/cjs/commands/map.js.map +1 -1
- package/dist/cjs/config/access.d.cts +2 -2
- package/dist/cjs/config/access.d.ts +2 -2
- package/dist/cjs/config/access.js.map +1 -1
- package/dist/cjs/config/index.d.cts +1 -1
- package/dist/cjs/config/index.d.ts +1 -1
- package/dist/cjs/config/index.js.map +1 -1
- package/dist/cjs/config/load.js +3 -2
- package/dist/cjs/config/load.js.map +1 -1
- package/dist/cjs/config/types.d.cts +2 -1
- package/dist/cjs/config/types.d.ts +2 -1
- package/dist/cjs/config/types.js.map +1 -1
- package/dist/cjs/config/validate.js +3 -0
- package/dist/cjs/config/validate.js.map +1 -1
- package/dist/cjs/embed/identity.js +3 -2
- package/dist/cjs/embed/identity.js.map +1 -1
- package/dist/cjs/embed/store.js +3 -3
- package/dist/cjs/embed/store.js.map +1 -1
- package/dist/cjs/index.d.cts +2 -2
- package/dist/cjs/index.d.ts +2 -2
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/lib/atomic-write.d.cts +1 -0
- package/dist/cjs/lib/atomic-write.d.ts +1 -0
- package/dist/cjs/lib/atomic-write.js +26 -0
- package/dist/cjs/lib/atomic-write.js.map +1 -0
- package/dist/cjs/store/duckdb/batch.js +3 -2
- package/dist/cjs/store/duckdb/batch.js.map +1 -1
- package/dist/cjs/store/duckdb/connection.js +12 -0
- package/dist/cjs/store/duckdb/connection.js.map +1 -1
- package/dist/cjs/store/duckdb/fieldStats.d.cts +2 -0
- package/dist/cjs/store/duckdb/fieldStats.d.ts +2 -0
- package/dist/cjs/store/duckdb/fieldStats.js +219 -0
- package/dist/cjs/store/duckdb/fieldStats.js.map +1 -0
- package/dist/cjs/store/duckdb/lexical.js +16 -9
- package/dist/cjs/store/duckdb/lexical.js.map +1 -1
- package/dist/cjs/store/duckdb/native.d.cts +0 -1
- package/dist/cjs/store/duckdb/native.d.ts +0 -1
- package/dist/cjs/store/duckdb/native.js +7 -336
- package/dist/cjs/store/duckdb/native.js.map +1 -1
- package/dist/cjs/store/duckdb/sql-functions.js +1 -1
- package/dist/cjs/store/duckdb/sql-functions.js.map +1 -1
- package/dist/cjs/store/duckdb/store.js +16 -1
- package/dist/cjs/store/duckdb/store.js.map +1 -1
- package/dist/cjs/store/duckdb/vectors.js +5 -17
- package/dist/cjs/store/duckdb/vectors.js.map +1 -1
- package/dist/cjs/store/index.js +6 -0
- package/dist/cjs/store/index.js.map +1 -1
- package/dist/cjs/store/native.d.cts +7 -0
- package/dist/cjs/store/native.d.ts +7 -0
- package/dist/cjs/store/native.js +350 -0
- package/dist/cjs/store/native.js.map +1 -0
- package/dist/cjs/store/sqlite/fieldStats.d.cts +2 -0
- package/dist/cjs/store/sqlite/fieldStats.d.ts +2 -0
- package/dist/cjs/store/sqlite/fieldStats.js +201 -0
- package/dist/cjs/store/sqlite/fieldStats.js.map +1 -0
- package/dist/cjs/store/sqlite/open.js +82 -29
- package/dist/cjs/store/sqlite/open.js.map +1 -1
- package/dist/cjs/store/sqlite/store.js +32 -4
- package/dist/cjs/store/sqlite/store.js.map +1 -1
- package/dist/cjs/store/sqlite/vectors.d.cts +0 -5
- package/dist/cjs/store/sqlite/vectors.d.ts +0 -5
- package/dist/cjs/store/sqlite/vectors.js +5 -71
- package/dist/cjs/store/sqlite/vectors.js.map +1 -1
- package/dist/cjs/store/turso/connection.d.cts +3 -0
- package/dist/cjs/store/turso/connection.d.ts +3 -0
- package/dist/cjs/store/turso/connection.js +529 -0
- package/dist/cjs/store/turso/connection.js.map +1 -0
- package/dist/cjs/store/turso/fieldStats.d.cts +2 -0
- package/dist/cjs/store/turso/fieldStats.d.ts +2 -0
- package/dist/cjs/store/turso/fieldStats.js +201 -0
- package/dist/cjs/store/turso/fieldStats.js.map +1 -0
- package/dist/cjs/store/turso/native.d.cts +2 -0
- package/dist/cjs/store/turso/native.d.ts +2 -0
- package/dist/cjs/store/turso/native.js +31 -0
- package/dist/cjs/store/turso/native.js.map +1 -0
- package/dist/cjs/store/turso/open.d.cts +12 -0
- package/dist/cjs/store/turso/open.d.ts +12 -0
- package/dist/cjs/store/turso/open.js +500 -0
- package/dist/cjs/store/turso/open.js.map +1 -0
- package/dist/cjs/store/turso/reconcile.d.cts +6 -0
- package/dist/cjs/store/turso/reconcile.d.ts +6 -0
- package/dist/cjs/store/turso/reconcile.js +793 -0
- package/dist/cjs/store/turso/reconcile.js.map +1 -0
- package/dist/cjs/store/turso/store.d.cts +5 -0
- package/dist/cjs/store/turso/store.d.ts +5 -0
- package/dist/cjs/store/turso/store.js +609 -0
- package/dist/cjs/store/turso/store.js.map +1 -0
- package/dist/cjs/store/types.d.cts +9 -1
- package/dist/cjs/store/types.d.ts +9 -1
- package/dist/cjs/store/types.js +0 -7
- package/dist/cjs/store/types.js.map +1 -1
- package/dist/cjs/store/vectors.d.cts +9 -0
- package/dist/cjs/store/vectors.d.ts +9 -0
- package/dist/cjs/store/vectors.js +215 -0
- package/dist/cjs/store/vectors.js.map +1 -0
- package/dist/esm/cli/status.js +5 -7
- package/dist/esm/cli/status.js.map +1 -1
- package/dist/esm/commands/map.js +7 -19
- package/dist/esm/commands/map.js.map +1 -1
- package/dist/esm/config/access.d.ts +2 -2
- package/dist/esm/config/access.js.map +1 -1
- package/dist/esm/config/index.d.ts +1 -1
- package/dist/esm/config/index.js.map +1 -1
- package/dist/esm/config/load.js +4 -3
- package/dist/esm/config/load.js.map +1 -1
- package/dist/esm/config/types.d.ts +2 -1
- package/dist/esm/config/types.js.map +1 -1
- package/dist/esm/config/validate.js +3 -0
- package/dist/esm/config/validate.js.map +1 -1
- package/dist/esm/embed/identity.js +4 -3
- package/dist/esm/embed/identity.js.map +1 -1
- package/dist/esm/embed/store.js +3 -3
- package/dist/esm/embed/store.js.map +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/lib/atomic-write.d.ts +1 -0
- package/dist/esm/lib/atomic-write.js +17 -0
- package/dist/esm/lib/atomic-write.js.map +1 -0
- package/dist/esm/store/duckdb/batch.js +3 -2
- package/dist/esm/store/duckdb/batch.js.map +1 -1
- package/dist/esm/store/duckdb/connection.js +12 -0
- package/dist/esm/store/duckdb/connection.js.map +1 -1
- package/dist/esm/store/duckdb/fieldStats.d.ts +2 -0
- package/dist/esm/store/duckdb/fieldStats.js +39 -0
- package/dist/esm/store/duckdb/fieldStats.js.map +1 -0
- package/dist/esm/store/duckdb/lexical.js +19 -9
- package/dist/esm/store/duckdb/lexical.js.map +1 -1
- package/dist/esm/store/duckdb/native.d.ts +0 -1
- package/dist/esm/store/duckdb/native.js +7 -84
- package/dist/esm/store/duckdb/native.js.map +1 -1
- package/dist/esm/store/duckdb/sql-functions.js +1 -1
- package/dist/esm/store/duckdb/sql-functions.js.map +1 -1
- package/dist/esm/store/duckdb/store.js +8 -5
- package/dist/esm/store/duckdb/store.js.map +1 -1
- package/dist/esm/store/duckdb/vectors.js +2 -12
- package/dist/esm/store/duckdb/vectors.js.map +1 -1
- package/dist/esm/store/index.js +6 -0
- package/dist/esm/store/index.js.map +1 -1
- package/dist/esm/store/native.d.ts +7 -0
- package/dist/esm/store/native.js +83 -0
- package/dist/esm/store/native.js.map +1 -0
- package/dist/esm/store/sqlite/fieldStats.d.ts +2 -0
- package/dist/esm/store/sqlite/fieldStats.js +25 -0
- package/dist/esm/store/sqlite/fieldStats.js.map +1 -0
- package/dist/esm/store/sqlite/open.js +21 -8
- package/dist/esm/store/sqlite/open.js.map +1 -1
- package/dist/esm/store/sqlite/store.js +14 -1
- package/dist/esm/store/sqlite/store.js.map +1 -1
- package/dist/esm/store/sqlite/vectors.d.ts +0 -5
- package/dist/esm/store/sqlite/vectors.js +2 -22
- package/dist/esm/store/sqlite/vectors.js.map +1 -1
- package/dist/esm/store/turso/connection.d.ts +3 -0
- package/dist/esm/store/turso/connection.js +52 -0
- package/dist/esm/store/turso/connection.js.map +1 -0
- package/dist/esm/store/turso/fieldStats.d.ts +2 -0
- package/dist/esm/store/turso/fieldStats.js +24 -0
- package/dist/esm/store/turso/fieldStats.js.map +1 -0
- package/dist/esm/store/turso/native.d.ts +2 -0
- package/dist/esm/store/turso/native.js +19 -0
- package/dist/esm/store/turso/native.js.map +1 -0
- package/dist/esm/store/turso/open.d.ts +12 -0
- package/dist/esm/store/turso/open.js +113 -0
- package/dist/esm/store/turso/open.js.map +1 -0
- package/dist/esm/store/turso/reconcile.d.ts +6 -0
- package/dist/esm/store/turso/reconcile.js +158 -0
- package/dist/esm/store/turso/reconcile.js.map +1 -0
- package/dist/esm/store/turso/store.d.ts +5 -0
- package/dist/esm/store/turso/store.js +93 -0
- package/dist/esm/store/turso/store.js.map +1 -0
- package/dist/esm/store/types.d.ts +9 -1
- package/dist/esm/store/types.js +0 -7
- package/dist/esm/store/types.js.map +1 -1
- package/dist/esm/store/vectors.d.ts +9 -0
- package/dist/esm/store/vectors.js +26 -0
- package/dist/esm/store/vectors.js.map +1 -0
- package/package.json +2 -1
package/dist/esm/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Peek, PresetCoverage, SearchOptions, TreeMap } from './commands/index.ts';\nexport { mapTree, peek, presetCoverage, search } from './commands/index.ts';\nexport type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights } from './config/index.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, SIGNAL_NAMES, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config/index.ts';\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\nexport type { Format, Row, RowFormat } from './output/output.ts';\nexport { printRows } from './output/output.ts';\nexport type { OpenResult } from './store/index.ts';\nexport { clearCache, openStore as open } from './store/index.ts';\nexport type { Capability, DocumentStore, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["mapTree","peek","presetCoverage","search","CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","SIGNAL_NAMES","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","printRows","clearCache","openStore","open","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,OAAO,EAAEC,IAAI,EAAEC,cAAc,EAAEC,MAAM,QAAQ,sBAAsB;AAE5E,SAASC,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,YAAY,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,oBAAoB;AAE9I,SAASC,UAAU,QAAQ,cAAc;AAEzC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,SAASC,UAAU,EAAEC,aAAaC,IAAI,QAAQ,mBAAmB;AAIjE,SAASC,QAAQ,QAAQ,aAAa"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/index.ts"],"sourcesContent":["// Public library API. Deliberately small: every export is a stability promise;\n// internals (feature registry, graph, scan, meta) stay module-private.\n\nexport type { Peek, PresetCoverage, SearchOptions, TreeMap } from './commands/index.ts';\nexport { mapTree, peek, presetCoverage, search } from './commands/index.ts';\nexport type { Config, EmbedConfig, FeatureName, InitOverrides, Preset, ResolvedConfig, SavedQuery, SavedSearch, SearchOverrides, SignalName, SignalWeights, StoreName } from './config/index.ts';\nexport { CONFIG_FILENAME, initConfig, loadConfig, migrateConfig, SIGNAL_NAMES, STATE_DIR, SUPPORTED_CONFIG_VERSION } from './config/index.ts';\nexport type { SenseErrorCode } from './errors.ts';\nexport { SenseError } from './errors.ts';\nexport type { Format, Row, RowFormat } from './output/output.ts';\nexport { printRows } from './output/output.ts';\nexport type { OpenResult } from './store/index.ts';\nexport { clearCache, openStore as open } from './store/index.ts';\nexport type { Capability, DocumentStore, FieldStat, LexicalHit, LexicalIndex, LexicalQueryOptions, RawStatement, RunResult, SqlSession, Statement, Store, VectorCandidate, VectorSimilar, VectorStore, VectorWriteRow } from './store/types.ts';\n\nexport type { WatchEvent, WatchOptions } from './watch.ts';\nexport { runWatch } from './watch.ts';\n"],"names":["mapTree","peek","presetCoverage","search","CONFIG_FILENAME","initConfig","loadConfig","migrateConfig","SIGNAL_NAMES","STATE_DIR","SUPPORTED_CONFIG_VERSION","SenseError","printRows","clearCache","openStore","open","runWatch"],"mappings":"AAAA,+EAA+E;AAC/E,uEAAuE;AAGvE,SAASA,OAAO,EAAEC,IAAI,EAAEC,cAAc,EAAEC,MAAM,QAAQ,sBAAsB;AAE5E,SAASC,eAAe,EAAEC,UAAU,EAAEC,UAAU,EAAEC,aAAa,EAAEC,YAAY,EAAEC,SAAS,EAAEC,wBAAwB,QAAQ,oBAAoB;AAE9I,SAASC,UAAU,QAAQ,cAAc;AAEzC,SAASC,SAAS,QAAQ,qBAAqB;AAE/C,SAASC,UAAU,EAAEC,aAAaC,IAAI,QAAQ,mBAAmB;AAIjE,SAASC,QAAQ,QAAQ,aAAa"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function writeFileAtomic(path: string, data: string | Buffer): void;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
// Stage-then-swap: write beside the target, then rename over it. A crash or full disk mid-write
|
|
3
|
+
// leaves the target untouched instead of truncated. Matches embed/store.ts's model-download idiom.
|
|
4
|
+
export function writeFileAtomic(path, data) {
|
|
5
|
+
const tmp = `${path}.part`;
|
|
6
|
+
writeFileSync(tmp, data);
|
|
7
|
+
try {
|
|
8
|
+
renameSync(tmp, path);
|
|
9
|
+
} catch (err) {
|
|
10
|
+
try {
|
|
11
|
+
unlinkSync(tmp);
|
|
12
|
+
} catch {
|
|
13
|
+
// best-effort cleanup; the rename error below is what matters
|
|
14
|
+
}
|
|
15
|
+
throw err;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/lib/atomic-write.ts"],"sourcesContent":["import { renameSync, unlinkSync, writeFileSync } from 'node:fs';\n\n// Stage-then-swap: write beside the target, then rename over it. A crash or full disk mid-write\n// leaves the target untouched instead of truncated. Matches embed/store.ts's model-download idiom.\nexport function writeFileAtomic(path: string, data: string | Buffer): void {\n const tmp = `${path}.part`;\n writeFileSync(tmp, data);\n try {\n renameSync(tmp, path);\n } catch (err) {\n try {\n unlinkSync(tmp);\n } catch {\n // best-effort cleanup; the rename error below is what matters\n }\n throw err;\n }\n}\n"],"names":["renameSync","unlinkSync","writeFileSync","writeFileAtomic","path","data","tmp","err"],"mappings":"AAAA,SAASA,UAAU,EAAEC,UAAU,EAAEC,aAAa,QAAQ,UAAU;AAEhE,gGAAgG;AAChG,mGAAmG;AACnG,OAAO,SAASC,gBAAgBC,IAAY,EAAEC,IAAqB;IACjE,MAAMC,MAAM,GAAGF,KAAK,KAAK,CAAC;IAC1BF,cAAcI,KAAKD;IACnB,IAAI;QACFL,WAAWM,KAAKF;IAClB,EAAE,OAAOG,KAAK;QACZ,IAAI;YACFN,WAAWK;QACb,EAAE,OAAM;QACN,8DAA8D;QAChE;QACA,MAAMC;IACR;AACF"}
|
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
// not just the async crossing to reach them, so this recognizes the three statement shapes the
|
|
5
5
|
// store issues (INSERT ... VALUES (...), UPDATE ... SET ... WHERE ..., DELETE ... WHERE ...) and
|
|
6
6
|
// rewrites each into a multi-row form, verified against a live DuckDB connection at
|
|
7
|
-
// implementation time (
|
|
8
|
-
// falls back to a prepare-once, bind-and-run loop -- still one call
|
|
7
|
+
// implementation time (PRINCIPLES: documented-means-tested). Unrecognized SQL returns null;
|
|
8
|
+
// connection.ts's runBatch falls back to a prepare-once, bind-and-run loop -- still one call
|
|
9
|
+
// from outside the store.
|
|
9
10
|
//
|
|
10
11
|
// This only ever runs against sense's own SQL literals (reconcile, the feature hooks), never
|
|
11
12
|
// arbitrary user SQL (`sense sql` is a separate, unrewritten passthrough), so the shapes below
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/batch.ts"],"sourcesContent":["// Rewrites a single-row parameterized statement -- the shape every write loop in this codebase\n// prepares once and calls with N different param sets -- into one statement DuckDB executes over\n// all N rows in a single round trip. A columnar engine's cost is row-at-a-time writes themselves,\n// not just the async crossing to reach them, so this recognizes the three statement shapes the\n// store issues (INSERT ... VALUES (...), UPDATE ... SET ... WHERE ..., DELETE ... WHERE ...) and\n// rewrites each into a multi-row form, verified against a live DuckDB connection at\n// implementation time (
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/batch.ts"],"sourcesContent":["// Rewrites a single-row parameterized statement -- the shape every write loop in this codebase\n// prepares once and calls with N different param sets -- into one statement DuckDB executes over\n// all N rows in a single round trip. A columnar engine's cost is row-at-a-time writes themselves,\n// not just the async crossing to reach them, so this recognizes the three statement shapes the\n// store issues (INSERT ... VALUES (...), UPDATE ... SET ... WHERE ..., DELETE ... WHERE ...) and\n// rewrites each into a multi-row form, verified against a live DuckDB connection at\n// implementation time (PRINCIPLES: documented-means-tested). Unrecognized SQL returns null;\n// connection.ts's runBatch falls back to a prepare-once, bind-and-run loop -- still one call\n// from outside the store.\n//\n// This only ever runs against sense's own SQL literals (reconcile, the feature hooks), never\n// arbitrary user SQL (`sense sql` is a separate, unrewritten passthrough), so the shapes below\n// are exactly what those call sites emit, not a general parser.\n\nconst INSERT_RE = /^(INSERT\\s+(?:OR\\s+\\w+\\s+)?INTO\\s+\\S+\\s*\\([^)]*\\)\\s*VALUES\\s*)\\(([^()]*)\\)(.*)$/is;\nconst UPDATE_RE = /^UPDATE\\s+(\\S+)\\s+SET\\s+(.+?)\\s+WHERE\\s+(.+)$/is;\nconst DELETE_RE = /^DELETE\\s+FROM\\s+(\\S+)\\s+WHERE\\s+(.+)$/is;\n\nfunction placeholderCount(tuple: string): number {\n return (tuple.match(/\\?/g) ?? []).length;\n}\n\n// \"a = ?, b = ?\" or \"a = ? AND b = ?\" -> [\"a\", \"b\"]. Only ever called on clauses already\n// shaped that way by the regexes above; a clause that doesn't fit yields a column list whose\n// length won't match the caller's row width, which rewriteUpdate/rewriteDelete then reject.\nfunction equalityColumns(clause: string, sep: RegExp): string[] | null {\n const parts = clause.split(sep).map((p) => p.trim());\n const cols: string[] = [];\n for (const part of parts) {\n const m = /^(.+?)=\\s*\\?$/.exec(part);\n if (!m) return null;\n cols.push(m[1].trim());\n }\n return cols;\n}\n\nfunction tuples(rowCount: number, width: number): string {\n const row = `(${Array.from({ length: width }, () => '?').join(', ')})`;\n return Array.from({ length: rowCount }, () => row).join(', ');\n}\n\nexport function rewriteInsert(sql: string, rowCount: number): { sql: string; width: number } | null {\n const m = INSERT_RE.exec(sql.trim());\n if (!m) return null;\n const [, head, tuple, tail] = m;\n const width = placeholderCount(tuple);\n if (width === 0) return null;\n const rows = Array.from({ length: rowCount }, () => `(${tuple})`).join(', ');\n return { sql: `${head}${rows}${tail}`, width };\n}\n\nexport function rewriteUpdate(sql: string, rowCount: number): { sql: string; width: number } | null {\n const m = UPDATE_RE.exec(sql.trim());\n if (!m) return null;\n const [, table, setClause, whereClause] = m;\n const setCols = equalityColumns(setClause, /,/);\n const whereCols = equalityColumns(whereClause, /\\bAND\\b/i);\n if (!setCols || !whereCols) return null;\n const dataCols = [...setCols, ...whereCols];\n const setAssign = setCols.map((c) => `${c} = data.${c}`).join(', ');\n const joinCond = whereCols.map((c) => `${table}.${c} = data.${c}`).join(' AND ');\n return { sql: `UPDATE ${table} SET ${setAssign} FROM (VALUES ${tuples(rowCount, dataCols.length)}) AS data(${dataCols.join(', ')}) WHERE ${joinCond}`, width: dataCols.length };\n}\n\nexport function rewriteDelete(sql: string, rowCount: number): { sql: string; width: number } | null {\n const m = DELETE_RE.exec(sql.trim());\n if (!m) return null;\n const [, table, whereClause] = m;\n const cols = equalityColumns(whereClause, /\\bAND\\b/i);\n if (!cols) return null;\n if (cols.length === 1) {\n return { sql: `DELETE FROM ${table} WHERE ${cols[0]} IN (${Array.from({ length: rowCount }, () => '?').join(', ')})`, width: 1 };\n }\n return { sql: `DELETE FROM ${table} WHERE (${cols.join(', ')}) IN (VALUES ${tuples(rowCount, cols.length)})`, width: cols.length };\n}\n\n// Tries each shape in turn; null means the caller should fall back to a per-row loop.\nexport function rewriteBatch(sql: string, rowCount: number): { sql: string; width: number } | null {\n return rewriteInsert(sql, rowCount) ?? rewriteUpdate(sql, rowCount) ?? rewriteDelete(sql, rowCount);\n}\n"],"names":["INSERT_RE","UPDATE_RE","DELETE_RE","placeholderCount","tuple","match","length","equalityColumns","clause","sep","parts","split","map","p","trim","cols","part","m","exec","push","tuples","rowCount","width","row","Array","from","join","rewriteInsert","sql","head","tail","rows","rewriteUpdate","table","setClause","whereClause","setCols","whereCols","dataCols","setAssign","c","joinCond","rewriteDelete","rewriteBatch"],"mappings":"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"}
|
|
@@ -12,6 +12,17 @@ export function storeRowToJs(row) {
|
|
|
12
12
|
for (const [key, value] of Object.entries(row))out[key] = storeValueToJs(value);
|
|
13
13
|
return out;
|
|
14
14
|
}
|
|
15
|
+
// @duckdb/node-api tracks every prepared statement itself and destroys any survivor when the
|
|
16
|
+
// connection closes, but sense's stores stay open for a whole `sense watch` session -- nothing
|
|
17
|
+
// destroys one before then. The portable Statement interface has no dispose (sqlite's node:sqlite
|
|
18
|
+
// statements need none, and callers like graph/traverse.ts's ring loop reuse one Statement across
|
|
19
|
+
// several run/get/all calls, so a per-call destroy would be wrong), so cleanup happens here,
|
|
20
|
+
// locally, once nothing references the wrapper any more. destroySync() is safe to call more than
|
|
21
|
+
// once -- runBatch below and vectors.ts already call it explicitly while the same connection-close
|
|
22
|
+
// sweep can also reach a statement that outlives its wrapper, with no reported issue.
|
|
23
|
+
const preparedFinalizer = new FinalizationRegistry((prepared)=>{
|
|
24
|
+
prepared.destroySync();
|
|
25
|
+
});
|
|
15
26
|
// Wraps one already-prepared DuckDB statement: prepare() is the only async step (Store.prepare
|
|
16
27
|
// already crosses the async boundary once), so columns() reads back synchronously from it and
|
|
17
28
|
// run/get/all rebind and re-execute the same native statement, matching how a caller (e.g.
|
|
@@ -52,6 +63,7 @@ let DuckdbStatement = class DuckdbStatement {
|
|
|
52
63
|
}
|
|
53
64
|
constructor(prepared){
|
|
54
65
|
this.prepared = prepared;
|
|
66
|
+
preparedFinalizer.register(this, prepared);
|
|
55
67
|
}
|
|
56
68
|
};
|
|
57
69
|
export function createConnection(duckdb) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/connection.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBPreparedStatement, DuckDBValue } from '@duckdb/node-api';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { rewriteBatch } from './batch.ts';\n\n// getRowObjectsJS returns INT64 columns as BigInt regardless of magnitude, while sqlite's\n// small ints are numbers and consumers assume number. Beyond the safe range stays BigInt (the\n// setReadBigInts contract); stringifyJson keeps JSON output safe.\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);\nexport function storeValueToJs(value: unknown): unknown {\n return typeof value === 'bigint' && value >= -MAX_SAFE && value <= MAX_SAFE ? Number(value) : value;\n}\nexport function storeRowToJs(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) out[key] = storeValueToJs(value);\n return out;\n}\n\n// Wraps one already-prepared DuckDB statement: prepare() is the only async step (Store.prepare\n// already crosses the async boundary once), so columns() reads back synchronously from it and\n// run/get/all rebind and re-execute the same native statement, matching how a caller (e.g.\n// graph/traverse.ts's ring loop) reuses one Statement across several calls.\nclass DuckdbStatement implements Statement {\n private prepared: DuckDBPreparedStatement;\n\n constructor(prepared: DuckDBPreparedStatement) {\n this.prepared = prepared;\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const result = await this.prepared.run();\n return { changes: result.rowsChanged, lastInsertRowid: 0 };\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n const rows = await this.all(...params);\n return rows[0];\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const reader = await this.prepared.runAndReadAll();\n return (reader.getRowObjectsJS() as Array<Record<string, unknown>>).map(storeRowToJs);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n // Never called on the portable Connection/Statement surface today (only Store.raw streams);\n // materializing keeps this a correct, if eager, AsyncIterable.\n yield* await this.all(...params);\n }\n\n columns(): Array<{ name: string }> {\n const out: Array<{ name: string }> = [];\n for (let i = 0; i < this.prepared.columnCount; i++) out.push({ name: this.prepared.columnName(i) });\n return out;\n }\n\n setReadBigInts(_enabled: boolean): void {\n // No-op: in-range ints are already numbers here (storeRowToJs converts what getRowObjectsJS\n // hands back as BigInt), so the node:sqlite throw-at-step-time problem does not exist.\n }\n}\n\nexport function createConnection(duckdb: DuckDBConnection): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await duckdb.run(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new DuckdbStatement(await duckdb.prepare(sql));\n },\n // One crossing regardless of row count: rewriteBatch turns the caller's single-row SQL into\n // one multi-row statement when it recognizes the shape (see batch.ts); anything else falls\n // back to a bind-and-run loop over one prepared statement, still a single call to the caller.\n // Joins the caller's transaction when there is one (reconcile); otherwise opens its own, so\n // a standalone batch stays atomic.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(conn, async () => {\n const rewritten = rewriteBatch(sql, paramRows.length);\n if (rewritten) {\n const stmt = await duckdb.prepare(rewritten.sql);\n try {\n stmt.bind(paramRows.flat() as DuckDBValue[]);\n await stmt.run();\n } finally {\n stmt.destroySync();\n }\n return;\n }\n const stmt = await duckdb.prepare(sql);\n try {\n for (const row of paramRows) {\n stmt.bind(row as DuckDBValue[]);\n await stmt.run();\n }\n } finally {\n stmt.destroySync();\n }\n });\n },\n };\n return conn;\n}\n"],"names":["withTransaction","rewriteBatch","MAX_SAFE","BigInt","Number","MAX_SAFE_INTEGER","storeValueToJs","value","storeRowToJs","row","out","key","Object","entries","DuckdbStatement","run","params","length","
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/connection.ts"],"sourcesContent":["import type { DuckDBConnection, DuckDBPreparedStatement, DuckDBValue } from '@duckdb/node-api';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, RunResult, Statement } from '../types.ts';\nimport { rewriteBatch } from './batch.ts';\n\n// getRowObjectsJS returns INT64 columns as BigInt regardless of magnitude, while sqlite's\n// small ints are numbers and consumers assume number. Beyond the safe range stays BigInt (the\n// setReadBigInts contract); stringifyJson keeps JSON output safe.\nconst MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);\nexport function storeValueToJs(value: unknown): unknown {\n return typeof value === 'bigint' && value >= -MAX_SAFE && value <= MAX_SAFE ? Number(value) : value;\n}\nexport function storeRowToJs(row: Record<string, unknown>): Record<string, unknown> {\n const out: Record<string, unknown> = {};\n for (const [key, value] of Object.entries(row)) out[key] = storeValueToJs(value);\n return out;\n}\n\n// @duckdb/node-api tracks every prepared statement itself and destroys any survivor when the\n// connection closes, but sense's stores stay open for a whole `sense watch` session -- nothing\n// destroys one before then. The portable Statement interface has no dispose (sqlite's node:sqlite\n// statements need none, and callers like graph/traverse.ts's ring loop reuse one Statement across\n// several run/get/all calls, so a per-call destroy would be wrong), so cleanup happens here,\n// locally, once nothing references the wrapper any more. destroySync() is safe to call more than\n// once -- runBatch below and vectors.ts already call it explicitly while the same connection-close\n// sweep can also reach a statement that outlives its wrapper, with no reported issue.\nconst preparedFinalizer = new FinalizationRegistry<DuckDBPreparedStatement>((prepared) => {\n prepared.destroySync();\n});\n\n// Wraps one already-prepared DuckDB statement: prepare() is the only async step (Store.prepare\n// already crosses the async boundary once), so columns() reads back synchronously from it and\n// run/get/all rebind and re-execute the same native statement, matching how a caller (e.g.\n// graph/traverse.ts's ring loop) reuses one Statement across several calls.\nclass DuckdbStatement implements Statement {\n private prepared: DuckDBPreparedStatement;\n\n constructor(prepared: DuckDBPreparedStatement) {\n this.prepared = prepared;\n preparedFinalizer.register(this, prepared);\n }\n\n async run(...params: unknown[]): Promise<RunResult> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const result = await this.prepared.run();\n return { changes: result.rowsChanged, lastInsertRowid: 0 };\n }\n\n async get(...params: unknown[]): Promise<unknown> {\n const rows = await this.all(...params);\n return rows[0];\n }\n\n async all(...params: unknown[]): Promise<unknown[]> {\n if (params.length > 0) this.prepared.bind(params as DuckDBValue[]);\n const reader = await this.prepared.runAndReadAll();\n return (reader.getRowObjectsJS() as Array<Record<string, unknown>>).map(storeRowToJs);\n }\n\n async *iterate(...params: unknown[]): AsyncIterable<unknown> {\n // Never called on the portable Connection/Statement surface today (only Store.raw streams);\n // materializing keeps this a correct, if eager, AsyncIterable.\n yield* await this.all(...params);\n }\n\n columns(): Array<{ name: string }> {\n const out: Array<{ name: string }> = [];\n for (let i = 0; i < this.prepared.columnCount; i++) out.push({ name: this.prepared.columnName(i) });\n return out;\n }\n\n setReadBigInts(_enabled: boolean): void {\n // No-op: in-range ints are already numbers here (storeRowToJs converts what getRowObjectsJS\n // hands back as BigInt), so the node:sqlite throw-at-step-time problem does not exist.\n }\n}\n\nexport function createConnection(duckdb: DuckDBConnection): Connection {\n const conn: Connection = {\n async exec(sql: string): Promise<void> {\n await duckdb.run(sql);\n },\n async prepare(sql: string): Promise<Statement> {\n return new DuckdbStatement(await duckdb.prepare(sql));\n },\n // One crossing regardless of row count: rewriteBatch turns the caller's single-row SQL into\n // one multi-row statement when it recognizes the shape (see batch.ts); anything else falls\n // back to a bind-and-run loop over one prepared statement, still a single call to the caller.\n // Joins the caller's transaction when there is one (reconcile); otherwise opens its own, so\n // a standalone batch stays atomic.\n async runBatch(sql: string, paramRows: unknown[][]): Promise<void> {\n if (paramRows.length === 0) return;\n await withTransaction(conn, async () => {\n const rewritten = rewriteBatch(sql, paramRows.length);\n if (rewritten) {\n const stmt = await duckdb.prepare(rewritten.sql);\n try {\n stmt.bind(paramRows.flat() as DuckDBValue[]);\n await stmt.run();\n } finally {\n stmt.destroySync();\n }\n return;\n }\n const stmt = await duckdb.prepare(sql);\n try {\n for (const row of paramRows) {\n stmt.bind(row as DuckDBValue[]);\n await stmt.run();\n }\n } finally {\n stmt.destroySync();\n }\n });\n },\n };\n return conn;\n}\n"],"names":["withTransaction","rewriteBatch","MAX_SAFE","BigInt","Number","MAX_SAFE_INTEGER","storeValueToJs","value","storeRowToJs","row","out","key","Object","entries","preparedFinalizer","FinalizationRegistry","prepared","destroySync","DuckdbStatement","run","params","length","bind","result","changes","rowsChanged","lastInsertRowid","get","rows","all","reader","runAndReadAll","getRowObjectsJS","map","iterate","columns","i","columnCount","push","name","columnName","setReadBigInts","_enabled","register","createConnection","duckdb","conn","exec","sql","prepare","runBatch","paramRows","rewritten","stmt","flat"],"mappings":"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"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { quoteIdent } from '../shared.js';
|
|
2
|
+
// DuckDB's frontmatter columns are VARIANT (reconcile.ts's comment on why), so typeof() reports
|
|
3
|
+
// VARIANT for every field regardless of the boxed value; variant_typeof() reports the boxed
|
|
4
|
+
// native type instead (spike-verified against mapValue()'s actual bind shapes: a bound JS bigint
|
|
5
|
+
// always boxes as INT128, a non-integer JS number always boxes as DOUBLE, a string as VARCHAR --
|
|
6
|
+
// literal SQL values box narrower, e.g. INTEGER/DECIMAL(p, s), which this still covers since a
|
|
7
|
+
// query issued through `sense sql` can produce either). Mapped onto the shared integer/real/text
|
|
8
|
+
// vocabulary sqlite/turso's typeof() already emits natively; an unrecognized name throws rather
|
|
9
|
+
// than being silently called text (no-silent-modes).
|
|
10
|
+
function classifyVariantType(name, field) {
|
|
11
|
+
if (/^U?INT\d+$/.test(name)) return 'integer';
|
|
12
|
+
if (name === 'FLOAT' || name === 'DOUBLE' || name.startsWith('DECIMAL(')) return 'real';
|
|
13
|
+
if (name === 'VARCHAR') return 'text';
|
|
14
|
+
throw new Error(`duckdb: variant_typeof() returned unrecognized type "${name}" for column "${field}"`);
|
|
15
|
+
}
|
|
16
|
+
// One aggregate query, one result row: COUNT + string_agg(DISTINCT variant_typeof()) per
|
|
17
|
+
// column, so the query stays O(columns) rather than O(rows x columns). '|' is the string_agg
|
|
18
|
+
// separator, not ',': DECIMAL's own display name ("DECIMAL(4, 2)") contains a comma, which a
|
|
19
|
+
// ',' separator could not be split back apart from.
|
|
20
|
+
export async function fieldStats(conn, columns, scopeWhere) {
|
|
21
|
+
const exprs = columns.map((name, i)=>{
|
|
22
|
+
const quoted = quoteIdent(name);
|
|
23
|
+
return `COUNT(${quoted}) AS n${i}, string_agg(DISTINCT variant_typeof(${quoted}), '|') FILTER (WHERE ${quoted} IS NOT NULL) AS t${i}`;
|
|
24
|
+
});
|
|
25
|
+
const stmt = await conn.prepare(`SELECT ${exprs.join(', ')} FROM frontmatter ${scopeWhere}`);
|
|
26
|
+
const row = await stmt.get();
|
|
27
|
+
return columns.map((name, i)=>{
|
|
28
|
+
var _row_, _row_1;
|
|
29
|
+
const raw = ((_row_ = row[`t${i}`]) !== null && _row_ !== void 0 ? _row_ : '').split('|').filter(Boolean);
|
|
30
|
+
const types = [
|
|
31
|
+
...new Set(raw.map((t)=>classifyVariantType(t, name)))
|
|
32
|
+
].sort();
|
|
33
|
+
return {
|
|
34
|
+
field: name,
|
|
35
|
+
coverage: Number((_row_1 = row[`n${i}`]) !== null && _row_1 !== void 0 ? _row_1 : 0),
|
|
36
|
+
type: types.join(',')
|
|
37
|
+
};
|
|
38
|
+
});
|
|
39
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/fieldStats.ts"],"sourcesContent":["import { quoteIdent } from '../shared.ts';\nimport type { Connection, FieldStat } from '../types.ts';\n\n// DuckDB's frontmatter columns are VARIANT (reconcile.ts's comment on why), so typeof() reports\n// VARIANT for every field regardless of the boxed value; variant_typeof() reports the boxed\n// native type instead (spike-verified against mapValue()'s actual bind shapes: a bound JS bigint\n// always boxes as INT128, a non-integer JS number always boxes as DOUBLE, a string as VARCHAR --\n// literal SQL values box narrower, e.g. INTEGER/DECIMAL(p, s), which this still covers since a\n// query issued through `sense sql` can produce either). Mapped onto the shared integer/real/text\n// vocabulary sqlite/turso's typeof() already emits natively; an unrecognized name throws rather\n// than being silently called text (no-silent-modes).\nfunction classifyVariantType(name: string, field: string): 'integer' | 'real' | 'text' {\n if (/^U?INT\\d+$/.test(name)) return 'integer';\n if (name === 'FLOAT' || name === 'DOUBLE' || name.startsWith('DECIMAL(')) return 'real';\n if (name === 'VARCHAR') return 'text';\n throw new Error(`duckdb: variant_typeof() returned unrecognized type \"${name}\" for column \"${field}\"`);\n}\n\n// One aggregate query, one result row: COUNT + string_agg(DISTINCT variant_typeof()) per\n// column, so the query stays O(columns) rather than O(rows x columns). '|' is the string_agg\n// separator, not ',': DECIMAL's own display name (\"DECIMAL(4, 2)\") contains a comma, which a\n// ',' separator could not be split back apart from.\nexport async function fieldStats(conn: Connection, columns: string[], scopeWhere: string): Promise<FieldStat[]> {\n const exprs = columns.map((name, i) => {\n const quoted = quoteIdent(name);\n return `COUNT(${quoted}) AS n${i}, string_agg(DISTINCT variant_typeof(${quoted}), '|') FILTER (WHERE ${quoted} IS NOT NULL) AS t${i}`;\n });\n const stmt = await conn.prepare(`SELECT ${exprs.join(', ')} FROM frontmatter ${scopeWhere}`);\n const row = (await stmt.get()) as Record<string, number | string | null>;\n return columns.map((name, i) => {\n const raw = ((row[`t${i}`] as string) ?? '').split('|').filter(Boolean);\n const types = [...new Set(raw.map((t) => classifyVariantType(t, name)))].sort();\n return { field: name, coverage: Number(row[`n${i}`] ?? 0), type: types.join(',') };\n });\n}\n"],"names":["quoteIdent","classifyVariantType","name","field","test","startsWith","Error","fieldStats","conn","columns","scopeWhere","exprs","map","i","quoted","stmt","prepare","join","row","get","raw","split","filter","Boolean","types","Set","t","sort","coverage","Number","type"],"mappings":"AAAA,SAASA,UAAU,QAAQ,eAAe;AAG1C,gGAAgG;AAChG,4FAA4F;AAC5F,iGAAiG;AACjG,iGAAiG;AACjG,+FAA+F;AAC/F,iGAAiG;AACjG,gGAAgG;AAChG,qDAAqD;AACrD,SAASC,oBAAoBC,IAAY,EAAEC,KAAa;IACtD,IAAI,aAAaC,IAAI,CAACF,OAAO,OAAO;IACpC,IAAIA,SAAS,WAAWA,SAAS,YAAYA,KAAKG,UAAU,CAAC,aAAa,OAAO;IACjF,IAAIH,SAAS,WAAW,OAAO;IAC/B,MAAM,IAAII,MAAM,CAAC,qDAAqD,EAAEJ,KAAK,cAAc,EAAEC,MAAM,CAAC,CAAC;AACvG;AAEA,yFAAyF;AACzF,6FAA6F;AAC7F,6FAA6F;AAC7F,oDAAoD;AACpD,OAAO,eAAeI,WAAWC,IAAgB,EAAEC,OAAiB,EAAEC,UAAkB;IACtF,MAAMC,QAAQF,QAAQG,GAAG,CAAC,CAACV,MAAMW;QAC/B,MAAMC,SAASd,WAAWE;QAC1B,OAAO,CAAC,MAAM,EAAEY,OAAO,MAAM,EAAED,EAAE,qCAAqC,EAAEC,OAAO,sBAAsB,EAAEA,OAAO,kBAAkB,EAAED,GAAG;IACvI;IACA,MAAME,OAAO,MAAMP,KAAKQ,OAAO,CAAC,CAAC,OAAO,EAAEL,MAAMM,IAAI,CAAC,MAAM,kBAAkB,EAAEP,YAAY;IAC3F,MAAMQ,MAAO,MAAMH,KAAKI,GAAG;IAC3B,OAAOV,QAAQG,GAAG,CAAC,CAACV,MAAMW;YACVK,OAEyBA;QAFvC,MAAME,MAAM,EAAEF,QAAAA,GAAG,CAAC,CAAC,CAAC,EAAEL,GAAG,CAAC,cAAZK,mBAAAA,QAA2B,IAAIG,KAAK,CAAC,KAAKC,MAAM,CAACC;QAC/D,MAAMC,QAAQ;eAAI,IAAIC,IAAIL,IAAIR,GAAG,CAAC,CAACc,IAAMzB,oBAAoByB,GAAGxB;SAAQ,CAACyB,IAAI;QAC7E,OAAO;YAAExB,OAAOD;YAAM0B,UAAUC,QAAOX,SAAAA,GAAG,CAAC,CAAC,CAAC,EAAEL,GAAG,CAAC,cAAZK,oBAAAA,SAAgB;YAAIY,MAAMN,MAAMP,IAAI,CAAC;QAAK;IACnF;AACF"}
|
|
@@ -18,8 +18,9 @@ const FIELD_WEIGHT = {
|
|
|
18
18
|
};
|
|
19
19
|
const UNSPACED_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');
|
|
20
20
|
// FTS5 operator syntax (sqlite.org/fts5.html sec. 3) that words/substrings below would
|
|
21
|
-
// otherwise silently treat as literal terms instead of honoring (PRINCIPLES
|
|
22
|
-
// against terms with quoted spans blanked -- those go through contains() and are
|
|
21
|
+
// otherwise silently treat as literal terms instead of honoring (PRINCIPLES: no-silent-modes).
|
|
22
|
+
// Checked against terms with quoted spans blanked -- those go through contains() and are
|
|
23
|
+
// supported.
|
|
23
24
|
const FTS5_OPERATORS = [
|
|
24
25
|
{
|
|
25
26
|
label: 'prefix query',
|
|
@@ -95,33 +96,42 @@ async function ensureFtsFresh(conn, state) {
|
|
|
95
96
|
}
|
|
96
97
|
// Per-word-term and per-substring-term SQL fragments, field-weighted the same way for both
|
|
97
98
|
// (title 10 / summary 5 / text 1), plus the params in the exact left-to-right order they are
|
|
98
|
-
// emitted -- positional `?` binding requires that order to match the assembled SQL text.
|
|
99
|
+
// emitted -- positional `?` binding requires that order to match the assembled SQL text. Score
|
|
100
|
+
// and gate params are collected apart and joined at the end because the assembled SQL puts
|
|
101
|
+
// every score part ahead of every gate part: appending to one array term by term matches that
|
|
102
|
+
// text only while the query has words or substrings, never both, and silently shifts each `?`
|
|
103
|
+
// by one as soon as it has both (PRINCIPLES: no-silent-modes -- a shifted bind answers a
|
|
104
|
+
// different question rather than failing).
|
|
99
105
|
function buildScoreAndGate(words, substrings) {
|
|
100
106
|
const scoreParts = [];
|
|
101
107
|
const gateParts = [];
|
|
102
|
-
const
|
|
108
|
+
const scoreParams = [];
|
|
109
|
+
const gateParams = [];
|
|
103
110
|
if (words.length > 0) {
|
|
104
111
|
const wordQuery = words.join(' ');
|
|
105
112
|
for (const field of FIELDS){
|
|
106
113
|
scoreParts.push(`${FIELD_WEIGHT[field]}.0 * COALESCE(fts_main_content.match_bm25(content."path", ?, fields := '${field}', conjunctive := false), 0)`);
|
|
107
|
-
|
|
114
|
+
scoreParams.push(wordQuery);
|
|
108
115
|
}
|
|
109
116
|
gateParts.push(`fts_main_content.match_bm25(content."path", ?, conjunctive := true) IS NOT NULL`);
|
|
110
|
-
|
|
117
|
+
gateParams.push(wordQuery);
|
|
111
118
|
}
|
|
112
119
|
for (const raw of substrings){
|
|
113
120
|
const needle = raw.toLowerCase();
|
|
114
121
|
for (const field of FIELDS){
|
|
115
122
|
scoreParts.push(`${FIELD_WEIGHT[field]}.0 * CAST(contains(lower(content.${field}), ?) AS INTEGER)`);
|
|
116
|
-
|
|
123
|
+
scoreParams.push(needle);
|
|
117
124
|
}
|
|
118
125
|
gateParts.push(`contains(lower(content.title) || ' ' || lower(content.summary) || ' ' || lower(content.text), ?)`);
|
|
119
|
-
|
|
126
|
+
gateParams.push(needle);
|
|
120
127
|
}
|
|
121
128
|
return {
|
|
122
129
|
scoreSql: scoreParts.join(' + '),
|
|
123
130
|
gateSql: gateParts.join(' AND '),
|
|
124
|
-
params
|
|
131
|
+
params: [
|
|
132
|
+
...scoreParams,
|
|
133
|
+
...gateParams
|
|
134
|
+
]
|
|
125
135
|
};
|
|
126
136
|
}
|
|
127
137
|
// Ranked word-match query, scoped by the caller-built SQL fragments (same fragments sqlite's
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/lexical.ts"],"sourcesContent":["// D1: fts BM25 for ranking, contains() scans for exact substring / phrase verification /\n// unspaced scripts, JS excerpts for every doc (no snippet()).\nimport { SenseError } from '../../errors.ts';\nimport { UNSPACED_SCRIPTS } from '../../text/segment.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\nconst FIELDS = ['title', 'summary', 'text'] as const;\ntype Field = (typeof FIELDS)[number];\n// Mirrors sqlite's bm25(content, 10.0, 5.0, 1.0, ...) column-weight intent; DuckDB's\n// match_bm25 has no field_weights argument (verified 1.5.5), so each field is scored\n// separately and combined here.\nconst FIELD_WEIGHT: Record<Field, number> = { title: 10, summary: 5, text: 1 };\n\nconst UNSPACED_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');\n\n// FTS5 operator syntax (sqlite.org/fts5.html sec. 3) that words/substrings below would\n// otherwise silently treat as literal terms instead of honoring (PRINCIPLES.md #6). Checked\n// against terms with quoted spans blanked -- those go through contains() and are supported.\nconst FTS5_OPERATORS: Array<{ label: string; re: RegExp }> = [\n { label: 'prefix query', re: /[\\p{L}\\p{N}_]+\\*(?=\\s|$)/u },\n { label: 'boolean operator', re: /(?:^|\\s)(?:AND|OR|NOT)(?=\\s|$)/ },\n { label: 'NEAR operator', re: /(?:^|\\s)NEAR\\b/ },\n { label: 'initial-token operator', re: /(?:^|\\s)\\^\\S+/ },\n { label: 'column filter', re: /(?:^|\\s)[\\p{L}_]\\w*\\s*:/u },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, ' ');\n for (const { label, re } of FTS5_OPERATORS) {\n const m = withoutPhrases.match(re);\n if (m) return { label, token: m[0].trim() };\n }\n return null;\n}\n\ninterface FtsIndexState {\n stale: boolean;\n}\n\n// A run whose script marks no word boundaries makes match_bm25's whitespace tokenizer index\n// the whole run as one token (same gap FTS5 has without the `_seg` sidecar), so such runs --\n// and any author-quoted phrase, any script -- go through contains() instead of the fts index.\nfunction splitTerms(terms: string): { words: string[]; substrings: string[] } {\n const words: string[] = [];\n const substrings: string[] = [];\n const withoutPhrases = terms.replace(/\"([^\"]*)\"/g, (_m, inner: string) => {\n const phrase = inner.trim();\n if (phrase.length === 0) return ' ';\n substrings.push(phrase);\n if (!UNSPACED_RUN.test(phrase)) for (const w of phrase.split(/\\s+/)) if (w.length > 0) words.push(w);\n return ' ';\n });\n for (const tok of withoutPhrases.split(/\\s+/)) {\n if (tok.length === 0) continue;\n if (UNSPACED_RUN.test(tok)) substrings.push(tok);\n else words.push(tok);\n }\n return { words, substrings };\n}\n\n// No incremental update (verified 1.5.5: PRAGMA create_fts_index is rebuild-only), so this\n// pays the full rebuild -- but only once per store instance's first lexical query, and only\n// when content actually changed since (see FtsIndexState / duckdb/store.ts's markStale()).\nasync function ensureFtsFresh(conn: Connection, state: FtsIndexState): Promise<void> {\n if (!state.stale) return;\n await conn.exec('INSTALL fts; LOAD fts;');\n await withTransaction(conn, async () => {\n // stopwords='none': sqlite's porter/unicode61 tokenizer never removes stopwords either\n // (verified 1.5.5), and the fts extension's default 571-word English list would otherwise\n // silently drop common query words (e.g. \"and\") from the index but not from match_bm25's\n // conjunctive gate, making a bare multi-word query that contains one match nothing.\n await conn.exec(`PRAGMA create_fts_index('content', 'path', 'title', 'summary', 'text', stopwords='none', overwrite=1)`);\n });\n state.stale = false;\n}\n\n// Per-word-term and per-substring-term SQL fragments, field-weighted the same way for both\n// (title 10 / summary 5 / text 1), plus the params in the exact left-to-right order they are\n// emitted -- positional `?` binding requires that order to match the assembled SQL text.\nfunction buildScoreAndGate(words: string[], substrings: string[]): { scoreSql: string; gateSql: string; params: unknown[] } {\n const scoreParts: string[] = [];\n const gateParts: string[] = [];\n const params: unknown[] = [];\n\n if (words.length > 0) {\n const wordQuery = words.join(' ');\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * COALESCE(fts_main_content.match_bm25(content.\"path\", ?, fields := '${field}', conjunctive := false), 0)`);\n params.push(wordQuery);\n }\n gateParts.push(`fts_main_content.match_bm25(content.\"path\", ?, conjunctive := true) IS NOT NULL`);\n params.push(wordQuery);\n }\n\n for (const raw of substrings) {\n const needle = raw.toLowerCase();\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * CAST(contains(lower(content.${field}), ?) AS INTEGER)`);\n params.push(needle);\n }\n gateParts.push(`contains(lower(content.title) || ' ' || lower(content.summary) || ' ' || lower(content.text), ?)`);\n params.push(needle);\n }\n\n return { scoreSql: scoreParts.join(' + '), gateSql: gateParts.join(' AND '), params };\n}\n\n// Ranked word-match query, scoped by the caller-built SQL fragments (same fragments sqlite's\n// queryLexical takes). `hit` is always NULL: no snippet() equivalent exists, so every row goes\n// through the caller's JS excerpt fallback (commands/search.ts) rather than a second excerpt path.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions, state: FtsIndexState): Promise<LexicalHit[]> {\n const unsupported = unsupportedOperator(terms);\n if (unsupported !== null) {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"duckdb\" does not implement FTS5's ${unsupported.label} (\"${unsupported.token}\") in this build; rephrase \"${terms.trim()}\" without it, or set \"store\" to \"sqlite\" in this tree's config to search it as written`);\n }\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const { words, substrings } = splitTerms(terms);\n if (words.length === 0 && substrings.length === 0) return [];\n\n if (words.length > 0) await ensureFtsFresh(conn, state);\n\n const { scoreSql, gateSql, params } = buildScoreAndGate(words, substrings);\n const sql = `SELECT path, NULL AS hit FROM (\n SELECT content.\"path\" AS path, (${scoreSql}) AS score\n FROM content\n ${whereJoin}\n WHERE ${gateSql}\n ${whereCond} ${scopeCond}\n ) sq ORDER BY score DESC, path LIMIT ?`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(...params, limit)) as unknown as LexicalHit[];\n}\n\n// One instance per store: `stale` starts true (a fresh connection cannot know whether an\n// on-disk fts schema still matches `content`), and store.ts's reconcile wrapper flips it back\n// to true whenever content changes, so the next query rebuilds before ranking against it.\nexport function createLexicalIndex(conn: Connection): { query: (terms: string, opts: LexicalQueryOptions) => Promise<LexicalHit[]>; markStale: () => void } {\n const state: FtsIndexState = { stale: true };\n return {\n query: (terms, opts) => queryLexical(conn, terms, opts, state),\n markStale: () => {\n state.stale = true;\n },\n };\n}\n"],"names":["SenseError","UNSPACED_SCRIPTS","withTransaction","FIELDS","FIELD_WEIGHT","title","summary","text","UNSPACED_RUN","RegExp","FTS5_OPERATORS","label","re","unsupportedOperator","terms","withoutPhrases","replace","m","match","token","trim","splitTerms","words","substrings","_m","inner","phrase","length","push","test","w","split","tok","ensureFtsFresh","conn","state","stale","exec","buildScoreAndGate","scoreParts","gateParts","params","wordQuery","join","field","raw","needle","toLowerCase","scoreSql","gateSql","queryLexical","opts","unsupported","whereJoin","whereCond","scopeCond","limit","sql","stmt","prepare","all","createLexicalIndex","query","markStale"],"mappings":"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"}
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/lexical.ts"],"sourcesContent":["// D1: fts BM25 for ranking, contains() scans for exact substring / phrase verification /\n// unspaced scripts, JS excerpts for every doc (no snippet()).\nimport { SenseError } from '../../errors.ts';\nimport { UNSPACED_SCRIPTS } from '../../text/segment.ts';\nimport { withTransaction } from '../transaction.ts';\nimport type { Connection, LexicalHit, LexicalQueryOptions } from '../types.ts';\n\nconst FIELDS = ['title', 'summary', 'text'] as const;\ntype Field = (typeof FIELDS)[number];\n// Mirrors sqlite's bm25(content, 10.0, 5.0, 1.0, ...) column-weight intent; DuckDB's\n// match_bm25 has no field_weights argument (verified 1.5.5), so each field is scored\n// separately and combined here.\nconst FIELD_WEIGHT: Record<Field, number> = { title: 10, summary: 5, text: 1 };\n\nconst UNSPACED_RUN = new RegExp(`[${UNSPACED_SCRIPTS}]`, 'u');\n\n// FTS5 operator syntax (sqlite.org/fts5.html sec. 3) that words/substrings below would\n// otherwise silently treat as literal terms instead of honoring (PRINCIPLES: no-silent-modes).\n// Checked against terms with quoted spans blanked -- those go through contains() and are\n// supported.\nconst FTS5_OPERATORS: Array<{ label: string; re: RegExp }> = [\n { label: 'prefix query', re: /[\\p{L}\\p{N}_]+\\*(?=\\s|$)/u },\n { label: 'boolean operator', re: /(?:^|\\s)(?:AND|OR|NOT)(?=\\s|$)/ },\n { label: 'NEAR operator', re: /(?:^|\\s)NEAR\\b/ },\n { label: 'initial-token operator', re: /(?:^|\\s)\\^\\S+/ },\n { label: 'column filter', re: /(?:^|\\s)[\\p{L}_]\\w*\\s*:/u },\n];\n\nfunction unsupportedOperator(terms: string): { label: string; token: string } | null {\n const withoutPhrases = terms.replace(/\"[^\"]*\"/g, ' ');\n for (const { label, re } of FTS5_OPERATORS) {\n const m = withoutPhrases.match(re);\n if (m) return { label, token: m[0].trim() };\n }\n return null;\n}\n\ninterface FtsIndexState {\n stale: boolean;\n}\n\n// A run whose script marks no word boundaries makes match_bm25's whitespace tokenizer index\n// the whole run as one token (same gap FTS5 has without the `_seg` sidecar), so such runs --\n// and any author-quoted phrase, any script -- go through contains() instead of the fts index.\nfunction splitTerms(terms: string): { words: string[]; substrings: string[] } {\n const words: string[] = [];\n const substrings: string[] = [];\n const withoutPhrases = terms.replace(/\"([^\"]*)\"/g, (_m, inner: string) => {\n const phrase = inner.trim();\n if (phrase.length === 0) return ' ';\n substrings.push(phrase);\n if (!UNSPACED_RUN.test(phrase)) for (const w of phrase.split(/\\s+/)) if (w.length > 0) words.push(w);\n return ' ';\n });\n for (const tok of withoutPhrases.split(/\\s+/)) {\n if (tok.length === 0) continue;\n if (UNSPACED_RUN.test(tok)) substrings.push(tok);\n else words.push(tok);\n }\n return { words, substrings };\n}\n\n// No incremental update (verified 1.5.5: PRAGMA create_fts_index is rebuild-only), so this\n// pays the full rebuild -- but only once per store instance's first lexical query, and only\n// when content actually changed since (see FtsIndexState / duckdb/store.ts's markStale()).\nasync function ensureFtsFresh(conn: Connection, state: FtsIndexState): Promise<void> {\n if (!state.stale) return;\n await conn.exec('INSTALL fts; LOAD fts;');\n await withTransaction(conn, async () => {\n // stopwords='none': sqlite's porter/unicode61 tokenizer never removes stopwords either\n // (verified 1.5.5), and the fts extension's default 571-word English list would otherwise\n // silently drop common query words (e.g. \"and\") from the index but not from match_bm25's\n // conjunctive gate, making a bare multi-word query that contains one match nothing.\n await conn.exec(`PRAGMA create_fts_index('content', 'path', 'title', 'summary', 'text', stopwords='none', overwrite=1)`);\n });\n state.stale = false;\n}\n\n// Per-word-term and per-substring-term SQL fragments, field-weighted the same way for both\n// (title 10 / summary 5 / text 1), plus the params in the exact left-to-right order they are\n// emitted -- positional `?` binding requires that order to match the assembled SQL text. Score\n// and gate params are collected apart and joined at the end because the assembled SQL puts\n// every score part ahead of every gate part: appending to one array term by term matches that\n// text only while the query has words or substrings, never both, and silently shifts each `?`\n// by one as soon as it has both (PRINCIPLES: no-silent-modes -- a shifted bind answers a\n// different question rather than failing).\nfunction buildScoreAndGate(words: string[], substrings: string[]): { scoreSql: string; gateSql: string; params: unknown[] } {\n const scoreParts: string[] = [];\n const gateParts: string[] = [];\n const scoreParams: unknown[] = [];\n const gateParams: unknown[] = [];\n\n if (words.length > 0) {\n const wordQuery = words.join(' ');\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * COALESCE(fts_main_content.match_bm25(content.\"path\", ?, fields := '${field}', conjunctive := false), 0)`);\n scoreParams.push(wordQuery);\n }\n gateParts.push(`fts_main_content.match_bm25(content.\"path\", ?, conjunctive := true) IS NOT NULL`);\n gateParams.push(wordQuery);\n }\n\n for (const raw of substrings) {\n const needle = raw.toLowerCase();\n for (const field of FIELDS) {\n scoreParts.push(`${FIELD_WEIGHT[field]}.0 * CAST(contains(lower(content.${field}), ?) AS INTEGER)`);\n scoreParams.push(needle);\n }\n gateParts.push(`contains(lower(content.title) || ' ' || lower(content.summary) || ' ' || lower(content.text), ?)`);\n gateParams.push(needle);\n }\n\n return { scoreSql: scoreParts.join(' + '), gateSql: gateParts.join(' AND '), params: [...scoreParams, ...gateParams] };\n}\n\n// Ranked word-match query, scoped by the caller-built SQL fragments (same fragments sqlite's\n// queryLexical takes). `hit` is always NULL: no snippet() equivalent exists, so every row goes\n// through the caller's JS excerpt fallback (commands/search.ts) rather than a second excerpt path.\nexport async function queryLexical(conn: Connection, terms: string, opts: LexicalQueryOptions, state: FtsIndexState): Promise<LexicalHit[]> {\n const unsupported = unsupportedOperator(terms);\n if (unsupported !== null) {\n throw new SenseError('STORE_CAPABILITY_MISSING', `store \"duckdb\" does not implement FTS5's ${unsupported.label} (\"${unsupported.token}\") in this build; rephrase \"${terms.trim()}\" without it, or set \"store\" to \"sqlite\" in this tree's config to search it as written`);\n }\n const { whereJoin, whereCond, scopeCond, limit } = opts;\n const { words, substrings } = splitTerms(terms);\n if (words.length === 0 && substrings.length === 0) return [];\n\n if (words.length > 0) await ensureFtsFresh(conn, state);\n\n const { scoreSql, gateSql, params } = buildScoreAndGate(words, substrings);\n const sql = `SELECT path, NULL AS hit FROM (\n SELECT content.\"path\" AS path, (${scoreSql}) AS score\n FROM content\n ${whereJoin}\n WHERE ${gateSql}\n ${whereCond} ${scopeCond}\n ) sq ORDER BY score DESC, path LIMIT ?`;\n const stmt = await conn.prepare(sql);\n return (await stmt.all(...params, limit)) as unknown as LexicalHit[];\n}\n\n// One instance per store: `stale` starts true (a fresh connection cannot know whether an\n// on-disk fts schema still matches `content`), and store.ts's reconcile wrapper flips it back\n// to true whenever content changes, so the next query rebuilds before ranking against it.\nexport function createLexicalIndex(conn: Connection): { query: (terms: string, opts: LexicalQueryOptions) => Promise<LexicalHit[]>; markStale: () => void } {\n const state: FtsIndexState = { stale: true };\n return {\n query: (terms, opts) => queryLexical(conn, terms, opts, state),\n markStale: () => {\n state.stale = true;\n },\n };\n}\n"],"names":["SenseError","UNSPACED_SCRIPTS","withTransaction","FIELDS","FIELD_WEIGHT","title","summary","text","UNSPACED_RUN","RegExp","FTS5_OPERATORS","label","re","unsupportedOperator","terms","withoutPhrases","replace","m","match","token","trim","splitTerms","words","substrings","_m","inner","phrase","length","push","test","w","split","tok","ensureFtsFresh","conn","state","stale","exec","buildScoreAndGate","scoreParts","gateParts","scoreParams","gateParams","wordQuery","join","field","raw","needle","toLowerCase","scoreSql","gateSql","params","queryLexical","opts","unsupported","whereJoin","whereCond","scopeCond","limit","sql","stmt","prepare","all","createLexicalIndex","query","markStale"],"mappings":"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"}
|
|
@@ -1,87 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import Module from 'node:module';
|
|
3
|
-
import { dirname, join } from 'node:path';
|
|
4
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
|
-
import { SenseError } from '../../errors.js';
|
|
1
|
+
import { loadOrInstall, packageNodeModules } from '../native.js';
|
|
6
2
|
export const DUCKDB_PACKAGE = '@duckdb/node-api';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
// Walks up from this file's own directory to find the real package.json, rather than a fixed
|
|
13
|
-
// dot-count from __dirname (xz-compat's `../../node_modules`): `tsds test:node` runs straight
|
|
14
|
-
// off src/, one directory shallower than dist/{cjs,esm}/store/duckdb sits under the built
|
|
15
|
-
// package, so a hardcoded depth is right for one and wrong for the other. Works the same way
|
|
16
|
-
// for a globally-installed `sense`, whose node_modules this resolves to -- never the caller's
|
|
17
|
-
// cwd.
|
|
18
|
-
function packageNodeModules() {
|
|
19
|
-
let dir = _dirname;
|
|
20
|
-
for(;;){
|
|
21
|
-
const candidate = join(dir, 'package.json');
|
|
22
|
-
if (existsSync(candidate)) {
|
|
23
|
-
const pkg = JSON.parse(readFileSync(candidate, 'utf8'));
|
|
24
|
-
if (pkg.name === 'sensemaking') return join(dir, 'node_modules');
|
|
25
|
-
}
|
|
26
|
-
const parent = dirname(dir);
|
|
27
|
-
if (parent === dir) throw new Error('could not locate the sensemaking package root to install @duckdb/node-api into');
|
|
28
|
-
dir = parent;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
const installing = new Map();
|
|
32
|
-
// De-dupes concurrent installs within this process (see spawn-term's loadInk for the shape).
|
|
33
|
-
// The install here is async -- the load site already awaits a dynamic import -- so a bare
|
|
34
|
-
// attempted flag (xz-compat's `installationAttempted`, enough for its synchronous single-target
|
|
35
|
-
// install) isn't enough on its own; concurrent callers instead await the same in-flight promise,
|
|
36
|
-
// keyed by specifier and target so tests exercising several installs in one process don't share
|
|
37
|
-
// one slot. A failed install is not cached, so a later call retries.
|
|
38
|
-
function installOnce(specifier, nodeModulesPath) {
|
|
39
|
-
const key = `${specifier}\n${nodeModulesPath}`;
|
|
40
|
-
let promise = installing.get(key);
|
|
41
|
-
if (!promise) {
|
|
42
|
-
const installModuleLinked = _require('install-module-linked').default;
|
|
43
|
-
promise = installModuleLinked(specifier, nodeModulesPath).then(()=>{}, (err)=>{
|
|
44
|
-
installing.delete(key);
|
|
45
|
-
throw err;
|
|
46
|
-
});
|
|
47
|
-
installing.set(key, promise);
|
|
48
|
-
}
|
|
49
|
-
return promise;
|
|
50
|
-
}
|
|
51
|
-
// Node caches a bare specifier's package resolution -- including a "not found" miss -- for the
|
|
52
|
-
// life of the process, so retrying `import(importName)` right after installing sees the same
|
|
53
|
-
// stale miss and falls through to a bogus index.js guess instead of the package's real entry
|
|
54
|
-
// (verified against a real global install: the plain-specifier retry failed there, this did
|
|
55
|
-
// not). Reading the freshly-installed package's own package.json and importing its entry file
|
|
56
|
-
// by an absolute file: URL sidesteps that stale cache instead of re-resolving the bare name.
|
|
57
|
-
function resolveEntryUrl(importName, nodeModulesPath) {
|
|
58
|
-
var _pkg_main;
|
|
59
|
-
const pkgDir = join(nodeModulesPath, ...importName.split('/'));
|
|
60
|
-
const pkg = JSON.parse(readFileSync(join(pkgDir, 'package.json'), 'utf8'));
|
|
61
|
-
const entry = typeof pkg.exports === 'string' ? pkg.exports : (_pkg_main = pkg.main) !== null && _pkg_main !== void 0 ? _pkg_main : 'index.js';
|
|
62
|
-
return pathToFileURL(join(pkgDir, entry)).href;
|
|
63
|
-
}
|
|
64
|
-
// Import-then-install-then-retry, over an injectable target so the failure path (import AND
|
|
65
|
-
// install both fail) can be exercised for real in tests without downloading @duckdb/node-api's
|
|
66
|
-
// real ~110MB native binding. Production always passes @duckdb/node-api's own name; the thrown
|
|
67
|
-
// error names @duckdb/node-api regardless, since that is what a real user needs to install.
|
|
68
|
-
export async function loadOrInstall(importName, nodeModulesPath) {
|
|
69
|
-
try {
|
|
70
|
-
return await import(importName);
|
|
71
|
-
} catch {
|
|
72
|
-
console.error(`sense: store "duckdb" needs ${DUCKDB_PACKAGE}; installing it now (one-time download, ~110MB)...`);
|
|
73
|
-
try {
|
|
74
|
-
await installOnce(importName, nodeModulesPath);
|
|
75
|
-
} catch (installErr) {
|
|
76
|
-
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" needs ${DUCKDB_PACKAGE}, and installing it automatically failed (${installErr.message}); this can happen offline, in a sandboxed environment, or when node_modules is read-only or owned by another user (e.g. under a global install) -- run \`npm install ${DUCKDB_PACKAGE}\` yourself and try again`);
|
|
77
|
-
}
|
|
78
|
-
try {
|
|
79
|
-
return await import(resolveEntryUrl(importName, nodeModulesPath));
|
|
80
|
-
} catch (loadErr) {
|
|
81
|
-
throw new SenseError('STORE_DEPENDENCY_MISSING', `store "duckdb" installed ${DUCKDB_PACKAGE} but it could not be loaded (${loadErr.message}); it may not be available for this platform (${process.platform}-${process.arch}) -- run \`npm install ${DUCKDB_PACKAGE}\` to see the underlying error`);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
3
|
+
const DUCKDB = {
|
|
4
|
+
store: 'duckdb',
|
|
5
|
+
pkg: DUCKDB_PACKAGE,
|
|
6
|
+
sizeHint: '~110MB'
|
|
7
|
+
};
|
|
85
8
|
let duckdbApiPromise;
|
|
86
9
|
// The one accessor every consumer of @duckdb/node-api (open.ts, sql-functions.ts, vectors.ts)
|
|
87
10
|
// must go through, rather than each doing its own `import('@duckdb/node-api')`: once a fresh
|
|
@@ -91,6 +14,6 @@ let duckdbApiPromise;
|
|
|
91
14
|
// problem loadOrInstall works around for its own retry). Caching the resolved module here means
|
|
92
15
|
// only the first caller ever resolves the bare specifier at all.
|
|
93
16
|
export function duckdbApi() {
|
|
94
|
-
if (!duckdbApiPromise) duckdbApiPromise = loadOrInstall(
|
|
17
|
+
if (!duckdbApiPromise) duckdbApiPromise = loadOrInstall(DUCKDB, packageNodeModules());
|
|
95
18
|
return duckdbApiPromise;
|
|
96
19
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/native.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/native.ts"],"sourcesContent":["import { loadOrInstall, type NativeDescriptor, packageNodeModules } from '../native.ts';\n\nexport const DUCKDB_PACKAGE = '@duckdb/node-api';\n\nconst DUCKDB: NativeDescriptor = { store: 'duckdb', pkg: DUCKDB_PACKAGE, sizeHint: '~110MB' };\n\nlet duckdbApiPromise: Promise<typeof import('@duckdb/node-api')> | undefined;\n\n// The one accessor every consumer of @duckdb/node-api (open.ts, sql-functions.ts, vectors.ts)\n// must go through, rather than each doing its own `import('@duckdb/node-api')`: once a fresh\n// install has happened in this process, Node keeps returning that pre-install \"not found\" miss\n// to any NEW bare-specifier lookup of the same path, so a second independent import elsewhere\n// in the process fails even though the package is now genuinely on disk (the same stale-cache\n// problem loadOrInstall works around for its own retry). Caching the resolved module here means\n// only the first caller ever resolves the bare specifier at all.\nexport function duckdbApi(): Promise<typeof import('@duckdb/node-api')> {\n if (!duckdbApiPromise) duckdbApiPromise = loadOrInstall<typeof import('@duckdb/node-api')>(DUCKDB, packageNodeModules());\n return duckdbApiPromise;\n}\n"],"names":["loadOrInstall","packageNodeModules","DUCKDB_PACKAGE","DUCKDB","store","pkg","sizeHint","duckdbApiPromise","duckdbApi"],"mappings":"AAAA,SAASA,aAAa,EAAyBC,kBAAkB,QAAQ,eAAe;AAExF,OAAO,MAAMC,iBAAiB,mBAAmB;AAEjD,MAAMC,SAA2B;IAAEC,OAAO;IAAUC,KAAKH;IAAgBI,UAAU;AAAS;AAE5F,IAAIC;AAEJ,8FAA8F;AAC9F,6FAA6F;AAC7F,+FAA+F;AAC/F,8FAA8F;AAC9F,8FAA8F;AAC9F,gGAAgG;AAChG,iEAAiE;AACjE,OAAO,SAASC;IACd,IAAI,CAACD,kBAAkBA,mBAAmBP,cAAiDG,QAAQF;IACnG,OAAOM;AACT"}
|
|
@@ -39,7 +39,7 @@ export async function registerFunctions(conn) {
|
|
|
39
39
|
// segment() rewrites unspaced-script runs into the grapheme phrase FTS5's `_seg` sidecars
|
|
40
40
|
// need (see src/text/segment.ts); this store's own lexical index uses contains() instead (D1)
|
|
41
41
|
// and never calls this, but hand-written raw SQL naming it still gets the real implementation
|
|
42
|
-
// rather than a silent passthrough (
|
|
42
|
+
// rather than a silent passthrough (PRINCIPLES: no-silent-modes).
|
|
43
43
|
conn.registerScalarFunction(DuckDBScalarFunction.create({
|
|
44
44
|
name: 'segment',
|
|
45
45
|
parameterTypes: [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/sql-functions.ts"],"sourcesContent":["import type { DuckDBConnection } from '@duckdb/node-api';\nimport { segmentMatch } from '../../text/segment.ts';\nimport { basenameImpl, hasImpl } from '../sql-functions.ts';\nimport { duckdbApi } from './native.ts';\n\n// Dynamic, not a top-level import: sqlite trees must never even attempt to resolve this optional\n// peer dependency (see open.ts's connect()). Goes through native.ts's shared duckdbApi(), not a\n// bare `import('@duckdb/node-api')` of its own -- by the time this runs, open.ts has already\n// resolved the package, possibly via an on-demand install, and a second independent bare-specifier\n// lookup in the same process would hit Node's stale pre-install resolution miss (see native.ts).\nexport async function registerFunctions(conn: DuckDBConnection): Promise<void> {\n const { DuckDBIntegerType, DuckDBScalarFunction, DuckDBVarCharType } = await duckdbApi();\n conn.registerScalarFunction(\n DuckDBScalarFunction.create({\n name: 'has',\n parameterTypes: [DuckDBVarCharType.instance, DuckDBVarCharType.instance],\n returnType: DuckDBIntegerType.instance,\n mainFunction(_info, inputChunk, outputVector) {\n const fieldVec = inputChunk.getColumnVector(0);\n const valueVec = inputChunk.getColumnVector(1);\n for (let i = 0; i < inputChunk.rowCount; i++) outputVector.setItem(i, hasImpl(fieldVec.getItem(i), valueVec.getItem(i)));\n outputVector.flush();\n },\n })\n );\n conn.registerScalarFunction(\n DuckDBScalarFunction.create({\n name: 'basename',\n parameterTypes: [DuckDBVarCharType.instance],\n varArgsType: DuckDBVarCharType.instance,\n returnType: DuckDBVarCharType.instance,\n mainFunction(_info, inputChunk, outputVector) {\n const pathVec = inputChunk.getColumnVector(0);\n const suffixVec = inputChunk.columnCount > 1 ? inputChunk.getColumnVector(1) : null;\n for (let i = 0; i < inputChunk.rowCount; i++) outputVector.setItem(i, basenameImpl(pathVec.getItem(i), suffixVec ? suffixVec.getItem(i) : undefined));\n outputVector.flush();\n },\n })\n );\n // segment() rewrites unspaced-script runs into the grapheme phrase FTS5's `_seg` sidecars\n // need (see src/text/segment.ts); this store's own lexical index uses contains() instead (D1)\n // and never calls this, but hand-written raw SQL naming it still gets the real implementation\n // rather than a silent passthrough (
|
|
1
|
+
{"version":3,"sources":["/Users/kevin/Dev/OpenSource/ai/sensemaking/src/store/duckdb/sql-functions.ts"],"sourcesContent":["import type { DuckDBConnection } from '@duckdb/node-api';\nimport { segmentMatch } from '../../text/segment.ts';\nimport { basenameImpl, hasImpl } from '../sql-functions.ts';\nimport { duckdbApi } from './native.ts';\n\n// Dynamic, not a top-level import: sqlite trees must never even attempt to resolve this optional\n// peer dependency (see open.ts's connect()). Goes through native.ts's shared duckdbApi(), not a\n// bare `import('@duckdb/node-api')` of its own -- by the time this runs, open.ts has already\n// resolved the package, possibly via an on-demand install, and a second independent bare-specifier\n// lookup in the same process would hit Node's stale pre-install resolution miss (see native.ts).\nexport async function registerFunctions(conn: DuckDBConnection): Promise<void> {\n const { DuckDBIntegerType, DuckDBScalarFunction, DuckDBVarCharType } = await duckdbApi();\n conn.registerScalarFunction(\n DuckDBScalarFunction.create({\n name: 'has',\n parameterTypes: [DuckDBVarCharType.instance, DuckDBVarCharType.instance],\n returnType: DuckDBIntegerType.instance,\n mainFunction(_info, inputChunk, outputVector) {\n const fieldVec = inputChunk.getColumnVector(0);\n const valueVec = inputChunk.getColumnVector(1);\n for (let i = 0; i < inputChunk.rowCount; i++) outputVector.setItem(i, hasImpl(fieldVec.getItem(i), valueVec.getItem(i)));\n outputVector.flush();\n },\n })\n );\n conn.registerScalarFunction(\n DuckDBScalarFunction.create({\n name: 'basename',\n parameterTypes: [DuckDBVarCharType.instance],\n varArgsType: DuckDBVarCharType.instance,\n returnType: DuckDBVarCharType.instance,\n mainFunction(_info, inputChunk, outputVector) {\n const pathVec = inputChunk.getColumnVector(0);\n const suffixVec = inputChunk.columnCount > 1 ? inputChunk.getColumnVector(1) : null;\n for (let i = 0; i < inputChunk.rowCount; i++) outputVector.setItem(i, basenameImpl(pathVec.getItem(i), suffixVec ? suffixVec.getItem(i) : undefined));\n outputVector.flush();\n },\n })\n );\n // segment() rewrites unspaced-script runs into the grapheme phrase FTS5's `_seg` sidecars\n // need (see src/text/segment.ts); this store's own lexical index uses contains() instead (D1)\n // and never calls this, but hand-written raw SQL naming it still gets the real implementation\n // rather than a silent passthrough (PRINCIPLES: no-silent-modes).\n conn.registerScalarFunction(\n DuckDBScalarFunction.create({\n name: 'segment',\n parameterTypes: [DuckDBVarCharType.instance],\n returnType: DuckDBVarCharType.instance,\n mainFunction(_info, inputChunk, outputVector) {\n const textVec = inputChunk.getColumnVector(0);\n for (let i = 0; i < inputChunk.rowCount; i++) outputVector.setItem(i, segmentMatch(String(textVec.getItem(i) ?? '')));\n outputVector.flush();\n },\n })\n );\n}\n"],"names":["segmentMatch","basenameImpl","hasImpl","duckdbApi","registerFunctions","conn","DuckDBIntegerType","DuckDBScalarFunction","DuckDBVarCharType","registerScalarFunction","create","name","parameterTypes","instance","returnType","mainFunction","_info","inputChunk","outputVector","fieldVec","getColumnVector","valueVec","i","rowCount","setItem","getItem","flush","varArgsType","pathVec","suffixVec","columnCount","undefined","textVec","String"],"mappings":"AACA,SAASA,YAAY,QAAQ,wBAAwB;AACrD,SAASC,YAAY,EAAEC,OAAO,QAAQ,sBAAsB;AAC5D,SAASC,SAAS,QAAQ,cAAc;AAExC,iGAAiG;AACjG,gGAAgG;AAChG,6FAA6F;AAC7F,mGAAmG;AACnG,iGAAiG;AACjG,OAAO,eAAeC,kBAAkBC,IAAsB;IAC5D,MAAM,EAAEC,iBAAiB,EAAEC,oBAAoB,EAAEC,iBAAiB,EAAE,GAAG,MAAML;IAC7EE,KAAKI,sBAAsB,CACzBF,qBAAqBG,MAAM,CAAC;QAC1BC,MAAM;QACNC,gBAAgB;YAACJ,kBAAkBK,QAAQ;YAAEL,kBAAkBK,QAAQ;SAAC;QACxEC,YAAYR,kBAAkBO,QAAQ;QACtCE,cAAaC,KAAK,EAAEC,UAAU,EAAEC,YAAY;YAC1C,MAAMC,WAAWF,WAAWG,eAAe,CAAC;YAC5C,MAAMC,WAAWJ,WAAWG,eAAe,CAAC;YAC5C,IAAK,IAAIE,IAAI,GAAGA,IAAIL,WAAWM,QAAQ,EAAED,IAAKJ,aAAaM,OAAO,CAACF,GAAGpB,QAAQiB,SAASM,OAAO,CAACH,IAAID,SAASI,OAAO,CAACH;YACpHJ,aAAaQ,KAAK;QACpB;IACF;IAEFrB,KAAKI,sBAAsB,CACzBF,qBAAqBG,MAAM,CAAC;QAC1BC,MAAM;QACNC,gBAAgB;YAACJ,kBAAkBK,QAAQ;SAAC;QAC5Cc,aAAanB,kBAAkBK,QAAQ;QACvCC,YAAYN,kBAAkBK,QAAQ;QACtCE,cAAaC,KAAK,EAAEC,UAAU,EAAEC,YAAY;YAC1C,MAAMU,UAAUX,WAAWG,eAAe,CAAC;YAC3C,MAAMS,YAAYZ,WAAWa,WAAW,GAAG,IAAIb,WAAWG,eAAe,CAAC,KAAK;YAC/E,IAAK,IAAIE,IAAI,GAAGA,IAAIL,WAAWM,QAAQ,EAAED,IAAKJ,aAAaM,OAAO,CAACF,GAAGrB,aAAa2B,QAAQH,OAAO,CAACH,IAAIO,YAAYA,UAAUJ,OAAO,CAACH,KAAKS;YAC1Ib,aAAaQ,KAAK;QACpB;IACF;IAEF,0FAA0F;IAC1F,8FAA8F;IAC9F,8FAA8F;IAC9F,kEAAkE;IAClErB,KAAKI,sBAAsB,CACzBF,qBAAqBG,MAAM,CAAC;QAC1BC,MAAM;QACNC,gBAAgB;YAACJ,kBAAkBK,QAAQ;SAAC;QAC5CC,YAAYN,kBAAkBK,QAAQ;QACtCE,cAAaC,KAAK,EAAEC,UAAU,EAAEC,YAAY;gBAEgDc;YAD1F,MAAMA,UAAUf,WAAWG,eAAe,CAAC;YAC3C,IAAK,IAAIE,IAAI,GAAGA,IAAIL,WAAWM,QAAQ,EAAED,IAAKJ,aAAaM,OAAO,CAACF,GAAGtB,aAAaiC,QAAOD,mBAAAA,QAAQP,OAAO,CAACH,gBAAhBU,8BAAAA,mBAAsB;YAChHd,aAAaQ,KAAK;QACpB;IACF;AAEJ"}
|
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { STORE_DIMS } from '../../embed/types.js';
|
|
2
2
|
import { getColumns } from '../shared.js';
|
|
3
|
-
// pending()/hasVector() are IS NULL/IS NOT NULL checks the portable Connection already
|
|
4
|
-
// answers identically regardless of the vector column's type, so this store reuses sqlite's
|
|
5
|
-
// (engine-neutral despite the module name) instead of duplicating them.
|
|
6
|
-
import { hasVectorRow, pendingRows } from '../sqlite/vectors.js';
|
|
7
3
|
import { withTransaction } from '../transaction.js';
|
|
4
|
+
import { hasVectorRow, pendingRows } from '../vectors.js';
|
|
8
5
|
import { storeRowToJs } from './connection.js';
|
|
6
|
+
import { fieldStats } from './fieldStats.js';
|
|
9
7
|
import { createLexicalIndex } from './lexical.js';
|
|
10
8
|
import { reconcile } from './reconcile.js';
|
|
11
9
|
import { scanCandidates, scanSimilar, writeVectorBatch } from './vectors.js';
|
|
@@ -54,7 +52,8 @@ export function createStore(instance, duckdb, conn, cfg, baseDir) {
|
|
|
54
52
|
return [
|
|
55
53
|
...await getColumns(conn)
|
|
56
54
|
];
|
|
57
|
-
}
|
|
55
|
+
},
|
|
56
|
+
fieldStats: (columns, scopeWhere)=>fieldStats(conn, columns, scopeWhere)
|
|
58
57
|
},
|
|
59
58
|
lexical: {
|
|
60
59
|
query: lex.query
|
|
@@ -69,6 +68,10 @@ export function createStore(instance, duckdb, conn, cfg, baseDir) {
|
|
|
69
68
|
similar: (path, opts)=>scanSimilar(duckdb, conn, STORE_DIMS, path, opts),
|
|
70
69
|
hasVector: (path)=>hasVectorRow(conn, path)
|
|
71
70
|
},
|
|
71
|
+
// No comparable setting to report yet.
|
|
72
|
+
async engineStatus () {
|
|
73
|
+
return {};
|
|
74
|
+
},
|
|
72
75
|
raw: {
|
|
73
76
|
async prepare (sql) {
|
|
74
77
|
const prepared = await duckdb.prepare(sql);
|