sonamu 0.10.7 → 0.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/api/decorators.d.ts.map +1 -1
  2. package/dist/api/decorators.js +5 -12
  3. package/dist/bin/cli.js +10 -253
  4. package/dist/database/base-model.d.ts +1 -0
  5. package/dist/database/base-model.d.ts.map +1 -1
  6. package/dist/database/base-model.js +14 -8
  7. package/dist/database/db.d.ts +2 -1
  8. package/dist/database/db.d.ts.map +1 -1
  9. package/dist/database/db.js +13 -3
  10. package/dist/database/puri-wrapper.d.ts.map +1 -1
  11. package/dist/database/puri-wrapper.js +4 -11
  12. package/dist/database/transaction-context.d.ts +8 -3
  13. package/dist/database/transaction-context.d.ts.map +1 -1
  14. package/dist/database/transaction-context.js +12 -7
  15. package/dist/migration/code-generation.js +2 -2
  16. package/dist/naite/naite-reporter.d.ts +8 -3
  17. package/dist/naite/naite-reporter.d.ts.map +1 -1
  18. package/dist/naite/naite-reporter.js +9 -4
  19. package/dist/ui-web/assets/{index-D0MHYbxl.js → index-CJf8uJYf.js} +47 -41
  20. package/dist/ui-web/assets/index-GMMIVGja.css +1 -0
  21. package/dist/ui-web/index.html +2 -2
  22. package/package.json +3 -3
  23. package/src/api/decorators.ts +11 -22
  24. package/src/bin/cli.ts +12 -375
  25. package/src/database/__tests__/transaction-scope.test.ts +227 -0
  26. package/src/database/base-model.ts +31 -6
  27. package/src/database/db.ts +17 -2
  28. package/src/database/puri-wrapper.ts +5 -22
  29. package/src/database/transaction-context.ts +14 -6
  30. package/src/migration/code-generation.ts +1 -1
  31. package/src/naite/naite-reporter.ts +8 -3
  32. package/dist/ui-web/assets/index-Dx_JX4aQ.css +0 -1
  33. package/src/skills/AGENTS.md +0 -108
  34. package/src/skills/sonamu/SKILL.md +0 -75
  35. package/src/skills/sonamu-ai-agents/SKILL.md +0 -205
  36. package/src/skills/sonamu-api/SKILL.md +0 -480
  37. package/src/skills/sonamu-auth/SKILL.md +0 -327
  38. package/src/skills/sonamu-auth/references/plugins.md +0 -310
  39. package/src/skills/sonamu-auth/references/user-id-migration-followups.md +0 -192
  40. package/src/skills/sonamu-auth/references/user-id-migration.md +0 -455
  41. package/src/skills/sonamu-config/SKILL.md +0 -203
  42. package/src/skills/sonamu-config/references/database.md +0 -452
  43. package/src/skills/sonamu-config/references/environments.md +0 -178
  44. package/src/skills/sonamu-config/references/server-options.md +0 -400
  45. package/src/skills/sonamu-entity/SKILL.md +0 -180
  46. package/src/skills/sonamu-entity/references/creation-workflow.md +0 -581
  47. package/src/skills/sonamu-entity/references/design-guides.md +0 -243
  48. package/src/skills/sonamu-entity/references/field-types.md +0 -170
  49. package/src/skills/sonamu-entity/references/relations-detail.md +0 -245
  50. package/src/skills/sonamu-entity/references/relations.md +0 -463
  51. package/src/skills/sonamu-entity/references/subset.md +0 -156
  52. package/src/skills/sonamu-fixture/SKILL.md +0 -180
  53. package/src/skills/sonamu-fixture/references/cli-usage.md +0 -439
  54. package/src/skills/sonamu-fixture/references/cone.md +0 -298
  55. package/src/skills/sonamu-frontend/SKILL.md +0 -142
  56. package/src/skills/sonamu-frontend/references/components.md +0 -323
  57. package/src/skills/sonamu-frontend/references/examples.md +0 -64
  58. package/src/skills/sonamu-frontend/references/hooks.md +0 -273
  59. package/src/skills/sonamu-frontend/references/runtime.md +0 -165
  60. package/src/skills/sonamu-frontend/references/scaffolding.md +0 -439
  61. package/src/skills/sonamu-i18n/SKILL.md +0 -287
  62. package/src/skills/sonamu-migration/SKILL.md +0 -316
  63. package/src/skills/sonamu-naite/SKILL.md +0 -266
  64. package/src/skills/sonamu-query/SKILL.md +0 -48
  65. package/src/skills/sonamu-query/references/model-patterns.md +0 -390
  66. package/src/skills/sonamu-query/references/model.md +0 -366
  67. package/src/skills/sonamu-query/references/puri.md +0 -413
  68. package/src/skills/sonamu-query/references/search.md +0 -238
  69. package/src/skills/sonamu-query/references/upsert.md +0 -324
  70. package/src/skills/sonamu-tasks/SKILL.md +0 -236
  71. package/src/skills/sonamu-testing/SKILL.md +0 -251
  72. package/src/skills/sonamu-testing/references/devrunner.md +0 -405
  73. package/src/skills/sonamu-testing/references/helpers.md +0 -185
  74. package/src/skills/sonamu-testing/references/patterns.md +0 -263
  75. package/src/skills/sonamu-testing/references/pitfalls.md +0 -588
  76. package/src/skills/sonamu-testing/references/quick-start.md +0 -285
  77. package/src/skills/sonamu-testing/references/type-safety.md +0 -172
  78. package/src/skills/sonamu-testing/references/writing-plan.md +0 -375
  79. package/src/skills/sonamu-vector/SKILL.md +0 -222
@@ -1,413 +0,0 @@
1
- # Puri — Query Building
2
-
3
- ## Starting a Query
4
-
5
- ```typescript
6
- // Read
7
- const users = await this.getPuri("r").table("users").select({ id: "id", name: "username" });
8
-
9
- // Write
10
- await this.getPuri("w").table("users").where("id", 1).update({ is_active: false });
11
-
12
- // Using aliases
13
- const users = await db.table({ u: "users" }).select({ id: "u.id" });
14
- ```
15
-
16
- ### Where to obtain a Puri instance
17
-
18
- Route every query — reads and writes alike — through Puri. Do NOT run queries directly on a raw `getDB()` knex handle (the only exceptions are migration files, `db.ts`, tests, and the `.knex` escape hatch below).
19
-
20
- ```typescript
21
- // Inside a Model
22
- const rows = await this.getPuri("r").table("users").select({ id: "id" });
23
-
24
- // Inside a Frame — Frame has no getPuri (only getDB / getUpsertBuilder),
25
- // so use the associated Model's getPuri
26
- const rows = await UserModel.getPuri("r").table("users").select({ id: "id" });
27
-
28
- // Outside a Model (seed / batch / monitoring scripts) — wrap knex in a PuriWrapper
29
- import { DB, PuriWrapper, UpsertBuilder } from "sonamu";
30
- const puri = new PuriWrapper(DB.getDB("r"), new UpsertBuilder());
31
- const rows = await puri.table("users").select({ id: "id" });
32
- ```
33
-
34
- > `UpsertBuilder` is a required constructor argument even for read-only wrappers; it simply stays unused for pure reads.
35
-
36
- ### Escape hatch: `.knex` for non-entity / framework-internal tables
37
-
38
- Puri's typed `.table()` only knows registered entity tables. For framework-internal or unregistered tables (e.g. `workflow_runs`), reach the raw knex handle via `puri.knex` instead of calling `DB.getDB()` directly:
39
-
40
- ```typescript
41
- const puri = new PuriWrapper(DB.getDB("r"), new UpsertBuilder());
42
- const runs = await puri.knex.table("workflow_runs").where("status", "running").select("*");
43
- ```
44
-
45
- ## SELECT
46
-
47
- > **CRITICAL: `.select()` must always be used with an object argument.**
48
- >
49
- > Passing a string argument causes the string to be spread character-by-character, generating incorrect SQL like `select "i" as "0", "d" as "1", ...`.
50
- > Be especially careful when chaining after an `as any` cast.
51
- >
52
- > ```typescript
53
- > // WRONG — character spread bug
54
- > db.table("files").select("files.entity_id", "files.file_type");
55
- >
56
- > // CORRECT
57
- > db.table("files").select({ entity_id: "files.entity_id", file_type: "files.file_type" });
58
- > ```
59
-
60
- ```typescript
61
- // Basic select
62
- const users = await db.table("users").select({ id: "id", name: "username" });
63
-
64
- // All columns
65
- const users = await db.table("users").selectAll();
66
-
67
- // Nested objects (auto-converted during hydration)
68
- db.select({
69
- id: "users.id",
70
- parent: {
71
- id: "parent.id",
72
- name: "parent.name",
73
- },
74
- });
75
-
76
- // Append to existing select
77
- db.select({ id: "id" }).appendSelect({ name: "username" });
78
- ```
79
-
80
- ## Static Functions (for SELECT)
81
-
82
- ### Aggregate Functions
83
-
84
- ```typescript
85
- // COUNT
86
- const [{ total }] = await db.table("users").select({ total: Puri.count() });
87
- const [{ cnt }] = await db.table("users").select({ cnt: Puri.count("id") });
88
-
89
- // SUM / AVG / MAX / MIN
90
- db.select({
91
- totalAmount: Puri.sum("amount"),
92
- avgPrice: Puri.avg("price"),
93
- maxScore: Puri.max("score"),
94
- minAge: Puri.min("age"),
95
- });
96
- ```
97
-
98
- ### String Functions
99
-
100
- ```typescript
101
- db.select({
102
- fullName: Puri.concat("first_name", "' '", "last_name"),
103
- upperName: Puri.upper("name"),
104
- lowerEmail: Puri.lower("email"),
105
- });
106
- ```
107
-
108
- ### Raw SQL Expressions
109
-
110
- Bind parameters can be passed as the second argument `params`. Do not interpolate values directly into SQL; use params instead.
111
-
112
- ```typescript
113
- // Without parameters
114
- db.select({
115
- custom: Puri.rawString("COALESCE(nickname, username)"),
116
- total: Puri.rawNumber("price * quantity"),
117
- isActive: Puri.rawBoolean("status = 'active'"),
118
- expireAt: Puri.rawDate("created_at + INTERVAL '30 days'"),
119
- tags: Puri.rawStringArray("string_to_array(tags, ',')"),
120
- });
121
-
122
- // Bind with params array (prevents SQL injection)
123
- db.select({
124
- score: Puri.rawNumber(
125
- `word_similarity(?, items.title) * 5 + word_similarity(?, items.tags) * 2`,
126
- [query, query],
127
- ),
128
- label: Puri.rawString(`COALESCE(??, ?)`, ["items.name", "Unspecified"]),
129
- });
130
- ```
131
-
132
- ## WHERE
133
-
134
- ```typescript
135
- // Basic
136
- db.where("role", "admin");
137
- db.where("age", ">=", 18);
138
- db.where("deleted_at", null); // IS NULL
139
- db.where("deleted_at", "!=", null); // IS NOT NULL
140
-
141
- // Multiple conditions (AND)
142
- db.where("role", "admin").where("is_active", true);
143
-
144
- // IN / NOT IN
145
- db.whereIn("role", ["admin", "moderator"]);
146
- db.whereNotIn("status", ["deleted", "banned"]);
147
-
148
- // LIKE
149
- db.where("email", "like", `%${keyword}%`);
150
-
151
- // Raw WHERE
152
- db.whereRaw("EXTRACT(YEAR FROM created_at) = ?", [2024]);
153
- ```
154
-
155
- ### WHERE Grouping (Parentheses)
156
-
157
- ```typescript
158
- // (role = 'admin' OR role = 'moderator') AND is_active = true
159
- db.whereGroup((g) => {
160
- g.where("role", "admin").orWhere("role", "moderator");
161
- }).where("is_active", true);
162
-
163
- // OR group
164
- db.where("status", "active").orWhereGroup((g) => {
165
- g.where("role", "admin").where("is_verified", true);
166
- });
167
- ```
168
-
169
- ### JSONB Containment (`@>`)
170
-
171
- Use `whereJsonSupersetOf()` to require a JSONB column to contain a JSON value. The column and
172
- containment value are checked against the generated Entity type, and the value is serialized and
173
- bound internally.
174
-
175
- ```typescript
176
- db.table("products").whereJsonSupersetOf("metadata", { warranty: 2 });
177
-
178
- // Aliased columns are supported.
179
- db.table({ p: "products" }).whereJsonSupersetOf("p.metadata", {
180
- tags: ["featured"],
181
- });
182
- ```
183
-
184
- Inside a WHERE group, both AND and OR variants are available:
185
-
186
- ```typescript
187
- db.whereGroup((g) => {
188
- g.where("status", "active").orWhereJsonSupersetOf("metadata", {
189
- tags: ["featured"],
190
- });
191
- });
192
- ```
193
-
194
- Do not call `JSON.stringify()` yourself. For other JSONB operators such as `->>`, `->`, and `?`,
195
- continue to use parameterized `whereRaw()`.
196
-
197
- ## JOIN
198
-
199
- ```typescript
200
- // INNER JOIN
201
- db.table("employees")
202
- .join("users", "employees.user_id", "users.id")
203
- .select({ empId: "employees.id", userName: "users.username" });
204
-
205
- // LEFT JOIN
206
- db.table("employees").leftJoin("departments", "employees.department_id", "departments.id");
207
-
208
- // Using aliases
209
- db.table({ e: "employees" })
210
- .join({ u: "users" }, "e.user_id", "u.id")
211
- .leftJoin({ d: "departments" }, "e.department_id", "d.id");
212
-
213
- // Complex JOIN conditions with callback
214
- db.table("orders").join("products", (j) => {
215
- j.on("orders.product_id", "products.id").on("orders.store_id", "products.store_id");
216
- });
217
-
218
- // Subquery JOIN
219
- const subquery = db
220
- .table("order_items")
221
- .select({ order_id: "order_id", total: Puri.sum("amount") })
222
- .groupBy("order_id");
223
-
224
- db.table("orders")
225
- .join({ oi: subquery }, "orders.id", "oi.order_id")
226
- .select({ id: "orders.id", total: "oi.total" });
227
- ```
228
-
229
- ### Reusing generated JOINs
230
-
231
- Use `ensureJoin()` or `ensureLeftJoin()` when a Model adds a JOIN that may already have been added
232
- by a Subset query.
233
-
234
- ```typescript
235
- db.table("patient_timeline_events")
236
- .ensureJoin(
237
- { patient: "patients" },
238
- "patient_timeline_events.patient_id",
239
- "patient.id",
240
- )
241
- .where("patient.organization_id", organizationId);
242
- ```
243
-
244
- Puri compares JOINs by alias. If the alias, table, JOIN type, left column, and right column all
245
- match, the existing JOIN is reused. If the alias is new, the JOIN is added. Reusing an alias with a
246
- different definition throws an error before SQL execution.
247
-
248
- Different aliases for the same physical table remain valid:
249
-
250
- ```typescript
251
- db.table("documents")
252
- .ensureJoin({ created_by: "users" }, "documents.created_by_id", "created_by.id")
253
- .ensureJoin({ updated_by: "users" }, "documents.updated_by_id", "updated_by.id");
254
- ```
255
-
256
- `ensureJoin()` and `ensureLeftJoin()` support table equality JOINs only. Callback and subquery JOINs
257
- continue to use `join()` or `leftJoin()` and are not considered reusable.
258
-
259
- ## ORDER BY & LIMIT
260
-
261
- ```typescript
262
- db.orderBy("created_at", "desc").limit(20).offset(40); // Page 3
263
- ```
264
-
265
- ### NULLS position & array form
266
-
267
- `orderBy` has two overloads:
268
-
269
- 1. Single column: `orderBy(column, direction?, nulls?)` — `nulls` is `"first" | "last"`
270
- 2. Array: `orderBy(entries[])` — each entry is a column string, a `Puri.raw*` SQL expression, or an object `{ column, order?, nulls? }`
271
-
272
- ```typescript
273
- // Single column with NULLS position
274
- db.orderBy("published_at", "desc", "last");
275
-
276
- // Array form: multiple sort keys, per-key direction and NULLS
277
- db.orderBy([
278
- { column: "is_pinned", order: "desc" },
279
- { column: "published_at", order: "desc", nulls: "last" },
280
- "title", // bare string defaults to asc
281
- ]);
282
-
283
- // Sort by a SQL expression
284
- db.orderBy([Puri.rawNumber("view_count * 2"), { column: "id", order: "desc" }]);
285
- ```
286
-
287
- ## GROUP BY & HAVING
288
-
289
- ```typescript
290
- db.table("orders")
291
- .select({
292
- userId: "user_id",
293
- total: Puri.sum("amount"),
294
- count: Puri.count(),
295
- })
296
- .groupBy("user_id")
297
- .having("COUNT(*) > 5");
298
-
299
- // Column, operator, value form
300
- db.groupBy("user_id").having("count", ">", 10);
301
- ```
302
-
303
- ## INSERT
304
-
305
- ```typescript
306
- // Basic INSERT
307
- await db.table("users").insert({ username: "john", email: "john@test.com" });
308
-
309
- // RETURNING
310
- const [{ id }] = await db.table("users").insert({ username: "john" }).returning("id");
311
-
312
- // Multiple columns RETURNING
313
- const [row] = await db.table("users").insert({ username: "john" }).returning(["id", "created_at"]);
314
-
315
- // All columns RETURNING
316
- const [user] = await db.table("users").insert({ username: "john" }).returning("*");
317
- ```
318
-
319
- ### INSERT onConflict (Upsert)
320
-
321
- ```typescript
322
- // DO NOTHING
323
- await db.table("users").insert({ id: 1, username: "john" }).onConflict("id"); // or .onConflict("id", "nothing")
324
-
325
- // DO UPDATE - specific columns only
326
- await db
327
- .table("users")
328
- .insert({ id: 1, username: "john", email: "new@test.com" })
329
- .onConflict("id", { update: ["username", "email"] });
330
-
331
- // DO UPDATE - with specified values
332
- await db
333
- .table("users")
334
- .insert({ id: 1, username: "john" })
335
- .onConflict("id", {
336
- update: {
337
- username: "updated_john",
338
- updated_at: Puri.rawDate("NOW()"),
339
- },
340
- });
341
-
342
- // Composite key conflict
343
- await db
344
- .table("user_settings")
345
- .insert({ user_id: 1, key: "theme", value: "dark" })
346
- .onConflict(["user_id", "key"], { update: ["value"] });
347
- ```
348
-
349
- ## UPDATE
350
-
351
- ```typescript
352
- await db.table("users").where("id", 1).update({ username: "updated" });
353
-
354
- // INCREMENT / DECREMENT
355
- await db.table("users").where("id", 1).increment("points", 10);
356
- await db.table("users").where("id", 1).decrement("credit", 100);
357
- ```
358
-
359
- ## DELETE
360
-
361
- ```typescript
362
- await db.table("users").where("id", 1).delete();
363
- ```
364
-
365
- ## Result Methods
366
-
367
- | Method | Returns | Description |
368
- | -------------- | ------------------------- | ------------------------------- |
369
- | `await query` | `T[]` | Array result (Puri is Thenable) |
370
- | `first()` | `Promise<T \| undefined>` | First record |
371
- | `pluck("col")` | `Promise<V[]>` | Array of a specific column only |
372
-
373
- ```typescript
374
- const users = await db.table("users").select({ id: "id" }); // T[]
375
- const user = await db.table("users").where("id", 1).first(); // T | undefined
376
- const ids = await db.table("users").where("role", "admin").pluck("id"); // number[]
377
- ```
378
-
379
- ## Utilities
380
-
381
- ```typescript
382
- // Inspect query string
383
- const sql = db.table("users").where("id", 1).toQuery();
384
-
385
- // Debug log output (prints query to console, then continues chaining)
386
- await db.table("users").where("id", 1).debug().first();
387
-
388
- // Clone a query
389
- const baseQuery = db.table("users").where("is_active", true);
390
- const query1 = baseQuery.clone().where("role", "admin");
391
- const query2 = baseQuery.clone().where("role", "user");
392
-
393
- // Clear parts of a query
394
- db.clear("select"); // Clear SELECT clause
395
- db.clear("order"); // Clear ORDER BY
396
- db.clear("limit"); // Clear LIMIT
397
- db.clear("offset"); // Clear OFFSET
398
-
399
- // Remove a specific JOIN
400
- db.clearJoin("alias");
401
- ```
402
-
403
- ## Transactions
404
-
405
- ```typescript
406
- await this.getPuri("w").transaction(async (trx) => {
407
- await trx.table("users").where("id", fromId).decrement("points", amount);
408
- await trx.table("users").where("id", toId).increment("points", amount);
409
- await trx.table("point_logs").insert({ from_id: fromId, to_id: toId, amount });
410
- });
411
- ```
412
-
413
- ---
@@ -1,238 +0,0 @@
1
- # Puri — Full-Text, Vector, and Fuzzy Search
2
-
3
- ## PostgreSQL Full-Text Search (tsvector)
4
-
5
- ### whereTsSearch
6
-
7
- ```typescript
8
- // Basic search (websearch_to_tsquery, simple config)
9
- db.whereTsSearch("search_vector", "search term");
10
-
11
- // Specify config
12
- db.whereTsSearch("search_vector", "search term", "korean");
13
-
14
- // Detailed options
15
- db.whereTsSearch("search_vector", "search term", {
16
- parser: "plainto_tsquery", // websearch_to_tsquery | plainto_tsquery | phraseto_tsquery
17
- config: "korean",
18
- });
19
- ```
20
-
21
- ### tsHighlight (Search Term Highlighting)
22
-
23
- ```typescript
24
- db.select({
25
- title: "title",
26
- highlighted: Puri.tsHighlight("content", "search term"),
27
- });
28
-
29
- // Options
30
- db.select({
31
- highlighted: Puri.tsHighlight("content", "search term", {
32
- config: "korean",
33
- startSel: "<mark>",
34
- stopSel: "</mark>",
35
- maxFragments: 3,
36
- maxWords: 35,
37
- minWords: 15,
38
- }),
39
- });
40
- ```
41
-
42
- ### tsRank / tsRankCd (Search Ranking)
43
-
44
- The first argument must be `Puri.toTsVector()` rather than a column name string.
45
-
46
- ```typescript
47
- // Must wrap with toTsVector() (required)
48
- db.select({
49
- rank: Puri.tsRank(Puri.toTsVector("documents.search_vector"), "search term"),
50
- })
51
- .whereTsSearch("documents.search_vector", "search term")
52
- .orderBy("rank", "desc");
53
-
54
- // Specify config
55
- db.select({
56
- rank: Puri.tsRank(Puri.toTsVector("documents.title", "korean"), "search term"),
57
- });
58
-
59
- // Options
60
- db.select({
61
- rank: Puri.tsRank(Puri.toTsVector("documents.title"), "search term", {
62
- normalization: 1, // Document length normalization
63
- weights: [0.1, 0.2, 0.4, 1.0], // D, C, B, A weights
64
- }),
65
- });
66
-
67
- // tsRankCd (Cover Density)
68
- db.select({
69
- rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "search term"),
70
- });
71
-
72
- // tsRankCd options
73
- db.select({
74
- rank: Puri.tsRankCd(Puri.toTsVector("documents.title"), "search term", {
75
- parser: "phraseto_tsquery",
76
- normalization: 16,
77
- }),
78
- });
79
- ```
80
-
81
- ---
82
-
83
- ## PGroonga Full-Text Search
84
-
85
- ### whereSearch
86
-
87
- ```typescript
88
- // Single column search
89
- db.whereSearch("title", "search term");
90
-
91
- // Multi-column search (requires same column composition as index)
92
- db.whereSearch(["title", "content"], "search term");
93
-
94
- // Weight options
95
- db.whereSearch(["title", "content"], "search term", {
96
- weights: [10, 1], // 10x weight on title
97
- });
98
- ```
99
-
100
- ### score (Search Score)
101
-
102
- ```typescript
103
- db.select({
104
- id: "id",
105
- title: "title",
106
- score: Puri.score(),
107
- })
108
- .whereSearch("title", "search term")
109
- .orderBy("score", "desc");
110
- ```
111
-
112
- ### highlight (Highlighting)
113
-
114
- ```typescript
115
- // Single column
116
- db.select({
117
- highlighted: Puri.highlight("title", "search term"),
118
- });
119
-
120
- // Multiple columns (returns array)
121
- db.select({
122
- highlighted: Puri.highlight(["title", "content"], "search term"),
123
- });
124
-
125
- // Array of search terms
126
- db.select({
127
- highlighted: Puri.highlight("title", ["term1", "term2"]),
128
- });
129
- ```
130
-
131
- ---
132
-
133
- ## Vector Search (pgvector)
134
-
135
- ### vectorSimilarity
136
-
137
- ```typescript
138
- const embedding = await getEmbedding("search query");
139
-
140
- // Default (cosine similarity)
141
- const results = await db
142
- .table("documents")
143
- .select({ id: "id", title: "title" })
144
- .vectorSimilarity("embedding", embedding);
145
- // → SELECT *, 1 - (embedding <=> '[...]'::vector) as similarity
146
- // ORDER BY embedding <=> '[...]'::vector
147
-
148
- // L2 distance
149
- db.vectorSimilarity("embedding", embedding, { method: "l2" });
150
-
151
- // Inner product
152
- db.vectorSimilarity("embedding", embedding, { method: "inner_product" });
153
-
154
- // threshold filter
155
- db.vectorSimilarity("embedding", embedding, {
156
- method: "cosine",
157
- threshold: 0.7, // only similarity >= 0.7
158
- });
159
-
160
- // distinctOn (deduplicate)
161
- db.vectorSimilarity("embedding", embedding, {
162
- distinctOn: "document_id", // best similarity per document_id only
163
- });
164
- ```
165
-
166
- **Return value**: `similarity` column is automatically added
167
-
168
- | method | similarity meaning | sort order |
169
- | ------------- | ------------------------------------ | ---------- |
170
- | cosine | 1 - distance (higher = more similar) | desc |
171
- | l2 | distance (lower = more similar) | asc |
172
- | inner_product | -distance (higher = more similar) | desc |
173
-
174
- ---
175
-
176
- ## pg_trgm Fuzzy Search
177
-
178
- Requires `CREATE EXTENSION IF NOT EXISTS pg_trgm`. Typically used together with a generated column created via the `searchText` prop and a GIN index.
179
-
180
- ### whereFuzzy — Candidate Filtering
181
-
182
- The SQL operand order differs per operator:
183
-
184
- | operator | meaning | SQL |
185
- | -------------- | ---------------------- | -------------------- |
186
- | `<%` (default) | word similarity | `'query' <% column` |
187
- | `%` | similarity | `column % 'query'` |
188
- | `<<%` | strict word similarity | `'query' <<% column` |
189
-
190
- ```typescript
191
- puri.whereFuzzy("items.search_text", query);
192
- puri.whereFuzzy("items.search_text", query, { operator: "%" });
193
- puri.whereFuzzy("items.search_text", query, { operator: "<<%" });
194
- ```
195
-
196
- ### Similarity Scores — Static Methods
197
-
198
- Returns `SqlExpression<"number">`, so use it as a score column in select:
199
-
200
- ```typescript
201
- // Puri.wordSimilarity(column, query) → word_similarity(?, ??)
202
- // Puri.similarity(column, query) → similarity(??, ?)
203
- // Puri.strictWordSimilarity(column, query) → strict_word_similarity(?, ??)
204
-
205
- const results = await this.getPuri("r")
206
- .table("items")
207
- .whereFuzzy("items.search_text", query)
208
- .select({
209
- id: "items.id",
210
- title: "items.title_ko",
211
- score: Puri.rawNumber(
212
- `word_similarity(?, items.title_ko) * 5 + word_similarity(?, items.title_en) * 3`,
213
- [query, query],
214
- ),
215
- })
216
- .orderByRaw("score DESC");
217
- ```
218
-
219
- ### Language-specific Characteristics
220
-
221
- | Language | Suitability | Notes |
222
- | -------- | ----------- | ----------------------------------------------- |
223
- | English | Excellent | Word-level splitting + word_similarity |
224
- | Korean | Good | Performance degrades for 1-2 character searches |
225
- | Japanese | Good | Performance degrades for 1-2 character searches |
226
-
227
- ---
228
-
229
- ## Rules
230
-
231
- - Puri is the standard for BOTH reads and writes in ALL contexts (Model, Frame, scripts) — do NOT run queries on a raw `DB.getDB()` handle (exceptions: migration files, `db.ts`, tests, and the `.knex` escape hatch)
232
- - Inside a Frame, use the associated Model's `getPuri` (Frame exposes only `getDB` / `getUpsertBuilder`, not `getPuri`)
233
- - Outside a Model, wrap knex with `new PuriWrapper(DB.getDB(which), new UpsertBuilder())`
234
- - Use the `puri.knex` escape hatch only for non-entity / framework-internal tables (e.g. `workflow_runs`)
235
- - MUST use `getPuri("r")` for read queries, `getPuri("w")` for write queries
236
- - MUST include WHERE condition for UPDATE/DELETE operations
237
- - MUST use `transaction()` for multiple write operations
238
- - JSON/JSONB columns are automatically JSON.stringify'd on insert/update