pgkiln 0.68.0 → 0.70.0

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 (65) hide show
  1. package/db/migrations/135_fulltext_search.sql +60 -0
  2. package/db/migrations/136_vector_search.sql +252 -0
  3. package/docs/apex-feature-parity.md +5 -4
  4. package/docs/guide/03-builder.md +76 -0
  5. package/docs/guide/04-pages-and-regions.md +86 -1
  6. package/docs/guide/09-reference.md +19 -0
  7. package/docs/guide/12-development.md +41 -2
  8. package/docs/guide/15-extensions.md +1 -1
  9. package/examples/hr/README.md +2 -0
  10. package/examples/hr/hr_71_fulltext_search.sql +43 -0
  11. package/examples/hr/hr_72_vector_search.sql +158 -0
  12. package/package.json +1 -1
  13. package/public/builder.css +3 -0
  14. package/src/ai/embeddings.ts +197 -0
  15. package/src/ai/service.ts +13 -9
  16. package/src/app.ts +3 -0
  17. package/src/builder/advisor.ts +79 -3
  18. package/src/builder/ai-agents.ts +13 -5
  19. package/src/builder/components.ts +32 -1
  20. package/src/builder/fulltext.ts +160 -0
  21. package/src/builder/home.ts +1 -0
  22. package/src/builder/i18n/de.ts +54 -0
  23. package/src/builder/i18n/en.ts +55 -0
  24. package/src/builder/i18n/es.ts +54 -0
  25. package/src/builder/i18n/fr.ts +54 -0
  26. package/src/builder/i18n/it.ts +54 -0
  27. package/src/builder/i18n/ja.ts +54 -0
  28. package/src/builder/i18n/ko.ts +54 -0
  29. package/src/builder/i18n/nl.ts +54 -0
  30. package/src/builder/i18n/pt.ts +54 -0
  31. package/src/builder/i18n/zh-cn.ts +54 -0
  32. package/src/builder/i18n/zh-tw.ts +54 -0
  33. package/src/builder/object-actions.ts +2 -0
  34. package/src/builder/shared.ts +1 -1
  35. package/src/builder/sql.ts +6 -0
  36. package/src/builder/table-editor.ts +2 -0
  37. package/src/builder/vector-column.ts +229 -0
  38. package/src/builder/vector-providers.ts +286 -0
  39. package/src/cli/replace.ts +3 -1
  40. package/src/i18n/ar.ts +1 -0
  41. package/src/i18n/cs.ts +1 -0
  42. package/src/i18n/da.ts +1 -0
  43. package/src/i18n/de.ts +1 -0
  44. package/src/i18n/el.ts +1 -0
  45. package/src/i18n/es.ts +1 -0
  46. package/src/i18n/fi.ts +1 -0
  47. package/src/i18n/fr.ts +1 -0
  48. package/src/i18n/he.ts +1 -0
  49. package/src/i18n/it.ts +1 -0
  50. package/src/i18n/ja.ts +1 -0
  51. package/src/i18n/ko.ts +1 -0
  52. package/src/i18n/nb.ts +1 -0
  53. package/src/i18n/pl.ts +1 -0
  54. package/src/i18n/pt.ts +1 -0
  55. package/src/i18n/ru.ts +1 -0
  56. package/src/i18n/sv.ts +1 -0
  57. package/src/i18n/tr.ts +1 -0
  58. package/src/i18n/uk.ts +1 -0
  59. package/src/i18n/zh.ts +1 -0
  60. package/src/i18n.ts +2 -0
  61. package/src/runtime/ai-agents.ts +53 -2
  62. package/src/runtime/report.ts +26 -2
  63. package/src/runtime/search.ts +290 -23
  64. package/src/server.ts +3 -0
  65. package/src/vector.ts +288 -0
@@ -404,6 +404,8 @@ Attributes (most of them are also in the page designer's **Report settings** for
404
404
  | `lazy` | `false` | Load the region after the page shows (see [large tables](#large-tables)) |
405
405
  | `cache` | none | Keep the rendered region: `{"scope": "user" \| "session" \| "all", "seconds": 300}` |
406
406
  | `searchable` | `true` | Show the search box (`false` = a "classic report") |
407
+ | `fulltext` | none | (sprint 135) A `tsvector` column of the source that the search box also uses: a row is found when its text contains the search text, **or** its tsvector matches it (`websearch_to_tsquery`: stemmed words, `"phrases"`, `or`, `-word`). It may be a `hidden` column (you chose it). Without it, the only `tsvector` column the user may see is used, if there is one. Not with a search in one column |
408
+ | `fulltext_config` | language's | (sprint 135) The text search configuration of that search (`english`, `dutch`…, the one the tsvector was made with); by default the application's primary language's, else `simple`. Only names in `pg_ts_config` are used |
407
409
  | `interactive` | `true` | Show the Actions menu (only when searchable) |
408
410
  | `sortable` | `true` | Allow sorting (`false` keeps the query's order, e.g. for trees) |
409
411
  | `mobile` | `"reflow"` | `"scroll"` keeps a horizontally scrolling table on phones |
@@ -1639,6 +1641,15 @@ Search configurations):
1639
1641
  | Icon | An [icon name](09-reference.md#icons), or `#COLUMN#` for a column that holds icon names |
1640
1642
  | Link | Where a result goes: `{"page": 3, "items": {"P3_ID": "#ID#"}}` (`#COLUMN#` is the row's value; the link gets a checksum, a modal page opens as a dialog) |
1641
1643
  | Maximum results | Per configuration, 1–100 (default 10); "Showing the first *n* rows." when there are more |
1644
+ | Search type | (sprint 135) `standard`: the words as below · `fulltext`: PostgreSQL full-text search (table and sql sources; see [full-text search](#full-text-search)) · (sprint 136) `vector`: by meaning, the rows whose embedding is nearest to the search text's · `hybrid`: full-text and vector together (see [vector search](#vector-search)) |
1645
+ | tsvector column or expression | `fulltext`: a `tsvector` column of the source (best: with a GIN index), or an expression such as `to_tsvector('english', title \|\| ' ' \|\| body)`. Empty: `to_tsvector` over the searchable columns on every search |
1646
+ | Text search configuration | `fulltext`: `english`, `dutch`, `german`, `simple`… (`pg_ts_config`): the one the tsvector was made with. Empty: the application's primary language's, else `simple` |
1647
+ | Did you mean | With the `pg_trgm` extension installed: when nothing is found, a link that searches for the closest words of the searched columns (any search type; reads up to 10,000 rows). Without `pg_trgm` nothing is suggested |
1648
+ | Vector provider | `vector`, `hybrid`: the [vector provider](03-builder.md#vector-providers) (static ID) that embeds the search text: the same one that embedded the rows |
1649
+ | Vector column | `vector`, `hybrid`: the source's `vector(n)` column, *n* the provider's dimensions |
1650
+ | Key column | `hybrid`: the key (e.g. `id`) that matches the full-text and the vector results |
1651
+ | Distance | `cosine` (default; most embedding models), `l2` or `inner_product`; an HNSW index helps only when it is built for the same one |
1652
+ | Minimum similarity | `vector`, `hybrid`: rows less similar than this are left out (cosine: 1 − distance; inner product: the product; l2: 1 / (1 + distance)). Empty: the nearest rows, however far |
1642
1653
 
1643
1654
  **How it searches.** Each word of the search text must be found (case-insensitively, anywhere in
1644
1655
  the value) in one of the searchable columns, so `clerk mil` finds MILLER the clerk. `%` and `_` are
@@ -1655,6 +1666,80 @@ select e.empno, initcap(e.ename) as name, initcap(e.job) as job, d.dname as depa
1655
1666
  with searchable columns `name, job, department`, title `name`, subtitle `job`, badge `department`
1656
1667
  and link `{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}`.
1657
1668
 
1669
+ #### Full-text search
1670
+
1671
+ A configuration of search type **fulltext** (APEX: the search type *Oracle Text*) uses PostgreSQL's full-text
1672
+ search instead of matching the words letter for letter:
1673
+
1674
+ - **Stemming**: in the `english` configuration *running shoes* finds a row with *run* and *shoe*. The text is
1675
+ read with `websearch_to_tsquery`, so users can write `"exact phrase"`, `shoes or boots` and `shoes -polish`.
1676
+ - **Ranking**: the best match first (`ts_rank_cd`; words close together and in a column with weight A count
1677
+ more), then by the title.
1678
+ - **Highlights**: the title and subtitle come back with every matched word marked, the description as the
1679
+ fragments around the matches (`ts_headline`). Everything is escaped: only ts_headline's own marks become
1680
+ `<mark>`, the data's characters are always text.
1681
+ - **The tsvector**: name a `tsvector` column of the source. The quickest way to make one is SQL Workshop ›
1682
+ Table Properties › **Add full-text search** ([chapter 3](03-builder.md)): a generated column over the text
1683
+ columns, weighted, and a GIN index on it. An expression works too (a GIN index on the same expression helps);
1684
+ with neither, the searchable columns are turned into a tsvector on every search, which reads the whole table.
1685
+ - **The text search configuration** is checked against `pg_ts_config` and passed as a query parameter; use the
1686
+ one the tsvector was made with. Empty: the configuration of the application's primary language (`en` →
1687
+ `english`, `nl` → `dutch`, `de` → `german`, `fr`, `es`, `it`, `pt`, `sv`, `da`, `nb`, `fi`, `ru`, `tr`, `el`,
1688
+ `ar`…), else `simple` (no stemming; for Japanese, Chinese and Korean).
1689
+ - **Did you mean**: with `pg_trgm` installed (`create extension pg_trgm`), a configuration that asks for it
1690
+ suggests, when nothing at all is found, the search text with each unknown word replaced by the most similar
1691
+ word of its searched columns (as the user: row level security applies). Without `pg_trgm` nothing is
1692
+ suggested and nothing fails.
1693
+
1694
+ ```sql
1695
+ -- what Add full-text search writes (HR example part 71, hr.policy)
1696
+ alter table hr.policy
1697
+ add column search_vector tsvector generated always as (
1698
+ setweight(to_tsvector('english'::regconfig, coalesce(title, '')), 'A') ||
1699
+ setweight(to_tsvector('english'::regconfig, coalesce(body, '')), 'B')) stored;
1700
+ create index policy_search_vector_idx on hr.policy using gin (search_vector);
1701
+ ```
1702
+
1703
+ with search type `fulltext`, tsvector `search_vector`, configuration `english`, title `title` and description
1704
+ `body`. The Advisor warns when a fulltext configuration's column has no GIN index (or its tsvector is built on
1705
+ every search), reports a configuration name that doesn't exist, and a column that isn't a tsvector. A report's
1706
+ search box can use the same engine: see its `fulltext` attribute.
1707
+
1708
+ #### Vector search
1709
+
1710
+ A configuration of search type **vector** (APEX: the search type *Oracle Vector Search*) finds rows by meaning:
1711
+ *time off for a family move* finds the policy *Relocation leave … relocate to another city … the new house*,
1712
+ though they share no word. It needs the [pgvector](https://github.com/pgvector/pgvector) extension (see
1713
+ [chapter 3](03-builder.md#vector-providers) for installing it), a vector provider and a vector column:
1714
+
1715
+ 1. **A vector provider** (Shared Components → Vector providers, [chapter 3](03-builder.md#vector-providers)): an
1716
+ AI service with an embedding model (OpenAI `text-embedding-3-small`, Gemini, Ollama's `nomic-embed-text` …)
1717
+ or a SQL function returning `vector`.
1718
+ 2. **A vector column** on the table, kept current: SQL Workshop › Table Properties › **Add vector column**
1719
+ ([chapter 3](03-builder.md)) adds `embedding vector(n)`, an HNSW index and a trigger, and queues the rows for
1720
+ their embeddings.
1721
+ 3. **The configuration**: search type `vector`, the provider, the vector column, title and description columns,
1722
+ icon and link as for any configuration.
1723
+
1724
+ When a user searches, pgkiln embeds the search text with the provider (an AI service: one request, remembered a
1725
+ few minutes for search-as-you-type, logged under AI usage; the text goes to that provider), then asks the
1726
+ database for the nearest rows (`order by embedding <=> $1 limit n`, which the HNSW index serves) as the
1727
+ application's role, so **row level security decides what is found**. The vector is a query parameter; pgvector's
1728
+ operators are called schema-qualified. Results are not marked word by word: they need not contain the words.
1729
+
1730
+ **Hybrid** (`hybrid`) runs the full-text search (the tsvector and text search configuration above) and the vector
1731
+ search, takes the best 50 rows of each, and ranks them together by **reciprocal rank fusion**: each row scores
1732
+ 1 / (60 + its rank) in each list it is in, summed, best first. Rows found by both rise to the top; a row that only
1733
+ one of them finds still shows. Hybrid needs the key column that tells which rows are the same.
1734
+
1735
+ Without pgvector a vector or hybrid configuration shows that the extension is missing (and the Advisor reports
1736
+ it); the other configurations answer as before. The Advisor also checks that the provider exists, that the
1737
+ column is `vector(n)` with the provider's *n*, and warns when it has no HNSW index or one for another distance.
1738
+ The HR example (part 72) adds `POLICIES_MEANING` (hybrid, with a stand-in function provider) to page 42.
1739
+
1740
+ An AI agent can use any table or SQL configuration as a **Retrieve** tool: the model gives a text, the agent gets
1741
+ the best rows ([AI agents](03-builder.md#ai-agents)).
1742
+
1658
1743
  **The region**: attributes
1659
1744
 
1660
1745
  | Key | Meaning |
@@ -1669,7 +1754,7 @@ The region's own field is a GET form (`?s<region id>=text`, so a search can be b
1669
1754
  without JavaScript; with JavaScript it searches while the user types and replaces only the results.
1670
1755
  The Advisor plans every configuration with the columns it names and reports names a region uses
1671
1756
  that don't exist. Search configurations are exported with the application. The HR example's page 42
1672
- searches employees, departments and the shortcuts list.
1757
+ searches employees, departments, the HR policies (full-text, part 71; by meaning, part 72) and the shortcuts list.
1673
1758
 
1674
1759
  ### `url` (URL region)
1675
1760
 
@@ -167,6 +167,24 @@ select meta.ai_generate('CLAUDE', 'Classify: ' || :P5_TEXT, null,
167
167
  enabled and the application is allowed); whether the provider accepts the key shows only when a
168
168
  request is made.
169
169
 
170
+ ### Embeddings in SQL
171
+
172
+ (sprint 136) `meta.embed(text, provider [, app_id])` returns the embedding of a text by a
173
+ [vector provider](03-builder.md#vector-providers) of the current application (or of `app_id`, one the
174
+ login may see) as `real[]`; cast it for pgvector: `meta.embed(:P1_Q, 'DOC_EMBEDDINGS')::vector`. It
175
+ works for **function providers** and calls the function as the role of the query (security invoker:
176
+ its grants apply), checking the dimensions. A provider that uses an AI service is called by the
177
+ pgkiln server, never from SQL: `meta.embed` raises an error that says so (use a vector search
178
+ configuration, a Retrieve tool or a vector column's queue). `meta.vector_provider(name [, app_id])`
179
+ returns a provider's definition (they hold no secrets).
180
+
181
+ ```sql
182
+ -- the nearest documents to a text, as the application's role (row level security applies)
183
+ select title from sales.doc
184
+ order by embedding <=> meta.embed('how do I return an order?', 'DOC_EMBEDDINGS')::vector
185
+ limit 5;
186
+ ```
187
+
170
188
  ### Parsing files in SQL
171
189
 
172
190
  `meta.parse_data()` reads CSV/TSV and JSON in a `bytea` (an uploaded file in `meta.temp_files`, a
@@ -488,6 +506,7 @@ navigation entries and application processes have the same `build_option` column
488
506
  | `app_ai_service` | Which applications may use which AI service, with daily limits `max_requests` and `max_tokens` (null: no limit). Not exported | no (through `meta.ai_available`) |
489
507
  | `ai_usage` | One row per AI request: `at`, `app_id`, `page_no`, `username`, `service`, `provider`, `model`, `source`, `input_tokens`, `output_tokens`, `duration_ms`, `status`, `message` (an error class, never prompt or answer text). Not exported | no |
490
508
  | `ai_tool_call` | (121) Every tool call of an [AI assistant or agent](03-builder.md#ai-agents): `at`, `app_id`, `page_no`, `region_id`, `username`, `agent`, `tool`, `type`, `arguments` (the model's, cut to 4 KB), `status` (`ok`, `error`, `refused`), `duration_ms`, `message`. Kept 30 days, not exported | no |
509
+ | `embedding_queue` | (136) Rows whose vector column the server re-embeds: `app_id`, `provider`, `tbl` (the table's oid), `vector_column`, `key_column`, `source_columns`, `key_value`, `queued_at`, `next_at`, `attempts`, `last_error`. Filled by `meta.embedding_enqueue` triggers and Backfill; not exported | no |
491
510
  | `ai_conversation` | [AI assistant](04-pages-and-regions.md#ai_assistant-ai-assistant) conversations: one per session and region (`messages`: the provider's history, `turns`: what the page shows); deleted with the session. Not exported | no |
492
511
  | `ai_table_note` | Descriptions of tables and columns for models ([App Builder AI](03-builder.md#app-builder-ai)): `schema_name`, `table_name`, `column_name` (`''` for the table), `note`. Installation data, not exported | no |
493
512
  | `blueprint` | Saved [blueprints](03-builder.md#creating-an-application-from-a-blueprint): `name`, `spec` (the JSON), `app_id` (the application last created from it), `created_by`. Builder data, not exported | no |
@@ -145,6 +145,12 @@ src/
145
145
  scramVerifier (SCRAM-SHA-256, SASLprep) and the password slot (maskSql for review and log, withVerifier to run)
146
146
  cron-schedule.ts (sprint 101, DA-12) pg_cron schedules without a database: parseSchedule (cron fields, $, n seconds, macros),
147
147
  nextRuns in a time zone, describeSchedule in words, the builder's presets and fields
148
+ vector.ts (sprint 136, AP-1) pgvector: pgvectorStatus(), vectorSchema() (operators and the type schema-qualified),
149
+ DISTANCES (cosine/l2/inner_product: operator, HNSW opclass, similarity), vectorColumnDims(); the embedding
150
+ queue (meta.embedding_queue, migration 136): embeddingTick() (groups, FOR UPDATE SKIP LOCKED, batches
151
+ within the provider's per-minute limit, written with devDbForApp(), retries with backoff), drainEmbeddings(),
152
+ kickEmbeddings(), startEmbeddingRunner() (server.ts; EMBEDDINGS=off, EMBEDDING_INTERVAL_S), vectorTriggers()
153
+ (meta.embedding_set / embedding_enqueue triggers and their arguments), backfill(), queueState()
148
154
  pgcron.ts (sprint 101, DA-12) pg_cron for SQL Workshop → Scheduling: cronState (installed, loaded, cron.database_name,
149
155
  access), jobs and runs, jobSql/activeSql/unscheduleSql/purgeSql (reviewed, then applySql), runNow
150
156
  secrets.ts secrets at rest (web credentials): AES-256-GCM with PGKILN_SECRET_KEY (or a key given, for rotate-key);
@@ -176,6 +182,11 @@ src/
176
182
  (121) an agent's effort (Claude output_config.effort, OpenAI reasoning_effort) and output limit).
177
183
  Providers (121): anthropic, openai, gemini (OpenAI-compatible endpoint, GEMINI_BASE_URL), openai_compatible
178
184
  (Ollama, vLLM, Azure OpenAI; base URL required, key optional): service.ts serviceKey(), baseUrlFor(), providerLabel()
185
+ (sprint 136, AP-1) embeddings.ts: vector providers (meta.app.vector_providers: ai_service with an embedding
186
+ model, or a function), vectorProviderProblems(), embedTexts() (the openai SDK's embeddings API, float
187
+ encoding, dimensions checked, daily limits via checkDailyLimit(), meta.ai_usage source "embedding"; a
188
+ function provider through meta.embed on the caller's connection), embedQuery() (search texts remembered
189
+ 10 minutes per process), vectorLiteral()
179
190
  debug.ts debug messages: DebugLog (levels, timed steps, NOTICEs of meta.debug from appTx), started in
180
191
  loadContext, stored after the response (onResponse hook → meta.debug_save), hourly purge
181
192
  icons.ts icon helper (sprite in public/icons.svg)
@@ -255,7 +266,8 @@ src/
255
266
  definitionProblems), agentFor(), prepareTools() (authorization schemes, navigate pages the user may
256
267
  open, strict schemas), runAgentTool() (sql: read-only transaction as the app role with a time limit;
257
268
  function: named arguments, committed; rest; javascript: queued for the browser; navigate: URL and
258
- checksum built here), logToolCall() (meta.ai_tool_call, debug level 4, 30-day purge)
269
+ checksum built here), logToolCall() (meta.ai_tool_call, debug level 4, 30-day purge); (136) retrieve:
270
+ retrieveTool() (QUERY only, search.ts retrieve() in a read-only transaction as the app role)
259
271
  ai-filter.ts "Ask in your own words" on a report ("ai_filter", sprint 112: AI interactive reports): the box, the chips
260
272
  form (Edit/Remove/Undo, undo history in the session), ask and chips routes; an AI service's structured
261
273
  output (planSchema) or the phrase parser, then ir-plan.ts
@@ -266,6 +278,14 @@ src/
266
278
  pdf.ts report PDFs with report layouts (pdfkit); rows from a cursor in batches (tablePdf takes batches)
267
279
  search.ts search region and search configurations (migration 080): words as ILIKE parameters, table/sql/list
268
280
  sources as the app role in savepoints, marked results, configColumns/searchConfigProblems for the Advisor
281
+ (sprint 135, migration 135) search type fulltext: tsvectorSql (column, expression or built from the
282
+ searchable columns), websearch_to_tsquery / ts_rank_cd / ts_headline between private-use markers
283
+ (headlined() escapes, only the markers become <mark>), textSearchConfig() (pg_ts_config names only,
284
+ passed as $1::regconfig; tsConfigFor(language)), trigramSchema() and "did you mean" with pg_trgm
285
+ (sprint 136, migration 136) search types vector (vectorRows: the text embedded by the provider, nearest
286
+ rows by the schema-qualified distance operator, the vector as $1, min_similarity) and hybrid (full-text
287
+ and vector rankings, 50 rows each, reciprocal rank fusion 1/(60+rank) per key_column); retrieve() for
288
+ an agent's Retrieve tool
269
289
  url-region.ts URL region (migration 078): literal origins only (urlTarget), &ITEM. URL-encoded, sandbox and height,
270
290
  frameOrigins() for the CSP's frame-src (security.ts)
271
291
  ajax.ts Ajax callbacks (migration 085: POST …/ajax/:name, point "ajax" processes, x01 … x10 binds) and the
@@ -353,6 +373,16 @@ src/
353
373
  object-browser.ts (sprint 75) the Object Browser: objects by type, system objects hidden, a table in tabs, column actions as SQL to review
354
374
  table-editor.ts (sprint 95) Table Properties: GET/POST /sql/objects/table (create, edit ?o=, ?template=), fromForm(), Preview / Apply
355
375
  (one transaction) / Open in the worksheet
376
+ fulltext.ts (sprint 135, AP-2) Table Properties › Add full-text search: GET/POST /sql/objects/fulltext?o= (text columns
377
+ with weights A–D, a configuration from pg_ts_config, fulltextDdl(): a generated tsvector column and a GIN
378
+ index), Preview / Apply (one transaction, recorded in the Change Log) / Open in the worksheet
379
+ vector-column.ts (sprint 136, AP-1) Table Properties › Add vector column: GET/POST /sql/objects/vector?o= (a provider of the
380
+ workspace's applications, source columns, distance; vectorDdl(): vector(n), HNSW index, the
381
+ meta.embedding_set / embedding_enqueue trigger), Preview / Apply (Change Log; the rows queued) / worksheet;
382
+ the table's vector columns with progress and Backfill (mode=backfill, all=on)
383
+ vector-providers.ts (sprint 136, AP-1) Shared Components → Vector providers (/apps/:id/vector-providers: list, provider form,
384
+ test, delete) and Workspace utilities → Vector providers (/vector-providers: pgvector status, Create
385
+ extension for administrators, the workspace's providers and vector columns); pgvectorNote()
356
386
  data-security.ts (sprint 119) a table's Security: GET/POST /sql/objects/security?o= (policies, roles × columns, masks), Preview / Apply
357
387
  object-actions.ts (sprint 94) an object's ⋯ menu (objectMenu) in the worksheet and the Object Browser: Quick DDL (objectDdl), Drop with
358
388
  dependents, Add index / trigger / policy as drafts; GET /sql/objects/action, /sql/objects/panel (Open in a panel)
@@ -495,6 +525,13 @@ test/
495
525
  changelog.test.ts (sprint 106) DDL recognition and names, recording from Table Properties, the worksheet and Compare,
496
526
  downloads (Flyway, Liquibase, SQL), stable numbers, the latest application export
497
527
  table-editor.test.ts (sprint 95) the Table Properties page: preview, apply, a failing apply rolls back, template, worksheet
528
+ fulltext-search.test.ts (sprint 135) full-text search configurations: stemming, rank order, highlights, websearch syntax, the
529
+ configuration fallback, did you mean (pg_trgm when it can be installed), the report search on a tsvector
530
+ column, the Advisor's GIN check, Add full-text search (preview, apply, change log), export/import
531
+ vector-search.test.ts (sprint 136) vector providers and their checks, meta.embed, Add vector column (preview, apply, the queue
532
+ with the mock's embeddings, a change queued again, Backfill, a failing batch), vector and hybrid search
533
+ ("time off for a family move" → Relocation leave; RLS), the Advisor, Retrieve tools on page 49, the
534
+ builder pages, export; skips what needs pgvector when it isn't installed
498
535
  object-actions.test.ts (sprint 94) object menus: Quick DDL, Drop with dependents, Add index / trigger / policy, the panel, not-found
499
536
  data-modeler.test.ts (sprint 98) Data Modeler: "hr core" with its FK lines, saved positions, SVG, objects, DDL, dictionary, geometry
500
537
  worksheet.test.ts (sprint 91) the SQL Worksheet: statement at the cursor, Run Script log, plans, notices, history, navigator, Run as
@@ -556,7 +593,9 @@ test/
556
593
  find-workspace.test.ts developers' addresses, builder mail, the workspaces mail, the same answer, limits
557
594
  ai-builder.test.ts App Builder AI: the builder's service, SQL from a question, explain, describe tables, pages with AI
558
595
  ai-script-mock.ts a scripted mock of Claude (SSE, tool_use with a thinking block) and OpenAI (tool calls), reply by reply
559
- ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions
596
+ ai-mock.ts a local mock of the Claude Messages API (SSE stream) and OpenAI Chat Completions; (136) OpenAI's
597
+ embeddings (/embeddings, also in ai-script-mock.ts): conceptEmbedding(), eight "topics", the same
598
+ as the HR example's hr.policy_embedding
560
599
  helpers.ts a cookie-keeping test browser
561
600
  e2e/responsive.test.ts browser tests at phone/tablet/desktop widths (Playwright)
562
601
  e2e/builder-i18n.test.ts (sprint 121) Deutsch on the sign-in page, the status bar's language menu, German screens at 390 px, CSP
@@ -63,7 +63,7 @@ that includes them.
63
63
  | **[pg_cron](https://github.com/citusdata/pg_cron)** | Run SQL on a schedule inside the database (`select cron.schedule('nightly', '0 2 * * *', 'call hr.close_month()')`) | **Automations**, `DBMS_SCHEDULER` |
64
64
  | **[pgaudit](https://github.com/pgaudit/pgaudit)** | Statement-level audit logging (reads, writes, DDL, role changes) to the server log, for compliance | Oracle Unified Auditing |
65
65
  | **PostGIS** | Geometry and geography types, spatial indexes and functions: the foundation for a map region | **Map region**, Oracle Spatial |
66
- | **pgvector** | Vector similarity search (embeddings) for semantic search and retrieval-augmented generation | APEX AI (RAG sources), Oracle AI Vector Search |
66
+ | **pgvector** | Vector similarity search (embeddings) for semantic search and retrieval-augmented generation; pgkiln's vector providers, vector and hybrid search configurations and Retrieve tools use it (sprint 136, [chapter 4](04-pages-and-regions.md#vector-search)) | APEX AI (RAG sources), Oracle AI Vector Search |
67
67
  | **[plpgsql_check](https://github.com/okbob/plpgsql_check)** | Static analysis of PL/pgSQL: wrong columns, unused variables, missing `RETURN`, hidden casts. Run it on your app's functions in CI | **Advisor** (for code) |
68
68
 
69
69
  Scheduling example with pg_cron. pg_cron must be in `shared_preload_libraries` (a server setting;
@@ -42,6 +42,8 @@ Then open http://127.0.0.1:3100/a/hr and sign in as `king`, `blake`, `jones`, `a
42
42
  | `hr_49_push_notifications.sql` | Push notifications: the app may send them (users turn them on under *My account → Notifications*); a new leave request notifies the manager, a decision notifies the employee, and the notification opens the request (page 7) |
43
43
  | `hr_64_ai_agent.sql` | *HR agent* (page 46): the AI agent `HR_AGENT` (Shared Components → AI Agents) with four tools: `top_earners` (SQL, read-only as `hr_app`), `open_employee` (navigate: a link to the employee form with its checksum, only for users who may open page 3), `give_raise` (the PL/pgSQL function `hr.give_raise`, managers only) and `note` (a JavaScript tool: the custom event `hr_agent_note`). Ask *Who in Sales earns most?* Needs an AI service named `HR_ASSISTANT` (see part 41) |
44
44
  | `hr_68_component_details.sql` | *Component details* (page 47): a popup LOV over 6,000 rows that loads more as the dialog scrolls, a wait popup around a two-second process (*Recalculate*) and around an Ajax call (*Check in the background*), and an *Actions* menu whose entries run dynamic actions (event `menu_entry`) |
45
+ | `hr_71_fulltext_search.sql` | Full-text search: `hr.policy` gets a generated `search_vector` column (title weight A, body B, english) with a GIN index, and the search configuration `POLICIES` (search type *fulltext*, *Did you mean* when `pg_trgm` is installed) joins page 42 *Search*: "works home" finds *Working from home*, the matched words highlighted |
46
+ | `hr_72_vector_search.sql` | Search by meaning: `hr.policy` gets *Relocation leave* and a managers-only *Salary bands* with row level security (`audience`); with pgvector, the vector provider `POLICY_EMBEDDINGS` (a stand-in function, `hr.policy_embedding`, eight topics), `hr.policy.embedding vector(8)` with an HNSW index and a trigger, the hybrid search configuration `POLICIES_MEANING` on page 42 (*time off for a family move* finds *Relocation leave*), and *Policy questions* (page 49): the agent `POLICY_AGENT` with the Retrieve tool `policy_search`. Needs an AI service named `HR_ASSISTANT` for page 49 |
45
47
 
46
48
  The files run in order, each once (recorded in `public.pgkiln_seed`), so new ones can be added
47
49
  later. pgkiln's own tests use this application as their fixture (`npm test` installs it first).
@@ -0,0 +1,43 @@
1
+ -- =====================================================================
2
+ -- HR example, part 71: a full-text search configuration (sprint 135, AP-2;
3
+ -- migration 135; docs/guide/04-pages-and-regions.md, search region)
4
+ --
5
+ -- hr.policy (part 42) gets what SQL Workshop › Table Properties › Add
6
+ -- full-text search writes: a stored generated tsvector column over the
7
+ -- title (weight A) and the body (weight B) in the english text search
8
+ -- configuration, and a GIN index on it.
9
+ --
10
+ -- The search configuration POLICIES searches it with search type fulltext:
11
+ -- page 42 "Search" then finds "Requesting leave" for "request" and
12
+ -- "Working from home" for "works home", the best match first and the
13
+ -- matched words highlighted. "Did you mean" suggests the closest words when
14
+ -- nothing is found, once the pg_trgm extension is installed (without it,
15
+ -- nothing is suggested).
16
+ -- =====================================================================
17
+
18
+ alter table hr.policy
19
+ add column search_vector tsvector generated always as (
20
+ setweight(to_tsvector('english'::regconfig, coalesce(title, '')), 'A') ||
21
+ setweight(to_tsvector('english'::regconfig, coalesce(body, '')), 'B')) stored;
22
+ create index policy_search_vector_idx on hr.policy using gin (search_vector);
23
+
24
+ insert into meta.search_config (app_id, name, label, seq, source_type, source, search_type, fts_vector, fts_config, did_you_mean,
25
+ title_column, description_column, icon, max_results)
26
+ select id, 'POLICIES', 'HR policies', 25, 'table', 'hr.policy', 'fulltext', 'search_vector', 'english', true,
27
+ 'title', 'body', 'book', 5
28
+ from meta.app where alias = 'hr';
29
+
30
+ update meta.region r
31
+ set config = jsonb_set(r.config, '{configs}', (r.config->'configs') || '["POLICIES"]'::jsonb)
32
+ || '{"placeholder": "Search employees, departments, policies and shortcuts…"}'::jsonb
33
+ from meta.page p join meta.app a on a.id = p.app_id
34
+ where r.page_id = p.id and a.alias = 'hr' and p.page_no = 42 and r.type = 'search';
35
+
36
+ insert into meta.translation (app_id, language, source, target)
37
+ select a.id, 'nl', t.source, t.target
38
+ from meta.app a, (values
39
+ ('HR policies', 'HR-regelingen'),
40
+ ('Search employees, departments, policies and shortcuts…', 'Zoek medewerkers, afdelingen, regelingen en snelkoppelingen…')
41
+ ) as t (source, target)
42
+ where a.alias = 'hr'
43
+ on conflict do nothing;
@@ -0,0 +1,158 @@
1
+ -- =====================================================================
2
+ -- HR example, part 72: search by meaning (sprint 136, AP-1; migration
3
+ -- 136; docs/guide/04-pages-and-regions.md "Vector search", 03-builder.md
4
+ -- "Vector providers", 20-ai-agents.md)
5
+ --
6
+ -- hr.policy (parts 42 and 71) gets two more policies and an audience:
7
+ -- "Salary bands" is for managers only, and row level security now shows
8
+ -- hr_app only the policies the user may read. Every search below runs as
9
+ -- hr_app, so an employee never finds the managers' policy.
10
+ --
11
+ -- With pgvector installed (the extension is created here when the server
12
+ -- has it and this connection may; otherwise this part only adds the
13
+ -- policies and says so):
14
+ --
15
+ -- - the vector provider POLICY_EMBEDDINGS (Shared Components → Vector
16
+ -- providers) of type function: hr.policy_embedding(text), a tiny
17
+ -- stand-in for an embedding model that maps words to eight topics
18
+ -- (moving house, time off, home working, sickness, pay, requests,
19
+ -- public holidays, anything else). It keeps the example working without
20
+ -- an AI service; for real use, make the provider an AI service with an
21
+ -- embedding model (e.g. OpenAI text-embedding-3-small, 1536 dimensions,
22
+ -- or Ollama's nomic-embed-text, 768) and add the column again with
23
+ -- Table Properties › Add vector column;
24
+ -- - what Table Properties › Add vector column writes: hr.policy.embedding
25
+ -- vector(8), an HNSW index (vector_cosine_ops) and a trigger
26
+ -- (meta.embedding_set) that embeds a policy when its title or body
27
+ -- changes;
28
+ -- - the search configuration POLICIES_MEANING (search type hybrid:
29
+ -- full-text on search_vector and the embedding, merged by reciprocal
30
+ -- rank fusion) joins page 42 "Search": "time off for a family move"
31
+ -- finds "Relocation leave", which uses none of those words;
32
+ -- - page 49 "Policy questions": an AI assistant region with the agent
33
+ -- POLICY_AGENT, whose Retrieve tool policy_search hands the model the
34
+ -- policies that match a question (as hr_app). Like parts 41, 42 and 64
35
+ -- it needs an AI service named HR_ASSISTANT.
36
+ -- =====================================================================
37
+
38
+ alter table hr.policy add column audience text not null default 'all' check (audience in ('all', 'managers'));
39
+
40
+ insert into hr.policy (title, body, audience) values
41
+ ('Relocation leave', 'Employees who relocate to another city for the company get three paid days to settle into the new house, and help with the removal costs.', 'all'),
42
+ ('Salary bands', 'Managers review the salary bands every spring; a raise above the band needs the director''s approval.', 'managers');
43
+
44
+ alter table hr.policy enable row level security;
45
+ create policy policy_visible on hr.policy for select
46
+ using (audience = 'all' or meta.has_role('manager') or meta.has_role('admin'));
47
+
48
+ -- a stand-in for an embedding model: each word counts for one of eight topics (unknown words a little for the
49
+ -- last), and the vector is normalised, so the cosine of two texts says how much they share topics
50
+ create function hr.policy_embedding(p_text text) returns real[]
51
+ language sql immutable parallel safe set search_path = pg_catalog as $$
52
+ with w as (select lower(m[1]) as w from regexp_matches(coalesce(p_text, ''), '([[:alpha:]]+)', 'g') m),
53
+ d as (select case
54
+ when w ~ '^(relocat|move|moving|removal|house|settl|city|family)' then 1
55
+ when w ~ '^(leave|time|off|holiday|vacation|days|absence)$' then 2
56
+ when w ~ '^(home|remote|working|work)$' then 3
57
+ when w ~ '^(sick|ill|doctor)' then 4
58
+ when w ~ '^(salar|pay|band|raise|wage|scale)' then 5
59
+ when w ~ '^(request|approv|manager)' then 6
60
+ when w ~ '^(public|closed|office)' then 7
61
+ else 8 end as dim,
62
+ case when w ~ '^(relocat|move|moving|removal|house|settl|city|family|leave|time|off|holiday|vacation|days|absence|home|remote|working|work|sick|ill|doctor|salar|pay|band|raise|wage|scale|request|approv|manager|public|closed|office)'
63
+ then 1.0 else 0.05 end as weight
64
+ from w),
65
+ v as (select i, coalesce((select sum(weight) from d where d.dim = i), 0) as x from generate_series(1, 8) i),
66
+ n as (select sqrt(sum(x * x)) as n from v)
67
+ select array_agg((case when n.n = 0 then (case when i = 8 then 1 else 0 end) else x / n.n end)::real order by i) from v, n
68
+ $$;
69
+ grant execute on function hr.policy_embedding(text) to hr_app;
70
+
71
+ update meta.app set
72
+ vector_providers = vector_providers || $j$[
73
+ {"name": "POLICY_EMBEDDINGS", "type": "function", "function": "hr.policy_embedding", "dimensions": 8,
74
+ "description": "A stand-in for an embedding model (eight topics); use an AI service for real texts"}
75
+ ]$j$::jsonb,
76
+ ai_tools = ai_tools || $j$[
77
+ {"name": "policy_search", "type": "retrieve", "search_config": "POLICIES_MEANING", "max_rows": 3,
78
+ "description": "The HR policies that match a question, best first (title and text). Use it for every question about rules, leave, pay or working arrangements."}
79
+ ]$j$::jsonb,
80
+ ai_agents = ai_agents || $j$[
81
+ {"name": "POLICY_AGENT", "description": "Answers questions about the HR policies the user may read",
82
+ "service": "HR_ASSISTANT", "effort": "low", "tools": ["policy_search"], "max_steps": 3, "max_tokens": 1500,
83
+ "system": "You answer questions of &APP_USER. about the company's HR policies. Look the policies up with policy_search and answer only from what it returns; say so when no policy covers the question. Name the policy you used. Be brief."}
84
+ ]$j$::jsonb
85
+ where alias = 'hr';
86
+
87
+ -- page 49: questions about the policies, answered through the Retrieve tool
88
+ insert into meta.page (app_id, page_no, name, title, parent_page)
89
+ select id, 49, 'Policy questions', 'Policy questions', 1 from meta.app where alias = 'hr';
90
+
91
+ insert into meta.region (page_id, seq, title, type, columns, template, source, condition, config)
92
+ select p.id, r.seq, r.title, r.type, r.cols, 'standard', r.source, r.cond, r.config::jsonb
93
+ from meta.page p join meta.app a on a.id = p.app_id, (values
94
+ (10, 'Not set up yet', 'static', 12,
95
+ '<p class="alert alert-info">Policy questions need the pgvector extension in the database and an AI service named <b>HR_ASSISTANT</b> (App Builder → Workspace utilities → AI services, allowed for the HR application).</p>',
96
+ 'not (meta.ai_available(''HR_ASSISTANT'') and exists (select from pg_catalog.pg_extension where extname = ''vector''))', '{}'),
97
+ (20, 'Ask about the policies', 'ai_assistant', 8, null,
98
+ 'meta.ai_available(''HR_ASSISTANT'') and exists (select from pg_catalog.pg_extension where extname = ''vector'')',
99
+ $j${"agent": "POLICY_AGENT",
100
+ "welcome": "Hello! Ask me about the HR policies, e.g. how much time off you get when you move for the company.",
101
+ "placeholder": "e.g. Do I get time off for a family move?",
102
+ "max_turns": 20,
103
+ "error_message": "The policy assistant could not answer just now. Please try again later."}$j$)
104
+ ) as r (seq, title, type, cols, source, cond, config)
105
+ where a.alias = 'hr' and p.page_no = 49;
106
+
107
+ insert into meta.nav_entry (app_id, seq, label, icon, target_page)
108
+ select id, 30, 'Policy questions', 'book', 49 from meta.app where alias = 'hr';
109
+
110
+ -- with pgvector: the extension (when this connection may create it), the vector column, the search configuration
111
+ -- and the search page
112
+ do $$
113
+ declare
114
+ v_schema text;
115
+ begin
116
+ if not exists (select from pg_extension where extname = 'vector') then
117
+ if exists (select from pg_available_extensions where name = 'vector') and (select rolsuper from pg_roles where rolname = current_user) then
118
+ create extension vector;
119
+ else
120
+ raise notice 'HR part 72: pgvector is not installed: hr.policy gets no vector column (install pgvector, then add one with Table Properties › Add vector column).';
121
+ return;
122
+ end if;
123
+ end if;
124
+ select n.nspname into v_schema from pg_extension e join pg_namespace n on n.oid = e.extnamespace where e.extname = 'vector';
125
+ execute format('alter table hr.policy add column embedding %I.vector(8)', v_schema);
126
+ execute format('create index policy_embedding_idx on hr.policy using hnsw (embedding %I.vector_cosine_ops)', v_schema);
127
+ execute format($t$create trigger embedding_embed before insert or update of title, body on hr.policy
128
+ for each row execute function meta.embedding_set(%L, 'POLICY_EMBEDDINGS', 'embedding', 'id', 'title', 'body')$t$,
129
+ (select id from meta.app where alias = 'hr'));
130
+ -- the policies there are: embedded now (the trigger does it for new and changed ones)
131
+ execute format($u$update hr.policy set embedding = translate(meta.embed(meta.embedding_text(to_jsonb(policy), '{title,body}'), 'POLICY_EMBEDDINGS', %s)::text, '{}', '[]')::text::%I.vector$u$,
132
+ (select id from meta.app where alias = 'hr'), v_schema);
133
+ insert into meta.search_config (app_id, name, label, seq, source_type, source, search_type, fts_vector, fts_config,
134
+ vector_provider, vector_column, key_column, distance, min_similarity,
135
+ title_column, description_column, icon, max_results)
136
+ select id, 'POLICIES_MEANING', 'Policies by meaning', 26, 'table', 'hr.policy', 'hybrid', 'search_vector', 'english',
137
+ 'POLICY_EMBEDDINGS', 'embedding', 'id', 'cosine', 0.6,
138
+ 'title', 'body', 'book', 3
139
+ from meta.app where alias = 'hr';
140
+ update meta.region r
141
+ set config = jsonb_set(r.config, '{configs}', (r.config->'configs') || '["POLICIES_MEANING"]'::jsonb)
142
+ from meta.page p join meta.app a on a.id = p.app_id
143
+ where r.page_id = p.id and a.alias = 'hr' and p.page_no = 42 and r.type = 'search';
144
+ end $$;
145
+
146
+ insert into meta.translation (app_id, language, source, target)
147
+ select a.id, 'nl', t.source, t.target
148
+ from meta.app a, (values
149
+ ('Policies by meaning', 'Regelingen op betekenis'),
150
+ ('Policy questions', 'Vragen over regelingen'),
151
+ ('Ask about the policies', 'Vraag naar de regelingen'),
152
+ ('Not set up yet', 'Nog niet ingesteld'),
153
+ ('Hello! Ask me about the HR policies, e.g. how much time off you get when you move for the company.',
154
+ 'Hallo! Vraag me naar de HR-regelingen, bijv. hoeveel vrij je krijgt als je voor het bedrijf verhuist.'),
155
+ ('e.g. Do I get time off for a family move?', 'bijv. Krijg ik vrij voor een verhuizing met het gezin?')
156
+ ) as t (source, target)
157
+ where a.alias = 'hr'
158
+ on conflict do nothing;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.68.0",
3
+ "version": "0.70.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
@@ -1597,3 +1597,6 @@ pre.wrap-pre { white-space: pre-wrap; overflow-wrap: anywhere; }
1597
1597
  --font: "Inter", "Segoe UI Variable Text", "Segoe UI", system-ui, -apple-system, "Apple SD Gothic Neo", "Malgun Gothic", "Noto Sans KR", "Noto Sans CJK KR", "Source Han Sans KR", sans-serif; }
1598
1598
  :is(.ide-lang-form, .signin-langs) [lang] { font-family: var(--font); }
1599
1599
  :is(.ide, .ide-login):lang(ko) { word-break: keep-all; overflow-wrap: break-word; }
1600
+
1601
+ /* (sprint 136) a button inside an alert (e.g. Add vector column without pgvector) wraps on a phone instead of overflowing */
1602
+ .alert .btn { white-space: normal; overflow-wrap: anywhere; max-width: 100%; }