@kernl-sdk/pg 0.1.10 → 0.1.12

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 (153) hide show
  1. package/.turbo/turbo-build.log +4 -5
  2. package/.turbo/turbo-check-types.log +36 -0
  3. package/CHANGELOG.md +41 -0
  4. package/README.md +124 -0
  5. package/dist/__tests__/integration.test.js +81 -1
  6. package/dist/__tests__/memory-integration.test.d.ts +2 -0
  7. package/dist/__tests__/memory-integration.test.d.ts.map +1 -0
  8. package/dist/__tests__/memory-integration.test.js +287 -0
  9. package/dist/__tests__/memory.test.d.ts +2 -0
  10. package/dist/__tests__/memory.test.d.ts.map +1 -0
  11. package/dist/__tests__/memory.test.js +357 -0
  12. package/dist/index.d.ts +5 -3
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +5 -3
  15. package/dist/memory/sql.d.ts +30 -0
  16. package/dist/memory/sql.d.ts.map +1 -0
  17. package/dist/memory/sql.js +100 -0
  18. package/dist/memory/store.d.ts +41 -0
  19. package/dist/memory/store.d.ts.map +1 -0
  20. package/dist/memory/store.js +114 -0
  21. package/dist/migrations.d.ts +1 -1
  22. package/dist/migrations.d.ts.map +1 -1
  23. package/dist/migrations.js +9 -3
  24. package/dist/pgvector/__tests__/handle.test.d.ts +2 -0
  25. package/dist/pgvector/__tests__/handle.test.d.ts.map +1 -0
  26. package/dist/pgvector/__tests__/handle.test.js +277 -0
  27. package/dist/pgvector/__tests__/hit.test.d.ts +2 -0
  28. package/dist/pgvector/__tests__/hit.test.d.ts.map +1 -0
  29. package/dist/pgvector/__tests__/hit.test.js +134 -0
  30. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts +7 -0
  31. package/dist/pgvector/__tests__/integration/document.integration.test.d.ts.map +1 -0
  32. package/dist/pgvector/__tests__/integration/document.integration.test.js +587 -0
  33. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts +8 -0
  34. package/dist/pgvector/__tests__/integration/edge.integration.test.d.ts.map +1 -0
  35. package/dist/pgvector/__tests__/integration/edge.integration.test.js +663 -0
  36. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts +8 -0
  37. package/dist/pgvector/__tests__/integration/filters.integration.test.d.ts.map +1 -0
  38. package/dist/pgvector/__tests__/integration/filters.integration.test.js +609 -0
  39. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts +8 -0
  40. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.d.ts.map +1 -0
  41. package/dist/pgvector/__tests__/integration/lifecycle.integration.test.js +449 -0
  42. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts +8 -0
  43. package/dist/pgvector/__tests__/integration/query.integration.test.d.ts.map +1 -0
  44. package/dist/pgvector/__tests__/integration/query.integration.test.js +544 -0
  45. package/dist/pgvector/__tests__/search.test.d.ts +2 -0
  46. package/dist/pgvector/__tests__/search.test.d.ts.map +1 -0
  47. package/dist/pgvector/__tests__/search.test.js +279 -0
  48. package/dist/pgvector/handle.d.ts +60 -0
  49. package/dist/pgvector/handle.d.ts.map +1 -0
  50. package/dist/pgvector/handle.js +213 -0
  51. package/dist/pgvector/hit.d.ts +10 -0
  52. package/dist/pgvector/hit.d.ts.map +1 -0
  53. package/dist/pgvector/hit.js +44 -0
  54. package/dist/pgvector/index.d.ts +7 -0
  55. package/dist/pgvector/index.d.ts.map +1 -0
  56. package/dist/pgvector/index.js +5 -0
  57. package/dist/pgvector/search.d.ts +60 -0
  58. package/dist/pgvector/search.d.ts.map +1 -0
  59. package/dist/pgvector/search.js +227 -0
  60. package/dist/pgvector/sql/__tests__/limit.test.d.ts +2 -0
  61. package/dist/pgvector/sql/__tests__/limit.test.d.ts.map +1 -0
  62. package/dist/pgvector/sql/__tests__/limit.test.js +161 -0
  63. package/dist/pgvector/sql/__tests__/order.test.d.ts +2 -0
  64. package/dist/pgvector/sql/__tests__/order.test.d.ts.map +1 -0
  65. package/dist/pgvector/sql/__tests__/order.test.js +218 -0
  66. package/dist/pgvector/sql/__tests__/query.test.d.ts +2 -0
  67. package/dist/pgvector/sql/__tests__/query.test.d.ts.map +1 -0
  68. package/dist/pgvector/sql/__tests__/query.test.js +392 -0
  69. package/dist/pgvector/sql/__tests__/select.test.d.ts +2 -0
  70. package/dist/pgvector/sql/__tests__/select.test.d.ts.map +1 -0
  71. package/dist/pgvector/sql/__tests__/select.test.js +293 -0
  72. package/dist/pgvector/sql/__tests__/where.test.d.ts +2 -0
  73. package/dist/pgvector/sql/__tests__/where.test.d.ts.map +1 -0
  74. package/dist/pgvector/sql/__tests__/where.test.js +488 -0
  75. package/dist/pgvector/sql/index.d.ts +7 -0
  76. package/dist/pgvector/sql/index.d.ts.map +1 -0
  77. package/dist/pgvector/sql/index.js +6 -0
  78. package/dist/pgvector/sql/limit.d.ts +8 -0
  79. package/dist/pgvector/sql/limit.d.ts.map +1 -0
  80. package/dist/pgvector/sql/limit.js +20 -0
  81. package/dist/pgvector/sql/order.d.ts +9 -0
  82. package/dist/pgvector/sql/order.d.ts.map +1 -0
  83. package/dist/pgvector/sql/order.js +47 -0
  84. package/dist/pgvector/sql/query.d.ts +46 -0
  85. package/dist/pgvector/sql/query.d.ts.map +1 -0
  86. package/dist/pgvector/sql/query.js +54 -0
  87. package/dist/pgvector/sql/schema.d.ts +16 -0
  88. package/dist/pgvector/sql/schema.d.ts.map +1 -0
  89. package/dist/pgvector/sql/schema.js +47 -0
  90. package/dist/pgvector/sql/select.d.ts +11 -0
  91. package/dist/pgvector/sql/select.d.ts.map +1 -0
  92. package/dist/pgvector/sql/select.js +87 -0
  93. package/dist/pgvector/sql/where.d.ts +8 -0
  94. package/dist/pgvector/sql/where.d.ts.map +1 -0
  95. package/dist/pgvector/sql/where.js +137 -0
  96. package/dist/pgvector/types.d.ts +20 -0
  97. package/dist/pgvector/types.d.ts.map +1 -0
  98. package/dist/pgvector/types.js +1 -0
  99. package/dist/pgvector/utils.d.ts +18 -0
  100. package/dist/pgvector/utils.d.ts.map +1 -0
  101. package/dist/pgvector/utils.js +22 -0
  102. package/dist/postgres.d.ts +19 -26
  103. package/dist/postgres.d.ts.map +1 -1
  104. package/dist/postgres.js +15 -27
  105. package/dist/storage.d.ts +62 -0
  106. package/dist/storage.d.ts.map +1 -1
  107. package/dist/storage.js +55 -10
  108. package/dist/thread/sql.d.ts +38 -0
  109. package/dist/thread/sql.d.ts.map +1 -0
  110. package/dist/thread/sql.js +112 -0
  111. package/dist/thread/store.d.ts +7 -3
  112. package/dist/thread/store.d.ts.map +1 -1
  113. package/dist/thread/store.js +46 -105
  114. package/package.json +8 -5
  115. package/src/__tests__/integration.test.ts +114 -15
  116. package/src/__tests__/memory-integration.test.ts +355 -0
  117. package/src/__tests__/memory.test.ts +428 -0
  118. package/src/index.ts +19 -3
  119. package/src/memory/sql.ts +141 -0
  120. package/src/memory/store.ts +166 -0
  121. package/src/migrations.ts +13 -3
  122. package/src/pgvector/README.md +50 -0
  123. package/src/pgvector/__tests__/handle.test.ts +335 -0
  124. package/src/pgvector/__tests__/hit.test.ts +165 -0
  125. package/src/pgvector/__tests__/integration/document.integration.test.ts +717 -0
  126. package/src/pgvector/__tests__/integration/edge.integration.test.ts +835 -0
  127. package/src/pgvector/__tests__/integration/filters.integration.test.ts +721 -0
  128. package/src/pgvector/__tests__/integration/lifecycle.integration.test.ts +570 -0
  129. package/src/pgvector/__tests__/integration/query.integration.test.ts +667 -0
  130. package/src/pgvector/__tests__/search.test.ts +366 -0
  131. package/src/pgvector/handle.ts +285 -0
  132. package/src/pgvector/hit.ts +56 -0
  133. package/src/pgvector/index.ts +7 -0
  134. package/src/pgvector/search.ts +330 -0
  135. package/src/pgvector/sql/__tests__/limit.test.ts +180 -0
  136. package/src/pgvector/sql/__tests__/order.test.ts +248 -0
  137. package/src/pgvector/sql/__tests__/query.test.ts +548 -0
  138. package/src/pgvector/sql/__tests__/select.test.ts +367 -0
  139. package/src/pgvector/sql/__tests__/where.test.ts +554 -0
  140. package/src/pgvector/sql/index.ts +14 -0
  141. package/src/pgvector/sql/limit.ts +29 -0
  142. package/src/pgvector/sql/order.ts +55 -0
  143. package/src/pgvector/sql/query.ts +112 -0
  144. package/src/pgvector/sql/schema.ts +61 -0
  145. package/src/pgvector/sql/select.ts +100 -0
  146. package/src/pgvector/sql/where.ts +152 -0
  147. package/src/pgvector/types.ts +21 -0
  148. package/src/pgvector/utils.ts +24 -0
  149. package/src/postgres.ts +31 -33
  150. package/src/storage.ts +102 -11
  151. package/src/thread/sql.ts +159 -0
  152. package/src/thread/store.ts +58 -127
  153. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,112 @@
1
+ import type {
2
+ Filter,
3
+ OrderBy,
4
+ SearchQuery,
5
+ RankingSignal,
6
+ } from "@kernl-sdk/retrieval";
7
+
8
+ import type { PGIndexConfig } from "../types";
9
+
10
+ /**
11
+ * Convert QueryOptions to SQL codec inputs.
12
+ *
13
+ * pgvector constraints:
14
+ * - Only single vector signal supported (no multi-signal fusion)
15
+ * - No hybrid (vector + text) in same signal
16
+ * - Filter-only and orderBy-only queries are allowed
17
+ */
18
+ export function sqlize(query: SearchQuery, config: SqlizeConfig): SqlizedQuery {
19
+ const signals = query.query ?? query.max ?? [];
20
+
21
+ // Validate pgvector constraints
22
+ if (signals.length > 1) {
23
+ throw new Error(
24
+ "pgvector does not support multi-signal fusion. " +
25
+ "Use a single vector signal, or run multiple queries and fuse client-side.",
26
+ );
27
+ }
28
+
29
+ if (signals.length === 1) {
30
+ const { weight, ...fields } = signals[0];
31
+ let vectorCount = 0;
32
+ let textCount = 0;
33
+
34
+ for (const value of Object.values(fields)) {
35
+ if (value === undefined) continue;
36
+ if (Array.isArray(value)) vectorCount++;
37
+ else if (typeof value === "string") textCount++;
38
+ }
39
+
40
+ if (vectorCount > 1) {
41
+ throw new Error(
42
+ "pgvector does not support multi-vector fusion. " +
43
+ "Use a single vector signal, or run multiple queries and fuse client-side.",
44
+ );
45
+ }
46
+
47
+ if (vectorCount > 0 && textCount > 0) {
48
+ throw new Error(
49
+ "pgvector does not support hybrid (vector + text) fusion. " +
50
+ "Use a single vector signal, or run multiple queries and fuse client-side.",
51
+ );
52
+ }
53
+ }
54
+
55
+ return {
56
+ select: {
57
+ pkey: config.pkey,
58
+ signals,
59
+ binding: config.binding,
60
+ include: query.include,
61
+ },
62
+ where: { filter: query.filter },
63
+ order: {
64
+ signals,
65
+ orderBy: query.orderBy,
66
+ binding: config.binding,
67
+ schema: config.schema,
68
+ table: config.table,
69
+ },
70
+ limit: { topK: query.topK ?? 10, offset: query.offset ?? 0 },
71
+ };
72
+ }
73
+
74
+ export interface SqlizedQuery {
75
+ select: SelectInput;
76
+ where: Omit<WhereInput, "startIdx">;
77
+ order: OrderInput;
78
+ limit: Omit<LimitInput, "startIdx">;
79
+ }
80
+
81
+ export interface SqlizeConfig {
82
+ pkey: string;
83
+ schema: string;
84
+ table: string;
85
+ binding?: PGIndexConfig;
86
+ }
87
+
88
+ export interface SelectInput {
89
+ pkey: string;
90
+ signals: RankingSignal[];
91
+ binding?: PGIndexConfig;
92
+ include?: string[] | boolean;
93
+ }
94
+
95
+ export interface WhereInput {
96
+ filter?: Filter;
97
+ startIdx: number;
98
+ }
99
+
100
+ export interface OrderInput {
101
+ signals: RankingSignal[];
102
+ orderBy?: OrderBy;
103
+ binding?: PGIndexConfig;
104
+ schema?: string;
105
+ table?: string;
106
+ }
107
+
108
+ export interface LimitInput {
109
+ topK: number;
110
+ offset: number;
111
+ startIdx: number;
112
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Schema conversion codecs for pgvector.
3
+ */
4
+
5
+ import type { Codec } from "@kernl-sdk/shared/lib";
6
+ import type {
7
+ FieldSchema,
8
+ VectorFieldSchema,
9
+ ScalarFieldSchema,
10
+ } from "@kernl-sdk/retrieval";
11
+
12
+ type ScalarType = ScalarFieldSchema["type"];
13
+ type Similarity = VectorFieldSchema["similarity"];
14
+
15
+ /**
16
+ * Mapping from kernl scalar types to Postgres column types.
17
+ */
18
+ const SCALAR_TO_PG: Record<string, string> = {
19
+ string: "TEXT",
20
+ int: "INTEGER",
21
+ bigint: "BIGINT",
22
+ float: "DOUBLE PRECISION",
23
+ boolean: "BOOLEAN",
24
+ date: "TIMESTAMPTZ",
25
+ object: "JSONB",
26
+ geopoint: "POINT",
27
+ };
28
+
29
+ /**
30
+ * Codec for converting FieldSchema to Postgres column type.
31
+ */
32
+ export const FIELD_TYPE: Codec<FieldSchema, string> = {
33
+ encode: (schema) => {
34
+ if (schema.type === "vector") {
35
+ return `vector(${schema.dimensions})`;
36
+ }
37
+ return SCALAR_TO_PG[schema.type] ?? "TEXT";
38
+ },
39
+ decode: () => {
40
+ throw new Error("FIELD_TYPE.decode not implemented");
41
+ },
42
+ };
43
+
44
+ /**
45
+ * Mapping from similarity metric to pgvector operator class.
46
+ */
47
+ const SIMILARITY_TO_OPCLASS: Record<string, string> = {
48
+ cosine: "vector_cosine_ops",
49
+ euclidean: "vector_l2_ops",
50
+ dot_product: "vector_ip_ops",
51
+ };
52
+
53
+ /**
54
+ * Codec for converting similarity metric to pgvector HNSW operator class.
55
+ */
56
+ export const SIMILARITY: Codec<Similarity | undefined, string> = {
57
+ encode: (similarity) => SIMILARITY_TO_OPCLASS[similarity ?? "cosine"],
58
+ decode: () => {
59
+ throw new Error("SIMILARITY.decode not implemented");
60
+ },
61
+ };
@@ -0,0 +1,100 @@
1
+ import type { Codec } from "@kernl-sdk/shared/lib";
2
+ import type { SelectInput } from "./query";
3
+
4
+ export interface SQLClause {
5
+ sql: string;
6
+ params: unknown[];
7
+ }
8
+
9
+ /**
10
+ * pgvector distance operators by similarity metric.
11
+ */
12
+ const DISTANCE_OPS = {
13
+ cosine: "<=>",
14
+ euclidean: "<->",
15
+ dot_product: "<#>",
16
+ } as const;
17
+
18
+ /**
19
+ * Codec for building SELECT clause with score expression.
20
+ */
21
+ export const SQL_SELECT: Codec<SelectInput, SQLClause> = {
22
+ encode({ pkey, signals, binding, include }) {
23
+ const parts: string[] = [`"${pkey}" as id`];
24
+ const params: unknown[] = [];
25
+
26
+ // find vector signal for scoring
27
+ const vsig = signals.find((s) => {
28
+ for (const [key, val] of Object.entries(s)) {
29
+ if (key !== "weight" && Array.isArray(val)) return true;
30
+ }
31
+ return false;
32
+ });
33
+
34
+ if (vsig) {
35
+ // Extract vector field and value
36
+ let vecField: string | null = null;
37
+ let vecValue: number[] | null = null;
38
+
39
+ for (const [key, val] of Object.entries(vsig)) {
40
+ if (key !== "weight" && Array.isArray(val)) {
41
+ vecField = key;
42
+ vecValue = val as number[];
43
+ break;
44
+ }
45
+ }
46
+
47
+ if (vecField && vecValue) {
48
+ const col = binding?.fields[vecField]?.column ?? vecField;
49
+ const similarity = binding?.fields[vecField]?.similarity ?? "cosine";
50
+ const op = DISTANCE_OPS[similarity];
51
+
52
+ params.push(JSON.stringify(vecValue));
53
+
54
+ // score expression varies by metric
55
+ switch (similarity) {
56
+ case "cosine":
57
+ parts.push(`1 - ("${col}" ${op} $1::vector) as score`);
58
+ break;
59
+ case "euclidean":
60
+ parts.push(`1 / (1 + ("${col}" ${op} $1::vector)) as score`);
61
+ break;
62
+ case "dot_product":
63
+ parts.push(`-("${col}" ${op} $1::vector) as score`);
64
+ break;
65
+ }
66
+ }
67
+ } else {
68
+ parts.push("1 as score");
69
+ }
70
+
71
+ // Select columns based on include
72
+ // Note: We always select a calculated "score", so skip any document field named "score"
73
+ // to avoid ambiguity and ensure hit.score is always the similarity score.
74
+ if (binding && include !== false) {
75
+ const fields = Object.entries(binding.fields);
76
+ if (Array.isArray(include)) {
77
+ // Select only specified fields
78
+ for (const fieldName of include) {
79
+ if (fieldName === "score") continue; // Skip to avoid overriding calculated score
80
+ const field = binding.fields[fieldName];
81
+ if (field) {
82
+ parts.push(`"${field.column}"`);
83
+ }
84
+ }
85
+ } else {
86
+ // include: true or undefined → select all (except score)
87
+ for (const [fieldName, field] of fields) {
88
+ if (fieldName === "score") continue; // Skip to avoid overriding calculated score
89
+ parts.push(`"${field.column}"`);
90
+ }
91
+ }
92
+ }
93
+
94
+ return { sql: parts.join(", "), params };
95
+ },
96
+
97
+ decode() {
98
+ throw new Error("SQL_SELECT.decode not implemented");
99
+ },
100
+ };
@@ -0,0 +1,152 @@
1
+ import type { Codec } from "@kernl-sdk/shared/lib";
2
+ import type { Filter, FieldOps } from "@kernl-sdk/retrieval";
3
+ import type { WhereInput } from "./query";
4
+ import type { SQLClause } from "./select";
5
+
6
+ /**
7
+ * Codec for building WHERE clause from MongoDB-style filter.
8
+ */
9
+ export const SQL_WHERE: Codec<WhereInput, SQLClause> = {
10
+ encode({ filter, startIdx }) {
11
+ if (!filter) {
12
+ return { sql: "", params: [] };
13
+ }
14
+ return encodeFilter(filter, startIdx);
15
+ },
16
+
17
+ decode() {
18
+ throw new Error("SQL_WHERE.decode not implemented");
19
+ },
20
+ };
21
+
22
+ /**
23
+ * Encode a filter to SQL (recursive).
24
+ */
25
+ function encodeFilter(filter: Filter, startIdx: number): SQLClause {
26
+ const conditions: string[] = [];
27
+ const params: unknown[] = [];
28
+ let idx = startIdx;
29
+
30
+ for (const [key, value] of Object.entries(filter)) {
31
+ if (value === undefined) continue;
32
+
33
+ // $and
34
+ if (key === "$and" && Array.isArray(value)) {
35
+ const parts: string[] = [];
36
+ for (const sub of value as Filter[]) {
37
+ const clause = encodeFilter(sub, idx);
38
+ if (clause.sql) {
39
+ parts.push(`(${clause.sql})`);
40
+ params.push(...clause.params);
41
+ idx += clause.params.length;
42
+ }
43
+ }
44
+ if (parts.length > 0) {
45
+ conditions.push(`(${parts.join(" AND ")})`);
46
+ }
47
+ continue;
48
+ }
49
+
50
+ // $or
51
+ if (key === "$or" && Array.isArray(value)) {
52
+ const parts: string[] = [];
53
+ for (const sub of value as Filter[]) {
54
+ const clause = encodeFilter(sub, idx);
55
+ if (clause.sql) {
56
+ parts.push(`(${clause.sql})`);
57
+ params.push(...clause.params);
58
+ idx += clause.params.length;
59
+ }
60
+ }
61
+ if (parts.length > 0) {
62
+ conditions.push(`(${parts.join(" OR ")})`);
63
+ }
64
+ continue;
65
+ }
66
+
67
+ // $not
68
+ if (key === "$not" && typeof value === "object" && !Array.isArray(value)) {
69
+ const clause = encodeFilter(value as Filter, idx);
70
+ if (clause.sql) {
71
+ conditions.push(`NOT (${clause.sql})`);
72
+ params.push(...clause.params);
73
+ idx += clause.params.length;
74
+ }
75
+ continue;
76
+ }
77
+
78
+ // field operators
79
+ if (isFieldOps(value)) {
80
+ const ops = value;
81
+
82
+ if (ops.$eq !== undefined) {
83
+ conditions.push(`"${key}" = $${idx++}`);
84
+ params.push(ops.$eq);
85
+ }
86
+ if (ops.$neq !== undefined) {
87
+ conditions.push(`"${key}" != $${idx++}`);
88
+ params.push(ops.$neq);
89
+ }
90
+ if (ops.$gt !== undefined) {
91
+ conditions.push(`"${key}" > $${idx++}`);
92
+ params.push(ops.$gt);
93
+ }
94
+ if (ops.$gte !== undefined) {
95
+ conditions.push(`"${key}" >= $${idx++}`);
96
+ params.push(ops.$gte);
97
+ }
98
+ if (ops.$lt !== undefined) {
99
+ conditions.push(`"${key}" < $${idx++}`);
100
+ params.push(ops.$lt);
101
+ }
102
+ if (ops.$lte !== undefined) {
103
+ conditions.push(`"${key}" <= $${idx++}`);
104
+ params.push(ops.$lte);
105
+ }
106
+ if (ops.$in !== undefined) {
107
+ conditions.push(`"${key}" = ANY($${idx++})`);
108
+ params.push(ops.$in);
109
+ }
110
+ if (ops.$nin !== undefined) {
111
+ conditions.push(`"${key}" != ALL($${idx++})`);
112
+ params.push(ops.$nin);
113
+ }
114
+ if (ops.$contains !== undefined) {
115
+ conditions.push(`"${key}" ILIKE $${idx++}`);
116
+ params.push(`%${ops.$contains}%`);
117
+ }
118
+ if (ops.$startsWith !== undefined) {
119
+ conditions.push(`"${key}" ILIKE $${idx++}`);
120
+ params.push(`${ops.$startsWith}%`);
121
+ }
122
+ if (ops.$endsWith !== undefined) {
123
+ conditions.push(`"${key}" ILIKE $${idx++}`);
124
+ params.push(`%${ops.$endsWith}`);
125
+ }
126
+ if (ops.$exists !== undefined) {
127
+ conditions.push(
128
+ ops.$exists ? `"${key}" IS NOT NULL` : `"${key}" IS NULL`,
129
+ );
130
+ }
131
+ continue;
132
+ }
133
+
134
+ // Equality shorthand: { status: "active" }
135
+ if (value === null) {
136
+ conditions.push(`"${key}" IS NULL`);
137
+ } else {
138
+ conditions.push(`"${key}" = $${idx++}`);
139
+ params.push(value);
140
+ }
141
+ }
142
+
143
+ return { sql: conditions.join(" AND "), params };
144
+ }
145
+
146
+ /**
147
+ * Check if value is an operator object (has $-prefixed keys).
148
+ */
149
+ function isFieldOps(value: unknown): value is FieldOps {
150
+ if (typeof value !== "object" || value === null) return false;
151
+ return Object.keys(value).some((k) => k.startsWith("$"));
152
+ }
@@ -0,0 +1,21 @@
1
+ import type { FieldSchema } from "@kernl-sdk/retrieval";
2
+
3
+ /**
4
+ * Mapping from a logical field name to a Postgres column.
5
+ */
6
+ export interface PGFieldBinding {
7
+ column: string;
8
+ type: FieldSchema["type"];
9
+ dimensions?: number;
10
+ similarity?: "cosine" | "euclidean" | "dot_product";
11
+ }
12
+
13
+ /**
14
+ * Configuration for binding a Postgres table as a search index.
15
+ */
16
+ export interface PGIndexConfig {
17
+ schema: string;
18
+ table: string;
19
+ pkey: string;
20
+ fields: Record<string, PGFieldBinding>;
21
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * pgvector utility functions.
3
+ */
4
+
5
+ /**
6
+ * Parse index id into schema and table.
7
+ *
8
+ * - "docs" → { schema: "public", table: "docs" }
9
+ * - "analytics.events" → { schema: "analytics", table: "events" }
10
+ */
11
+ export function parseIndexId(id: string): { schema: string; table: string } {
12
+ const parts = id.split(".");
13
+ if (parts.length === 2) {
14
+ return { schema: parts[0], table: parts[1] };
15
+ }
16
+ return { schema: "public", table: id };
17
+ }
18
+
19
+ /**
20
+ * Check if a value is a numeric array (vector).
21
+ */
22
+ export function isVector(val: unknown): val is number[] {
23
+ return Array.isArray(val) && val.length > 0 && typeof val[0] === "number";
24
+ }
package/src/postgres.ts CHANGED
@@ -1,12 +1,29 @@
1
1
  import { Pool } from "pg";
2
2
  import type { KernlStorage } from "kernl";
3
3
 
4
- import { PGStorage } from "./storage";
4
+ import { PGStorage, type PGVectorConfig } from "./storage";
5
+ import { PGSearchIndex } from "./pgvector/search";
5
6
 
6
7
  /**
7
- * PostgreSQL connection configuration.
8
+ * Create a PostgreSQL storage adapter for Kernl.
8
9
  */
9
- export type PostgresConfig =
10
+ export function postgres(config: PostgresConfig): KernlStorage {
11
+ const p = pool(config);
12
+ return new PGStorage({ pool: p, vector: config.vector });
13
+ }
14
+
15
+ /**
16
+ * Create a pgvector-backed search index.
17
+ */
18
+ export function pgvector(config: ConnectionConfig) {
19
+ const p = pool(config);
20
+ return new PGSearchIndex({ pool: p });
21
+ }
22
+
23
+ /**
24
+ * Connection options for PostgreSQL.
25
+ */
26
+ export type ConnectionConfig =
10
27
  | { pool: Pool }
11
28
  | { connstr: string }
12
29
  | {
@@ -18,39 +35,22 @@ export type PostgresConfig =
18
35
  };
19
36
 
20
37
  /**
21
- * Create a PostgreSQL storage adapter for Kernl.
22
- *
23
- * @param config - Connection configuration (pool, connection string, or credentials)
24
- * @returns KernlStorage instance backed by PostgreSQL
25
- *
26
- * @example
27
- * ```ts
28
- * // with connection string
29
- * const storage = postgres({ connstr: "postgresql://localhost/mydb" });
30
- *
31
- * // with connection options
32
- * const storage = postgres({
33
- * host: "localhost",
34
- * port: 5432,
35
- * database: "mydb",
36
- * user: "user",
37
- * password: "password"
38
- * });
39
- *
40
- * // existing pool
41
- * const pool = new Pool({ ... });
42
- * const storage = postgres({ pool });
43
- * ```
38
+ * PostgreSQL storage configuration.
44
39
  */
45
- export function postgres(config: PostgresConfig): KernlStorage {
46
- let pool: Pool;
40
+ export type PostgresConfig = ConnectionConfig & {
41
+ /**
42
+ * Enable pgvector support for semantic search.
43
+ */
44
+ vector?: boolean | PGVectorConfig;
45
+ };
47
46
 
47
+ function pool(config: ConnectionConfig): Pool {
48
48
  if ("pool" in config) {
49
- pool = config.pool;
49
+ return config.pool;
50
50
  } else if ("connstr" in config) {
51
- pool = new Pool({ connectionString: config.connstr });
51
+ return new Pool({ connectionString: config.connstr });
52
52
  } else {
53
- pool = new Pool({
53
+ return new Pool({
54
54
  host: config.host,
55
55
  port: config.port,
56
56
  database: config.database,
@@ -58,6 +58,4 @@ export function postgres(config: PostgresConfig): KernlStorage {
58
58
  password: config.password,
59
59
  });
60
60
  }
61
-
62
- return new PGStorage({ pool });
63
61
  }