postgresdk 0.18.19 → 0.18.21
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 +84 -1
- package/dist/cli.js +148 -22
- package/dist/index.js +148 -22
- 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,80 @@ 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
|
+
**Multi-field trigram search:**
|
|
964
|
+
```typescript
|
|
965
|
+
// Greatest strategy (default): score = GREATEST(sim(name), sim(url))
|
|
966
|
+
const results = await sdk.websites.list({
|
|
967
|
+
trigram: { fields: ["name", "url"], query: "google", strategy: "greatest" }
|
|
968
|
+
});
|
|
969
|
+
|
|
970
|
+
// Concat strategy: concatenate fields before scoring ("name url")
|
|
971
|
+
const results = await sdk.websites.list({
|
|
972
|
+
trigram: { fields: ["name", "url"], query: "google", strategy: "concat" }
|
|
973
|
+
});
|
|
974
|
+
|
|
975
|
+
// Weighted strategy: weighted average of per-field scores
|
|
976
|
+
const results = await sdk.websites.list({
|
|
977
|
+
trigram: {
|
|
978
|
+
fields: [{ field: "name", weight: 2 }, { field: "url", weight: 1 }],
|
|
979
|
+
query: "google"
|
|
980
|
+
}
|
|
981
|
+
});
|
|
982
|
+
```
|
|
983
|
+
|
|
984
|
+
**Note:** `trigram` and `vector` are mutually exclusive on a single `list()` call.
|
|
985
|
+
|
|
986
|
+
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
987
|
|
|
905
988
|
---
|
|
906
989
|
|
package/dist/cli.js
CHANGED
|
@@ -3228,6 +3228,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
3228
3228
|
const fileTableName = table.name;
|
|
3229
3229
|
const Type = pascal(table.name);
|
|
3230
3230
|
const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
|
|
3231
|
+
const trigramMetricZod = `z.enum(["similarity", "wordSimilarity", "strictWordSimilarity"]).optional()`;
|
|
3231
3232
|
const vectorColumns = table.columns.filter((c) => isVectorType(c.pgType)).map((c) => c.name);
|
|
3232
3233
|
const jsonbColumns = table.columns.filter((c) => isJsonbType(c.pgType)).map((c) => c.name);
|
|
3233
3234
|
const rawPk = table.pk;
|
|
@@ -3293,7 +3294,28 @@ const listSchema = z.object({
|
|
|
3293
3294
|
query: z.array(z.number()),
|
|
3294
3295
|
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
3295
3296
|
maxDistance: z.number().optional()
|
|
3296
|
-
}).optional()
|
|
3297
|
+
}).optional(),` : ""}
|
|
3298
|
+
trigram: z.union([
|
|
3299
|
+
z.object({
|
|
3300
|
+
field: z.string(),
|
|
3301
|
+
query: z.string(),
|
|
3302
|
+
metric: ${trigramMetricZod},
|
|
3303
|
+
threshold: z.number().min(0).max(1).optional()
|
|
3304
|
+
}),
|
|
3305
|
+
z.object({
|
|
3306
|
+
fields: z.array(z.string()).min(1),
|
|
3307
|
+
strategy: z.enum(["greatest", "concat"]).optional(),
|
|
3308
|
+
query: z.string(),
|
|
3309
|
+
metric: ${trigramMetricZod},
|
|
3310
|
+
threshold: z.number().min(0).max(1).optional()
|
|
3311
|
+
}),
|
|
3312
|
+
z.object({
|
|
3313
|
+
fields: z.array(z.object({ field: z.string(), weight: z.number().positive() })).min(1),
|
|
3314
|
+
query: z.string(),
|
|
3315
|
+
metric: ${trigramMetricZod},
|
|
3316
|
+
threshold: z.number().min(0).max(1).optional()
|
|
3317
|
+
})
|
|
3318
|
+
]).optional()
|
|
3297
3319
|
}).strict().refine(
|
|
3298
3320
|
(data) => !(data.select && data.exclude),
|
|
3299
3321
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -3561,6 +3583,7 @@ function analyzeIncludeSpec(includeSpec) {
|
|
|
3561
3583
|
function emitClient(table, graph, opts, model) {
|
|
3562
3584
|
const Type = pascal(table.name);
|
|
3563
3585
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
3586
|
+
const trigramParamType = `{ field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: string[]; strategy?: "greatest" | "concat"; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: Array<{ field: string; weight: number }>; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number }`;
|
|
3564
3587
|
const hasVectorColumns = table.columns.some((c) => isVectorType2(c.pgType));
|
|
3565
3588
|
const hasJsonbColumns = table.columns.some((c) => isJsonbType2(c.pgType));
|
|
3566
3589
|
if (process.env.SDK_DEBUG) {
|
|
@@ -3964,10 +3987,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3964
3987
|
metric?: "cosine" | "l2" | "inner";
|
|
3965
3988
|
maxDistance?: number;
|
|
3966
3989
|
};` : ""}
|
|
3990
|
+
trigram?: ${trigramParamType};
|
|
3967
3991
|
orderBy?: string | string[];
|
|
3968
3992
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3969
3993
|
distinctOn?: string | string[];
|
|
3970
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3994
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3971
3995
|
/**
|
|
3972
3996
|
* List ${table.name} records with field exclusion
|
|
3973
3997
|
* @param params - Query parameters with exclude
|
|
@@ -3985,10 +4009,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3985
4009
|
metric?: "cosine" | "l2" | "inner";
|
|
3986
4010
|
maxDistance?: number;
|
|
3987
4011
|
};` : ""}
|
|
4012
|
+
trigram?: ${trigramParamType};
|
|
3988
4013
|
orderBy?: string | string[];
|
|
3989
4014
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3990
4015
|
distinctOn?: string | string[];
|
|
3991
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
4016
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3992
4017
|
/**
|
|
3993
4018
|
* List ${table.name} records with pagination and filtering
|
|
3994
4019
|
* @param params - Query parameters
|
|
@@ -4017,10 +4042,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4017
4042
|
metric?: "cosine" | "l2" | "inner";
|
|
4018
4043
|
maxDistance?: number;
|
|
4019
4044
|
};` : ""}
|
|
4045
|
+
trigram?: ${trigramParamType};
|
|
4020
4046
|
orderBy?: string | string[];
|
|
4021
4047
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4022
4048
|
distinctOn?: string | string[];
|
|
4023
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
4049
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4024
4050
|
async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
|
|
4025
4051
|
include?: ${Type}IncludeSpec;
|
|
4026
4052
|
select?: string[];
|
|
@@ -4034,11 +4060,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
4034
4060
|
metric?: "cosine" | "l2" | "inner";
|
|
4035
4061
|
maxDistance?: number;
|
|
4036
4062
|
};` : ""}
|
|
4063
|
+
trigram?: ${trigramParamType};
|
|
4037
4064
|
orderBy?: string | string[];
|
|
4038
4065
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4039
4066
|
distinctOn?: string | string[];
|
|
4040
4067
|
}): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
|
|
4041
|
-
return this.post<PaginatedResponse<Select${Type}<TJsonb
|
|
4068
|
+
return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4042
4069
|
}` : ` /**
|
|
4043
4070
|
* List ${table.name} records with field selection
|
|
4044
4071
|
* @param params - Query parameters with select
|
|
@@ -4056,10 +4083,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4056
4083
|
metric?: "cosine" | "l2" | "inner";
|
|
4057
4084
|
maxDistance?: number;
|
|
4058
4085
|
};` : ""}
|
|
4086
|
+
trigram?: ${trigramParamType};
|
|
4059
4087
|
orderBy?: string | string[];
|
|
4060
4088
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4061
4089
|
distinctOn?: string | string[];
|
|
4062
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
4090
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4063
4091
|
/**
|
|
4064
4092
|
* List ${table.name} records with field exclusion
|
|
4065
4093
|
* @param params - Query parameters with exclude
|
|
@@ -4077,10 +4105,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4077
4105
|
metric?: "cosine" | "l2" | "inner";
|
|
4078
4106
|
maxDistance?: number;
|
|
4079
4107
|
};` : ""}
|
|
4108
|
+
trigram?: ${trigramParamType};
|
|
4080
4109
|
orderBy?: string | string[];
|
|
4081
4110
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4082
4111
|
distinctOn?: string | string[];
|
|
4083
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
4112
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4084
4113
|
/**
|
|
4085
4114
|
* List ${table.name} records with pagination and filtering
|
|
4086
4115
|
* @param params - Query parameters
|
|
@@ -4103,10 +4132,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
4103
4132
|
metric?: "cosine" | "l2" | "inner";
|
|
4104
4133
|
maxDistance?: number;
|
|
4105
4134
|
};` : ""}
|
|
4135
|
+
trigram?: ${trigramParamType};
|
|
4106
4136
|
orderBy?: string | string[];
|
|
4107
4137
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4108
4138
|
distinctOn?: string | string[];
|
|
4109
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
4139
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
4110
4140
|
async list(params?: {
|
|
4111
4141
|
include?: ${Type}IncludeSpec;
|
|
4112
4142
|
select?: string[];
|
|
@@ -4120,11 +4150,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
4120
4150
|
metric?: "cosine" | "l2" | "inner";
|
|
4121
4151
|
maxDistance?: number;
|
|
4122
4152
|
};` : ""}
|
|
4153
|
+
trigram?: ${trigramParamType};
|
|
4123
4154
|
orderBy?: string | string[];
|
|
4124
4155
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
4125
4156
|
distinctOn?: string | string[];
|
|
4126
4157
|
}): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
|
|
4127
|
-
return this.post<PaginatedResponse<Select${Type}${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4158
|
+
return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
4128
4159
|
}`}
|
|
4129
4160
|
|
|
4130
4161
|
${hasJsonbColumns ? ` /**
|
|
@@ -5292,6 +5323,12 @@ export type WhereOperator<T> = {
|
|
|
5292
5323
|
$like?: T extends string ? string : never;
|
|
5293
5324
|
/** Case-insensitive LIKE (strings only) */
|
|
5294
5325
|
$ilike?: T extends string ? string : never;
|
|
5326
|
+
/** Trigram similarity match - "col" % value (pg_trgm required, uses similarity_threshold GUC) */
|
|
5327
|
+
$similarity?: T extends string ? string : never;
|
|
5328
|
+
/** Word trigram similarity match - value <% "col" (pg_trgm required) */
|
|
5329
|
+
$wordSimilarity?: T extends string ? string : never;
|
|
5330
|
+
/** Strict word trigram similarity match - value <<% "col" (pg_trgm required) */
|
|
5331
|
+
$strictWordSimilarity?: T extends string ? string : never;
|
|
5295
5332
|
/** IS NULL */
|
|
5296
5333
|
$is?: null;
|
|
5297
5334
|
/** IS NOT NULL */
|
|
@@ -5908,7 +5945,7 @@ export async function createRecord(
|
|
|
5908
5945
|
|
|
5909
5946
|
const preparedVals = vals.map((v, i) =>
|
|
5910
5947
|
v !== null && v !== undefined && typeof v === 'object' &&
|
|
5911
|
-
(ctx.jsonbColumns?.includes(cols[i]) || ctx.vectorColumns?.includes(cols[i]))
|
|
5948
|
+
(ctx.jsonbColumns?.includes(cols[i]!) || ctx.vectorColumns?.includes(cols[i]!))
|
|
5912
5949
|
? JSON.stringify(v)
|
|
5913
5950
|
: v
|
|
5914
5951
|
);
|
|
@@ -6068,6 +6105,24 @@ function buildWhereClause(
|
|
|
6068
6105
|
whereParams.push(opValue);
|
|
6069
6106
|
paramIndex++;
|
|
6070
6107
|
break;
|
|
6108
|
+
case '$similarity':
|
|
6109
|
+
// pg_trgm: boolean similarity operator (respects similarity_threshold GUC)
|
|
6110
|
+
whereParts.push(\`"\${key}" % $\${paramIndex}\`);
|
|
6111
|
+
whereParams.push(opValue);
|
|
6112
|
+
paramIndex++;
|
|
6113
|
+
break;
|
|
6114
|
+
case '$wordSimilarity':
|
|
6115
|
+
// pg_trgm: word similarity operator (value <% col)
|
|
6116
|
+
whereParts.push(\`$\${paramIndex} <% "\${key}"\`);
|
|
6117
|
+
whereParams.push(opValue);
|
|
6118
|
+
paramIndex++;
|
|
6119
|
+
break;
|
|
6120
|
+
case '$strictWordSimilarity':
|
|
6121
|
+
// pg_trgm: strict word similarity operator (value <<% col)
|
|
6122
|
+
whereParts.push(\`$\${paramIndex} <<% "\${key}"\`);
|
|
6123
|
+
whereParams.push(opValue);
|
|
6124
|
+
paramIndex++;
|
|
6125
|
+
break;
|
|
6071
6126
|
case '$is':
|
|
6072
6127
|
if (opValue === null) {
|
|
6073
6128
|
whereParts.push(\`"\${key}" IS NULL\`);
|
|
@@ -6241,6 +6296,53 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
6241
6296
|
}
|
|
6242
6297
|
}
|
|
6243
6298
|
|
|
6299
|
+
export type TrigramMetric = "similarity" | "wordSimilarity" | "strictWordSimilarity";
|
|
6300
|
+
|
|
6301
|
+
export type TrigramParam =
|
|
6302
|
+
| { field: string; query: string; metric?: TrigramMetric; threshold?: number }
|
|
6303
|
+
| { fields: string[]; strategy?: "greatest" | "concat"; query: string; metric?: TrigramMetric; threshold?: number }
|
|
6304
|
+
| { fields: Array<{ field: string; weight: number }>; query: string; metric?: TrigramMetric; threshold?: number };
|
|
6305
|
+
|
|
6306
|
+
/**
|
|
6307
|
+
* Build a pg_trgm function expression for a raw SQL field expression.
|
|
6308
|
+
* $1 is always the query string parameter.
|
|
6309
|
+
*/
|
|
6310
|
+
function trigramFnRaw(fieldExpr: string, metric?: TrigramMetric): string {
|
|
6311
|
+
switch (metric) {
|
|
6312
|
+
case "wordSimilarity": return \`word_similarity($1, \${fieldExpr})\`;
|
|
6313
|
+
case "strictWordSimilarity": return \`strict_word_similarity($1, \${fieldExpr})\`;
|
|
6314
|
+
default: return \`similarity(\${fieldExpr}, $1)\`;
|
|
6315
|
+
}
|
|
6316
|
+
}
|
|
6317
|
+
|
|
6318
|
+
/** Build the full pg_trgm SQL expression for the given trigram param. */
|
|
6319
|
+
function getTrigramFnExpr(trigram: TrigramParam): string {
|
|
6320
|
+
if ("field" in trigram) {
|
|
6321
|
+
return trigramFnRaw(\`"\${trigram.field}"\`, trigram.metric);
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
const { fields, metric } = trigram;
|
|
6325
|
+
|
|
6326
|
+
// Weighted: fields is Array<{ field, weight }>
|
|
6327
|
+
if (fields.length > 0 && typeof fields[0] === "object" && "weight" in fields[0]) {
|
|
6328
|
+
const wfields = fields as Array<{ field: string; weight: number }>;
|
|
6329
|
+
const totalWeight = wfields.reduce((sum, f) => sum + f.weight, 0);
|
|
6330
|
+
const terms = wfields.map(f => \`\${trigramFnRaw(\`"\${f.field}"\`, metric)} * \${f.weight}\`).join(" + ");
|
|
6331
|
+
return \`(\${terms}) / \${totalWeight}\`;
|
|
6332
|
+
}
|
|
6333
|
+
|
|
6334
|
+
const sfields = fields as string[];
|
|
6335
|
+
const strategy = (trigram as Extract<TrigramParam, { fields: string[] }>).strategy ?? "greatest";
|
|
6336
|
+
|
|
6337
|
+
if (strategy === "concat") {
|
|
6338
|
+
const concatExpr = sfields.map(f => \`"\${f}"\`).join(\` || ' ' || \`);
|
|
6339
|
+
return trigramFnRaw(concatExpr, metric);
|
|
6340
|
+
}
|
|
6341
|
+
|
|
6342
|
+
// greatest (default)
|
|
6343
|
+
return \`GREATEST(\${sfields.map(f => trigramFnRaw(\`"\${f}"\`, metric)).join(", ")})\`;
|
|
6344
|
+
}
|
|
6345
|
+
|
|
6244
6346
|
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
6245
6347
|
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
6246
6348
|
if (cols.length === 0) return "";
|
|
@@ -6248,7 +6350,7 @@ function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
|
6248
6350
|
}
|
|
6249
6351
|
|
|
6250
6352
|
/**
|
|
6251
|
-
* LIST operation - Get multiple records with optional filters and
|
|
6353
|
+
* LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
|
|
6252
6354
|
*/
|
|
6253
6355
|
export async function listRecords(
|
|
6254
6356
|
ctx: OperationContext,
|
|
@@ -6266,10 +6368,11 @@ export async function listRecords(
|
|
|
6266
6368
|
metric?: "cosine" | "l2" | "inner";
|
|
6267
6369
|
maxDistance?: number;
|
|
6268
6370
|
};
|
|
6371
|
+
trigram?: TrigramParam;
|
|
6269
6372
|
}
|
|
6270
6373
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
6271
6374
|
try {
|
|
6272
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, distinctOn } = params;
|
|
6375
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
|
|
6273
6376
|
|
|
6274
6377
|
// DISTINCT ON support
|
|
6275
6378
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
@@ -6280,20 +6383,28 @@ export async function listRecords(
|
|
|
6280
6383
|
|
|
6281
6384
|
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
6282
6385
|
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
6283
|
-
// overriding the requested ordering). Vector search always stays inline.
|
|
6386
|
+
// overriding the requested ordering). Vector/trigram search always stays inline.
|
|
6284
6387
|
const useSubquery: boolean =
|
|
6285
6388
|
distinctCols !== null &&
|
|
6286
6389
|
!vector &&
|
|
6390
|
+
!trigram &&
|
|
6287
6391
|
userOrderCols.some(col => !distinctCols.includes(col));
|
|
6288
6392
|
|
|
6289
6393
|
// Get distance operator if vector search
|
|
6290
6394
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
6291
6395
|
|
|
6292
|
-
//
|
|
6293
|
-
const
|
|
6396
|
+
// Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
|
|
6397
|
+
const trigramFnExpr = trigram ? getTrigramFnExpr(trigram) : "";
|
|
6398
|
+
|
|
6399
|
+
// Add vector or trigram query as $1 if present (mutually exclusive)
|
|
6400
|
+
const queryParams: any[] = vector
|
|
6401
|
+
? [JSON.stringify(vector.query)]
|
|
6402
|
+
: trigram
|
|
6403
|
+
? [trigram.query]
|
|
6404
|
+
: [];
|
|
6294
6405
|
|
|
6295
|
-
// Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
|
|
6296
|
-
let paramIndex = vector ? 2 : 1;
|
|
6406
|
+
// Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
|
|
6407
|
+
let paramIndex = vector || trigram ? 2 : 1;
|
|
6297
6408
|
const whereParts: string[] = [];
|
|
6298
6409
|
let whereParams: any[] = [];
|
|
6299
6410
|
|
|
@@ -6317,28 +6428,38 @@ export async function listRecords(
|
|
|
6317
6428
|
whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
|
|
6318
6429
|
}
|
|
6319
6430
|
|
|
6431
|
+
// Add trigram threshold filter if specified (inlined as literal — it's a float, safe)
|
|
6432
|
+
if (trigram?.threshold !== undefined) {
|
|
6433
|
+
whereParts.push(\`\${trigramFnExpr} >= \${trigram.threshold}\`);
|
|
6434
|
+
}
|
|
6435
|
+
|
|
6320
6436
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
6321
6437
|
|
|
6322
6438
|
// Build WHERE clause for COUNT query (may need different param indices)
|
|
6323
6439
|
let countWhereSQL = whereSQL;
|
|
6324
6440
|
let countParams = whereParams;
|
|
6325
6441
|
|
|
6326
|
-
|
|
6327
|
-
|
|
6442
|
+
// When a $1 query param exists (vector/trigram) but has NO threshold filter, the COUNT query
|
|
6443
|
+
// must not reference $1 since it's only needed in SELECT/ORDER BY. Rebuild WHERE from scratch
|
|
6444
|
+
// starting at $1. When a threshold IS set, $1 appears in the WHERE condition so keep it.
|
|
6445
|
+
const hasQueryParam = !!(vector || trigram);
|
|
6446
|
+
const hasThreshold = vector?.maxDistance !== undefined || trigram?.threshold !== undefined;
|
|
6447
|
+
|
|
6448
|
+
if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
|
|
6328
6449
|
const countWhereParts: string[] = [];
|
|
6329
6450
|
if (ctx.softDeleteColumn) {
|
|
6330
6451
|
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
6331
6452
|
}
|
|
6332
6453
|
if (whereClause) {
|
|
6333
|
-
const result = buildWhereClause(whereClause, 1); // Start at $1
|
|
6454
|
+
const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
|
|
6334
6455
|
if (result.sql) {
|
|
6335
6456
|
countWhereParts.push(result.sql);
|
|
6336
6457
|
countParams = result.params;
|
|
6337
6458
|
}
|
|
6338
6459
|
}
|
|
6339
6460
|
countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
|
|
6340
|
-
} else if (
|
|
6341
|
-
//
|
|
6461
|
+
} else if (hasQueryParam && hasThreshold) {
|
|
6462
|
+
// $1 appears in WHERE threshold condition — COUNT needs the full params
|
|
6342
6463
|
countParams = [...queryParams, ...whereParams];
|
|
6343
6464
|
}
|
|
6344
6465
|
|
|
@@ -6347,6 +6468,8 @@ export async function listRecords(
|
|
|
6347
6468
|
const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
6348
6469
|
const selectClause = vector
|
|
6349
6470
|
? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
|
|
6471
|
+
: trigram
|
|
6472
|
+
? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
|
|
6350
6473
|
: \`\${_distinctOnPrefix}\${baseColumns}\`;
|
|
6351
6474
|
|
|
6352
6475
|
// Build ORDER BY clause
|
|
@@ -6358,6 +6481,9 @@ export async function listRecords(
|
|
|
6358
6481
|
if (vector) {
|
|
6359
6482
|
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
6360
6483
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
6484
|
+
} else if (trigram) {
|
|
6485
|
+
// Trigram search orders by similarity score descending
|
|
6486
|
+
orderBySQL = \`ORDER BY _similarity DESC\`;
|
|
6361
6487
|
} else if (useSubquery) {
|
|
6362
6488
|
// Subquery form: outer query gets the user's full ORDER BY.
|
|
6363
6489
|
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
package/dist/index.js
CHANGED
|
@@ -2267,6 +2267,7 @@ function emitHonoRoutes(table, _graph, opts) {
|
|
|
2267
2267
|
const fileTableName = table.name;
|
|
2268
2268
|
const Type = pascal(table.name);
|
|
2269
2269
|
const hasVectorColumns = table.columns.some((c) => isVectorType(c.pgType));
|
|
2270
|
+
const trigramMetricZod = `z.enum(["similarity", "wordSimilarity", "strictWordSimilarity"]).optional()`;
|
|
2270
2271
|
const vectorColumns = table.columns.filter((c) => isVectorType(c.pgType)).map((c) => c.name);
|
|
2271
2272
|
const jsonbColumns = table.columns.filter((c) => isJsonbType(c.pgType)).map((c) => c.name);
|
|
2272
2273
|
const rawPk = table.pk;
|
|
@@ -2332,7 +2333,28 @@ const listSchema = z.object({
|
|
|
2332
2333
|
query: z.array(z.number()),
|
|
2333
2334
|
metric: z.enum(["cosine", "l2", "inner"]).optional(),
|
|
2334
2335
|
maxDistance: z.number().optional()
|
|
2335
|
-
}).optional()
|
|
2336
|
+
}).optional(),` : ""}
|
|
2337
|
+
trigram: z.union([
|
|
2338
|
+
z.object({
|
|
2339
|
+
field: z.string(),
|
|
2340
|
+
query: z.string(),
|
|
2341
|
+
metric: ${trigramMetricZod},
|
|
2342
|
+
threshold: z.number().min(0).max(1).optional()
|
|
2343
|
+
}),
|
|
2344
|
+
z.object({
|
|
2345
|
+
fields: z.array(z.string()).min(1),
|
|
2346
|
+
strategy: z.enum(["greatest", "concat"]).optional(),
|
|
2347
|
+
query: z.string(),
|
|
2348
|
+
metric: ${trigramMetricZod},
|
|
2349
|
+
threshold: z.number().min(0).max(1).optional()
|
|
2350
|
+
}),
|
|
2351
|
+
z.object({
|
|
2352
|
+
fields: z.array(z.object({ field: z.string(), weight: z.number().positive() })).min(1),
|
|
2353
|
+
query: z.string(),
|
|
2354
|
+
metric: ${trigramMetricZod},
|
|
2355
|
+
threshold: z.number().min(0).max(1).optional()
|
|
2356
|
+
})
|
|
2357
|
+
]).optional()
|
|
2336
2358
|
}).strict().refine(
|
|
2337
2359
|
(data) => !(data.select && data.exclude),
|
|
2338
2360
|
{ message: "Cannot specify both 'select' and 'exclude' parameters" }
|
|
@@ -2600,6 +2622,7 @@ function analyzeIncludeSpec(includeSpec) {
|
|
|
2600
2622
|
function emitClient(table, graph, opts, model) {
|
|
2601
2623
|
const Type = pascal(table.name);
|
|
2602
2624
|
const ext = opts.useJsExtensions ? ".js" : "";
|
|
2625
|
+
const trigramParamType = `{ field: string; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: string[]; strategy?: "greatest" | "concat"; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number } | { fields: Array<{ field: string; weight: number }>; query: string; metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity"; threshold?: number }`;
|
|
2603
2626
|
const hasVectorColumns = table.columns.some((c) => isVectorType2(c.pgType));
|
|
2604
2627
|
const hasJsonbColumns = table.columns.some((c) => isJsonbType2(c.pgType));
|
|
2605
2628
|
if (process.env.SDK_DEBUG) {
|
|
@@ -3003,10 +3026,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3003
3026
|
metric?: "cosine" | "l2" | "inner";
|
|
3004
3027
|
maxDistance?: number;
|
|
3005
3028
|
};` : ""}
|
|
3029
|
+
trigram?: ${trigramParamType};
|
|
3006
3030
|
orderBy?: string | string[];
|
|
3007
3031
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3008
3032
|
distinctOn?: string | string[];
|
|
3009
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3033
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3010
3034
|
/**
|
|
3011
3035
|
* List ${table.name} records with field exclusion
|
|
3012
3036
|
* @param params - Query parameters with exclude
|
|
@@ -3024,10 +3048,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3024
3048
|
metric?: "cosine" | "l2" | "inner";
|
|
3025
3049
|
maxDistance?: number;
|
|
3026
3050
|
};` : ""}
|
|
3051
|
+
trigram?: ${trigramParamType};
|
|
3027
3052
|
orderBy?: string | string[];
|
|
3028
3053
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3029
3054
|
distinctOn?: string | string[];
|
|
3030
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb
|
|
3055
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}<TJsonb>> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3031
3056
|
/**
|
|
3032
3057
|
* List ${table.name} records with pagination and filtering
|
|
3033
3058
|
* @param params - Query parameters
|
|
@@ -3056,10 +3081,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3056
3081
|
metric?: "cosine" | "l2" | "inner";
|
|
3057
3082
|
maxDistance?: number;
|
|
3058
3083
|
};` : ""}
|
|
3084
|
+
trigram?: ${trigramParamType};
|
|
3059
3085
|
orderBy?: string | string[];
|
|
3060
3086
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3061
3087
|
distinctOn?: string | string[];
|
|
3062
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
3088
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3063
3089
|
async list<TJsonb extends Partial<Select${Type}> = {}>(params?: {
|
|
3064
3090
|
include?: ${Type}IncludeSpec;
|
|
3065
3091
|
select?: string[];
|
|
@@ -3073,11 +3099,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
3073
3099
|
metric?: "cosine" | "l2" | "inner";
|
|
3074
3100
|
maxDistance?: number;
|
|
3075
3101
|
};` : ""}
|
|
3102
|
+
trigram?: ${trigramParamType};
|
|
3076
3103
|
orderBy?: string | string[];
|
|
3077
3104
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3078
3105
|
distinctOn?: string | string[];
|
|
3079
3106
|
}): Promise<PaginatedResponse<Select${Type}<TJsonb> | Partial<Select${Type}<TJsonb>>>> {
|
|
3080
|
-
return this.post<PaginatedResponse<Select${Type}<TJsonb
|
|
3107
|
+
return this.post<PaginatedResponse<Select${Type}<TJsonb> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3081
3108
|
}` : ` /**
|
|
3082
3109
|
* List ${table.name} records with field selection
|
|
3083
3110
|
* @param params - Query parameters with select
|
|
@@ -3095,10 +3122,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3095
3122
|
metric?: "cosine" | "l2" | "inner";
|
|
3096
3123
|
maxDistance?: number;
|
|
3097
3124
|
};` : ""}
|
|
3125
|
+
trigram?: ${trigramParamType};
|
|
3098
3126
|
orderBy?: string | string[];
|
|
3099
3127
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3100
3128
|
distinctOn?: string | string[];
|
|
3101
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
3129
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3102
3130
|
/**
|
|
3103
3131
|
* List ${table.name} records with field exclusion
|
|
3104
3132
|
* @param params - Query parameters with exclude
|
|
@@ -3116,10 +3144,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3116
3144
|
metric?: "cosine" | "l2" | "inner";
|
|
3117
3145
|
maxDistance?: number;
|
|
3118
3146
|
};` : ""}
|
|
3147
|
+
trigram?: ${trigramParamType};
|
|
3119
3148
|
orderBy?: string | string[];
|
|
3120
3149
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3121
3150
|
distinctOn?: string | string[];
|
|
3122
|
-
}): Promise<PaginatedResponse<Partial<Select${Type}
|
|
3151
|
+
}): Promise<PaginatedResponse<Partial<Select${Type}> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3123
3152
|
/**
|
|
3124
3153
|
* List ${table.name} records with pagination and filtering
|
|
3125
3154
|
* @param params - Query parameters
|
|
@@ -3142,10 +3171,11 @@ ${hasJsonbColumns ? ` /**
|
|
|
3142
3171
|
metric?: "cosine" | "l2" | "inner";
|
|
3143
3172
|
maxDistance?: number;
|
|
3144
3173
|
};` : ""}
|
|
3174
|
+
trigram?: ${trigramParamType};
|
|
3145
3175
|
orderBy?: string | string[];
|
|
3146
3176
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3147
3177
|
distinctOn?: string | string[];
|
|
3148
|
-
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude
|
|
3178
|
+
}): Promise<PaginatedResponse<${Type}WithIncludes<TInclude> & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
|
|
3149
3179
|
async list(params?: {
|
|
3150
3180
|
include?: ${Type}IncludeSpec;
|
|
3151
3181
|
select?: string[];
|
|
@@ -3159,11 +3189,12 @@ ${hasJsonbColumns ? ` /**
|
|
|
3159
3189
|
metric?: "cosine" | "l2" | "inner";
|
|
3160
3190
|
maxDistance?: number;
|
|
3161
3191
|
};` : ""}
|
|
3192
|
+
trigram?: ${trigramParamType};
|
|
3162
3193
|
orderBy?: string | string[];
|
|
3163
3194
|
order?: "asc" | "desc" | ("asc" | "desc")[];
|
|
3164
3195
|
distinctOn?: string | string[];
|
|
3165
3196
|
}): Promise<PaginatedResponse<Select${Type} | Partial<Select${Type}>>> {
|
|
3166
|
-
return this.post<PaginatedResponse<Select${Type}${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3197
|
+
return this.post<PaginatedResponse<Select${Type} & { _similarity?: number }${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
|
|
3167
3198
|
}`}
|
|
3168
3199
|
|
|
3169
3200
|
${hasJsonbColumns ? ` /**
|
|
@@ -4331,6 +4362,12 @@ export type WhereOperator<T> = {
|
|
|
4331
4362
|
$like?: T extends string ? string : never;
|
|
4332
4363
|
/** Case-insensitive LIKE (strings only) */
|
|
4333
4364
|
$ilike?: T extends string ? string : never;
|
|
4365
|
+
/** Trigram similarity match - "col" % value (pg_trgm required, uses similarity_threshold GUC) */
|
|
4366
|
+
$similarity?: T extends string ? string : never;
|
|
4367
|
+
/** Word trigram similarity match - value <% "col" (pg_trgm required) */
|
|
4368
|
+
$wordSimilarity?: T extends string ? string : never;
|
|
4369
|
+
/** Strict word trigram similarity match - value <<% "col" (pg_trgm required) */
|
|
4370
|
+
$strictWordSimilarity?: T extends string ? string : never;
|
|
4334
4371
|
/** IS NULL */
|
|
4335
4372
|
$is?: null;
|
|
4336
4373
|
/** IS NOT NULL */
|
|
@@ -4947,7 +4984,7 @@ export async function createRecord(
|
|
|
4947
4984
|
|
|
4948
4985
|
const preparedVals = vals.map((v, i) =>
|
|
4949
4986
|
v !== null && v !== undefined && typeof v === 'object' &&
|
|
4950
|
-
(ctx.jsonbColumns?.includes(cols[i]) || ctx.vectorColumns?.includes(cols[i]))
|
|
4987
|
+
(ctx.jsonbColumns?.includes(cols[i]!) || ctx.vectorColumns?.includes(cols[i]!))
|
|
4951
4988
|
? JSON.stringify(v)
|
|
4952
4989
|
: v
|
|
4953
4990
|
);
|
|
@@ -5107,6 +5144,24 @@ function buildWhereClause(
|
|
|
5107
5144
|
whereParams.push(opValue);
|
|
5108
5145
|
paramIndex++;
|
|
5109
5146
|
break;
|
|
5147
|
+
case '$similarity':
|
|
5148
|
+
// pg_trgm: boolean similarity operator (respects similarity_threshold GUC)
|
|
5149
|
+
whereParts.push(\`"\${key}" % $\${paramIndex}\`);
|
|
5150
|
+
whereParams.push(opValue);
|
|
5151
|
+
paramIndex++;
|
|
5152
|
+
break;
|
|
5153
|
+
case '$wordSimilarity':
|
|
5154
|
+
// pg_trgm: word similarity operator (value <% col)
|
|
5155
|
+
whereParts.push(\`$\${paramIndex} <% "\${key}"\`);
|
|
5156
|
+
whereParams.push(opValue);
|
|
5157
|
+
paramIndex++;
|
|
5158
|
+
break;
|
|
5159
|
+
case '$strictWordSimilarity':
|
|
5160
|
+
// pg_trgm: strict word similarity operator (value <<% col)
|
|
5161
|
+
whereParts.push(\`$\${paramIndex} <<% "\${key}"\`);
|
|
5162
|
+
whereParams.push(opValue);
|
|
5163
|
+
paramIndex++;
|
|
5164
|
+
break;
|
|
5110
5165
|
case '$is':
|
|
5111
5166
|
if (opValue === null) {
|
|
5112
5167
|
whereParts.push(\`"\${key}" IS NULL\`);
|
|
@@ -5280,6 +5335,53 @@ function getVectorDistanceOperator(metric?: string): string {
|
|
|
5280
5335
|
}
|
|
5281
5336
|
}
|
|
5282
5337
|
|
|
5338
|
+
export type TrigramMetric = "similarity" | "wordSimilarity" | "strictWordSimilarity";
|
|
5339
|
+
|
|
5340
|
+
export type TrigramParam =
|
|
5341
|
+
| { field: string; query: string; metric?: TrigramMetric; threshold?: number }
|
|
5342
|
+
| { fields: string[]; strategy?: "greatest" | "concat"; query: string; metric?: TrigramMetric; threshold?: number }
|
|
5343
|
+
| { fields: Array<{ field: string; weight: number }>; query: string; metric?: TrigramMetric; threshold?: number };
|
|
5344
|
+
|
|
5345
|
+
/**
|
|
5346
|
+
* Build a pg_trgm function expression for a raw SQL field expression.
|
|
5347
|
+
* $1 is always the query string parameter.
|
|
5348
|
+
*/
|
|
5349
|
+
function trigramFnRaw(fieldExpr: string, metric?: TrigramMetric): string {
|
|
5350
|
+
switch (metric) {
|
|
5351
|
+
case "wordSimilarity": return \`word_similarity($1, \${fieldExpr})\`;
|
|
5352
|
+
case "strictWordSimilarity": return \`strict_word_similarity($1, \${fieldExpr})\`;
|
|
5353
|
+
default: return \`similarity(\${fieldExpr}, $1)\`;
|
|
5354
|
+
}
|
|
5355
|
+
}
|
|
5356
|
+
|
|
5357
|
+
/** Build the full pg_trgm SQL expression for the given trigram param. */
|
|
5358
|
+
function getTrigramFnExpr(trigram: TrigramParam): string {
|
|
5359
|
+
if ("field" in trigram) {
|
|
5360
|
+
return trigramFnRaw(\`"\${trigram.field}"\`, trigram.metric);
|
|
5361
|
+
}
|
|
5362
|
+
|
|
5363
|
+
const { fields, metric } = trigram;
|
|
5364
|
+
|
|
5365
|
+
// Weighted: fields is Array<{ field, weight }>
|
|
5366
|
+
if (fields.length > 0 && typeof fields[0] === "object" && "weight" in fields[0]) {
|
|
5367
|
+
const wfields = fields as Array<{ field: string; weight: number }>;
|
|
5368
|
+
const totalWeight = wfields.reduce((sum, f) => sum + f.weight, 0);
|
|
5369
|
+
const terms = wfields.map(f => \`\${trigramFnRaw(\`"\${f.field}"\`, metric)} * \${f.weight}\`).join(" + ");
|
|
5370
|
+
return \`(\${terms}) / \${totalWeight}\`;
|
|
5371
|
+
}
|
|
5372
|
+
|
|
5373
|
+
const sfields = fields as string[];
|
|
5374
|
+
const strategy = (trigram as Extract<TrigramParam, { fields: string[] }>).strategy ?? "greatest";
|
|
5375
|
+
|
|
5376
|
+
if (strategy === "concat") {
|
|
5377
|
+
const concatExpr = sfields.map(f => \`"\${f}"\`).join(\` || ' ' || \`);
|
|
5378
|
+
return trigramFnRaw(concatExpr, metric);
|
|
5379
|
+
}
|
|
5380
|
+
|
|
5381
|
+
// greatest (default)
|
|
5382
|
+
return \`GREATEST(\${sfields.map(f => trigramFnRaw(\`"\${f}"\`, metric)).join(", ")})\`;
|
|
5383
|
+
}
|
|
5384
|
+
|
|
5283
5385
|
/** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
|
|
5284
5386
|
function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
5285
5387
|
if (cols.length === 0) return "";
|
|
@@ -5287,7 +5389,7 @@ function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
|
|
|
5287
5389
|
}
|
|
5288
5390
|
|
|
5289
5391
|
/**
|
|
5290
|
-
* LIST operation - Get multiple records with optional filters and
|
|
5392
|
+
* LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
|
|
5291
5393
|
*/
|
|
5292
5394
|
export async function listRecords(
|
|
5293
5395
|
ctx: OperationContext,
|
|
@@ -5305,10 +5407,11 @@ export async function listRecords(
|
|
|
5305
5407
|
metric?: "cosine" | "l2" | "inner";
|
|
5306
5408
|
maxDistance?: number;
|
|
5307
5409
|
};
|
|
5410
|
+
trigram?: TrigramParam;
|
|
5308
5411
|
}
|
|
5309
5412
|
): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
|
|
5310
5413
|
try {
|
|
5311
|
-
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, distinctOn } = params;
|
|
5414
|
+
const { where: whereClause, limit = 50, offset = 0, include, orderBy, order, vector, trigram, distinctOn } = params;
|
|
5312
5415
|
|
|
5313
5416
|
// DISTINCT ON support
|
|
5314
5417
|
const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
|
|
@@ -5319,20 +5422,28 @@ export async function listRecords(
|
|
|
5319
5422
|
|
|
5320
5423
|
// Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
|
|
5321
5424
|
// by a column outside of distinctOn (inline DISTINCT ON can't satisfy that without silently
|
|
5322
|
-
// overriding the requested ordering). Vector search always stays inline.
|
|
5425
|
+
// overriding the requested ordering). Vector/trigram search always stays inline.
|
|
5323
5426
|
const useSubquery: boolean =
|
|
5324
5427
|
distinctCols !== null &&
|
|
5325
5428
|
!vector &&
|
|
5429
|
+
!trigram &&
|
|
5326
5430
|
userOrderCols.some(col => !distinctCols.includes(col));
|
|
5327
5431
|
|
|
5328
5432
|
// Get distance operator if vector search
|
|
5329
5433
|
const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
|
|
5330
5434
|
|
|
5331
|
-
//
|
|
5332
|
-
const
|
|
5435
|
+
// Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
|
|
5436
|
+
const trigramFnExpr = trigram ? getTrigramFnExpr(trigram) : "";
|
|
5437
|
+
|
|
5438
|
+
// Add vector or trigram query as $1 if present (mutually exclusive)
|
|
5439
|
+
const queryParams: any[] = vector
|
|
5440
|
+
? [JSON.stringify(vector.query)]
|
|
5441
|
+
: trigram
|
|
5442
|
+
? [trigram.query]
|
|
5443
|
+
: [];
|
|
5333
5444
|
|
|
5334
|
-
// Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
|
|
5335
|
-
let paramIndex = vector ? 2 : 1;
|
|
5445
|
+
// Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
|
|
5446
|
+
let paramIndex = vector || trigram ? 2 : 1;
|
|
5336
5447
|
const whereParts: string[] = [];
|
|
5337
5448
|
let whereParams: any[] = [];
|
|
5338
5449
|
|
|
@@ -5356,28 +5467,38 @@ export async function listRecords(
|
|
|
5356
5467
|
whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
|
|
5357
5468
|
}
|
|
5358
5469
|
|
|
5470
|
+
// Add trigram threshold filter if specified (inlined as literal — it's a float, safe)
|
|
5471
|
+
if (trigram?.threshold !== undefined) {
|
|
5472
|
+
whereParts.push(\`\${trigramFnExpr} >= \${trigram.threshold}\`);
|
|
5473
|
+
}
|
|
5474
|
+
|
|
5359
5475
|
const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
|
|
5360
5476
|
|
|
5361
5477
|
// Build WHERE clause for COUNT query (may need different param indices)
|
|
5362
5478
|
let countWhereSQL = whereSQL;
|
|
5363
5479
|
let countParams = whereParams;
|
|
5364
5480
|
|
|
5365
|
-
|
|
5366
|
-
|
|
5481
|
+
// When a $1 query param exists (vector/trigram) but has NO threshold filter, the COUNT query
|
|
5482
|
+
// must not reference $1 since it's only needed in SELECT/ORDER BY. Rebuild WHERE from scratch
|
|
5483
|
+
// starting at $1. When a threshold IS set, $1 appears in the WHERE condition so keep it.
|
|
5484
|
+
const hasQueryParam = !!(vector || trigram);
|
|
5485
|
+
const hasThreshold = vector?.maxDistance !== undefined || trigram?.threshold !== undefined;
|
|
5486
|
+
|
|
5487
|
+
if (hasQueryParam && !hasThreshold && whereParams.length > 0) {
|
|
5367
5488
|
const countWhereParts: string[] = [];
|
|
5368
5489
|
if (ctx.softDeleteColumn) {
|
|
5369
5490
|
countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
|
|
5370
5491
|
}
|
|
5371
5492
|
if (whereClause) {
|
|
5372
|
-
const result = buildWhereClause(whereClause, 1); // Start at $1
|
|
5493
|
+
const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
|
|
5373
5494
|
if (result.sql) {
|
|
5374
5495
|
countWhereParts.push(result.sql);
|
|
5375
5496
|
countParams = result.params;
|
|
5376
5497
|
}
|
|
5377
5498
|
}
|
|
5378
5499
|
countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
|
|
5379
|
-
} else if (
|
|
5380
|
-
//
|
|
5500
|
+
} else if (hasQueryParam && hasThreshold) {
|
|
5501
|
+
// $1 appears in WHERE threshold condition — COUNT needs the full params
|
|
5381
5502
|
countParams = [...queryParams, ...whereParams];
|
|
5382
5503
|
}
|
|
5383
5504
|
|
|
@@ -5386,6 +5507,8 @@ export async function listRecords(
|
|
|
5386
5507
|
const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
|
|
5387
5508
|
const selectClause = vector
|
|
5388
5509
|
? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
|
|
5510
|
+
: trigram
|
|
5511
|
+
? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
|
|
5389
5512
|
: \`\${_distinctOnPrefix}\${baseColumns}\`;
|
|
5390
5513
|
|
|
5391
5514
|
// Build ORDER BY clause
|
|
@@ -5397,6 +5520,9 @@ export async function listRecords(
|
|
|
5397
5520
|
if (vector) {
|
|
5398
5521
|
// Vector search always orders by distance; DISTINCT ON + vector stays inline
|
|
5399
5522
|
orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
|
|
5523
|
+
} else if (trigram) {
|
|
5524
|
+
// Trigram search orders by similarity score descending
|
|
5525
|
+
orderBySQL = \`ORDER BY _similarity DESC\`;
|
|
5400
5526
|
} else if (useSubquery) {
|
|
5401
5527
|
// Subquery form: outer query gets the user's full ORDER BY.
|
|
5402
5528
|
// Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "postgresdk",
|
|
3
|
-
"version": "0.18.
|
|
3
|
+
"version": "0.18.21",
|
|
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",
|