@an5/adapters 0.2.5 → 0.2.9
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/README.md +283 -305
- package/dist/an5Adapter.d.ts +142 -7
- package/dist/an5Adapter.js +998 -68
- package/dist/base/metadata.d.ts +10 -0
- package/dist/base/metadata.js +107 -0
- package/dist/base/sql.d.ts +19 -1
- package/dist/base/sql.js +152 -24
- package/dist/base/types.d.ts +10 -1
- package/dist/browser.d.ts +4 -1
- package/dist/browser.js +9 -2
- package/dist/googlesheets/adapter.d.ts +1 -0
- package/dist/googlesheets/adapter.js +9 -2
- package/dist/googlesheets/config.d.ts +4 -4
- package/dist/googlesheets/helpers.js +2 -0
- package/dist/googlesheets/parseConnectionString.js +5 -2
- package/dist/googlesheets/sqlExecutor.js +29 -24
- package/dist/googlesheets/tableClient.js +14 -5
- package/dist/index.d.ts +5 -1
- package/dist/index.js +15 -1
- package/dist/mssql/engine.js +8 -4
- package/dist/mysql/engine.js +1 -1
- package/dist/nbase/client.d.ts +92 -0
- package/dist/nbase/client.js +147 -0
- package/dist/nbase/index.d.ts +47 -0
- package/dist/nbase/index.js +80 -0
- package/dist/postgres/engine.js +1 -1
- package/dist/sqlite/browserEngine.d.ts +27 -0
- package/dist/sqlite/browserEngine.js +161 -0
- package/dist/sqlite/engine.js +15 -3
- package/dist/sqlite/index.d.ts +1 -0
- package/dist/sqlite/index.js +1 -0
- package/dist/typed.d.ts +5 -0
- package/dist/typed.js +2 -0
- package/dotnet/An5Adapter.cs +1112 -275
- package/dotnet/Base/Core.cs +380 -82
- package/dotnet/Mssql/MssqlEngine.cs +145 -144
- package/dotnet/Postgres/PostgresEngine.cs +149 -121
- package/dotnet/Sqlite/SqliteEngine.cs +214 -0
- package/golang/an5_adapter.go +314 -0
- package/golang/base/base.go +100 -0
- package/golang/base/metadata.go +116 -0
- package/golang/base/where.go +311 -0
- package/golang/go.mod +3 -0
- package/golang/mssql/mssql.go +32 -0
- package/golang/postgres/postgres.go +37 -0
- package/golang/sqlite/sqlite.go +14 -0
- package/golang/sqlite_test.go +42 -0
- package/golang/table_client.go +1329 -0
- package/package.json +26 -9
- package/python/an5_adapter.py +184 -97
- package/python/base/__init__.py +3 -3
- package/python/base/dialects.py +11 -8
- package/python/base/metadata.py +59 -10
- package/python/base/sql.py +202 -121
- package/python/mssql/__init__.py +1 -1
- package/python/mssql/provider.py +62 -62
- package/python/postgres/__init__.py +1 -1
- package/python/postgres/provider.py +15 -15
- package/python/sqlite/__init__.py +3 -0
- package/python/sqlite/provider.py +83 -0
- package/python/table_client.py +636 -216
- package/rust/Cargo.toml +31 -0
- package/rust/README.md +71 -0
- package/rust/src/adapter.rs +1312 -0
- package/rust/src/base/dialect.rs +172 -0
- package/rust/src/base/metadata.rs +134 -0
- package/rust/src/base/mod.rs +17 -0
- package/rust/src/base/where_builder.rs +405 -0
- package/rust/src/lib.rs +75 -0
- package/typescript/src/an5Adapter.ts +1515 -505
- package/typescript/src/base/index.ts +3 -3
- package/typescript/src/base/metadata.ts +148 -28
- package/typescript/src/base/sql.ts +264 -98
- package/typescript/src/base/types.ts +33 -24
- package/typescript/src/browser.ts +10 -6
- package/typescript/src/config.ts +95 -95
- package/typescript/src/googlesheets/adapter.ts +269 -259
- package/typescript/src/googlesheets/an5SheetsAdapter.ts +3 -3
- package/typescript/src/googlesheets/config.ts +47 -47
- package/typescript/src/googlesheets/helpers.ts +144 -143
- package/typescript/src/googlesheets/index.ts +3 -3
- package/typescript/src/googlesheets/parseConnectionString.ts +48 -46
- package/typescript/src/googlesheets/retry.ts +126 -126
- package/typescript/src/googlesheets/sqlExecutor.ts +304 -301
- package/typescript/src/googlesheets/tableClient.ts +461 -455
- package/typescript/src/googlesheets/types.ts +6 -6
- package/typescript/src/index.ts +48 -27
- package/typescript/src/mssql/engine.ts +134 -132
- package/typescript/src/mssql/index.ts +1 -1
- package/typescript/src/mysql/engine.ts +80 -80
- package/typescript/src/mysql/index.ts +1 -1
- package/typescript/src/nbase/client.ts +214 -0
- package/typescript/src/nbase/index.ts +104 -0
- package/typescript/src/postgres/engine.ts +87 -87
- package/typescript/src/postgres/index.ts +1 -1
- package/typescript/src/sqlite/browserEngine.ts +192 -0
- package/typescript/src/sqlite/engine.ts +66 -55
- package/typescript/src/sqlite/index.ts +3 -1
- package/typescript/src/typed.ts +52 -0
|
@@ -0,0 +1,214 @@
|
|
|
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
|
+
|
|
13
|
+
export interface NBaseVector {
|
|
14
|
+
id: string | number;
|
|
15
|
+
vector: number[];
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface NBaseSearchOptions {
|
|
20
|
+
/** Number of neighbours to return. */
|
|
21
|
+
k?: number;
|
|
22
|
+
/** Distance metric; NBase accepts `cosine` and `euclidean`. */
|
|
23
|
+
distanceMetric?: 'cosine' | 'euclidean';
|
|
24
|
+
/** `hnsw` uses the graph index, anything else uses the clustered path. */
|
|
25
|
+
method?: 'hnsw' | 'clustered';
|
|
26
|
+
efSearch?: number;
|
|
27
|
+
filters?: Record<string, unknown> | Array<Record<string, unknown>>;
|
|
28
|
+
includeMetadata?: boolean;
|
|
29
|
+
includeVectors?: boolean;
|
|
30
|
+
skipCache?: boolean;
|
|
31
|
+
searchTimeoutMs?: number;
|
|
32
|
+
rerank?: boolean;
|
|
33
|
+
rerankingMethod?: string;
|
|
34
|
+
rerankLambda?: number;
|
|
35
|
+
partitionIds?: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface NBaseSearchResult {
|
|
39
|
+
id: string | number;
|
|
40
|
+
/** Distance from the query vector. Lower is more similar. */
|
|
41
|
+
dist: number;
|
|
42
|
+
metadata?: Record<string, unknown>;
|
|
43
|
+
vector?: number[];
|
|
44
|
+
dimension?: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NBaseClientConfig {
|
|
48
|
+
/** Base URL, e.g. `http://localhost:1307`. */
|
|
49
|
+
url: string;
|
|
50
|
+
/** Optional bearer token, for deployments behind an auth proxy. */
|
|
51
|
+
token?: string;
|
|
52
|
+
/** Per-request timeout in milliseconds. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
/** Injectable for tests and for runtimes without a global fetch. */
|
|
55
|
+
fetchImpl?: typeof fetch;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface NBaseStats {
|
|
59
|
+
vectorCount?: number;
|
|
60
|
+
metadataCount?: number;
|
|
61
|
+
memoryUsage?: number;
|
|
62
|
+
[key: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
66
|
+
|
|
67
|
+
export class NBaseError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
message: string,
|
|
70
|
+
readonly status?: number,
|
|
71
|
+
readonly body?: unknown,
|
|
72
|
+
) {
|
|
73
|
+
super(message);
|
|
74
|
+
this.name = 'NBaseError';
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export class NBaseVectorClient {
|
|
79
|
+
private readonly baseUrl: string;
|
|
80
|
+
private readonly fetchImpl: typeof fetch;
|
|
81
|
+
private readonly timeoutMs: number;
|
|
82
|
+
|
|
83
|
+
constructor(private readonly config: NBaseClientConfig) {
|
|
84
|
+
this.baseUrl = String(config.url).replace(/\/+$/, '');
|
|
85
|
+
const impl = config.fetchImpl ?? (globalThis as { fetch?: typeof fetch }).fetch;
|
|
86
|
+
if (!impl) {
|
|
87
|
+
throw new NBaseError('No fetch implementation available. Node 18+ or a fetch polyfill is required.');
|
|
88
|
+
}
|
|
89
|
+
this.fetchImpl = impl;
|
|
90
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
private async request<T>(path: string, init?: RequestInit): Promise<T> {
|
|
94
|
+
const controller = new AbortController();
|
|
95
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
96
|
+
try {
|
|
97
|
+
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
|
|
98
|
+
...init,
|
|
99
|
+
signal: controller.signal,
|
|
100
|
+
headers: {
|
|
101
|
+
'content-type': 'application/json',
|
|
102
|
+
...(this.config.token ? { authorization: `Bearer ${this.config.token}` } : {}),
|
|
103
|
+
...(init?.headers as Record<string, string> | undefined),
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
const text = await response.text();
|
|
108
|
+
let body: unknown;
|
|
109
|
+
try {
|
|
110
|
+
body = text ? JSON.parse(text) : undefined;
|
|
111
|
+
} catch {
|
|
112
|
+
body = text;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
const message =
|
|
117
|
+
(body && typeof body === 'object' && 'error' in (body as Record<string, unknown>)
|
|
118
|
+
? String((body as Record<string, unknown>).error)
|
|
119
|
+
: undefined) ?? `HTTP ${response.status}`;
|
|
120
|
+
throw new NBaseError(`NBase request ${path} failed: ${message}`, response.status, body);
|
|
121
|
+
}
|
|
122
|
+
return body as T;
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (err instanceof NBaseError) throw err;
|
|
125
|
+
if ((err as { name?: string })?.name === 'AbortError') {
|
|
126
|
+
throw new NBaseError(`NBase request ${path} timed out after ${this.timeoutMs}ms`);
|
|
127
|
+
}
|
|
128
|
+
throw new NBaseError(
|
|
129
|
+
`Could not reach NBase at ${this.baseUrl}: ${err instanceof Error ? err.message : String(err)}`,
|
|
130
|
+
);
|
|
131
|
+
} finally {
|
|
132
|
+
clearTimeout(timer);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Liveness probe. NBase mounts its status routes at the server root. */
|
|
137
|
+
async health(): Promise<{ status: string; version?: string }> {
|
|
138
|
+
return this.request<{ status: string; version?: string }>('/health');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** Vector count, memory use and other database statistics. */
|
|
142
|
+
async stats(): Promise<NBaseStats> {
|
|
143
|
+
return this.request<NBaseStats>('/stats');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Adds or replaces vectors. NBase accepts a single vector or a bulk array. */
|
|
147
|
+
async addVectors(vectors: NBaseVector[]): Promise<{ count: number; ids?: Array<string | number> }> {
|
|
148
|
+
if (vectors.length === 0) return { count: 0 };
|
|
149
|
+
const result = await this.request<{ count: number; addedIds?: Array<string | number> }>(
|
|
150
|
+
'/api/vectors',
|
|
151
|
+
{ method: 'POST', body: JSON.stringify({ vectors: vectors.length === 1 ? vectors[0] : vectors }) },
|
|
152
|
+
);
|
|
153
|
+
return { count: result.count ?? vectors.length, ...(result.addedIds ? { ids: result.addedIds } : {}) };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Nearest-neighbour search. Results are ordered by ascending `dist`. */
|
|
157
|
+
async search(vector: number[], options: NBaseSearchOptions = {}): Promise<NBaseSearchResult[]> {
|
|
158
|
+
const body = await this.request<{ results?: NBaseSearchResult[] }>('/api/search', {
|
|
159
|
+
method: 'POST',
|
|
160
|
+
body: JSON.stringify({
|
|
161
|
+
query: vector,
|
|
162
|
+
k: options.k ?? 10,
|
|
163
|
+
...(options.distanceMetric ? { distanceMetric: options.distanceMetric } : {}),
|
|
164
|
+
...(options.method ? { method: options.method } : {}),
|
|
165
|
+
...(options.efSearch !== undefined ? { efSearch: options.efSearch } : {}),
|
|
166
|
+
...(options.filters ? { filters: options.filters } : {}),
|
|
167
|
+
...(options.includeMetadata !== undefined ? { includeMetadata: options.includeMetadata } : {}),
|
|
168
|
+
...(options.includeVectors !== undefined ? { includeVectors: options.includeVectors } : {}),
|
|
169
|
+
...(options.skipCache !== undefined ? { skipCache: options.skipCache } : {}),
|
|
170
|
+
...(options.searchTimeoutMs !== undefined ? { searchTimeoutMs: options.searchTimeoutMs } : {}),
|
|
171
|
+
...(options.rerank !== undefined ? { rerank: options.rerank } : {}),
|
|
172
|
+
...(options.rerankingMethod ? { rerankingMethod: options.rerankingMethod } : {}),
|
|
173
|
+
...(options.rerankLambda !== undefined ? { rerankLambda: options.rerankLambda } : {}),
|
|
174
|
+
...(options.partitionIds ? { partitionIds: options.partitionIds } : {}),
|
|
175
|
+
}),
|
|
176
|
+
});
|
|
177
|
+
return body.results ?? [];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Reads a single vector. Returns undefined when the id is unknown. */
|
|
181
|
+
async get(id: string | number): Promise<NBaseSearchResult | undefined> {
|
|
182
|
+
try {
|
|
183
|
+
return await this.request<NBaseSearchResult>(`/api/vectors/${encodeURIComponent(String(id))}`);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err instanceof NBaseError && err.status === 404) return undefined;
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async exists(id: string | number): Promise<boolean> {
|
|
191
|
+
const result = await this.request<{ exists?: boolean }>(
|
|
192
|
+
`/api/vectors/${encodeURIComponent(String(id))}/exists`,
|
|
193
|
+
);
|
|
194
|
+
return result.exists === true;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async delete(id: string | number): Promise<boolean> {
|
|
198
|
+
await this.request(`/api/vectors/${encodeURIComponent(String(id))}`, { method: 'DELETE' });
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async updateMetadata(id: string | number, metadata: Record<string, unknown>): Promise<boolean> {
|
|
203
|
+
await this.request(`/api/vectors/${encodeURIComponent(String(id))}/metadata`, {
|
|
204
|
+
method: 'PATCH',
|
|
205
|
+
body: JSON.stringify(metadata),
|
|
206
|
+
});
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/** Creates an NBase vector client. */
|
|
212
|
+
export function createNBaseVectorClient(config: NBaseClientConfig): NBaseVectorClient {
|
|
213
|
+
return new NBaseVectorClient(config);
|
|
214
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
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 {
|
|
14
|
+
NBaseVectorClient,
|
|
15
|
+
NBaseError,
|
|
16
|
+
createNBaseVectorClient,
|
|
17
|
+
type NBaseClientConfig,
|
|
18
|
+
type NBaseVector,
|
|
19
|
+
type NBaseSearchOptions,
|
|
20
|
+
type NBaseSearchResult,
|
|
21
|
+
type NBaseStats,
|
|
22
|
+
} from './client';
|
|
23
|
+
|
|
24
|
+
export interface NBaseAdapterConfig extends NBaseClientConfig {
|
|
25
|
+
url: string;
|
|
26
|
+
/**
|
|
27
|
+
* Metadata key that holds the relational row id. `an5Id` by default, which
|
|
28
|
+
* is what `indexTableVectors` writes.
|
|
29
|
+
*/
|
|
30
|
+
idField?: string;
|
|
31
|
+
/** Metadata key holding the model name, used to keep collections apart. */
|
|
32
|
+
modelField?: string;
|
|
33
|
+
/** Force the search method instead of letting NBase choose. */
|
|
34
|
+
method?: 'hnsw' | 'clustered';
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Matches `nbase:`, `nbase://` and `nbase=http(s)://` at the start of a value. */
|
|
38
|
+
const NBASE_PREFIX = /^nbase:(?:\/\/)?/i;
|
|
39
|
+
|
|
40
|
+
/** True when the whole connection string points at NBase. */
|
|
41
|
+
export function isNBaseConnectionString(connectionString: string): boolean {
|
|
42
|
+
return NBASE_PREFIX.test(connectionString.trim());
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Builds options from a URL and an optional query string. */
|
|
46
|
+
function buildOptions(url: string, query?: string): NBaseAdapterConfig {
|
|
47
|
+
const params = new URLSearchParams(query ?? '');
|
|
48
|
+
const trimmed = url.trim().replace(/\/+$/, '');
|
|
49
|
+
// `nbase://host:port` is shorthand for the same host over HTTP; an explicit
|
|
50
|
+
// scheme is used as-is so https deployments work.
|
|
51
|
+
const withScheme = !trimmed
|
|
52
|
+
? 'http://localhost:1307'
|
|
53
|
+
: /^[a-z]+:\/\//i.test(trimmed)
|
|
54
|
+
? trimmed
|
|
55
|
+
: `http://${trimmed}`;
|
|
56
|
+
return {
|
|
57
|
+
url: withScheme,
|
|
58
|
+
...(params.get('token') ? { token: params.get('token') as string } : {}),
|
|
59
|
+
...(params.get('timeoutMs') ? { timeoutMs: Number(params.get('timeoutMs')) } : {}),
|
|
60
|
+
...(params.get('idField') ? { idField: params.get('idField') as string } : {}),
|
|
61
|
+
...(params.get('modelField') ? { modelField: params.get('modelField') as string } : {}),
|
|
62
|
+
...(params.get('method') ? { method: params.get('method') as 'hnsw' | 'clustered' } : {}),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Parses an NBase connection string.
|
|
68
|
+
*
|
|
69
|
+
* Accepts `nbase:http://host:port`, `nbase://host:port` and
|
|
70
|
+
* `nbase:host:port`, with optional `?token=…&timeoutMs=…` options.
|
|
71
|
+
*/
|
|
72
|
+
export function parseNBaseConnectionString(connectionString: string): NBaseAdapterConfig {
|
|
73
|
+
const raw = connectionString.trim();
|
|
74
|
+
if (!isNBaseConnectionString(raw)) {
|
|
75
|
+
throw new Error(`Not an NBase connection string: ${raw}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const [url, query] = raw.replace(NBASE_PREFIX, '').split('?');
|
|
79
|
+
return buildOptions(url ?? '', query);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Builds the vector id used to link a NBase vector back to a relational row. */
|
|
83
|
+
export function buildVectorId(model: string, id: string | number): string {
|
|
84
|
+
return `${model}:${id}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Splits a vector id produced by {@link buildVectorId}. */
|
|
88
|
+
export function parseVectorId(vectorId: string | number): { model: string; id: string } {
|
|
89
|
+
const raw = String(vectorId);
|
|
90
|
+
const separator = raw.indexOf(':');
|
|
91
|
+
if (separator === -1) return { model: '', id: raw };
|
|
92
|
+
return { model: raw.slice(0, separator), id: raw.slice(separator + 1) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function createNBaseClient(config: NBaseAdapterConfig | string): {
|
|
96
|
+
client: NBaseVectorClient;
|
|
97
|
+
options: NBaseAdapterConfig;
|
|
98
|
+
} {
|
|
99
|
+
const options: NBaseAdapterConfig = typeof config === 'string' ? parseNBaseConnectionString(config) : config;
|
|
100
|
+
return { client: createNBaseVectorClient(options), options };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export { NBaseVectorClient, NBaseError, createNBaseVectorClient };
|
|
104
|
+
export type { NBaseClientConfig, NBaseVector, NBaseSearchOptions, NBaseSearchResult, NBaseStats };
|
|
@@ -1,87 +1,87 @@
|
|
|
1
|
-
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from '../base/types';
|
|
2
|
-
|
|
3
|
-
// ─── PostgreSQL Engine ─────────────────────────────────────────────────────────────
|
|
4
|
-
|
|
5
|
-
let PgPool: any = null;
|
|
6
|
-
try { PgPool = require('pg').Pool; } catch { }
|
|
7
|
-
|
|
8
|
-
export class PostgresEngine implements QueryEngine {
|
|
9
|
-
dialect: Dialect = 'postgres';
|
|
10
|
-
private pool: any = null;
|
|
11
|
-
|
|
12
|
-
constructor(adapterConfig: An5AdapterConfig) {
|
|
13
|
-
if (!PgPool) throw new Error('pg package is required for PostgreSQL support. Run: npm install pg');
|
|
14
|
-
this.pool = new PgPool({
|
|
15
|
-
connectionString: adapterConfig.connectionString,
|
|
16
|
-
max: adapterConfig.poolMax ?? 10,
|
|
17
|
-
});
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/** Convert @name → $N; identical names reuse the same $N */
|
|
21
|
-
private transform(query: string, params?: Record<string, any>): { text: string; values: any[] } {
|
|
22
|
-
if (!params || Object.keys(params).length === 0) return { text: query, values: [] };
|
|
23
|
-
const values: any[] = [];
|
|
24
|
-
const seen: Record<string, number> = {};
|
|
25
|
-
const text = query.replace(/@(\w+)/g, (_, name: string) => {
|
|
26
|
-
if (seen[name] !== undefined) return `$${seen[name]}`;
|
|
27
|
-
const idx = values.length + 1;
|
|
28
|
-
seen[name] = idx;
|
|
29
|
-
values.push(params[name] ?? null);
|
|
30
|
-
return `$${idx}`;
|
|
31
|
-
});
|
|
32
|
-
return { text, values };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
36
|
-
const { text, values } = this.transform(query, params);
|
|
37
|
-
const result = await this.pool.query(text, values);
|
|
38
|
-
return result.rows as T[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
async executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
42
|
-
const { text, values } = this.transform(query, params);
|
|
43
|
-
const result = await this.pool.query(text, values);
|
|
44
|
-
return result.rowCount ?? 0;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async connect(): Promise<void> {
|
|
48
|
-
const client = await this.pool.connect();
|
|
49
|
-
client.release();
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
async disconnect(): Promise<void> { await this.pool.end(); }
|
|
53
|
-
|
|
54
|
-
async beginTransaction(): Promise<TransactionHandle> {
|
|
55
|
-
const client = await this.pool.connect();
|
|
56
|
-
await client.query('BEGIN');
|
|
57
|
-
|
|
58
|
-
const transform = (query: string, params?: Record<string, any>) => {
|
|
59
|
-
if (!params || Object.keys(params).length === 0) return { text: query, values: [] };
|
|
60
|
-
const values: any[] = [];
|
|
61
|
-
const seen: Record<string, number> = {};
|
|
62
|
-
const text = query.replace(/@(\w+)/g, (_, name: string) => {
|
|
63
|
-
if (seen[name] !== undefined) return `$${seen[name]}`;
|
|
64
|
-
const idx = values.length + 1;
|
|
65
|
-
seen[name] = idx;
|
|
66
|
-
values.push(params[name] ?? null);
|
|
67
|
-
return `$${idx}`;
|
|
68
|
-
});
|
|
69
|
-
return { text, values };
|
|
70
|
-
};
|
|
71
|
-
|
|
72
|
-
return {
|
|
73
|
-
exec: async <T>(q: string, p?: Record<string, any>) => {
|
|
74
|
-
const { text, values } = transform(q, p);
|
|
75
|
-
const r = await client.query(text, values);
|
|
76
|
-
return (r.rows || []) as T[];
|
|
77
|
-
},
|
|
78
|
-
executeRaw: async (q: string, p?: Record<string, any>) => {
|
|
79
|
-
const { text, values } = transform(q, p);
|
|
80
|
-
const r = await client.query(text, values);
|
|
81
|
-
return r.rowCount ?? 0;
|
|
82
|
-
},
|
|
83
|
-
commit: async () => { await client.query('COMMIT'); client.release(); },
|
|
84
|
-
rollback: async () => { await client.query('ROLLBACK'); client.release(); },
|
|
85
|
-
};
|
|
86
|
-
}
|
|
87
|
-
}
|
|
1
|
+
import type { An5AdapterConfig, Dialect, QueryEngine, TransactionHandle } from '../base/types';
|
|
2
|
+
|
|
3
|
+
// ─── PostgreSQL Engine ─────────────────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
let PgPool: any = null;
|
|
6
|
+
try { PgPool = require('pg').Pool; } catch { }
|
|
7
|
+
|
|
8
|
+
export class PostgresEngine implements QueryEngine {
|
|
9
|
+
dialect: Dialect = 'postgres';
|
|
10
|
+
private pool: any = null;
|
|
11
|
+
|
|
12
|
+
constructor(adapterConfig: An5AdapterConfig) {
|
|
13
|
+
if (!PgPool) throw new Error('pg package is required for PostgreSQL support. Run: npm install pg');
|
|
14
|
+
this.pool = new PgPool({
|
|
15
|
+
connectionString: adapterConfig.connectionString ?? '',
|
|
16
|
+
max: adapterConfig.poolMax ?? 10,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Convert @name → $N; identical names reuse the same $N */
|
|
21
|
+
private transform(query: string, params?: Record<string, any>): { text: string; values: any[] } {
|
|
22
|
+
if (!params || Object.keys(params).length === 0) return { text: query, values: [] };
|
|
23
|
+
const values: any[] = [];
|
|
24
|
+
const seen: Record<string, number> = {};
|
|
25
|
+
const text = query.replace(/@(\w+)/g, (_, name: string) => {
|
|
26
|
+
if (seen[name] !== undefined) return `$${seen[name]}`;
|
|
27
|
+
const idx = values.length + 1;
|
|
28
|
+
seen[name] = idx;
|
|
29
|
+
values.push(params[name] ?? null);
|
|
30
|
+
return `$${idx}`;
|
|
31
|
+
});
|
|
32
|
+
return { text, values };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
36
|
+
const { text, values } = this.transform(query, params);
|
|
37
|
+
const result = await this.pool.query(text, values);
|
|
38
|
+
return result.rows as T[];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
42
|
+
const { text, values } = this.transform(query, params);
|
|
43
|
+
const result = await this.pool.query(text, values);
|
|
44
|
+
return result.rowCount ?? 0;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async connect(): Promise<void> {
|
|
48
|
+
const client = await this.pool.connect();
|
|
49
|
+
client.release();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async disconnect(): Promise<void> { await this.pool.end(); }
|
|
53
|
+
|
|
54
|
+
async beginTransaction(): Promise<TransactionHandle> {
|
|
55
|
+
const client = await this.pool.connect();
|
|
56
|
+
await client.query('BEGIN');
|
|
57
|
+
|
|
58
|
+
const transform = (query: string, params?: Record<string, any>) => {
|
|
59
|
+
if (!params || Object.keys(params).length === 0) return { text: query, values: [] };
|
|
60
|
+
const values: any[] = [];
|
|
61
|
+
const seen: Record<string, number> = {};
|
|
62
|
+
const text = query.replace(/@(\w+)/g, (_, name: string) => {
|
|
63
|
+
if (seen[name] !== undefined) return `$${seen[name]}`;
|
|
64
|
+
const idx = values.length + 1;
|
|
65
|
+
seen[name] = idx;
|
|
66
|
+
values.push(params[name] ?? null);
|
|
67
|
+
return `$${idx}`;
|
|
68
|
+
});
|
|
69
|
+
return { text, values };
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
exec: async <T>(q: string, p?: Record<string, any>) => {
|
|
74
|
+
const { text, values } = transform(q, p);
|
|
75
|
+
const r = await client.query(text, values);
|
|
76
|
+
return (r.rows || []) as T[];
|
|
77
|
+
},
|
|
78
|
+
executeRaw: async (q: string, p?: Record<string, any>) => {
|
|
79
|
+
const { text, values } = transform(q, p);
|
|
80
|
+
const r = await client.query(text, values);
|
|
81
|
+
return r.rowCount ?? 0;
|
|
82
|
+
},
|
|
83
|
+
commit: async () => { await client.query('COMMIT'); client.release(); },
|
|
84
|
+
rollback: async () => { await client.query('ROLLBACK'); client.release(); },
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './engine';
|
|
1
|
+
export * from './engine';
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Dialect, QueryEngine, TransactionHandle } from '../base/types';
|
|
2
|
+
|
|
3
|
+
// ─── Browser SQLite Driver & Config ──────────────────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SqliteDriver {
|
|
6
|
+
exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> | T[];
|
|
7
|
+
executeRaw?(query: string, params?: Record<string, any>): Promise<number> | number;
|
|
8
|
+
close?(): Promise<void> | void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface SqliteBrowserConfig {
|
|
12
|
+
db?: any; // sql.js Database instance or SQLite WASM DB
|
|
13
|
+
driver?: SqliteDriver;
|
|
14
|
+
exec?: <T = any>(query: string, params?: Record<string, any>) => Promise<T[]> | T[];
|
|
15
|
+
executeRaw?: (query: string, params?: Record<string, any>) => Promise<number> | number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ─── Browser SQLite Engine ──────────────────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
export class SqliteBrowserEngine implements QueryEngine {
|
|
21
|
+
dialect: Dialect = 'sqlite';
|
|
22
|
+
private db: any = null;
|
|
23
|
+
private driver: SqliteDriver | null = null;
|
|
24
|
+
private customExec: ((query: string, params?: Record<string, any>) => Promise<any[]> | any[]) | null = null;
|
|
25
|
+
private customExecuteRaw: ((query: string, params?: Record<string, any>) => Promise<number> | number) | null = null;
|
|
26
|
+
|
|
27
|
+
constructor(config?: SqliteBrowserConfig | any) {
|
|
28
|
+
if (!config) return;
|
|
29
|
+
|
|
30
|
+
if (config.exec && typeof config.exec === 'function') {
|
|
31
|
+
this.customExec = config.exec;
|
|
32
|
+
this.customExecuteRaw = config.executeRaw ?? null;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (config.driver) {
|
|
37
|
+
this.driver = config.driver;
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Direct DB instance (e.g. sql.js Database or sqlite-wasm)
|
|
42
|
+
if (config.db) {
|
|
43
|
+
this.db = config.db;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// If config itself looks like a driver with exec()
|
|
48
|
+
if (typeof config.exec === 'function' && typeof config.run !== 'function' && typeof config.prepare !== 'function') {
|
|
49
|
+
this.driver = config as SqliteDriver;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// If config itself is a Database instance (has prepare, exec, run)
|
|
54
|
+
if (typeof config.prepare === 'function' || typeof config.run === 'function' || typeof config.exec === 'function') {
|
|
55
|
+
this.db = config;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private normalizeSqlJsParams(params?: Record<string, any>): Record<string, any> {
|
|
60
|
+
if (!params || typeof params !== 'object') return {};
|
|
61
|
+
const normalized: Record<string, any> = {};
|
|
62
|
+
for (const [k, v] of Object.entries(params)) {
|
|
63
|
+
const key = k.startsWith('@') || k.startsWith(':') || k.startsWith('$') ? k : `@${k}`;
|
|
64
|
+
normalized[key] = v;
|
|
65
|
+
}
|
|
66
|
+
return normalized;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async exec<T = any>(query: string, params?: Record<string, any>): Promise<T[]> {
|
|
70
|
+
if (this.customExec) {
|
|
71
|
+
return await this.customExec(query, params);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.driver) {
|
|
75
|
+
return await this.driver.exec<T>(query, params);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (this.db) {
|
|
79
|
+
// sql.js Database with prepare()
|
|
80
|
+
if (typeof this.db.prepare === 'function') {
|
|
81
|
+
const stmt = this.db.prepare(query);
|
|
82
|
+
try {
|
|
83
|
+
if (params && Object.keys(params).length > 0) {
|
|
84
|
+
stmt.bind(this.normalizeSqlJsParams(params));
|
|
85
|
+
}
|
|
86
|
+
const results: T[] = [];
|
|
87
|
+
while (stmt.step()) {
|
|
88
|
+
results.push(stmt.getAsObject() as T);
|
|
89
|
+
}
|
|
90
|
+
return results;
|
|
91
|
+
} finally {
|
|
92
|
+
stmt.free();
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Fallback sql.js db.exec()
|
|
97
|
+
if (typeof this.db.exec === 'function') {
|
|
98
|
+
const res = this.db.exec(query);
|
|
99
|
+
if (!res || res.length === 0) return [];
|
|
100
|
+
const { columns, values } = res[0];
|
|
101
|
+
return values.map((row: any[]) => {
|
|
102
|
+
const obj: any = {};
|
|
103
|
+
columns.forEach((col: string, i: number) => {
|
|
104
|
+
obj[col] = row[i];
|
|
105
|
+
});
|
|
106
|
+
return obj as T;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
throw new Error('SqliteBrowserEngine: No SQLite database or driver provided. Pass a sql.js db instance or custom driver.');
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async executeRaw(query: string, params?: Record<string, any>): Promise<number> {
|
|
115
|
+
if (this.customExecuteRaw) {
|
|
116
|
+
return await this.customExecuteRaw(query, params);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (this.driver && typeof this.driver.executeRaw === 'function') {
|
|
120
|
+
return await this.driver.executeRaw(query, params);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.db) {
|
|
124
|
+
if (typeof this.db.prepare === 'function') {
|
|
125
|
+
const stmt = this.db.prepare(query);
|
|
126
|
+
try {
|
|
127
|
+
if (params && Object.keys(params).length > 0) {
|
|
128
|
+
stmt.bind(this.normalizeSqlJsParams(params));
|
|
129
|
+
}
|
|
130
|
+
stmt.step();
|
|
131
|
+
if (typeof this.db.getRowsModified === 'function') {
|
|
132
|
+
return this.db.getRowsModified();
|
|
133
|
+
}
|
|
134
|
+
return 1;
|
|
135
|
+
} finally {
|
|
136
|
+
stmt.free();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (typeof this.db.run === 'function') {
|
|
141
|
+
this.db.run(query, this.normalizeSqlJsParams(params));
|
|
142
|
+
if (typeof this.db.getRowsModified === 'function') {
|
|
143
|
+
return this.db.getRowsModified();
|
|
144
|
+
}
|
|
145
|
+
return 1;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const rows = await this.exec(query, params);
|
|
150
|
+
return rows.length;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async connect(): Promise<void> {}
|
|
154
|
+
|
|
155
|
+
async disconnect(): Promise<void> {
|
|
156
|
+
if (this.driver && typeof this.driver.close === 'function') {
|
|
157
|
+
await this.driver.close();
|
|
158
|
+
} else if (this.db && typeof this.db.close === 'function') {
|
|
159
|
+
this.db.close();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
async beginTransaction(): Promise<TransactionHandle> {
|
|
164
|
+
await this.executeRaw('BEGIN');
|
|
165
|
+
let done = false;
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
exec: async <T>(q: string, p?: Record<string, any>) => this.exec<T>(q, p),
|
|
169
|
+
executeRaw: async (q: string, p?: Record<string, any>) => this.executeRaw(q, p),
|
|
170
|
+
commit: async () => {
|
|
171
|
+
if (!done) {
|
|
172
|
+
await this.executeRaw('COMMIT');
|
|
173
|
+
done = true;
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
rollback: async () => {
|
|
177
|
+
if (!done) {
|
|
178
|
+
await this.executeRaw('ROLLBACK');
|
|
179
|
+
done = true;
|
|
180
|
+
}
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ─── Browser SQLite Adapter Factory ──────────────────────────────────────────────────
|
|
187
|
+
|
|
188
|
+
export function createBrowserSqliteAdapter(config: SqliteBrowserConfig | any): any {
|
|
189
|
+
const { An5Adapter } = require('../an5Adapter');
|
|
190
|
+
return new An5Adapter({ engine: new SqliteBrowserEngine(config) });
|
|
191
|
+
}
|
|
192
|
+
|