postgresdk 0.18.18 → 0.18.20
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 +63 -1
- package/dist/cli.js +153 -51
- package/dist/index.js +153 -51
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -680,6 +680,16 @@ const sorted = await sdk.users.list({
|
|
|
680
680
|
order: ["asc", "desc"] // or use single direction: order: "asc"
|
|
681
681
|
});
|
|
682
682
|
|
|
683
|
+
// DISTINCT ON - one row per unique value (PostgreSQL)
|
|
684
|
+
const latestPerUser = await sdk.events.list({
|
|
685
|
+
distinctOn: "user_id", // or array: ["user_id", "type"]
|
|
686
|
+
orderBy: "created_at",
|
|
687
|
+
order: "desc"
|
|
688
|
+
});
|
|
689
|
+
// Returns one event per user_id, ordered by created_at DESC.
|
|
690
|
+
// When orderBy contains columns outside of distinctOn, a subquery is used
|
|
691
|
+
// automatically so the outer ordering is always respected.
|
|
692
|
+
|
|
683
693
|
// Advanced WHERE operators
|
|
684
694
|
const filtered = await sdk.users.list({
|
|
685
695
|
where: {
|
|
@@ -900,7 +910,59 @@ const uniqueResults = Array.from(
|
|
|
900
910
|
|
|
901
911
|
**Note:** Vector columns are auto-detected during introspection. Rows with `NULL` embeddings are excluded from vector search results.
|
|
902
912
|
|
|
903
|
-
|
|
913
|
+
#### Trigram Search (pg_trgm)
|
|
914
|
+
|
|
915
|
+
PostgreSDK supports full-text fuzzy search via [pg_trgm](https://www.postgresql.org/docs/current/pgtrgm.html). Requires the `pg_trgm` extension installed.
|
|
916
|
+
|
|
917
|
+
```sql
|
|
918
|
+
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
|
|
919
|
+
```
|
|
920
|
+
|
|
921
|
+
```typescript
|
|
922
|
+
// Basic trigram similarity search on a text field
|
|
923
|
+
const results = await sdk.books.list({
|
|
924
|
+
trigram: {
|
|
925
|
+
field: "title",
|
|
926
|
+
query: "postgrs", // typo-tolerant fuzzy match
|
|
927
|
+
metric: "similarity", // "similarity" (default), "wordSimilarity", "strictWordSimilarity"
|
|
928
|
+
threshold: 0.3 // optional: exclude rows below this score (0–1)
|
|
929
|
+
},
|
|
930
|
+
limit: 10
|
|
931
|
+
});
|
|
932
|
+
|
|
933
|
+
// _similarity score is automatically included in each result
|
|
934
|
+
results.data.forEach(book => {
|
|
935
|
+
console.log(book.title, book._similarity);
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
// Combine with WHERE filters
|
|
939
|
+
const filtered = await sdk.books.list({
|
|
940
|
+
trigram: { field: "title", query: "postgrs", threshold: 0.2 },
|
|
941
|
+
where: { published: true },
|
|
942
|
+
limit: 20
|
|
943
|
+
});
|
|
944
|
+
```
|
|
945
|
+
|
|
946
|
+
**Similarity Metrics:**
|
|
947
|
+
- `similarity` (default): Standard trigram similarity — `"col" % value`. Fraction of matching trigrams.
|
|
948
|
+
- `wordSimilarity`: Highest similarity between the query and any word in the column — `value <% "col"`.
|
|
949
|
+
- `strictWordSimilarity`: Strict word similarity — `value <<% "col"`. Requires the query to match an entire word.
|
|
950
|
+
|
|
951
|
+
**Inline `where` operators (without a top-level `trigram` param):**
|
|
952
|
+
```typescript
|
|
953
|
+
// Filter by trigram similarity inside a where clause
|
|
954
|
+
const results = await sdk.books.list({
|
|
955
|
+
where: {
|
|
956
|
+
title: { $similarity: "postgrs" } // % operator
|
|
957
|
+
// title: { $wordSimilarity: "postgrs" } // <% operator
|
|
958
|
+
// title: { $strictWordSimilarity: "postgrs" } // <<% operator
|
|
959
|
+
}
|
|
960
|
+
});
|
|
961
|
+
```
|
|
962
|
+
|
|
963
|
+
**Note:** `trigram` and `vector` are mutually exclusive on a single `list()` call.
|
|
964
|
+
|
|
965
|
+
See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$similarity`, `$wordSimilarity`, `$strictWordSimilarity`, `$is`, `$isNot`, `$or`, `$and`, `$jsonbContains`, `$jsonbContainedBy`, `$jsonbHasKey`, `$jsonbHasAnyKeys`, `$jsonbHasAllKeys`, `$jsonbPath`.
|
|
904
966
|
|
|
905
967
|
---
|
|
906
968
|
|
package/dist/cli.js
CHANGED
|
@@ -3293,7 +3293,13 @@ const listSchema = z.object({
|
|
|
3293
3293
|
query: z.array(z.number()),
|
|
3294
3294
|
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
3295
3295
|
maxDistance: z.number().optional()
|
|
3296
|
-
}).optional()
|
|
3296
|
+
}).optional(),` : ""}
|
|
3297
|
+
trigram: z.object({
|
|
3298
|
+
field: z.string(),
|
|
3299
|
+
query: z.string(),
|
|
3300
|
+
metric: z.enum(["similarity", "wordSimilarity", "strictWordSimilarity"]).optional(),
|
|
3301
|
+
threshold: z.number().min(0).max(1).optional()
|
|
3302
|
+
}).optional()
|
|
3297
3303
|
}).strict().refine(
|
|
3298
3304
|
(data) => !(data.select && data.exclude),
|
|
3299
3305
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -3964,10 +3970,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3964
3970
|
metric?: "cosine" | "l2" | "inner";
|
|
3965
3971
|
maxDistance?: number;
|
|
3966
3972
|
};` : ""}
|
|
3973
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3967
3974
|
orderBy?: string | string[];
|
|
3968
3975
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3969
3976
|
distinctOn?: string | string[];
|
|
3970
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3977
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3971
3978
|
/**
|
|
3972
3979
|
* List ${table.name} records with field exclusion
|
|
3973
3980
|
* @param params - Query parameters with exclude
|
|
@@ -3985,10 +3992,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3985
3992
|
metric?: "cosine" | "l2" | "inner";
|
|
3986
3993
|
maxDistance?: number;
|
|
3987
3994
|
};` : ""}
|
|
3995
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3988
3996
|
orderBy?: string | string[];
|
|
3989
3997
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3990
3998
|
distinctOn?: string | string[];
|
|
3991
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3999
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3992
4000
|
/**
|
|
3993
4001
|
* List ${table.name} records with pagination and filtering
|
|
3994
4002
|
* @param params - Query parameters
|
|
@@ -4017,10 +4025,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4017
4025
|
metric?: "cosine" | "l2" | "inner";
|
|
4018
4026
|
maxDistance?: number;
|
|
4019
4027
|
};` : ""}
|
|
4028
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4020
4029
|
orderBy?: string | string[];
|
|
4021
4030
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4022
4031
|
distinctOn?: string | string[];
|
|
4023
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
4032
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4024
4033
|
async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
|
|
4025
4034
|
include?: ${Type}IncludeSpec;
|
|
4026
4035
|
select?: string[];
|
|
@@ -4034,11 +4043,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
4034
4043
|
metric?: "cosine" | "l2" | "inner";
|
|
4035
4044
|
maxDistance?: number;
|
|
4036
4045
|
};` : ""}
|
|
4046
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4037
4047
|
orderBy?: string | string[];
|
|
4038
4048
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4039
4049
|
distinctOn?: string | string[];
|
|
4040
4050
|
}): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
|
|
4041
|
-
return this.post<PaginatedResponse<Select${Type}<TJsonb
|
|
4051
|
+
return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4042
4052
|
}` : ` /**
|
|
4043
4053
|
* List ${table.name} records with field selection
|
|
4044
4054
|
* @param params - Query parameters with select
|
|
@@ -4056,10 +4066,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4056
4066
|
metric?: "cosine" | "l2" | "inner";
|
|
4057
4067
|
maxDistance?: number;
|
|
4058
4068
|
};` : ""}
|
|
4069
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4059
4070
|
orderBy?: string | string[];
|
|
4060
4071
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4061
4072
|
distinctOn?: string | string[];
|
|
4062
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
4073
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4063
4074
|
/**
|
|
4064
4075
|
* List ${table.name} records with field exclusion
|
|
4065
4076
|
* @param params - Query parameters with exclude
|
|
@@ -4077,10 +4088,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4077
4088
|
metric?: "cosine" | "l2" | "inner";
|
|
4078
4089
|
maxDistance?: number;
|
|
4079
4090
|
};` : ""}
|
|
4091
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4080
4092
|
orderBy?: string | string[];
|
|
4081
4093
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4082
4094
|
distinctOn?: string | string[];
|
|
4083
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
4095
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4084
4096
|
/**
|
|
4085
4097
|
* List ${table.name} records with pagination and filtering
|
|
4086
4098
|
* @param params - Query parameters
|
|
@@ -4103,10 +4115,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4103
4115
|
metric?: "cosine" | "l2" | "inner";
|
|
4104
4116
|
maxDistance?: number;
|
|
4105
4117
|
};` : ""}
|
|
4118
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4106
4119
|
orderBy?: string | string[];
|
|
4107
4120
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4108
4121
|
distinctOn?: string | string[];
|
|
4109
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
4122
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4110
4123
|
async list(params?: {
|
|
4111
4124
|
include?: ${Type}IncludeSpec;
|
|
4112
4125
|
select?: string[];
|
|
@@ -4120,11 +4133,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
4120
4133
|
metric?: "cosine" | "l2" | "inner";
|
|
4121
4134
|
maxDistance?: number;
|
|
4122
4135
|
};` : ""}
|
|
4136
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
4123
4137
|
orderBy?: string | string[];
|
|
4124
4138
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4125
4139
|
distinctOn?: string | string[];
|
|
4126
4140
|
}): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
|
|
4127
|
-
return this.post<PaginatedResponse<Select${Type}${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4141
|
+
return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4128
4142
|
}`}
|
|
4129
4143
|
|
|
4130
4144
|
${hasJsonbColumns ? ` /**
|
|
@@ -5292,6 +5306,12 @@ export type WhereOperator<T> = {
|
|
|
5292
5306
|
$like?: T extends string ? string : never;
|
|
5293
5307
|
/** Case-insensitive LIKE (strings only) */
|
|
5294
5308
|
$ilike?: T extends string ? string : never;
|
|
5309
|
+
/** Trigram similarity match - "col" % value (pg_trgm required, uses similarity_threshold GUC) */
|
|
5310
|
+
$similarity?: T extends string ? string : never;
|
|
5311
|
+
/** Word trigram similarity match - value <% "col" (pg_trgm required) */
|
|
5312
|
+
$wordSimilarity?: T extends string ? string : never;
|
|
5313
|
+
/** Strict word trigram similarity match - value <<% "col" (pg_trgm required) */
|
|
5314
|
+
$strictWordSimilarity?: T extends string ? string : never;
|
|
5295
5315
|
/** IS NULL */
|
|
5296
5316
|
$is?: null;
|
|
5297
5317
|
/** IS NOT NULL */
|
|
@@ -6068,6 +6088,24 @@ function buildWhereClause(
|
|
|
6068
6088
|
whereParams.push(opValue);
|
|
6069
6089
|
paramIndex++;
|
|
6070
6090
|
break;
|
|
6091
|
+
case '$similarity':
|
|
6092
|
+
// pg_trgm: boolean similarity operator (respects similarity_threshold GUC)
|
|
6093
|
+
whereParts.push(\`"\${key}" % $\${paramIndex}\`);
|
|
6094
|
+
whereParams.push(opValue);
|
|
6095
|
+
paramIndex++;
|
|
6096
|
+
break;
|
|
6097
|
+
case '$wordSimilarity':
|
|
6098
|
+
// pg_trgm: word similarity operator (value <% col)
|
|
6099
|
+
whereParts.push(\`$\${paramIndex} <% "\${key}"\`);
|
|
6100
|
+
whereParams.push(opValue);
|
|
6101
|
+
paramIndex++;
|
|
6102
|
+
break;
|
|
6103
|
+
case '$strictWordSimilarity':
|
|
6104
|
+
// pg_trgm: strict word similarity operator (value <<% col)
|
|
6105
|
+
whereParts.push(\`$\${paramIndex} <<% "\${key}"\`);
|
|
6106
|
+
whereParams.push(opValue);
|
|
6107
|
+
paramIndex++;
|
|
6108
|
+
break;
|
|
6071
6109
|
case '$is':
|
|
6072
6110
|
if (opValue === null) {
|
|
6073
6111
|
whereParts.push(\`"\${key}" IS NULL\`);
|
|
@@ -6242,7 +6280,29 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
6242
6280
|
}
|
|
6243
6281
|
|
|
6244
6282
|
/**
|
|
6245
|
-
*
|
|
6283
|
+
* Build the pg_trgm SQL function expression for a given field and metric.
|
|
6284
|
+
* $1 is always the query string parameter.
|
|
6285
|
+
*/
|
|
6286
|
+
function getTrigramFnExpr(field: string, metric?: string): string {
|
|
6287
|
+
switch (metric) {
|
|
6288
|
+
case "wordSimilarity":
|
|
6289
|
+
return \`word_similarity($1, "\${field}")\`;
|
|
6290
|
+
case "strictWordSimilarity":
|
|
6291
|
+
return \`strict_word_similarity($1, "\${field}")\`;
|
|
6292
|
+
case "similarity":
|
|
6293
|
+
default:
|
|
6294
|
+
return \`similarity("\${field}", $1)\`;
|
|
6295
|
+
}
|
|
6296
|
+
}
|
|
6297
|
+
|
|
6298
|
+
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
6299
|
+
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
6300
|
+
if (cols.length === 0) return "";
|
|
6301
|
+
return \`ORDER BY \${cols.map((c, i) => \`"\${c}" \${(dirs[i] ?? "asc").toUpperCase()}\`).join(", ")}\`;
|
|
6302
|
+
}
|
|
6303
|
+
|
|
6304
|
+
/**
|
|
6305
|
+
* LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
|
|
6246
6306
|
*/
|
|
6247
6307
|
export async function listRecords(
|
|
6248
6308
|
ctx: OperationContext,
|
|
@@ -6260,23 +6320,48 @@ export async function listRecords(
|
|
|
6260
6320
|
metric?: "cosine" | "l2" | "inner";
|
|
6261
6321
|
maxDistance?: number;
|
|
6262
6322
|
};
|
|
6323
|
+
trigram?: {
|
|
6324
|
+
field: string;
|
|
6325
|
+
query: string;
|
|
6326
|
+
metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity";
|
|
6327
|
+
threshold?: number;
|
|
6328
|
+
};
|
|
6263
6329
|
}
|
|
6264
6330
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
6265
6331
|
try {
|
|
6266
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, distinctOn } = params;
|
|
6332
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
|
|
6267
6333
|
|
|
6268
6334
|
// DISTINCT ON support
|
|
6269
6335
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
6270
6336
|
const _distinctOnColsSQL = distinctCols ? distinctCols.map(c => '"' + c + '"').join(', ') : '';
|
|
6271
6337
|
|
|
6338
|
+
// Pre-compute user order cols (reused in ORDER BY block and useSubquery check)
|
|
6339
|
+
const userOrderCols: string[] = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
|
|
6340
|
+
|
|
6341
|
+
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
6342
|
+
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
6343
|
+
// overriding the requested ordering). Vector/trigram search always stays inline.
|
|
6344
|
+
const useSubquery: boolean =
|
|
6345
|
+
distinctCols !== null &&
|
|
6346
|
+
!vector &&
|
|
6347
|
+
!trigram &&
|
|
6348
|
+
userOrderCols.some(col => !distinctCols.includes(col));
|
|
6349
|
+
|
|
6272
6350
|
// Get distance operator if vector search
|
|
6273
6351
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
6274
6352
|
|
|
6275
|
-
//
|
|
6276
|
-
const
|
|
6353
|
+
// Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
|
|
6354
|
+
const trigramFnExpr = trigram ? getTrigramFnExpr(trigram.field, trigram.metric) : "";
|
|
6277
6355
|
|
|
6278
|
-
//
|
|
6279
|
-
|
|
6356
|
+
// Add vector or trigram query as $1 if present (mutually exclusive)
|
|
6357
|
+
const queryParams: any[] = vector
|
|
6358
|
+
? [JSON.stringify(vector.query)]
|
|
6359
|
+
: trigram
|
|
6360
|
+
? [trigram.query]
|
|
6361
|
+
: [];
|
|
6362
|
+
|
|
6363
|
+
// Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
|
|
6364
|
+
let paramIndex = vector || trigram ? 2 : 1;
|
|
6280
6365
|
const whereParts: string[] = [];
|
|
6281
6366
|
let whereParams: any[] = [];
|
|
6282
6367
|
|
|
@@ -6300,28 +6385,38 @@ export async function listRecords(
|
|
|
6300
6385
|
whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
|
|
6301
6386
|
}
|
|
6302
6387
|
|
|
6388
|
+
// Add trigram threshold filter if specified (inlined as literal — it's a float, safe)
|
|
6389
|
+
if (trigram?.threshold !== undefined) {
|
|
6390
|
+
whereParts.push(\`\${trigramFnExpr} >= \${trigram.threshold}\`);
|
|
6391
|
+
}
|
|
6392
|
+
|
|
6303
6393
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
6304
6394
|
|
|
6305
6395
|
// Build WHERE clause for COUNT query (may need different param indices)
|
|
6306
6396
|
let countWhereSQL = whereSQL;
|
|
6307
6397
|
let countParams = whereParams;
|
|
6308
6398
|
|
|
6309
|
-
|
|
6310
|
-
|
|
6399
|
+
// When a $1 query param exists (vector/trigram) but has NO threshold filter, the COUNT query
|
|
6400
|
+
// must not reference $1 since it's only needed in SELECT/ORDER BY. Rebuild WHERE from scratch
|
|
6401
|
+
// starting at $1. When a threshold IS set, $1 appears in the WHERE condition so keep it.
|
|
6402
|
+
const hasQueryParam = !!(vector || trigram);
|
|
6403
|
+
const hasThreshold = vector?.maxDistance !== undefined || trigram?.threshold !== undefined;
|
|
6404
|
+
|
|
6405
|
+
if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
|
|
6311
6406
|
const countWhereParts: string[] = [];
|
|
6312
6407
|
if (ctx.softDeleteColumn) {
|
|
6313
6408
|
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
6314
6409
|
}
|
|
6315
6410
|
if (whereClause) {
|
|
6316
|
-
const result = buildWhereClause(whereClause, 1); // Start at $1
|
|
6411
|
+
const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
|
|
6317
6412
|
if (result.sql) {
|
|
6318
6413
|
countWhereParts.push(result.sql);
|
|
6319
6414
|
countParams = result.params;
|
|
6320
6415
|
}
|
|
6321
6416
|
}
|
|
6322
6417
|
countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
|
|
6323
|
-
} else if (
|
|
6324
|
-
//
|
|
6418
|
+
} else if (hasQueryParam && hasThreshold) {
|
|
6419
|
+
// $1 appears in WHERE threshold condition — COUNT needs the full params
|
|
6325
6420
|
countParams = [...queryParams, ...whereParams];
|
|
6326
6421
|
}
|
|
6327
6422
|
|
|
@@ -6330,45 +6425,44 @@ export async function listRecords(
|
|
|
6330
6425
|
const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
6331
6426
|
const selectClause = vector
|
|
6332
6427
|
? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
|
|
6428
|
+
: trigram
|
|
6429
|
+
? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
|
|
6333
6430
|
: \`\${_distinctOnPrefix}\${baseColumns}\`;
|
|
6334
6431
|
|
|
6335
6432
|
// Build ORDER BY clause
|
|
6336
6433
|
let orderBySQL = "";
|
|
6434
|
+
const userDirs: ("asc" | "desc")[] = userOrderCols.length > 0
|
|
6435
|
+
? (Array.isArray(order) ? order : (order ? Array(userOrderCols.length).fill(order) : Array(userOrderCols.length).fill("asc")))
|
|
6436
|
+
: [];
|
|
6437
|
+
|
|
6337
6438
|
if (vector) {
|
|
6338
|
-
//
|
|
6439
|
+
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
6339
6440
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
6340
|
-
} else {
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6441
|
+
} else if (trigram) {
|
|
6442
|
+
// Trigram search orders by similarity score descending
|
|
6443
|
+
orderBySQL = \`ORDER BY _similarity DESC\`;
|
|
6444
|
+
} else if (useSubquery) {
|
|
6445
|
+
// Subquery form: outer query gets the user's full ORDER BY.
|
|
6446
|
+
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
|
6447
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
6448
|
+
} else if (distinctCols) {
|
|
6449
|
+
// Inline DISTINCT ON: prepend distinctCols as the leftmost ORDER BY prefix (PG requirement)
|
|
6346
6450
|
const finalCols: string[] = [];
|
|
6347
|
-
const finalDirs:
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
const userIdx = userCols.indexOf(col);
|
|
6353
|
-
finalCols.push(col);
|
|
6354
|
-
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] || "asc") : "asc");
|
|
6355
|
-
}
|
|
6356
|
-
// Append remaining user-specified cols not already covered by distinctOn
|
|
6357
|
-
for (let i = 0; i < userCols.length; i++) {
|
|
6358
|
-
if (!distinctCols.includes(userCols[i]!)) {
|
|
6359
|
-
finalCols.push(userCols[i]!);
|
|
6360
|
-
finalDirs.push(userDirs[i] || "asc");
|
|
6361
|
-
}
|
|
6362
|
-
}
|
|
6363
|
-
} else {
|
|
6364
|
-
finalCols.push(...userCols);
|
|
6365
|
-
finalDirs.push(...userDirs.map(d => d || "asc"));
|
|
6451
|
+
const finalDirs: ("asc" | "desc")[] = [];
|
|
6452
|
+
for (const col of distinctCols) {
|
|
6453
|
+
const userIdx = userOrderCols.indexOf(col);
|
|
6454
|
+
finalCols.push(col);
|
|
6455
|
+
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] ?? "asc") : "asc");
|
|
6366
6456
|
}
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6457
|
+
for (let i = 0; i < userOrderCols.length; i++) {
|
|
6458
|
+
if (!distinctCols.includes(userOrderCols[i]!)) {
|
|
6459
|
+
finalCols.push(userOrderCols[i]!);
|
|
6460
|
+
finalDirs.push(userDirs[i] ?? "asc");
|
|
6461
|
+
}
|
|
6371
6462
|
}
|
|
6463
|
+
orderBySQL = buildOrderBySQL(finalCols, finalDirs);
|
|
6464
|
+
} else {
|
|
6465
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
6372
6466
|
}
|
|
6373
6467
|
|
|
6374
6468
|
// Add limit and offset params
|
|
@@ -6385,7 +6479,15 @@ export async function listRecords(
|
|
|
6385
6479
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
6386
6480
|
|
|
6387
6481
|
// Get paginated data
|
|
6388
|
-
|
|
6482
|
+
let text: string;
|
|
6483
|
+
if (useSubquery) {
|
|
6484
|
+
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
6485
|
+
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
6486
|
+
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
6487
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
6488
|
+
} else {
|
|
6489
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
6490
|
+
}
|
|
6389
6491
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
6390
6492
|
|
|
6391
6493
|
const { rows } = await ctx.pg.query(text, allParams);
|
package/dist/index.js
CHANGED
|
@@ -2332,7 +2332,13 @@ const listSchema = z.object({
|
|
|
2332
2332
|
query: z.array(z.number()),
|
|
2333
2333
|
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
2334
2334
|
maxDistance: z.number().optional()
|
|
2335
|
-
}).optional()
|
|
2335
|
+
}).optional(),` : ""}
|
|
2336
|
+
trigram: z.object({
|
|
2337
|
+
field: z.string(),
|
|
2338
|
+
query: z.string(),
|
|
2339
|
+
metric: z.enum(["similarity", "wordSimilarity", "strictWordSimilarity"]).optional(),
|
|
2340
|
+
threshold: z.number().min(0).max(1).optional()
|
|
2341
|
+
}).optional()
|
|
2336
2342
|
}).strict().refine(
|
|
2337
2343
|
(data) => !(data.select && data.exclude),
|
|
2338
2344
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -3003,10 +3009,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3003
3009
|
metric?: "cosine" | "l2" | "inner";
|
|
3004
3010
|
maxDistance?: number;
|
|
3005
3011
|
};` : ""}
|
|
3012
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3006
3013
|
orderBy?: string | string[];
|
|
3007
3014
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3008
3015
|
distinctOn?: string | string[];
|
|
3009
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3016
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3010
3017
|
/**
|
|
3011
3018
|
* List ${table.name} records with field exclusion
|
|
3012
3019
|
* @param params - Query parameters with exclude
|
|
@@ -3024,10 +3031,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3024
3031
|
metric?: "cosine" | "l2" | "inner";
|
|
3025
3032
|
maxDistance?: number;
|
|
3026
3033
|
};` : ""}
|
|
3034
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3027
3035
|
orderBy?: string | string[];
|
|
3028
3036
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3029
3037
|
distinctOn?: string | string[];
|
|
3030
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3038
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3031
3039
|
/**
|
|
3032
3040
|
* List ${table.name} records with pagination and filtering
|
|
3033
3041
|
* @param params - Query parameters
|
|
@@ -3056,10 +3064,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3056
3064
|
metric?: "cosine" | "l2" | "inner";
|
|
3057
3065
|
maxDistance?: number;
|
|
3058
3066
|
};` : ""}
|
|
3067
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3059
3068
|
orderBy?: string | string[];
|
|
3060
3069
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3061
3070
|
distinctOn?: string | string[];
|
|
3062
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
3071
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3063
3072
|
async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
|
|
3064
3073
|
include?: ${Type}IncludeSpec;
|
|
3065
3074
|
select?: string[];
|
|
@@ -3073,11 +3082,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
3073
3082
|
metric?: "cosine" | "l2" | "inner";
|
|
3074
3083
|
maxDistance?: number;
|
|
3075
3084
|
};` : ""}
|
|
3085
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3076
3086
|
orderBy?: string | string[];
|
|
3077
3087
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3078
3088
|
distinctOn?: string | string[];
|
|
3079
3089
|
}): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
|
|
3080
|
-
return this.post<PaginatedResponse<Select${Type}<TJsonb
|
|
3090
|
+
return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3081
3091
|
}` : ` /**
|
|
3082
3092
|
* List ${table.name} records with field selection
|
|
3083
3093
|
* @param params - Query parameters with select
|
|
@@ -3095,10 +3105,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3095
3105
|
metric?: "cosine" | "l2" | "inner";
|
|
3096
3106
|
maxDistance?: number;
|
|
3097
3107
|
};` : ""}
|
|
3108
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3098
3109
|
orderBy?: string | string[];
|
|
3099
3110
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3100
3111
|
distinctOn?: string | string[];
|
|
3101
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
3112
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3102
3113
|
/**
|
|
3103
3114
|
* List ${table.name} records with field exclusion
|
|
3104
3115
|
* @param params - Query parameters with exclude
|
|
@@ -3116,10 +3127,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3116
3127
|
metric?: "cosine" | "l2" | "inner";
|
|
3117
3128
|
maxDistance?: number;
|
|
3118
3129
|
};` : ""}
|
|
3130
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3119
3131
|
orderBy?: string | string[];
|
|
3120
3132
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3121
3133
|
distinctOn?: string | string[];
|
|
3122
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
3134
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3123
3135
|
/**
|
|
3124
3136
|
* List ${table.name} records with pagination and filtering
|
|
3125
3137
|
* @param params - Query parameters
|
|
@@ -3142,10 +3154,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3142
3154
|
metric?: "cosine" | "l2" | "inner";
|
|
3143
3155
|
maxDistance?: number;
|
|
3144
3156
|
};` : ""}
|
|
3157
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3145
3158
|
orderBy?: string | string[];
|
|
3146
3159
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3147
3160
|
distinctOn?: string | string[];
|
|
3148
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
3161
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3149
3162
|
async list(params?: {
|
|
3150
3163
|
include?: ${Type}IncludeSpec;
|
|
3151
3164
|
select?: string[];
|
|
@@ -3159,11 +3172,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
3159
3172
|
metric?: "cosine" | "l2" | "inner";
|
|
3160
3173
|
maxDistance?: number;
|
|
3161
3174
|
};` : ""}
|
|
3175
|
+
trigram?: { field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number };
|
|
3162
3176
|
orderBy?: string | string[];
|
|
3163
3177
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3164
3178
|
distinctOn?: string | string[];
|
|
3165
3179
|
}): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
|
|
3166
|
-
return this.post<PaginatedResponse<Select${Type}${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3180
|
+
return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3167
3181
|
}`}
|
|
3168
3182
|
|
|
3169
3183
|
${hasJsonbColumns ? ` /**
|
|
@@ -4331,6 +4345,12 @@ export type WhereOperator<T> = {
|
|
|
4331
4345
|
$like?: T extends string ? string : never;
|
|
4332
4346
|
/** Case-insensitive LIKE (strings only) */
|
|
4333
4347
|
$ilike?: T extends string ? string : never;
|
|
4348
|
+
/** Trigram similarity match - "col" % value (pg_trgm required, uses similarity_threshold GUC) */
|
|
4349
|
+
$similarity?: T extends string ? string : never;
|
|
4350
|
+
/** Word trigram similarity match - value <% "col" (pg_trgm required) */
|
|
4351
|
+
$wordSimilarity?: T extends string ? string : never;
|
|
4352
|
+
/** Strict word trigram similarity match - value <<% "col" (pg_trgm required) */
|
|
4353
|
+
$strictWordSimilarity?: T extends string ? string : never;
|
|
4334
4354
|
/** IS NULL */
|
|
4335
4355
|
$is?: null;
|
|
4336
4356
|
/** IS NOT NULL */
|
|
@@ -5107,6 +5127,24 @@ function buildWhereClause(
|
|
|
5107
5127
|
whereParams.push(opValue);
|
|
5108
5128
|
paramIndex++;
|
|
5109
5129
|
break;
|
|
5130
|
+
case '$similarity':
|
|
5131
|
+
// pg_trgm: boolean similarity operator (respects similarity_threshold GUC)
|
|
5132
|
+
whereParts.push(\`"\${key}" % $\${paramIndex}\`);
|
|
5133
|
+
whereParams.push(opValue);
|
|
5134
|
+
paramIndex++;
|
|
5135
|
+
break;
|
|
5136
|
+
case '$wordSimilarity':
|
|
5137
|
+
// pg_trgm: word similarity operator (value <% col)
|
|
5138
|
+
whereParts.push(\`$\${paramIndex} <% "\${key}"\`);
|
|
5139
|
+
whereParams.push(opValue);
|
|
5140
|
+
paramIndex++;
|
|
5141
|
+
break;
|
|
5142
|
+
case '$strictWordSimilarity':
|
|
5143
|
+
// pg_trgm: strict word similarity operator (value <<% col)
|
|
5144
|
+
whereParts.push(\`$\${paramIndex} <<% "\${key}"\`);
|
|
5145
|
+
whereParams.push(opValue);
|
|
5146
|
+
paramIndex++;
|
|
5147
|
+
break;
|
|
5110
5148
|
case '$is':
|
|
5111
5149
|
if (opValue === null) {
|
|
5112
5150
|
whereParts.push(\`"\${key}" IS NULL\`);
|
|
@@ -5281,7 +5319,29 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
5281
5319
|
}
|
|
5282
5320
|
|
|
5283
5321
|
/**
|
|
5284
|
-
*
|
|
5322
|
+
* Build the pg_trgm SQL function expression for a given field and metric.
|
|
5323
|
+
* $1 is always the query string parameter.
|
|
5324
|
+
*/
|
|
5325
|
+
function getTrigramFnExpr(field: string, metric?: string): string {
|
|
5326
|
+
switch (metric) {
|
|
5327
|
+
case "wordSimilarity":
|
|
5328
|
+
return \`word_similarity($1, "\${field}")\`;
|
|
5329
|
+
case "strictWordSimilarity":
|
|
5330
|
+
return \`strict_word_similarity($1, "\${field}")\`;
|
|
5331
|
+
case "similarity":
|
|
5332
|
+
default:
|
|
5333
|
+
return \`similarity("\${field}", $1)\`;
|
|
5334
|
+
}
|
|
5335
|
+
}
|
|
5336
|
+
|
|
5337
|
+
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
5338
|
+
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
5339
|
+
if (cols.length === 0) return "";
|
|
5340
|
+
return \`ORDER BY \${cols.map((c, i) => \`"\${c}" \${(dirs[i] ?? "asc").toUpperCase()}\`).join(", ")}\`;
|
|
5341
|
+
}
|
|
5342
|
+
|
|
5343
|
+
/**
|
|
5344
|
+
* LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
|
|
5285
5345
|
*/
|
|
5286
5346
|
export async function listRecords(
|
|
5287
5347
|
ctx: OperationContext,
|
|
@@ -5299,23 +5359,48 @@ export async function listRecords(
|
|
|
5299
5359
|
metric?: "cosine" | "l2" | "inner";
|
|
5300
5360
|
maxDistance?: number;
|
|
5301
5361
|
};
|
|
5362
|
+
trigram?: {
|
|
5363
|
+
field: string;
|
|
5364
|
+
query: string;
|
|
5365
|
+
metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity";
|
|
5366
|
+
threshold?: number;
|
|
5367
|
+
};
|
|
5302
5368
|
}
|
|
5303
5369
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
5304
5370
|
try {
|
|
5305
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, distinctOn } = params;
|
|
5371
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
|
|
5306
5372
|
|
|
5307
5373
|
// DISTINCT ON support
|
|
5308
5374
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
5309
5375
|
const _distinctOnColsSQL = distinctCols ? distinctCols.map(c => '"' + c + '"').join(', ') : '';
|
|
5310
5376
|
|
|
5377
|
+
// Pre-compute user order cols (reused in ORDER BY block and useSubquery check)
|
|
5378
|
+
const userOrderCols: string[] = orderBy ? (Array.isArray(orderBy) ? orderBy : [orderBy]) : [];
|
|
5379
|
+
|
|
5380
|
+
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
5381
|
+
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
5382
|
+
// overriding the requested ordering). Vector/trigram search always stays inline.
|
|
5383
|
+
const useSubquery: boolean =
|
|
5384
|
+
distinctCols !== null &&
|
|
5385
|
+
!vector &&
|
|
5386
|
+
!trigram &&
|
|
5387
|
+
userOrderCols.some(col => !distinctCols.includes(col));
|
|
5388
|
+
|
|
5311
5389
|
// Get distance operator if vector search
|
|
5312
5390
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
5313
5391
|
|
|
5314
|
-
//
|
|
5315
|
-
const
|
|
5392
|
+
// Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
|
|
5393
|
+
const trigramFnExpr = trigram ? getTrigramFnExpr(trigram.field, trigram.metric) : "";
|
|
5316
5394
|
|
|
5317
|
-
//
|
|
5318
|
-
|
|
5395
|
+
// Add vector or trigram query as $1 if present (mutually exclusive)
|
|
5396
|
+
const queryParams: any[] = vector
|
|
5397
|
+
? [JSON.stringify(vector.query)]
|
|
5398
|
+
: trigram
|
|
5399
|
+
? [trigram.query]
|
|
5400
|
+
: [];
|
|
5401
|
+
|
|
5402
|
+
// Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
|
|
5403
|
+
let paramIndex = vector || trigram ? 2 : 1;
|
|
5319
5404
|
const whereParts: string[] = [];
|
|
5320
5405
|
let whereParams: any[] = [];
|
|
5321
5406
|
|
|
@@ -5339,28 +5424,38 @@ export async function listRecords(
|
|
|
5339
5424
|
whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
|
|
5340
5425
|
}
|
|
5341
5426
|
|
|
5427
|
+
// Add trigram threshold filter if specified (inlined as literal — it's a float, safe)
|
|
5428
|
+
if (trigram?.threshold !== undefined) {
|
|
5429
|
+
whereParts.push(\`\${trigramFnExpr} >= \${trigram.threshold}\`);
|
|
5430
|
+
}
|
|
5431
|
+
|
|
5342
5432
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
5343
5433
|
|
|
5344
5434
|
// Build WHERE clause for COUNT query (may need different param indices)
|
|
5345
5435
|
let countWhereSQL = whereSQL;
|
|
5346
5436
|
let countParams = whereParams;
|
|
5347
5437
|
|
|
5348
|
-
|
|
5349
|
-
|
|
5438
|
+
// When a $1 query param exists (vector/trigram) but has NO threshold filter, the COUNT query
|
|
5439
|
+
// must not reference $1 since it's only needed in SELECT/ORDER BY. Rebuild WHERE from scratch
|
|
5440
|
+
// starting at $1. When a threshold IS set, $1 appears in the WHERE condition so keep it.
|
|
5441
|
+
const hasQueryParam = !!(vector || trigram);
|
|
5442
|
+
const hasThreshold = vector?.maxDistance !== undefined || trigram?.threshold !== undefined;
|
|
5443
|
+
|
|
5444
|
+
if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
|
|
5350
5445
|
const countWhereParts: string[] = [];
|
|
5351
5446
|
if (ctx.softDeleteColumn) {
|
|
5352
5447
|
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
5353
5448
|
}
|
|
5354
5449
|
if (whereClause) {
|
|
5355
|
-
const result = buildWhereClause(whereClause, 1); // Start at $1
|
|
5450
|
+
const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
|
|
5356
5451
|
if (result.sql) {
|
|
5357
5452
|
countWhereParts.push(result.sql);
|
|
5358
5453
|
countParams = result.params;
|
|
5359
5454
|
}
|
|
5360
5455
|
}
|
|
5361
5456
|
countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
|
|
5362
|
-
} else if (
|
|
5363
|
-
//
|
|
5457
|
+
} else if (hasQueryParam && hasThreshold) {
|
|
5458
|
+
// $1 appears in WHERE threshold condition — COUNT needs the full params
|
|
5364
5459
|
countParams = [...queryParams, ...whereParams];
|
|
5365
5460
|
}
|
|
5366
5461
|
|
|
@@ -5369,45 +5464,44 @@ export async function listRecords(
|
|
|
5369
5464
|
const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
5370
5465
|
const selectClause = vector
|
|
5371
5466
|
? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
|
|
5467
|
+
: trigram
|
|
5468
|
+
? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
|
|
5372
5469
|
: \`\${_distinctOnPrefix}\${baseColumns}\`;
|
|
5373
5470
|
|
|
5374
5471
|
// Build ORDER BY clause
|
|
5375
5472
|
let orderBySQL = "";
|
|
5473
|
+
const userDirs: ("asc" | "desc")[] = userOrderCols.length > 0
|
|
5474
|
+
? (Array.isArray(order) ? order : (order ? Array(userOrderCols.length).fill(order) : Array(userOrderCols.length).fill("asc")))
|
|
5475
|
+
: [];
|
|
5476
|
+
|
|
5376
5477
|
if (vector) {
|
|
5377
|
-
//
|
|
5478
|
+
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
5378
5479
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
5379
|
-
} else {
|
|
5380
|
-
|
|
5381
|
-
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
|
|
5480
|
+
} else if (trigram) {
|
|
5481
|
+
// Trigram search orders by similarity score descending
|
|
5482
|
+
orderBySQL = \`ORDER BY _similarity DESC\`;
|
|
5483
|
+
} else if (useSubquery) {
|
|
5484
|
+
// Subquery form: outer query gets the user's full ORDER BY.
|
|
5485
|
+
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
|
5486
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
5487
|
+
} else if (distinctCols) {
|
|
5488
|
+
// Inline DISTINCT ON: prepend distinctCols as the leftmost ORDER BY prefix (PG requirement)
|
|
5385
5489
|
const finalCols: string[] = [];
|
|
5386
|
-
const finalDirs:
|
|
5387
|
-
|
|
5388
|
-
|
|
5389
|
-
|
|
5390
|
-
|
|
5391
|
-
const userIdx = userCols.indexOf(col);
|
|
5392
|
-
finalCols.push(col);
|
|
5393
|
-
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] || "asc") : "asc");
|
|
5394
|
-
}
|
|
5395
|
-
// Append remaining user-specified cols not already covered by distinctOn
|
|
5396
|
-
for (let i = 0; i < userCols.length; i++) {
|
|
5397
|
-
if (!distinctCols.includes(userCols[i]!)) {
|
|
5398
|
-
finalCols.push(userCols[i]!);
|
|
5399
|
-
finalDirs.push(userDirs[i] || "asc");
|
|
5400
|
-
}
|
|
5401
|
-
}
|
|
5402
|
-
} else {
|
|
5403
|
-
finalCols.push(...userCols);
|
|
5404
|
-
finalDirs.push(...userDirs.map(d => d || "asc"));
|
|
5490
|
+
const finalDirs: ("asc" | "desc")[] = [];
|
|
5491
|
+
for (const col of distinctCols) {
|
|
5492
|
+
const userIdx = userOrderCols.indexOf(col);
|
|
5493
|
+
finalCols.push(col);
|
|
5494
|
+
finalDirs.push(userIdx >= 0 ? (userDirs[userIdx] ?? "asc") : "asc");
|
|
5405
5495
|
}
|
|
5406
|
-
|
|
5407
|
-
|
|
5408
|
-
|
|
5409
|
-
|
|
5496
|
+
for (let i = 0; i < userOrderCols.length; i++) {
|
|
5497
|
+
if (!distinctCols.includes(userOrderCols[i]!)) {
|
|
5498
|
+
finalCols.push(userOrderCols[i]!);
|
|
5499
|
+
finalDirs.push(userDirs[i] ?? "asc");
|
|
5500
|
+
}
|
|
5410
5501
|
}
|
|
5502
|
+
orderBySQL = buildOrderBySQL(finalCols, finalDirs);
|
|
5503
|
+
} else {
|
|
5504
|
+
orderBySQL = buildOrderBySQL(userOrderCols, userDirs);
|
|
5411
5505
|
}
|
|
5412
5506
|
|
|
5413
5507
|
// Add limit and offset params
|
|
@@ -5424,7 +5518,15 @@ export async function listRecords(
|
|
|
5424
5518
|
const total = parseInt(countResult.rows[0].count, 10);
|
|
5425
5519
|
|
|
5426
5520
|
// Get paginated data
|
|
5427
|
-
|
|
5521
|
+
let text: string;
|
|
5522
|
+
if (useSubquery) {
|
|
5523
|
+
// Inner query: DISTINCT ON with only the distinctCols ORDER BY prefix (PG requirement).
|
|
5524
|
+
// Outer query: free ORDER BY from the user's full orderBy list, plus LIMIT/OFFSET.
|
|
5525
|
+
const innerQuery = \`SELECT DISTINCT ON (\${_distinctOnColsSQL}) \${baseColumns} FROM "\${ctx.table}" \${whereSQL} ORDER BY \${_distinctOnColsSQL}\`;
|
|
5526
|
+
text = \`SELECT * FROM (\${innerQuery}) __distinct \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
5527
|
+
} else {
|
|
5528
|
+
text = \`SELECT \${selectClause} FROM "\${ctx.table}" \${whereSQL} \${orderBySQL} LIMIT \${limitParam} OFFSET \${offsetParam}\`;
|
|
5529
|
+
}
|
|
5428
5530
|
log.debug(\`LIST \${ctx.table} SQL:\`, text, "params:", allParams);
|
|
5429
5531
|
|
|
5430
5532
|
const { rows } = await ctx.pg.query(text, allParams);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.20",
|
|
4
4
|
"description": "Generate a typed server/client SDK from a Postgres schema (includes, Zod, Hono).",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
},
|
|
23
23
|
"scripts": {
|
|
24
24
|
"build": "bun build src/cli.ts src/index.ts --outdir dist --target node --format esm --external=pg --external=zod --external=hono --external=prompts --external=node:* && tsc -p tsconfig.build.json --emitDeclarationOnly",
|
|
25
|
-
"test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts",
|
|
25
|
+
"test": "bun test:write-files && bun test:init && bun test:gen && bun test test/test-where-clause.test.ts && bun test test/test-where-or-and.test.ts && bun test test/test-nested-include-options.test.ts && bun test test/test-include-methods-with-options.test.ts && bun test:gen-with-tests && bun test:pull && bun test:enums && bun test:typecheck && bun test:drizzle-e2e && bun test test/test-numeric-mode-integration.test.ts && bun test test/test-jsonb-array-serialization.test.ts && bun test test/test-trigram-search.test.ts",
|
|
26
26
|
"test:write-files": "bun test/test-write-files-if-changed.ts",
|
|
27
27
|
"test:init": "bun test/test-init.ts",
|
|
28
28
|
"test:gen": "bun test/test-gen.ts",
|