@an5/adapters 0.2.6 → 0.2.10

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.
Files changed (43) hide show
  1. package/README.md +96 -2
  2. package/dist/an5Adapter.d.ts +57 -0
  3. package/dist/an5Adapter.js +263 -4
  4. package/dist/base/sql.js +16 -15
  5. package/dist/base/types.d.ts +6 -0
  6. package/dist/googlesheets/helpers.js +13 -8
  7. package/dist/index.d.ts +2 -0
  8. package/dist/index.js +9 -1
  9. package/dist/nbase/client.d.ts +92 -0
  10. package/dist/nbase/client.js +147 -0
  11. package/dist/nbase/index.d.ts +47 -0
  12. package/dist/nbase/index.js +80 -0
  13. package/dist/sqlite/engine.js +2 -1
  14. package/dotnet/An5Adapter.cs +41 -18
  15. package/dotnet/Base/Core.cs +56 -26
  16. package/dotnet/Postgres/PostgresEngine.cs +58 -30
  17. package/dotnet/Sqlite/SqliteEngine.cs +214 -0
  18. package/golang/base/where.go +32 -49
  19. package/golang/base/where_test.go +57 -0
  20. package/package.json +26 -11
  21. package/python/an5_adapter.py +74 -9
  22. package/python/base/__init__.py +1 -1
  23. package/python/base/metadata.py +25 -1
  24. package/python/base/sql.py +50 -21
  25. package/python/sqlite/__init__.py +3 -0
  26. package/python/sqlite/provider.py +83 -0
  27. package/python/table_client.py +53 -36
  28. package/rust/Cargo.toml +31 -0
  29. package/rust/README.md +71 -0
  30. package/rust/src/adapter.rs +1314 -0
  31. package/rust/src/base/dialect.rs +201 -0
  32. package/rust/src/base/metadata.rs +138 -0
  33. package/rust/src/base/mod.rs +17 -0
  34. package/rust/src/base/where_builder.rs +386 -0
  35. package/rust/src/lib.rs +75 -0
  36. package/typescript/src/an5Adapter.ts +300 -4
  37. package/typescript/src/base/sql.ts +15 -14
  38. package/typescript/src/base/types.ts +6 -0
  39. package/typescript/src/googlesheets/helpers.ts +11 -8
  40. package/typescript/src/index.ts +17 -0
  41. package/typescript/src/nbase/client.ts +214 -0
  42. package/typescript/src/nbase/index.ts +104 -0
  43. package/typescript/src/sqlite/engine.ts +2 -1
@@ -25,9 +25,7 @@ function matchWhere(row, where) {
25
25
  if (key.includes('_') &&
26
26
  value && typeof value === 'object' &&
27
27
  !(value instanceof Date) &&
28
- !value.in && !value.contains &&
29
- !value.not && !value.gte &&
30
- !value.lte && !value.gt && !value.lt) {
28
+ !['equals', 'in', 'notIn', 'contains', 'startsWith', 'endsWith', 'not', 'gte', 'lte', 'gt', 'lt'].some(op => op in value)) {
31
29
  Object.assign(cleanWhere, value);
32
30
  }
33
31
  else {
@@ -35,13 +33,16 @@ function matchWhere(row, where) {
35
33
  }
36
34
  }
37
35
  for (const [key, value] of Object.entries(cleanWhere)) {
36
+ if (value === undefined)
37
+ continue;
38
38
  if (key === 'OR' && Array.isArray(value)) {
39
39
  if (!value.some((v) => matchWhere(row, v)))
40
40
  return false;
41
41
  continue;
42
42
  }
43
- if (key === 'AND' && Array.isArray(value)) {
44
- if (!value.every((v) => matchWhere(row, v)))
43
+ if (key === 'AND') {
44
+ const clauses = Array.isArray(value) ? value : [value];
45
+ if (!clauses.every((v) => matchWhere(row, v)))
45
46
  return false;
46
47
  continue;
47
48
  }
@@ -65,21 +66,25 @@ function matchWhere(row, where) {
65
66
  if (cellVal === null || cellVal === undefined)
66
67
  return false;
67
68
  }
69
+ else if (v.not && typeof v.not === 'object' && !(v.not instanceof Date) && !Array.isArray(v.not)) {
70
+ if (matchWhere(row, { [key]: v.not }))
71
+ return false;
72
+ }
68
73
  else if (normalize(cellVal) == normalize(v.not))
69
74
  return false;
70
75
  }
71
76
  if (v.equals !== undefined && normalize(cellVal) != normalize(v.equals))
72
77
  return false;
73
78
  if (v.contains !== undefined) {
74
- if (!cellVal || !normalize(String(cellVal)).includes(normalize(v.contains)))
79
+ if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).includes(normalize(v.contains)))
75
80
  return false;
76
81
  }
77
82
  if (v.startsWith !== undefined) {
78
- if (!cellVal || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
83
+ if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).startsWith(normalize(v.startsWith)))
79
84
  return false;
80
85
  }
81
86
  if (v.endsWith !== undefined) {
82
- if (!cellVal || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
87
+ if (cellVal === null || cellVal === undefined || !normalize(String(cellVal)).endsWith(normalize(v.endsWith)))
83
88
  return false;
84
89
  }
85
90
  if (v.gte !== undefined && !(Number(cellVal) >= Number(v.gte)))
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { An5Adapter, AdapterTableClient, createAn5Adapter, createAdapterProxy, executorFromAdapter, setAdapterMetadata, } from './an5Adapter';
2
2
  export { parseWhere, buildOrderBy, quote } from './base/sql';
3
+ export { NBaseVectorClient, NBaseError, createNBaseVectorClient, createNBaseClient, parseNBaseConnectionString, buildVectorId, parseVectorId, } from './nbase';
4
+ export type { NBaseAdapterConfig, NBaseClientConfig, NBaseVector, NBaseSearchOptions, NBaseSearchResult, NBaseStats, } from './nbase';
3
5
  export type { An5AdapterConfig, AdapterMetadata, Dialect, } from './an5Adapter';
4
6
  export type { TypedAn5Adapter, AdapterAPI } from './typed';
5
7
  export { getLlmConfig, setLlmConfig, getEmbeddingConfig, setEmbeddingConfig, resetAdapter, } from './config';
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parseSheetsConnectionString = exports.createAn5SheetsAdapter = exports.SheetsTableClient = exports.An5SheetsAdapter = exports.resetAdapter = exports.setEmbeddingConfig = exports.getEmbeddingConfig = exports.setLlmConfig = exports.getLlmConfig = exports.quote = exports.buildOrderBy = exports.parseWhere = exports.setAdapterMetadata = exports.executorFromAdapter = exports.createAdapterProxy = exports.createAn5Adapter = exports.AdapterTableClient = exports.An5Adapter = void 0;
3
+ exports.parseSheetsConnectionString = exports.createAn5SheetsAdapter = exports.SheetsTableClient = exports.An5SheetsAdapter = exports.resetAdapter = exports.setEmbeddingConfig = exports.getEmbeddingConfig = exports.setLlmConfig = exports.getLlmConfig = exports.parseVectorId = exports.buildVectorId = exports.parseNBaseConnectionString = exports.createNBaseClient = exports.createNBaseVectorClient = exports.NBaseError = exports.NBaseVectorClient = exports.quote = exports.buildOrderBy = exports.parseWhere = exports.setAdapterMetadata = exports.executorFromAdapter = exports.createAdapterProxy = exports.createAn5Adapter = exports.AdapterTableClient = exports.An5Adapter = void 0;
4
4
  var an5Adapter_1 = require("./an5Adapter");
5
5
  Object.defineProperty(exports, "An5Adapter", { enumerable: true, get: function () { return an5Adapter_1.An5Adapter; } });
6
6
  Object.defineProperty(exports, "AdapterTableClient", { enumerable: true, get: function () { return an5Adapter_1.AdapterTableClient; } });
@@ -12,6 +12,14 @@ var sql_1 = require("./base/sql");
12
12
  Object.defineProperty(exports, "parseWhere", { enumerable: true, get: function () { return sql_1.parseWhere; } });
13
13
  Object.defineProperty(exports, "buildOrderBy", { enumerable: true, get: function () { return sql_1.buildOrderBy; } });
14
14
  Object.defineProperty(exports, "quote", { enumerable: true, get: function () { return sql_1.quote; } });
15
+ var nbase_1 = require("./nbase");
16
+ Object.defineProperty(exports, "NBaseVectorClient", { enumerable: true, get: function () { return nbase_1.NBaseVectorClient; } });
17
+ Object.defineProperty(exports, "NBaseError", { enumerable: true, get: function () { return nbase_1.NBaseError; } });
18
+ Object.defineProperty(exports, "createNBaseVectorClient", { enumerable: true, get: function () { return nbase_1.createNBaseVectorClient; } });
19
+ Object.defineProperty(exports, "createNBaseClient", { enumerable: true, get: function () { return nbase_1.createNBaseClient; } });
20
+ Object.defineProperty(exports, "parseNBaseConnectionString", { enumerable: true, get: function () { return nbase_1.parseNBaseConnectionString; } });
21
+ Object.defineProperty(exports, "buildVectorId", { enumerable: true, get: function () { return nbase_1.buildVectorId; } });
22
+ Object.defineProperty(exports, "parseVectorId", { enumerable: true, get: function () { return nbase_1.parseVectorId; } });
15
23
  var config_1 = require("./config");
16
24
  Object.defineProperty(exports, "getLlmConfig", { enumerable: true, get: function () { return config_1.getLlmConfig; } });
17
25
  Object.defineProperty(exports, "setLlmConfig", { enumerable: true, get: function () { return config_1.setLlmConfig; } });
@@ -0,0 +1,92 @@
1
+ /**
2
+ * NBase — Neural Vector Database client.
3
+ *
4
+ * NBase is a vector database with HNSW/LSH/KNN indexing exposed over a REST
5
+ * API. an5 uses it to move similarity search out of the database process: the
6
+ * vectors live in NBase, the rows stay in the relational table, and the search
7
+ * results are hydrated from the table by the id kept in vector metadata.
8
+ *
9
+ * The client only uses the global `fetch`, so it works in Node 18+ and in the
10
+ * browser without pulling in a dependency.
11
+ */
12
+ export interface NBaseVector {
13
+ id: string | number;
14
+ vector: number[];
15
+ metadata?: Record<string, unknown>;
16
+ }
17
+ export interface NBaseSearchOptions {
18
+ /** Number of neighbours to return. */
19
+ k?: number;
20
+ /** Distance metric; NBase accepts `cosine` and `euclidean`. */
21
+ distanceMetric?: 'cosine' | 'euclidean';
22
+ /** `hnsw` uses the graph index, anything else uses the clustered path. */
23
+ method?: 'hnsw' | 'clustered';
24
+ efSearch?: number;
25
+ filters?: Record<string, unknown> | Array<Record<string, unknown>>;
26
+ includeMetadata?: boolean;
27
+ includeVectors?: boolean;
28
+ skipCache?: boolean;
29
+ searchTimeoutMs?: number;
30
+ rerank?: boolean;
31
+ rerankingMethod?: string;
32
+ rerankLambda?: number;
33
+ partitionIds?: string[];
34
+ }
35
+ export interface NBaseSearchResult {
36
+ id: string | number;
37
+ /** Distance from the query vector. Lower is more similar. */
38
+ dist: number;
39
+ metadata?: Record<string, unknown>;
40
+ vector?: number[];
41
+ dimension?: number;
42
+ }
43
+ export interface NBaseClientConfig {
44
+ /** Base URL, e.g. `http://localhost:1307`. */
45
+ url: string;
46
+ /** Optional bearer token, for deployments behind an auth proxy. */
47
+ token?: string;
48
+ /** Per-request timeout in milliseconds. */
49
+ timeoutMs?: number;
50
+ /** Injectable for tests and for runtimes without a global fetch. */
51
+ fetchImpl?: typeof fetch;
52
+ }
53
+ export interface NBaseStats {
54
+ vectorCount?: number;
55
+ metadataCount?: number;
56
+ memoryUsage?: number;
57
+ [key: string]: unknown;
58
+ }
59
+ export declare class NBaseError extends Error {
60
+ readonly status?: number | undefined;
61
+ readonly body?: unknown;
62
+ constructor(message: string, status?: number | undefined, body?: unknown);
63
+ }
64
+ export declare class NBaseVectorClient {
65
+ private readonly config;
66
+ private readonly baseUrl;
67
+ private readonly fetchImpl;
68
+ private readonly timeoutMs;
69
+ constructor(config: NBaseClientConfig);
70
+ private request;
71
+ /** Liveness probe. NBase mounts its status routes at the server root. */
72
+ health(): Promise<{
73
+ status: string;
74
+ version?: string;
75
+ }>;
76
+ /** Vector count, memory use and other database statistics. */
77
+ stats(): Promise<NBaseStats>;
78
+ /** Adds or replaces vectors. NBase accepts a single vector or a bulk array. */
79
+ addVectors(vectors: NBaseVector[]): Promise<{
80
+ count: number;
81
+ ids?: Array<string | number>;
82
+ }>;
83
+ /** Nearest-neighbour search. Results are ordered by ascending `dist`. */
84
+ search(vector: number[], options?: NBaseSearchOptions): Promise<NBaseSearchResult[]>;
85
+ /** Reads a single vector. Returns undefined when the id is unknown. */
86
+ get(id: string | number): Promise<NBaseSearchResult | undefined>;
87
+ exists(id: string | number): Promise<boolean>;
88
+ delete(id: string | number): Promise<boolean>;
89
+ updateMetadata(id: string | number, metadata: Record<string, unknown>): Promise<boolean>;
90
+ }
91
+ /** Creates an NBase vector client. */
92
+ export declare function createNBaseVectorClient(config: NBaseClientConfig): NBaseVectorClient;
@@ -0,0 +1,147 @@
1
+ "use strict";
2
+ /**
3
+ * NBase — Neural Vector Database client.
4
+ *
5
+ * NBase is a vector database with HNSW/LSH/KNN indexing exposed over a REST
6
+ * API. an5 uses it to move similarity search out of the database process: the
7
+ * vectors live in NBase, the rows stay in the relational table, and the search
8
+ * results are hydrated from the table by the id kept in vector metadata.
9
+ *
10
+ * The client only uses the global `fetch`, so it works in Node 18+ and in the
11
+ * browser without pulling in a dependency.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.NBaseVectorClient = exports.NBaseError = void 0;
15
+ exports.createNBaseVectorClient = createNBaseVectorClient;
16
+ const DEFAULT_TIMEOUT_MS = 30000;
17
+ class NBaseError extends Error {
18
+ constructor(message, status, body) {
19
+ super(message);
20
+ this.status = status;
21
+ this.body = body;
22
+ this.name = 'NBaseError';
23
+ }
24
+ }
25
+ exports.NBaseError = NBaseError;
26
+ class NBaseVectorClient {
27
+ constructor(config) {
28
+ this.config = config;
29
+ this.baseUrl = String(config.url).replace(/\/+$/, '');
30
+ const impl = config.fetchImpl ?? globalThis.fetch;
31
+ if (!impl) {
32
+ throw new NBaseError('No fetch implementation available. Node 18+ or a fetch polyfill is required.');
33
+ }
34
+ this.fetchImpl = impl;
35
+ this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
36
+ }
37
+ async request(path, init) {
38
+ const controller = new AbortController();
39
+ const timer = setTimeout(() => controller.abort(), this.timeoutMs);
40
+ try {
41
+ const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
42
+ ...init,
43
+ signal: controller.signal,
44
+ headers: {
45
+ 'content-type': 'application/json',
46
+ ...(this.config.token ? { authorization: `Bearer ${this.config.token}` } : {}),
47
+ ...init?.headers,
48
+ },
49
+ });
50
+ const text = await response.text();
51
+ let body;
52
+ try {
53
+ body = text ? JSON.parse(text) : undefined;
54
+ }
55
+ catch {
56
+ body = text;
57
+ }
58
+ if (!response.ok) {
59
+ const message = (body && typeof body === 'object' && 'error' in body
60
+ ? String(body.error)
61
+ : undefined) ?? `HTTP ${response.status}`;
62
+ throw new NBaseError(`NBase request ${path} failed: ${message}`, response.status, body);
63
+ }
64
+ return body;
65
+ }
66
+ catch (err) {
67
+ if (err instanceof NBaseError)
68
+ throw err;
69
+ if (err?.name === 'AbortError') {
70
+ throw new NBaseError(`NBase request ${path} timed out after ${this.timeoutMs}ms`);
71
+ }
72
+ throw new NBaseError(`Could not reach NBase at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`);
73
+ }
74
+ finally {
75
+ clearTimeout(timer);
76
+ }
77
+ }
78
+ /** Liveness probe. NBase mounts its status routes at the server root. */
79
+ async health() {
80
+ return this.request('/health');
81
+ }
82
+ /** Vector count, memory use and other database statistics. */
83
+ async stats() {
84
+ return this.request('/stats');
85
+ }
86
+ /** Adds or replaces vectors. NBase accepts a single vector or a bulk array. */
87
+ async addVectors(vectors) {
88
+ if (vectors.length === 0)
89
+ return { count: 0 };
90
+ const result = await this.request('/api/vectors', { method: 'POST', body: JSON.stringify({ vectors: vectors.length === 1 ? vectors[0] : vectors }) });
91
+ return { count: result.count ?? vectors.length, ...(result.addedIds ? { ids: result.addedIds } : {}) };
92
+ }
93
+ /** Nearest-neighbour search. Results are ordered by ascending `dist`. */
94
+ async search(vector, options = {}) {
95
+ const body = await this.request('/api/search', {
96
+ method: 'POST',
97
+ body: JSON.stringify({
98
+ query: vector,
99
+ k: options.k ?? 10,
100
+ ...(options.distanceMetric ? { distanceMetric: options.distanceMetric } : {}),
101
+ ...(options.method ? { method: options.method } : {}),
102
+ ...(options.efSearch !== undefined ? { efSearch: options.efSearch } : {}),
103
+ ...(options.filters ? { filters: options.filters } : {}),
104
+ ...(options.includeMetadata !== undefined ? { includeMetadata: options.includeMetadata } : {}),
105
+ ...(options.includeVectors !== undefined ? { includeVectors: options.includeVectors } : {}),
106
+ ...(options.skipCache !== undefined ? { skipCache: options.skipCache } : {}),
107
+ ...(options.searchTimeoutMs !== undefined ? { searchTimeoutMs: options.searchTimeoutMs } : {}),
108
+ ...(options.rerank !== undefined ? { rerank: options.rerank } : {}),
109
+ ...(options.rerankingMethod ? { rerankingMethod: options.rerankingMethod } : {}),
110
+ ...(options.rerankLambda !== undefined ? { rerankLambda: options.rerankLambda } : {}),
111
+ ...(options.partitionIds ? { partitionIds: options.partitionIds } : {}),
112
+ }),
113
+ });
114
+ return body.results ?? [];
115
+ }
116
+ /** Reads a single vector. Returns undefined when the id is unknown. */
117
+ async get(id) {
118
+ try {
119
+ return await this.request(`/api/vectors/${encodeURIComponent(String(id))}`);
120
+ }
121
+ catch (err) {
122
+ if (err instanceof NBaseError && err.status === 404)
123
+ return undefined;
124
+ throw err;
125
+ }
126
+ }
127
+ async exists(id) {
128
+ const result = await this.request(`/api/vectors/${encodeURIComponent(String(id))}/exists`);
129
+ return result.exists === true;
130
+ }
131
+ async delete(id) {
132
+ await this.request(`/api/vectors/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
133
+ return true;
134
+ }
135
+ async updateMetadata(id, metadata) {
136
+ await this.request(`/api/vectors/${encodeURIComponent(String(id))}/metadata`, {
137
+ method: 'PATCH',
138
+ body: JSON.stringify(metadata),
139
+ });
140
+ return true;
141
+ }
142
+ }
143
+ exports.NBaseVectorClient = NBaseVectorClient;
144
+ /** Creates an NBase vector client. */
145
+ function createNBaseVectorClient(config) {
146
+ return new NBaseVectorClient(config);
147
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * NBase connection-string and adapter configuration.
3
+ *
4
+ * NBase is configured through the connection string alone:
5
+ *
6
+ * createAn5Adapter({ connectionString: 'nbase://localhost:1307' })
7
+ *
8
+ * Accepted forms are `nbase://host:port`, `nbase:host:port` and an explicit
9
+ * scheme, with optional `?token=…&timeoutMs=…&method=…` options. An adapter
10
+ * created this way is vector-only: it has no relational database, so a search
11
+ * returns the NBase hits instead of table rows.
12
+ */
13
+ import { NBaseVectorClient, NBaseError, createNBaseVectorClient, type NBaseClientConfig, type NBaseVector, type NBaseSearchOptions, type NBaseSearchResult, type NBaseStats } from './client';
14
+ export interface NBaseAdapterConfig extends NBaseClientConfig {
15
+ url: string;
16
+ /**
17
+ * Metadata key that holds the relational row id. `an5Id` by default, which
18
+ * is what `indexTableVectors` writes.
19
+ */
20
+ idField?: string;
21
+ /** Metadata key holding the model name, used to keep collections apart. */
22
+ modelField?: string;
23
+ /** Force the search method instead of letting NBase choose. */
24
+ method?: 'hnsw' | 'clustered';
25
+ }
26
+ /** True when the whole connection string points at NBase. */
27
+ export declare function isNBaseConnectionString(connectionString: string): boolean;
28
+ /**
29
+ * Parses an NBase connection string.
30
+ *
31
+ * Accepts `nbase:http://host:port`, `nbase://host:port` and
32
+ * `nbase:host:port`, with optional `?token=…&timeoutMs=…` options.
33
+ */
34
+ export declare function parseNBaseConnectionString(connectionString: string): NBaseAdapterConfig;
35
+ /** Builds the vector id used to link a NBase vector back to a relational row. */
36
+ export declare function buildVectorId(model: string, id: string | number): string;
37
+ /** Splits a vector id produced by {@link buildVectorId}. */
38
+ export declare function parseVectorId(vectorId: string | number): {
39
+ model: string;
40
+ id: string;
41
+ };
42
+ export declare function createNBaseClient(config: NBaseAdapterConfig | string): {
43
+ client: NBaseVectorClient;
44
+ options: NBaseAdapterConfig;
45
+ };
46
+ export { NBaseVectorClient, NBaseError, createNBaseVectorClient };
47
+ export type { NBaseClientConfig, NBaseVector, NBaseSearchOptions, NBaseSearchResult, NBaseStats };
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createNBaseVectorClient = exports.NBaseError = exports.NBaseVectorClient = void 0;
4
+ exports.isNBaseConnectionString = isNBaseConnectionString;
5
+ exports.parseNBaseConnectionString = parseNBaseConnectionString;
6
+ exports.buildVectorId = buildVectorId;
7
+ exports.parseVectorId = parseVectorId;
8
+ exports.createNBaseClient = createNBaseClient;
9
+ /**
10
+ * NBase connection-string and adapter configuration.
11
+ *
12
+ * NBase is configured through the connection string alone:
13
+ *
14
+ * createAn5Adapter({ connectionString: 'nbase://localhost:1307' })
15
+ *
16
+ * Accepted forms are `nbase://host:port`, `nbase:host:port` and an explicit
17
+ * scheme, with optional `?token=…&timeoutMs=…&method=…` options. An adapter
18
+ * created this way is vector-only: it has no relational database, so a search
19
+ * returns the NBase hits instead of table rows.
20
+ */
21
+ const client_1 = require("./client");
22
+ Object.defineProperty(exports, "NBaseVectorClient", { enumerable: true, get: function () { return client_1.NBaseVectorClient; } });
23
+ Object.defineProperty(exports, "NBaseError", { enumerable: true, get: function () { return client_1.NBaseError; } });
24
+ Object.defineProperty(exports, "createNBaseVectorClient", { enumerable: true, get: function () { return client_1.createNBaseVectorClient; } });
25
+ /** Matches `nbase:`, `nbase://` and `nbase=http(s)://` at the start of a value. */
26
+ const NBASE_PREFIX = /^nbase:(?:\/\/)?/i;
27
+ /** True when the whole connection string points at NBase. */
28
+ function isNBaseConnectionString(connectionString) {
29
+ return NBASE_PREFIX.test(connectionString.trim());
30
+ }
31
+ /** Builds options from a URL and an optional query string. */
32
+ function buildOptions(url, query) {
33
+ const params = new URLSearchParams(query ?? '');
34
+ const trimmed = url.trim().replace(/\/+$/, '');
35
+ // `nbase://host:port` is shorthand for the same host over HTTP; an explicit
36
+ // scheme is used as-is so https deployments work.
37
+ const withScheme = !trimmed
38
+ ? 'http://localhost:1307'
39
+ : /^[a-z]+:\/\//i.test(trimmed)
40
+ ? trimmed
41
+ : `http://${trimmed}`;
42
+ return {
43
+ url: withScheme,
44
+ ...(params.get('token') ? { token: params.get('token') } : {}),
45
+ ...(params.get('timeoutMs') ? { timeoutMs: Number(params.get('timeoutMs')) } : {}),
46
+ ...(params.get('idField') ? { idField: params.get('idField') } : {}),
47
+ ...(params.get('modelField') ? { modelField: params.get('modelField') } : {}),
48
+ ...(params.get('method') ? { method: params.get('method') } : {}),
49
+ };
50
+ }
51
+ /**
52
+ * Parses an NBase connection string.
53
+ *
54
+ * Accepts `nbase:http://host:port`, `nbase://host:port` and
55
+ * `nbase:host:port`, with optional `?token=…&timeoutMs=…` options.
56
+ */
57
+ function parseNBaseConnectionString(connectionString) {
58
+ const raw = connectionString.trim();
59
+ if (!isNBaseConnectionString(raw)) {
60
+ throw new Error(`Not an NBase connection string: ${raw}`);
61
+ }
62
+ const [url, query] = raw.replace(NBASE_PREFIX, '').split('?');
63
+ return buildOptions(url ?? '', query);
64
+ }
65
+ /** Builds the vector id used to link a NBase vector back to a relational row. */
66
+ function buildVectorId(model, id) {
67
+ return `${model}:${id}`;
68
+ }
69
+ /** Splits a vector id produced by {@link buildVectorId}. */
70
+ function parseVectorId(vectorId) {
71
+ const raw = String(vectorId);
72
+ const separator = raw.indexOf(':');
73
+ if (separator === -1)
74
+ return { model: '', id: raw };
75
+ return { model: raw.slice(0, separator), id: raw.slice(separator + 1) };
76
+ }
77
+ function createNBaseClient(config) {
78
+ const options = typeof config === 'string' ? parseNBaseConnectionString(config) : config;
79
+ return { client: (0, client_1.createNBaseVectorClient)(options), options };
80
+ }
@@ -15,7 +15,8 @@ class SqliteEngine {
15
15
  throw new Error('better-sqlite3 package is required for SQLite support. Run: npm install better-sqlite3');
16
16
  this.filePath = (adapterConfig.connectionString ?? '')
17
17
  .replace(/^sqlite:\/\/\//i, '/')
18
- .replace(/^sqlite:\/\//i, '');
18
+ .replace(/^sqlite:\/\//i, '')
19
+ .replace(/^sqlite:/i, '');
19
20
  }
20
21
  getDb() {
21
22
  if (!this.db)
@@ -25,6 +25,7 @@ namespace An5Orm
25
25
  _engine = DialectDetector.Detect(connectionString) switch
26
26
  {
27
27
  Dialect.Postgres => new PostgresEngine(connectionString, commandTimeout),
28
+ Dialect.Sqlite => new SqliteEngine(connectionString, commandTimeout),
28
29
  _ => new MssqlEngine(connectionString, commandTimeout)
29
30
  };
30
31
  }
@@ -44,8 +45,16 @@ namespace An5Orm
44
45
 
45
46
  // ── Stored Procedure Execution ────────────────────────────────────────
46
47
 
48
+ private void ThrowIfNoStoredProcedures()
49
+ {
50
+ if (Dialect == Dialect.Sqlite)
51
+ throw new NotSupportedException(
52
+ "SQLite has no stored procedures. Run the statements directly with QueryRaw or ExecuteRaw.");
53
+ }
54
+
47
55
  public List<T> QueryProc<T>(string procName, Dictionary<string, object> parameters = null) where T : new()
48
56
  {
57
+ ThrowIfNoStoredProcedures();
49
58
  var paramList = new List<string>();
50
59
  if (parameters != null)
51
60
  {
@@ -62,6 +71,7 @@ namespace An5Orm
62
71
 
63
72
  public int ExecuteProc(string procName, Dictionary<string, object> parameters = null)
64
73
  {
74
+ ThrowIfNoStoredProcedures();
65
75
  var paramList = new List<string>();
66
76
  if (parameters != null)
67
77
  {
@@ -418,12 +428,27 @@ namespace An5Orm
418
428
 
419
429
  private string Nolock => _dialect == Dialect.Mssql ? " WITH (NOLOCK)" : "";
420
430
 
431
+ /// <summary>
432
+ /// True for the dialects that paginate with LIMIT/OFFSET rather than
433
+ /// OFFSET ... FETCH NEXT, and have no SELECT TOP.
434
+ /// </summary>
435
+ private bool UsesLimitOffset => _dialect == Dialect.Postgres || _dialect == Dialect.Sqlite;
436
+
421
437
  private string BuildPagination(string orderBy, int? skip, int? take)
422
438
  {
423
- if (take == null) return "";
424
- if (_dialect == Dialect.Postgres)
425
- return $" LIMIT {take.Value} OFFSET {skip ?? 0}";
439
+ // A skip on its own still has to skip. OFFSET without FETCH NEXT is
440
+ // valid on MSSQL, and on the LIMIT/OFFSET dialects an unbounded page
441
+ // is LIMIT -1; without this, `skip` was silently discarded whenever
442
+ // `take` was null.
443
+ if (take == null && skip == null) return "";
444
+ if (UsesLimitOffset)
445
+ {
446
+ if (take == null) return $" LIMIT -1 OFFSET {skip.Value}";
447
+ if (skip == null) return $" LIMIT {take.Value}";
448
+ return $" LIMIT {take.Value} OFFSET {skip.Value}";
449
+ }
426
450
  var o = !string.IsNullOrEmpty(orderBy) ? $" ORDER BY {orderBy}" : " ORDER BY (SELECT NULL)";
451
+ if (take == null) return $"{o} OFFSET {skip.Value} ROWS";
427
452
  return $"{o} OFFSET {skip ?? 0} ROWS FETCH NEXT {take.Value} ROWS ONLY";
428
453
  }
429
454
 
@@ -432,17 +457,11 @@ namespace An5Orm
432
457
  {
433
458
  var sb = new StringBuilder($"SELECT * FROM {_tableName}{Nolock}");
434
459
  if (!string.IsNullOrEmpty(whereClause)) sb.Append($" WHERE {whereClause}");
435
- if (take != null && _dialect == Dialect.Postgres)
436
- {
437
- if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
438
- if (skip != null) sb.Append($" OFFSET {skip.Value} ROWS");
439
- sb.Append($" LIMIT {take.Value}");
440
- }
441
- else
442
- {
443
- if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
444
- sb.Append(BuildPagination(orderBy, skip, take));
445
- }
460
+ if (!string.IsNullOrEmpty(orderBy)) sb.Append($" ORDER BY {orderBy}");
461
+ // LIMIT precedes OFFSET, and neither carries MSSQL's `ROWS` keyword.
462
+ // Postgres tolerates either order, SQLite does not: `OFFSET n LIMIT m`
463
+ // is a syntax error there.
464
+ sb.Append(BuildPagination(orderBy, skip, take));
446
465
  return _adapter.QueryRaw<T>(sb.ToString(), parameters);
447
466
  }
448
467
 
@@ -457,7 +476,7 @@ namespace An5Orm
457
476
 
458
477
  var cols = "*";
459
478
  var keys = ArgsHelper.SelectedFields(args?.Select);
460
- var isPostgres = _dialect == Dialect.Postgres;
479
+ var usesLimitOffset = UsesLimitOffset;
461
480
 
462
481
  if (keys.Count > 0 && !ArgsHelper.HasRelationSelect(args?.Select, An5Metadata.GetRelationsForModel(_modelName)))
463
482
  {
@@ -468,7 +487,7 @@ namespace An5Orm
468
487
  string query;
469
488
  if (take != null && !hasSkip)
470
489
  {
471
- if (!isPostgres)
490
+ if (!usesLimitOffset)
472
491
  {
473
492
  query = $"SELECT TOP ({take.Value}) {cols} FROM {_tableName}{Nolock}";
474
493
  if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
@@ -484,7 +503,7 @@ namespace An5Orm
484
503
  }
485
504
  else if (hasSkip)
486
505
  {
487
- if (!isPostgres)
506
+ if (!usesLimitOffset)
488
507
  {
489
508
  query = $"SELECT {cols} FROM {_tableName}{Nolock}";
490
509
  if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
@@ -498,7 +517,7 @@ namespace An5Orm
498
517
  if (!string.IsNullOrEmpty(whereSql)) query += $" WHERE {whereSql}";
499
518
  if (!string.IsNullOrEmpty(orderSql)) query += $" {orderSql}";
500
519
  if (take != null) query += $" LIMIT {take.Value}";
501
- else query += " LIMIT ALL";
520
+ else query += _dialect == Dialect.Sqlite ? " LIMIT -1" : " LIMIT ALL";
502
521
  query += $" OFFSET {skip.Value}";
503
522
  }
504
523
  }
@@ -988,6 +1007,10 @@ namespace An5Orm
988
1007
  string vectorField = "Embedding", string distanceMetric = "cosine")
989
1008
  {
990
1009
  // 1. Primary path: Native database SQL vector query execution (MSSQL VECTOR_DISTANCE / Postgres pgvector)
1010
+ // SQLite has no vector operator, so it is skipped outright: building the
1011
+ // statement only to have it rejected costs a round trip and lands on the
1012
+ // in-memory path below anyway.
1013
+ if (_dialect != Dialect.Sqlite)
991
1014
  try
992
1015
  {
993
1016
  var dim = vector.Count;