singulio-postgres 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +866 -0
- package/dist/client.d.ts +52 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +127 -0
- package/dist/health.d.ts +44 -0
- package/dist/health.d.ts.map +1 -0
- package/dist/health.js +182 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +45 -0
- package/dist/pool.d.ts +52 -0
- package/dist/pool.d.ts.map +1 -0
- package/dist/pool.js +216 -0
- package/dist/rls.d.ts +53 -0
- package/dist/rls.d.ts.map +1 -0
- package/dist/rls.js +134 -0
- package/dist/transaction.d.ts +54 -0
- package/dist/transaction.d.ts.map +1 -0
- package/dist/transaction.js +138 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/vector.d.ts +74 -0
- package/dist/vector.d.ts.map +1 -0
- package/dist/vector.js +223 -0
- package/package.json +97 -0
- package/src/client.ts +153 -0
- package/src/health.ts +226 -0
- package/src/index.ts +110 -0
- package/src/pool.ts +268 -0
- package/src/rls.ts +169 -0
- package/src/transaction.ts +207 -0
- package/src/types.ts +142 -0
- package/src/vector.ts +312 -0
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - Type Definitions
|
|
3
|
+
* PostgreSQL 18.x types for Bun native client
|
|
4
|
+
*/
|
|
5
|
+
/** Connection configuration */
|
|
6
|
+
export interface PostgresConfig {
|
|
7
|
+
/** Connection string (postgres://user:pass@host:port/db) */
|
|
8
|
+
connectionString?: string;
|
|
9
|
+
/** Host (default: localhost) */
|
|
10
|
+
host?: string;
|
|
11
|
+
/** Port (default: 5432) */
|
|
12
|
+
port?: number;
|
|
13
|
+
/** Database name */
|
|
14
|
+
database?: string;
|
|
15
|
+
/** Username */
|
|
16
|
+
user?: string;
|
|
17
|
+
/** Password */
|
|
18
|
+
password?: string;
|
|
19
|
+
/** SSL mode */
|
|
20
|
+
ssl?: boolean | 'require' | 'prefer' | 'disable';
|
|
21
|
+
/** Application name for pg_stat_activity */
|
|
22
|
+
applicationName?: string;
|
|
23
|
+
}
|
|
24
|
+
/** Pool configuration */
|
|
25
|
+
export interface PoolConfig extends PostgresConfig {
|
|
26
|
+
/** Minimum connections (default: 2) */
|
|
27
|
+
min?: number;
|
|
28
|
+
/** Maximum connections (default: 20) */
|
|
29
|
+
max?: number;
|
|
30
|
+
/** Idle timeout in ms (default: 30000) */
|
|
31
|
+
idleTimeout?: number;
|
|
32
|
+
/** Connection timeout in ms (default: 10000) */
|
|
33
|
+
connectionTimeout?: number;
|
|
34
|
+
/** Max lifetime per connection in ms (default: 3600000 = 1 hour) */
|
|
35
|
+
maxLifetime?: number;
|
|
36
|
+
}
|
|
37
|
+
/** RLS context for multi-tenant isolation */
|
|
38
|
+
export interface RLSContext {
|
|
39
|
+
/** Tenant reference ID */
|
|
40
|
+
tenantId: string;
|
|
41
|
+
/** User ID (optional) */
|
|
42
|
+
userId?: string;
|
|
43
|
+
/** Is admin user (bypasses some RLS) */
|
|
44
|
+
isAdmin?: boolean;
|
|
45
|
+
/** Additional session GUCs */
|
|
46
|
+
extraGUCs?: Record<string, string>;
|
|
47
|
+
}
|
|
48
|
+
/** Query result row */
|
|
49
|
+
export type QueryRow = Record<string, unknown>;
|
|
50
|
+
/** Query result */
|
|
51
|
+
export interface QueryResult<T = QueryRow> {
|
|
52
|
+
/** Result rows */
|
|
53
|
+
rows: T[];
|
|
54
|
+
/** Number of affected rows */
|
|
55
|
+
rowCount: number;
|
|
56
|
+
/** Field metadata */
|
|
57
|
+
fields?: FieldInfo[];
|
|
58
|
+
}
|
|
59
|
+
/** Field metadata */
|
|
60
|
+
export interface FieldInfo {
|
|
61
|
+
name: string;
|
|
62
|
+
dataTypeID: number;
|
|
63
|
+
tableID?: number;
|
|
64
|
+
columnID?: number;
|
|
65
|
+
}
|
|
66
|
+
/** Transaction isolation levels */
|
|
67
|
+
export type IsolationLevel = 'READ UNCOMMITTED' | 'READ COMMITTED' | 'REPEATABLE READ' | 'SERIALIZABLE';
|
|
68
|
+
/** Transaction options */
|
|
69
|
+
export interface TransactionOptions {
|
|
70
|
+
/** Isolation level (default: READ COMMITTED) */
|
|
71
|
+
isolationLevel?: IsolationLevel;
|
|
72
|
+
/** Read-only transaction */
|
|
73
|
+
readOnly?: boolean;
|
|
74
|
+
/** Deferrable (only with SERIALIZABLE + readOnly) */
|
|
75
|
+
deferrable?: boolean;
|
|
76
|
+
/** RLS context to apply */
|
|
77
|
+
rlsContext?: RLSContext;
|
|
78
|
+
}
|
|
79
|
+
/** Health check result */
|
|
80
|
+
export interface HealthCheckResult {
|
|
81
|
+
healthy: boolean;
|
|
82
|
+
latencyMs: number;
|
|
83
|
+
poolStats?: PoolStats;
|
|
84
|
+
error?: string;
|
|
85
|
+
}
|
|
86
|
+
/** Pool statistics */
|
|
87
|
+
export interface PoolStats {
|
|
88
|
+
/** Total connections in pool */
|
|
89
|
+
total: number;
|
|
90
|
+
/** Idle connections */
|
|
91
|
+
idle: number;
|
|
92
|
+
/** Active connections */
|
|
93
|
+
active: number;
|
|
94
|
+
/** Pending connection requests */
|
|
95
|
+
pending: number;
|
|
96
|
+
}
|
|
97
|
+
/** Vector distance operators for pgvector */
|
|
98
|
+
export type VectorDistanceOperator = '<->' | '<=>' | '<#>' | '<+>';
|
|
99
|
+
/** Vector index types */
|
|
100
|
+
export type VectorIndexType = 'hnsw' | 'ivfflat';
|
|
101
|
+
/** Vector search options */
|
|
102
|
+
export interface VectorSearchOptions {
|
|
103
|
+
/** Distance operator (default: <->) */
|
|
104
|
+
operator?: VectorDistanceOperator;
|
|
105
|
+
/** Maximum results (default: 10) */
|
|
106
|
+
limit?: number;
|
|
107
|
+
/** Minimum similarity threshold (optional) */
|
|
108
|
+
threshold?: number;
|
|
109
|
+
/** Additional WHERE clause */
|
|
110
|
+
filter?: string;
|
|
111
|
+
/** Filter parameters */
|
|
112
|
+
filterParams?: unknown[];
|
|
113
|
+
}
|
|
114
|
+
/** Logger interface (matches @singulio/logger) */
|
|
115
|
+
export interface Logger {
|
|
116
|
+
debug(message: string, meta?: Record<string, unknown>): void;
|
|
117
|
+
info(message: string, meta?: Record<string, unknown>): void;
|
|
118
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
119
|
+
error(message: string, meta?: Record<string, unknown>): void;
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/dist/types.js
ADDED
package/dist/vector.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - pgvector Extension Support
|
|
3
|
+
* Vector operations for AI/ML embeddings
|
|
4
|
+
*/
|
|
5
|
+
import type { VectorDistanceOperator, VectorIndexType, VectorSearchOptions, QueryResult, Logger } from './types.js';
|
|
6
|
+
/** Vector type (array of numbers) */
|
|
7
|
+
export type Vector = number[] | Float32Array | Float64Array;
|
|
8
|
+
/**
|
|
9
|
+
* Format a vector for PostgreSQL insertion
|
|
10
|
+
* Converts array to '[1,2,3]' format expected by pgvector
|
|
11
|
+
*/
|
|
12
|
+
export declare function formatVector(vector: Vector): string;
|
|
13
|
+
/**
|
|
14
|
+
* Parse a vector from PostgreSQL result
|
|
15
|
+
* Converts '[1,2,3]' string back to number array
|
|
16
|
+
*/
|
|
17
|
+
export declare function parseVector(value: string | number[] | null): number[] | null;
|
|
18
|
+
/**
|
|
19
|
+
* Calculate vector dimension
|
|
20
|
+
*/
|
|
21
|
+
export declare function vectorDimension(vector: Vector): number;
|
|
22
|
+
/**
|
|
23
|
+
* Normalize a vector to unit length (for cosine similarity)
|
|
24
|
+
*/
|
|
25
|
+
export declare function normalizeVector(vector: Vector): number[];
|
|
26
|
+
/**
|
|
27
|
+
* Get the distance operator SQL string
|
|
28
|
+
*/
|
|
29
|
+
export declare function getDistanceOperator(op: VectorDistanceOperator): string;
|
|
30
|
+
/**
|
|
31
|
+
* Get human-readable name for distance operator
|
|
32
|
+
*/
|
|
33
|
+
export declare function getDistanceOperatorName(op: VectorDistanceOperator): string;
|
|
34
|
+
/**
|
|
35
|
+
* Ensure pgvector extension is installed
|
|
36
|
+
*/
|
|
37
|
+
export declare function ensureVectorExtension(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Create a vector column on a table
|
|
40
|
+
*/
|
|
41
|
+
export declare function createVectorColumn(table: string, column: string, dimensions: number): Promise<void>;
|
|
42
|
+
/**
|
|
43
|
+
* Create a vector index for similarity search
|
|
44
|
+
*/
|
|
45
|
+
export declare function createVectorIndex(table: string, column: string, indexType?: VectorIndexType, operator?: VectorDistanceOperator, options?: {
|
|
46
|
+
/** HNSW: max connections per layer (default: 16) */
|
|
47
|
+
m?: number;
|
|
48
|
+
/** HNSW: size of dynamic candidate list (default: 64) */
|
|
49
|
+
efConstruction?: number;
|
|
50
|
+
/** IVFFlat: number of lists (default: 100) */
|
|
51
|
+
lists?: number;
|
|
52
|
+
}): Promise<void>;
|
|
53
|
+
/**
|
|
54
|
+
* Search for similar vectors
|
|
55
|
+
*/
|
|
56
|
+
export declare function vectorSearch<T = Record<string, unknown>>(table: string, column: string, queryVector: Vector, options?: VectorSearchOptions, logger?: Logger): Promise<QueryResult<T & {
|
|
57
|
+
distance: number;
|
|
58
|
+
}>>;
|
|
59
|
+
/**
|
|
60
|
+
* Insert a row with vector data
|
|
61
|
+
*/
|
|
62
|
+
export declare function insertWithVector(table: string, data: Record<string, unknown>, vectorColumn: string, vector: Vector): Promise<QueryResult>;
|
|
63
|
+
/**
|
|
64
|
+
* Update vector data for a row
|
|
65
|
+
*/
|
|
66
|
+
export declare function updateVector(table: string, idColumn: string, idValue: unknown, vectorColumn: string, vector: Vector): Promise<QueryResult>;
|
|
67
|
+
/**
|
|
68
|
+
* Batch insert rows with vectors
|
|
69
|
+
*/
|
|
70
|
+
export declare function batchInsertWithVectors(table: string, columns: string[], vectorColumn: string, rows: Array<{
|
|
71
|
+
data: unknown[];
|
|
72
|
+
vector: Vector;
|
|
73
|
+
}>): Promise<QueryResult>;
|
|
74
|
+
//# sourceMappingURL=vector.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"vector.d.ts","sourceRoot":"","sources":["../src/vector.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,KAAK,EACV,sBAAsB,EACtB,eAAe,EACf,mBAAmB,EACnB,WAAW,EACX,MAAM,EACP,MAAM,YAAY,CAAC;AAEpB,qCAAqC;AACrC,MAAM,MAAM,MAAM,GAAG,MAAM,EAAE,GAAG,YAAY,GAAG,YAAY,CAAC;AAE5D;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAGnD;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,IAAI,GAAG,MAAM,EAAE,GAAG,IAAI,CAc5E;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAEtD;AAED;;GAEG;AACH,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAKxD;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAAE,EAAE,sBAAsB,GAAG,MAAM,CAEtE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,EAAE,EAAE,sBAAsB,GAAG,MAAM,CAa1E;AAED;;GAEG;AACH,wBAAsB,qBAAqB,IAAI,OAAO,CAAC,IAAI,CAAC,CAE3D;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CAEf;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,SAAS,GAAE,eAAwB,EACnC,QAAQ,GAAE,sBAA8B,EACxC,OAAO,CAAC,EAAE;IACR,oDAAoD;IACpD,CAAC,CAAC,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,GACA,OAAO,CAAC,IAAI,CAAC,CAmCf;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,CAAC,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC5D,KAAK,EAAE,MAAM,EACb,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,OAAO,GAAE,mBAAwB,EACjC,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,WAAW,CAAC,CAAC,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAyChD;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC7B,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,CAAC,CAsBtB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAChC,KAAK,EAAE,MAAM,EACb,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,EAChB,YAAY,EAAE,MAAM,EACpB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,WAAW,CAAC,CAetB;AAED;;GAEG;AACH,wBAAsB,sBAAsB,CAC1C,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EAAE,EACjB,YAAY,EAAE,MAAM,EACpB,IAAI,EAAE,KAAK,CAAC;IAAE,IAAI,EAAE,OAAO,EAAE,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,GAC/C,OAAO,CAAC,WAAW,CAAC,CAkCtB"}
|
package/dist/vector.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - pgvector Extension Support
|
|
3
|
+
* Vector operations for AI/ML embeddings
|
|
4
|
+
*/
|
|
5
|
+
import { sql } from 'bun';
|
|
6
|
+
/**
|
|
7
|
+
* Format a vector for PostgreSQL insertion
|
|
8
|
+
* Converts array to '[1,2,3]' format expected by pgvector
|
|
9
|
+
*/
|
|
10
|
+
export function formatVector(vector) {
|
|
11
|
+
const arr = Array.isArray(vector) ? vector : Array.from(vector);
|
|
12
|
+
return `[${arr.join(',')}]`;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Parse a vector from PostgreSQL result
|
|
16
|
+
* Converts '[1,2,3]' string back to number array
|
|
17
|
+
*/
|
|
18
|
+
export function parseVector(value) {
|
|
19
|
+
if (value === null)
|
|
20
|
+
return null;
|
|
21
|
+
// Already an array (some drivers return it parsed)
|
|
22
|
+
if (Array.isArray(value))
|
|
23
|
+
return value;
|
|
24
|
+
// Parse string format '[1,2,3]'
|
|
25
|
+
if (typeof value === 'string') {
|
|
26
|
+
const trimmed = value.replace(/^\[|\]$/g, '');
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
return [];
|
|
29
|
+
return trimmed.split(',').map(Number);
|
|
30
|
+
}
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Calculate vector dimension
|
|
35
|
+
*/
|
|
36
|
+
export function vectorDimension(vector) {
|
|
37
|
+
return Array.isArray(vector) ? vector.length : vector.length;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Normalize a vector to unit length (for cosine similarity)
|
|
41
|
+
*/
|
|
42
|
+
export function normalizeVector(vector) {
|
|
43
|
+
const arr = Array.isArray(vector) ? vector : Array.from(vector);
|
|
44
|
+
const magnitude = Math.sqrt(arr.reduce((sum, val) => sum + val * val, 0));
|
|
45
|
+
if (magnitude === 0)
|
|
46
|
+
return arr;
|
|
47
|
+
return arr.map(val => val / magnitude);
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Get the distance operator SQL string
|
|
51
|
+
*/
|
|
52
|
+
export function getDistanceOperator(op) {
|
|
53
|
+
return op;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Get human-readable name for distance operator
|
|
57
|
+
*/
|
|
58
|
+
export function getDistanceOperatorName(op) {
|
|
59
|
+
switch (op) {
|
|
60
|
+
case '<->':
|
|
61
|
+
return 'L2 (Euclidean)';
|
|
62
|
+
case '<=>':
|
|
63
|
+
return 'Cosine';
|
|
64
|
+
case '<#>':
|
|
65
|
+
return 'Inner Product';
|
|
66
|
+
case '<+>':
|
|
67
|
+
return 'L1 (Manhattan)';
|
|
68
|
+
default:
|
|
69
|
+
return 'Unknown';
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Ensure pgvector extension is installed
|
|
74
|
+
*/
|
|
75
|
+
export async function ensureVectorExtension() {
|
|
76
|
+
await sql `CREATE EXTENSION IF NOT EXISTS vector`;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Create a vector column on a table
|
|
80
|
+
*/
|
|
81
|
+
export async function createVectorColumn(table, column, dimensions) {
|
|
82
|
+
await sql.unsafe(`ALTER TABLE ${table} ADD COLUMN IF NOT EXISTS ${column} vector(${dimensions})`);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Create a vector index for similarity search
|
|
86
|
+
*/
|
|
87
|
+
export async function createVectorIndex(table, column, indexType = 'hnsw', operator = '<->', options) {
|
|
88
|
+
const indexName = `idx_${table}_${column}_${indexType}`;
|
|
89
|
+
// Determine operator class
|
|
90
|
+
let opClass;
|
|
91
|
+
switch (operator) {
|
|
92
|
+
case '<=>':
|
|
93
|
+
opClass = 'vector_cosine_ops';
|
|
94
|
+
break;
|
|
95
|
+
case '<#>':
|
|
96
|
+
opClass = 'vector_ip_ops';
|
|
97
|
+
break;
|
|
98
|
+
case '<+>':
|
|
99
|
+
opClass = 'vector_l1_ops';
|
|
100
|
+
break;
|
|
101
|
+
default:
|
|
102
|
+
opClass = 'vector_l2_ops';
|
|
103
|
+
}
|
|
104
|
+
if (indexType === 'hnsw') {
|
|
105
|
+
const m = options?.m ?? 16;
|
|
106
|
+
const efConstruction = options?.efConstruction ?? 64;
|
|
107
|
+
await sql.unsafe(`
|
|
108
|
+
CREATE INDEX IF NOT EXISTS ${indexName}
|
|
109
|
+
ON ${table} USING hnsw (${column} ${opClass})
|
|
110
|
+
WITH (m = ${m}, ef_construction = ${efConstruction})
|
|
111
|
+
`);
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
const lists = options?.lists ?? 100;
|
|
115
|
+
await sql.unsafe(`
|
|
116
|
+
CREATE INDEX IF NOT EXISTS ${indexName}
|
|
117
|
+
ON ${table} USING ivfflat (${column} ${opClass})
|
|
118
|
+
WITH (lists = ${lists})
|
|
119
|
+
`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Search for similar vectors
|
|
124
|
+
*/
|
|
125
|
+
export async function vectorSearch(table, column, queryVector, options = {}, logger) {
|
|
126
|
+
const { operator = '<->', limit = 10, threshold, filter, filterParams = [] } = options;
|
|
127
|
+
const vectorStr = formatVector(queryVector);
|
|
128
|
+
const params = [vectorStr, ...filterParams, limit];
|
|
129
|
+
let whereClause = '';
|
|
130
|
+
if (filter) {
|
|
131
|
+
whereClause = `WHERE ${filter}`;
|
|
132
|
+
}
|
|
133
|
+
if (threshold !== undefined) {
|
|
134
|
+
const thresholdClause = `${column} ${operator} $1 < ${threshold}`;
|
|
135
|
+
whereClause = whereClause
|
|
136
|
+
? `${whereClause} AND ${thresholdClause}`
|
|
137
|
+
: `WHERE ${thresholdClause}`;
|
|
138
|
+
}
|
|
139
|
+
const start = performance.now();
|
|
140
|
+
const result = await sql.unsafe(`
|
|
141
|
+
SELECT *, ${column} ${operator} $1 AS distance
|
|
142
|
+
FROM ${table}
|
|
143
|
+
${whereClause}
|
|
144
|
+
ORDER BY ${column} ${operator} $1
|
|
145
|
+
LIMIT $${params.length}
|
|
146
|
+
`, params);
|
|
147
|
+
logger?.debug('Vector search completed', {
|
|
148
|
+
table,
|
|
149
|
+
column,
|
|
150
|
+
operator: getDistanceOperatorName(operator),
|
|
151
|
+
results: result.length,
|
|
152
|
+
latencyMs: (performance.now() - start).toFixed(2),
|
|
153
|
+
});
|
|
154
|
+
return {
|
|
155
|
+
rows: result,
|
|
156
|
+
rowCount: result.length,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Insert a row with vector data
|
|
161
|
+
*/
|
|
162
|
+
export async function insertWithVector(table, data, vectorColumn, vector) {
|
|
163
|
+
const columns = Object.keys(data);
|
|
164
|
+
const values = Object.values(data);
|
|
165
|
+
columns.push(vectorColumn);
|
|
166
|
+
values.push(formatVector(vector));
|
|
167
|
+
const placeholders = values.map((_, i) => `$${i + 1}`).join(', ');
|
|
168
|
+
const result = await sql.unsafe(`
|
|
169
|
+
INSERT INTO ${table} (${columns.join(', ')})
|
|
170
|
+
VALUES (${placeholders})
|
|
171
|
+
RETURNING *
|
|
172
|
+
`, values);
|
|
173
|
+
return {
|
|
174
|
+
rows: result,
|
|
175
|
+
rowCount: result.length,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Update vector data for a row
|
|
180
|
+
*/
|
|
181
|
+
export async function updateVector(table, idColumn, idValue, vectorColumn, vector) {
|
|
182
|
+
const result = await sql.unsafe(`
|
|
183
|
+
UPDATE ${table}
|
|
184
|
+
SET ${vectorColumn} = $1
|
|
185
|
+
WHERE ${idColumn} = $2
|
|
186
|
+
RETURNING *
|
|
187
|
+
`, [formatVector(vector), idValue]);
|
|
188
|
+
return {
|
|
189
|
+
rows: result,
|
|
190
|
+
rowCount: result.length,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
/**
|
|
194
|
+
* Batch insert rows with vectors
|
|
195
|
+
*/
|
|
196
|
+
export async function batchInsertWithVectors(table, columns, vectorColumn, rows) {
|
|
197
|
+
if (rows.length === 0) {
|
|
198
|
+
return { rows: [], rowCount: 0 };
|
|
199
|
+
}
|
|
200
|
+
const allColumns = [...columns, vectorColumn];
|
|
201
|
+
const valueSets = [];
|
|
202
|
+
const params = [];
|
|
203
|
+
let paramIndex = 1;
|
|
204
|
+
for (const row of rows) {
|
|
205
|
+
const placeholders = [];
|
|
206
|
+
for (const value of row.data) {
|
|
207
|
+
placeholders.push(`$${paramIndex++}`);
|
|
208
|
+
params.push(value);
|
|
209
|
+
}
|
|
210
|
+
placeholders.push(`$${paramIndex++}`);
|
|
211
|
+
params.push(formatVector(row.vector));
|
|
212
|
+
valueSets.push(`(${placeholders.join(', ')})`);
|
|
213
|
+
}
|
|
214
|
+
const result = await sql.unsafe(`
|
|
215
|
+
INSERT INTO ${table} (${allColumns.join(', ')})
|
|
216
|
+
VALUES ${valueSets.join(', ')}
|
|
217
|
+
RETURNING *
|
|
218
|
+
`, params);
|
|
219
|
+
return {
|
|
220
|
+
rows: result,
|
|
221
|
+
rowCount: result.length,
|
|
222
|
+
};
|
|
223
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "singulio-postgres",
|
|
3
|
+
"version": "1.1.0",
|
|
4
|
+
"description": "PostgreSQL client wrapper with RLS, pooling, transactions, and pgvector support",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"require": "./dist/index.cjs"
|
|
13
|
+
},
|
|
14
|
+
"./pool": {
|
|
15
|
+
"types": "./dist/pool.d.ts",
|
|
16
|
+
"import": "./dist/pool.js"
|
|
17
|
+
},
|
|
18
|
+
"./rls": {
|
|
19
|
+
"types": "./dist/rls.d.ts",
|
|
20
|
+
"import": "./dist/rls.js"
|
|
21
|
+
},
|
|
22
|
+
"./vector": {
|
|
23
|
+
"types": "./dist/vector.d.ts",
|
|
24
|
+
"import": "./dist/vector.js"
|
|
25
|
+
},
|
|
26
|
+
"./transaction": {
|
|
27
|
+
"types": "./dist/transaction.d.ts",
|
|
28
|
+
"import": "./dist/transaction.js"
|
|
29
|
+
},
|
|
30
|
+
"./health": {
|
|
31
|
+
"types": "./dist/health.d.ts",
|
|
32
|
+
"import": "./dist/health.js"
|
|
33
|
+
},
|
|
34
|
+
"./types": {
|
|
35
|
+
"types": "./dist/types.d.ts",
|
|
36
|
+
"import": "./dist/types.js"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"dist",
|
|
41
|
+
"src"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"build": "tsc",
|
|
45
|
+
"clean": "node -e \"require('fs').rmSync('dist', {recursive: true, force: true})\"",
|
|
46
|
+
"test": "node --test",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
},
|
|
49
|
+
"keywords": [
|
|
50
|
+
"postgres",
|
|
51
|
+
"postgresql",
|
|
52
|
+
"rls",
|
|
53
|
+
"pgvector",
|
|
54
|
+
"multi-tenant",
|
|
55
|
+
"database"
|
|
56
|
+
],
|
|
57
|
+
"author": "Singulio",
|
|
58
|
+
"license": "MIT",
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"registry": "https://registry.npmjs.org",
|
|
61
|
+
"access": "public"
|
|
62
|
+
},
|
|
63
|
+
"repository": {
|
|
64
|
+
"type": "git",
|
|
65
|
+
"url": "git+https://github.com/singuliodev/postgres.git"
|
|
66
|
+
},
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/singuliodev/postgres/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/singuliodev/postgres#readme",
|
|
71
|
+
"dependencies": {},
|
|
72
|
+
"devDependencies": {
|
|
73
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
74
|
+
"@semantic-release/commit-analyzer": "^13.0.1",
|
|
75
|
+
"@semantic-release/git": "^10.0.1",
|
|
76
|
+
"@semantic-release/github": "^11.0.1",
|
|
77
|
+
"@semantic-release/npm": "^12.0.1",
|
|
78
|
+
"@semantic-release/release-notes-generator": "^14.0.3",
|
|
79
|
+
"conventional-changelog-conventionalcommits": "^8.0.0",
|
|
80
|
+
"typescript": "^5.9.3"
|
|
81
|
+
},
|
|
82
|
+
"engines": {
|
|
83
|
+
"node": ">=18.0.0",
|
|
84
|
+
"bun": ">=1.0.0"
|
|
85
|
+
},
|
|
86
|
+
"os": [
|
|
87
|
+
"linux",
|
|
88
|
+
"darwin",
|
|
89
|
+
"win32"
|
|
90
|
+
],
|
|
91
|
+
"cpu": [
|
|
92
|
+
"x64",
|
|
93
|
+
"ia32",
|
|
94
|
+
"arm64",
|
|
95
|
+
"arm"
|
|
96
|
+
]
|
|
97
|
+
}
|
package/src/client.ts
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @singulio/postgres - PostgreSQL Client
|
|
3
|
+
* Thin wrapper around bun:postgres native client
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { sql } from 'bun';
|
|
7
|
+
import type { PostgresConfig, QueryResult, QueryRow, Logger } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* PostgreSQL client wrapping Bun's native postgres driver
|
|
11
|
+
*/
|
|
12
|
+
export class PostgresClient {
|
|
13
|
+
private connectionString: string;
|
|
14
|
+
private logger?: Logger;
|
|
15
|
+
private connected = false;
|
|
16
|
+
|
|
17
|
+
constructor(config: PostgresConfig | string, logger?: Logger) {
|
|
18
|
+
this.connectionString =
|
|
19
|
+
typeof config === 'string' ? config : this.buildConnectionString(config);
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
private buildConnectionString(config: PostgresConfig): string {
|
|
24
|
+
if (config.connectionString) return config.connectionString;
|
|
25
|
+
|
|
26
|
+
const {
|
|
27
|
+
host = 'localhost',
|
|
28
|
+
port = 5432,
|
|
29
|
+
database = 'postgres',
|
|
30
|
+
user = 'postgres',
|
|
31
|
+
password = '',
|
|
32
|
+
ssl,
|
|
33
|
+
applicationName,
|
|
34
|
+
} = config;
|
|
35
|
+
|
|
36
|
+
let connStr = `postgres://${encodeURIComponent(user)}:${encodeURIComponent(password)}@${host}:${port}/${database}`;
|
|
37
|
+
|
|
38
|
+
const params: string[] = [];
|
|
39
|
+
if (ssl === true || ssl === 'require') {
|
|
40
|
+
params.push('sslmode=require');
|
|
41
|
+
} else if (ssl === 'prefer') {
|
|
42
|
+
params.push('sslmode=prefer');
|
|
43
|
+
}
|
|
44
|
+
if (applicationName) {
|
|
45
|
+
params.push(`application_name=${encodeURIComponent(applicationName)}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (params.length > 0) {
|
|
49
|
+
connStr += '?' + params.join('&');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return connStr;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Execute a SQL query with parameters
|
|
57
|
+
*/
|
|
58
|
+
async query<T = QueryRow>(queryText: string, params?: unknown[]): Promise<QueryResult<T>> {
|
|
59
|
+
const start = performance.now();
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
// Use Bun's sql tagged template for parameterized queries
|
|
63
|
+
const result = await sql.unsafe(queryText, params ?? []);
|
|
64
|
+
|
|
65
|
+
const latency = performance.now() - start;
|
|
66
|
+
this.logger?.debug('Query executed', {
|
|
67
|
+
query: queryText.substring(0, 100),
|
|
68
|
+
params: params?.length ?? 0,
|
|
69
|
+
rows: result.length,
|
|
70
|
+
latencyMs: latency.toFixed(2),
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
rows: result as T[],
|
|
75
|
+
rowCount: result.length,
|
|
76
|
+
};
|
|
77
|
+
} catch (error) {
|
|
78
|
+
const latency = performance.now() - start;
|
|
79
|
+
this.logger?.error('Query failed', {
|
|
80
|
+
query: queryText.substring(0, 100),
|
|
81
|
+
error: error instanceof Error ? error.message : String(error),
|
|
82
|
+
latencyMs: latency.toFixed(2),
|
|
83
|
+
});
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Execute a query and return the first row or null
|
|
90
|
+
*/
|
|
91
|
+
async queryOne<T = QueryRow>(queryText: string, params?: unknown[]): Promise<T | null> {
|
|
92
|
+
const result = await this.query<T>(queryText, params);
|
|
93
|
+
return result.rows[0] ?? null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Execute a query and return all rows
|
|
98
|
+
*/
|
|
99
|
+
async queryAll<T = QueryRow>(queryText: string, params?: unknown[]): Promise<T[]> {
|
|
100
|
+
const result = await this.query<T>(queryText, params);
|
|
101
|
+
return result.rows;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Execute a command (INSERT/UPDATE/DELETE) and return affected row count
|
|
106
|
+
*/
|
|
107
|
+
async execute(queryText: string, params?: unknown[]): Promise<number> {
|
|
108
|
+
const result = await this.query(queryText, params);
|
|
109
|
+
return result.rowCount;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Check if connected and working
|
|
114
|
+
*/
|
|
115
|
+
async ping(): Promise<boolean> {
|
|
116
|
+
try {
|
|
117
|
+
const result = await sql`SELECT 1 as ok`;
|
|
118
|
+
return result.length > 0;
|
|
119
|
+
} catch {
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Get server version
|
|
126
|
+
*/
|
|
127
|
+
async version(): Promise<string> {
|
|
128
|
+
const result = await sql`SELECT version()`;
|
|
129
|
+
return result[0]?.version ?? 'unknown';
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Close all connections (for bun:postgres this is mostly a no-op as it manages connections)
|
|
134
|
+
*/
|
|
135
|
+
async close(): Promise<void> {
|
|
136
|
+
this.connected = false;
|
|
137
|
+
this.logger?.info('PostgresClient closed');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Get connection string (for debugging, sensitive parts masked)
|
|
142
|
+
*/
|
|
143
|
+
get maskedConnectionString(): string {
|
|
144
|
+
return this.connectionString.replace(/:([^@]+)@/, ':***@');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Create a new PostgresClient instance
|
|
150
|
+
*/
|
|
151
|
+
export function createClient(config: PostgresConfig | string, logger?: Logger): PostgresClient {
|
|
152
|
+
return new PostgresClient(config, logger);
|
|
153
|
+
}
|