postgresdk 0.18.19 → 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 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
- See the generated SDK documentation for all available operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$like`, `$ilike`, `$is`, `$isNot`, `$or`, `$and`, `$jsonbContains`, `$jsonbContainedBy`, `$jsonbHasKey`, `$jsonbHasAnyKeys`, `$jsonbHasAllKeys`, `$jsonbPath`.
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>>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
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}>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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}>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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\`);
@@ -6241,6 +6279,22 @@ function getVectorDistanceOperator(metric?: string): string {
6241
6279
  }
6242
6280
  }
6243
6281
 
6282
+ /**
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
+
6244
6298
  /** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
6245
6299
  function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
6246
6300
  if (cols.length === 0) return "";
@@ -6248,7 +6302,7 @@ function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
6248
6302
  }
6249
6303
 
6250
6304
  /**
6251
- * LIST operation - Get multiple records with optional filters and vector search
6305
+ * LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
6252
6306
  */
6253
6307
  export async function listRecords(
6254
6308
  ctx: OperationContext,
@@ -6266,10 +6320,16 @@ export async function listRecords(
6266
6320
  metric?: "cosine" | "l2" | "inner";
6267
6321
  maxDistance?: number;
6268
6322
  };
6323
+ trigram?: {
6324
+ field: string;
6325
+ query: string;
6326
+ metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity";
6327
+ threshold?: number;
6328
+ };
6269
6329
  }
6270
6330
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
6271
6331
  try {
6272
- 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;
6273
6333
 
6274
6334
  // DISTINCT ON support
6275
6335
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -6280,20 +6340,28 @@ export async function listRecords(
6280
6340
 
6281
6341
  // Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
6282
6342
  // 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.
6343
+ // overriding the requested ordering). Vector/trigram search always stays inline.
6284
6344
  const useSubquery: boolean =
6285
6345
  distinctCols !== null &&
6286
6346
  !vector &&
6347
+ !trigram &&
6287
6348
  userOrderCols.some(col => !distinctCols.includes(col));
6288
6349
 
6289
6350
  // Get distance operator if vector search
6290
6351
  const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
6291
6352
 
6292
- // Add vector to params array if present
6293
- const queryParams: any[] = vector ? [JSON.stringify(vector.query)] : [];
6353
+ // Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
6354
+ const trigramFnExpr = trigram ? getTrigramFnExpr(trigram.field, trigram.metric) : "";
6355
+
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
+ : [];
6294
6362
 
6295
- // Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
6296
- let paramIndex = vector ? 2 : 1;
6363
+ // Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
6364
+ let paramIndex = vector || trigram ? 2 : 1;
6297
6365
  const whereParts: string[] = [];
6298
6366
  let whereParams: any[] = [];
6299
6367
 
@@ -6317,28 +6385,38 @@ export async function listRecords(
6317
6385
  whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
6318
6386
  }
6319
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
+
6320
6393
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
6321
6394
 
6322
6395
  // Build WHERE clause for COUNT query (may need different param indices)
6323
6396
  let countWhereSQL = whereSQL;
6324
6397
  let countParams = whereParams;
6325
6398
 
6326
- if (vector && vector.maxDistance === undefined && whereParams.length > 0) {
6327
- // COUNT query doesn't use vector, so rebuild WHERE without vector offset
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) {
6328
6406
  const countWhereParts: string[] = [];
6329
6407
  if (ctx.softDeleteColumn) {
6330
6408
  countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
6331
6409
  }
6332
6410
  if (whereClause) {
6333
- const result = buildWhereClause(whereClause, 1); // Start at $1 for count
6411
+ const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
6334
6412
  if (result.sql) {
6335
6413
  countWhereParts.push(result.sql);
6336
6414
  countParams = result.params;
6337
6415
  }
6338
6416
  }
6339
6417
  countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
6340
- } else if (vector?.maxDistance !== undefined) {
6341
- // COUNT query includes vector for maxDistance filter
6418
+ } else if (hasQueryParam && hasThreshold) {
6419
+ // $1 appears in WHERE threshold condition — COUNT needs the full params
6342
6420
  countParams = [...queryParams, ...whereParams];
6343
6421
  }
6344
6422
 
@@ -6347,6 +6425,8 @@ export async function listRecords(
6347
6425
  const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
6348
6426
  const selectClause = vector
6349
6427
  ? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
6428
+ : trigram
6429
+ ? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
6350
6430
  : \`\${_distinctOnPrefix}\${baseColumns}\`;
6351
6431
 
6352
6432
  // Build ORDER BY clause
@@ -6358,6 +6438,9 @@ export async function listRecords(
6358
6438
  if (vector) {
6359
6439
  // Vector search always orders by distance; DISTINCT ON + vector stays inline
6360
6440
  orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
6441
+ } else if (trigram) {
6442
+ // Trigram search orders by similarity score descending
6443
+ orderBySQL = \`ORDER BY _similarity DESC\`;
6361
6444
  } else if (useSubquery) {
6362
6445
  // Subquery form: outer query gets the user's full ORDER BY.
6363
6446
  // Inner query only needs to satisfy PG's DISTINCT ON prefix requirement (built at query assembly).
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>>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>(\`\${this.resource}/list\`, params ?? {});
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}>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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}>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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>${hasVectorColumns ? " & { _distance?: number }" : ""}>>;
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\`);
@@ -5280,6 +5318,22 @@ function getVectorDistanceOperator(metric?: string): string {
5280
5318
  }
5281
5319
  }
5282
5320
 
5321
+ /**
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
+
5283
5337
  /** Builds a SQL ORDER BY clause from parallel cols/dirs arrays. Returns "" when cols is empty. */
5284
5338
  function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
5285
5339
  if (cols.length === 0) return "";
@@ -5287,7 +5341,7 @@ function buildOrderBySQL(cols: string[], dirs: ("asc" | "desc")[]): string {
5287
5341
  }
5288
5342
 
5289
5343
  /**
5290
- * LIST operation - Get multiple records with optional filters and vector search
5344
+ * LIST operation - Get multiple records with optional filters, vector search, and trigram similarity search
5291
5345
  */
5292
5346
  export async function listRecords(
5293
5347
  ctx: OperationContext,
@@ -5305,10 +5359,16 @@ export async function listRecords(
5305
5359
  metric?: "cosine" | "l2" | "inner";
5306
5360
  maxDistance?: number;
5307
5361
  };
5362
+ trigram?: {
5363
+ field: string;
5364
+ query: string;
5365
+ metric?: "similarity" | "wordSimilarity" | "strictWordSimilarity";
5366
+ threshold?: number;
5367
+ };
5308
5368
  }
5309
5369
  ): Promise<{ data?: any; total?: number; limit?: number; offset?: number; hasMore?: boolean; error?: string; issues?: any; needsIncludes?: boolean; includeSpec?: any; status: number }> {
5310
5370
  try {
5311
- 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;
5312
5372
 
5313
5373
  // DISTINCT ON support
5314
5374
  const distinctCols: string[] | null = distinctOn ? (Array.isArray(distinctOn) ? distinctOn : [distinctOn]) : null;
@@ -5319,20 +5379,28 @@ export async function listRecords(
5319
5379
 
5320
5380
  // Auto-detect subquery form: needed when distinctOn is set AND the caller wants to order
5321
5381
  // 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.
5382
+ // overriding the requested ordering). Vector/trigram search always stays inline.
5323
5383
  const useSubquery: boolean =
5324
5384
  distinctCols !== null &&
5325
5385
  !vector &&
5386
+ !trigram &&
5326
5387
  userOrderCols.some(col => !distinctCols.includes(col));
5327
5388
 
5328
5389
  // Get distance operator if vector search
5329
5390
  const distanceOp = vector ? getVectorDistanceOperator(vector.metric) : "";
5330
5391
 
5331
- // Add vector to params array if present
5332
- const queryParams: any[] = vector ? [JSON.stringify(vector.query)] : [];
5392
+ // Get trigram similarity SQL expression (pg_trgm). $1 is always the query string.
5393
+ const trigramFnExpr = trigram ? getTrigramFnExpr(trigram.field, trigram.metric) : "";
5394
+
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
+ : [];
5333
5401
 
5334
- // Build WHERE clause for SELECT/UPDATE queries (with vector as $1 if present)
5335
- let paramIndex = vector ? 2 : 1;
5402
+ // Build WHERE clause for SELECT/UPDATE queries (with vector/trigram as $1 if present)
5403
+ let paramIndex = vector || trigram ? 2 : 1;
5336
5404
  const whereParts: string[] = [];
5337
5405
  let whereParams: any[] = [];
5338
5406
 
@@ -5356,28 +5424,38 @@ export async function listRecords(
5356
5424
  whereParts.push(\`("\${vector.field}" \${distanceOp} ($1)::vector) < \${vector.maxDistance}\`);
5357
5425
  }
5358
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
+
5359
5432
  const whereSQL = whereParts.length > 0 ? \`WHERE \${whereParts.join(" AND ")}\` : "";
5360
5433
 
5361
5434
  // Build WHERE clause for COUNT query (may need different param indices)
5362
5435
  let countWhereSQL = whereSQL;
5363
5436
  let countParams = whereParams;
5364
5437
 
5365
- if (vector && vector.maxDistance === undefined && whereParams.length > 0) {
5366
- // COUNT query doesn't use vector, so rebuild WHERE without vector offset
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) {
5367
5445
  const countWhereParts: string[] = [];
5368
5446
  if (ctx.softDeleteColumn) {
5369
5447
  countWhereParts.push(\`"\${ctx.softDeleteColumn}" IS NULL\`);
5370
5448
  }
5371
5449
  if (whereClause) {
5372
- const result = buildWhereClause(whereClause, 1); // Start at $1 for count
5450
+ const result = buildWhereClause(whereClause, 1); // Start at $1, no query-param offset
5373
5451
  if (result.sql) {
5374
5452
  countWhereParts.push(result.sql);
5375
5453
  countParams = result.params;
5376
5454
  }
5377
5455
  }
5378
5456
  countWhereSQL = countWhereParts.length > 0 ? \`WHERE \${countWhereParts.join(" AND ")}\` : "";
5379
- } else if (vector?.maxDistance !== undefined) {
5380
- // COUNT query includes vector for maxDistance filter
5457
+ } else if (hasQueryParam && hasThreshold) {
5458
+ // $1 appears in WHERE threshold condition — COUNT needs the full params
5381
5459
  countParams = [...queryParams, ...whereParams];
5382
5460
  }
5383
5461
 
@@ -5386,6 +5464,8 @@ export async function listRecords(
5386
5464
  const baseColumns = buildColumnList(ctx.select, ctx.exclude, ctx.allColumnNames);
5387
5465
  const selectClause = vector
5388
5466
  ? \`\${_distinctOnPrefix}\${baseColumns}, ("\${vector.field}" \${distanceOp} ($1)::vector) AS _distance\`
5467
+ : trigram
5468
+ ? \`\${_distinctOnPrefix}\${baseColumns}, \${trigramFnExpr} AS _similarity\`
5389
5469
  : \`\${_distinctOnPrefix}\${baseColumns}\`;
5390
5470
 
5391
5471
  // Build ORDER BY clause
@@ -5397,6 +5477,9 @@ export async function listRecords(
5397
5477
  if (vector) {
5398
5478
  // Vector search always orders by distance; DISTINCT ON + vector stays inline
5399
5479
  orderBySQL = \`ORDER BY "\${vector.field}" \${distanceOp} ($1)::vector\`;
5480
+ } else if (trigram) {
5481
+ // Trigram search orders by similarity score descending
5482
+ orderBySQL = \`ORDER BY _similarity DESC\`;
5400
5483
  } else if (useSubquery) {
5401
5484
  // Subquery form: outer query gets the user's full ORDER BY.
5402
5485
  // 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.19",
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",