pgkiln 0.68.0 → 0.69.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 (48) hide show
  1. package/db/migrations/135_fulltext_search.sql +60 -0
  2. package/docs/apex-feature-parity.md +2 -2
  3. package/docs/guide/03-builder.md +7 -0
  4. package/docs/guide/04-pages-and-regions.md +46 -1
  5. package/docs/guide/12-development.md +10 -0
  6. package/examples/hr/README.md +1 -0
  7. package/examples/hr/hr_71_fulltext_search.sql +43 -0
  8. package/package.json +1 -1
  9. package/src/builder/advisor.ts +39 -3
  10. package/src/builder/components.ts +14 -1
  11. package/src/builder/fulltext.ts +160 -0
  12. package/src/builder/i18n/de.ts +20 -0
  13. package/src/builder/i18n/en.ts +21 -0
  14. package/src/builder/i18n/es.ts +20 -0
  15. package/src/builder/i18n/fr.ts +20 -0
  16. package/src/builder/i18n/it.ts +20 -0
  17. package/src/builder/i18n/ja.ts +20 -0
  18. package/src/builder/i18n/ko.ts +20 -0
  19. package/src/builder/i18n/nl.ts +20 -0
  20. package/src/builder/i18n/pt.ts +20 -0
  21. package/src/builder/i18n/zh-cn.ts +20 -0
  22. package/src/builder/i18n/zh-tw.ts +20 -0
  23. package/src/builder/object-actions.ts +1 -0
  24. package/src/builder/sql.ts +3 -0
  25. package/src/builder/table-editor.ts +1 -0
  26. package/src/i18n/ar.ts +1 -0
  27. package/src/i18n/cs.ts +1 -0
  28. package/src/i18n/da.ts +1 -0
  29. package/src/i18n/de.ts +1 -0
  30. package/src/i18n/el.ts +1 -0
  31. package/src/i18n/es.ts +1 -0
  32. package/src/i18n/fi.ts +1 -0
  33. package/src/i18n/fr.ts +1 -0
  34. package/src/i18n/he.ts +1 -0
  35. package/src/i18n/it.ts +1 -0
  36. package/src/i18n/ja.ts +1 -0
  37. package/src/i18n/ko.ts +1 -0
  38. package/src/i18n/nb.ts +1 -0
  39. package/src/i18n/pl.ts +1 -0
  40. package/src/i18n/pt.ts +1 -0
  41. package/src/i18n/ru.ts +1 -0
  42. package/src/i18n/sv.ts +1 -0
  43. package/src/i18n/tr.ts +1 -0
  44. package/src/i18n/uk.ts +1 -0
  45. package/src/i18n/zh.ts +1 -0
  46. package/src/i18n.ts +2 -0
  47. package/src/runtime/report.ts +26 -2
  48. package/src/runtime/search.ts +184 -21
@@ -0,0 +1,60 @@
1
+ -- =====================================================================
2
+ -- 135: full-text search configurations (sprint 135, AP-2; APEX: a search
3
+ -- configuration's Search Type "Oracle Text" next to "Standard")
4
+ --
5
+ -- A search configuration gets a search type:
6
+ --
7
+ -- standard every word of the search text in one of the searchable
8
+ -- columns (ILIKE), as before (080);
9
+ -- fulltext PostgreSQL full-text search: the source's tsvector (a column,
10
+ -- an expression, or built from the searchable columns) matched
11
+ -- with websearch_to_tsquery(<text search configuration>, <text>),
12
+ -- the best match first (ts_rank_cd), the matched words
13
+ -- highlighted with ts_headline (src/runtime/search.ts).
14
+ --
15
+ -- The type is open for later ones: `vector` (embeddings) and `hybrid`
16
+ -- (full-text and vector results merged by reciprocal rank fusion) will be
17
+ -- added to the same check with their own columns.
18
+ --
19
+ -- fts_vector a tsvector column of the source, or an expression giving a
20
+ -- tsvector; empty: to_tsvector over the searchable columns
21
+ -- fts_config the text search configuration (pg_ts_config: english,
22
+ -- dutch, german, simple…); empty: the one of the
23
+ -- application's primary language, else simple
24
+ -- did_you_mean with pg_trgm installed: when nothing is found, suggest
25
+ -- the closest words of the source (any search type)
26
+ --
27
+ -- export_app / import_app need no change: the configuration is exported as
28
+ -- to_jsonb of its row, and the before-insert trigger below fills the new
29
+ -- columns for exports made before 135.
30
+ -- =====================================================================
31
+
32
+ alter table meta.search_config
33
+ add column search_type text not null default 'standard',
34
+ add column fts_vector text,
35
+ add column fts_config text,
36
+ add column did_you_mean boolean not null default false;
37
+
38
+ alter table meta.search_config
39
+ add constraint search_config_search_type_check check (search_type in ('standard', 'fulltext')),
40
+ -- a name only (it is checked against pg_ts_config and passed as a query parameter, never as SQL)
41
+ add constraint search_config_fts_config_check check (fts_config is null or fts_config ~ '^[a-z_][a-z0-9_]{0,62}$'),
42
+ add constraint search_config_fts_list_check check (search_type = 'standard' or source_type <> 'list');
43
+
44
+ comment on column meta.search_config.search_type is 'standard (ILIKE per word) or fulltext (websearch_to_tsquery, ts_rank_cd, ts_headline); later vector, hybrid';
45
+ comment on column meta.search_config.fts_vector is 'fulltext: a tsvector column or expression; empty: to_tsvector over the searchable columns';
46
+ comment on column meta.search_config.fts_config is 'fulltext: the text search configuration (pg_ts_config); empty: the application language''s, else simple';
47
+ comment on column meta.search_config.did_you_mean is 'with pg_trgm: suggest the closest words when nothing is found';
48
+
49
+ -- an export made before 135 has none of the new columns
50
+ create or replace function meta.search_config_defaults() returns trigger
51
+ language plpgsql set search_path = pg_catalog as $$
52
+ begin
53
+ new.seq := coalesce(new.seq, 10);
54
+ new.source_type := coalesce(new.source_type, 'table');
55
+ new.searchable_columns := coalesce(new.searchable_columns, '{}');
56
+ new.max_results := coalesce(new.max_results, 10);
57
+ new.search_type := coalesce(new.search_type, 'standard');
58
+ new.did_you_mean := coalesce(new.did_you_mean, false);
59
+ return new;
60
+ end $$;
@@ -7,7 +7,7 @@ open item and open an issue or pull request; see [CONTRIBUTING.md](../CONTRIBUTI
7
7
  Legend: ✅ available · 🟡 partial (see notes) · ❌ not yet · ➖ not planned (a deliberate choice,
8
8
  or better served by the PostgreSQL ecosystem; see the notes and [extensions](guide/15-extensions.md))
9
9
 
10
- Last reviewed: 2026-10-27 against **APEX 26.2** (sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
10
+ Last reviewed: 2026-10-27 against **APEX 26.2** (sprint 135: full-text search configurations, APEX's Oracle Text search type on PostgreSQL full-text search with stemming, ranking, highlights, *Did you mean* and *Add full-text search* in Table Properties (AP-2); sprint 134: the builder also in Italiano, 中文 (简体), 中文 (繁體), 日本語 and 한국어, APEX's ten builder languages plus Nederlands (SEC-5); sprints 121, 123–125: the builder in Deutsch, English, Español, Français, Nederlands and Português, step by step: the frame, sign-in, Workspace home, App Builder, Page Designer menus, the SQL Workshop's home, SQL Worksheet, Object Browser and Table Properties, Shared Components, application settings, users, identity providers, developers and workspaces (WS-7); sprint 122: *Ask in your own words* sets faceted search and smart filter facets, smart filters read a typed phrase (WS-5), and Sample Maps, Sample Approvals, Sample Data Loading and Sample Files in the App Gallery (WS-8); sprint 120: the App Gallery's single-topic sample apps (charts, cards, calendar, interactive grids, dynamic actions, reporting), the UX Pattern Catalog and a Utility Apps tab with Lookup Lists, Release Notes and Announcements (WS-8); sprint 119: data security per table, a roles × columns matrix and masked values per application role in a masking view, the same identity for pages, AI tools, REST, AutoREST and MCP (WS-13); sprint 118: Administration → Mail queue with delivery per hour and per workspace, the slowest provider calls and failures with Retry and Purge, a workspace's own mail service, and workspace network access lists that narrow the installation's allow-list (WS-9); sprint 117: Pillar Light and Pillar Dark navigation appearances with a header strip, clearer navigation tree states, subtle colour utilities, RTL; sprint 116: the comparison moved from 26.1 to 26.2, with a section for the 26.2 workspace items WS-1 … WS-13 and OCI managed identities; the App Builder list with Updated by, working copies and the lock; `pgkiln import --only`, a file-level import; stale rows on facet menus and blueprints fixed; sprint 115: popup LOVs that load on scroll, Wait Popup dynamic actions and dynamic actions on menu entries; sprint 114: Workflow 26.2 with custom task outcomes, task events and notifications, Send E-Mail and draft steps, migrating running instances and the diagram as SVG / PNG; sprint 113: Team Development with issues, labels, milestones, templates, a board and a set-up wizard; sprint 112: AI interactive reports with chips, Undo, computed columns and a phrase parser that needs no AI service; sprint 111: AI Agents with tools as a shared component, a Navigate tool with checksummed URLs, Gemini and OpenAI-compatible services; sprint 110: Data Reporter as a workspace tool with datasets, reporting applications, editors and viewers, published reports with stable URLs and pins; sprint 109: the Workspace home above the App Builder and SQL Workshop, with a summary, top applications, users and developers, and an optional release check; sprint 108: the code editor's syntax colours in the builder's light and dark themes, read-only SQL highlighted, the Data Modeler in dark mode; sprint 107: a transaction mode in the SQL Worksheet with Commit and Rollback; DA-18: JavaScript (MLE) not planned, plv8 where installed; sprint 106: the change log of DDL change sets with Flyway, Liquibase and SQL downloads; sprint 104: RESTful Services modules, templates and handlers with PL/pgSQL and media handlers, privileges, OpenAPI and Try it; sprint 103: Schema Export / Import with pg_dump and pg_restore; sprint 102: the SQL Monitor with live sessions, Top SQL and a statement's plan; sprint 101: Scheduling of pg_cron jobs with a schedule builder, run now and run history; sprint 100: RESTful Services, tables and views published as REST endpoints with paging, filters, keys and OpenAPI; sprint 99: Administration → Database Users for PostgreSQL roles; sprint 98: the Data Modeler with relational diagrams, DDL and a data dictionary; sprint 97: charts of a query in the worksheet, saved charts and their dashboard; sprint 96: the SQL Workshop's Launchpad with pins and tool groups, the builder's search with Ctrl+K; sprint 95: Table Properties: create, edit or copy a table in a form with a DDL preview and Apply in one transaction; sprint 94: an object's menu with Open in a panel, Quick DDL, Add index / trigger / policy and Drop with dependents; sprint 93: worksheets saved as SQL Scripts with tabs and a Files list, Format, Convert case, find and replace, bind and substitution variables; sprint 92: the worksheet's Query Result with sort, value dialog, copy and downloads, plans as a tree with the slowest step and misestimates, SQL History with status, search and run again; sprint 91: the SQL Worksheet like Database Actions' SQL: navigator, Run Statement at the cursor and Run Script with a log per statement, Explain Plan, Autotrace, notices, history, Run as an application role; sprint 90: Comment Out in the page designer, gallery labels in title case; sprint 89: the Object Browser's schema, one row of tabs, blank NULLs, Modify column; sprint 88: home cards with colour bands and round icons, the hero on the page background, a page footer with the version and Back to top; sprint 87: the Create Application wizard reloads the page list for a schema, RLS badges, the alias from the name; sprint 86: the page designer by page number, HTML error pages in the builder; sprint 85: the Form Designer, beyond APEX; sprint 84: the date picker, facet menus; sprint 83: Component Settings and Component Groups; sprint 82: Report Queries; sprint 81: Application Computations and Shortcuts in Shared Components; sprint 80: the master grid's first row selected when the page opens and after a search; grid cells as text with their format mask until edited, with cell navigation by arrow keys, Enter, F2 and Escape; sprint 79: generated dashboards with sums, donuts and lines, leaving out charts that say nothing; applications open in their own light or dark mode until the user chooses, per application; a user menu with Appearance and Sign in when signed out; the developer toolbar's App and Page texts and debug state; the report search's column scope in the magnifier; sprint 78: generated faceted search reports with one column per foreign key, the faceted search's total row count, Show all and Yes/No values, one search field, friendly headings and number formats in generated grids and master-detail pages, friendly names in the Create Application wizard's page list; sprint 77: the Page Designer's layout and tree by display position, button positions, the tree's context menu with Duplicate and Copy to other page, Show Common / Show All, Go to group and the Template Options dialog; display positions, button positions and side-column page templates at runtime; sprint 76: the interactive report's menus and pagination, date items in the date format; sprint 75: the SQL Workshop's Object Browser and tabs; sprint 74: the builder's information architecture: Shared Components grid, Settings tabs, application home search and tasks; sprint 73: the Create Application wizard on one page with page types and features; sprint 72: the runtime chrome compared with APEX 26.2's: the developer toolbar, the Iris look, the navigation bar's Help menu and Install button; sprint 71: generated applications compared with APEX 26.2's: labels, foreign key names in reports, file columns, format masks, the Home page and the dashboard; sprint 70: treemap, sunburst and timeline charts; sprint 69: dragging tasks on a Gantt chart; sprint 68: the Data Reporter's welcome page, Find my workspace; sprint 67: REST source catalogs from OpenAPI documents, a workspace's export and import (two rows added); sprint 66: a blueprint from an existing application; sprint 65: facet charts; sprint 64: approval vacation rules and expiration policies; sprint 63: range, box plot and stock charts; sprint 62: workspace preferences (SQL Workshop and RESTful services per workspace), extension links, Manage backups across applications, SQL Workshop utilities with object reports, About Database and a database monitor; sprint 61: the builder's screens compared with APEX 26.1: the workspace sign-in page, the Administration menu with workspace message, environment banner and utilization, the SQL Workshop home, the App Gallery, application groups and cross application reports (four rows added); sprint 60: isolated workspaces with their own database logins, row-level security on pgkiln's tables and workspace database roles, workspace administrators; sprint 59: the Page Designer's help pane, Quick edit from the developer toolbar; sprint 58: page groups; sprint 57: region templates hero, title bar, buttons container, inline dialog and drawer, wizard with wizard progress, the carousel; sprint 56: Generate DDL and Schema Comparison in the SQL Workshop, `pgkiln ddl` and `pgkiln compare`; sprint 55: an editable login page with a Login region and the Minimal page template, Forgot password through the application's mail service; sprint 54: interactive grid views, the single row view, group by and chart; sprint 53: application settings, user preferences and items kept per user, dynamic translations, `meta.data_export` and data export downloads, display image and geocoded address items; sprint 52: every dynamic action event and action, Ajax callbacks, an error handling function, the session timeout warning; sprint 51: checked imports, application backups, availability and Run only, feedback, type-specific attribute help; sprint 50: lost update detection, warn on unsaved changes, the URL and search regions with search configurations, the developer toolbar with a session state viewer, and e-mail through SMTP with OAuth2, Microsoft Graph, Amazon SES and HTTP APIs; import checks, application availability, property help and e-mail compared again; 2026-10-06: pgkiln 0.31.0, in progress: static application files and the Execute JavaScript dynamic action; region, item, dynamic action and process plug-ins; conditional and dynamic theme styles; XML and Excel in `meta.parse_data`, zips in SQL, `meta.v_boolean`; file items in S3-compatible object storage; map layers by the visible area and as vector tiles; a graphical query builder; tenants for workflows and tasks; application files as YAML and region static IDs; Lucide icons and icon modifiers; PWA push notifications; 0.30.0: 136 icons and an icon picker; an axe-core accessibility audit; cropping pictures; session sharing between applications; twenty-two built-in languages; 0.29.0: workspaces; the Iris base style; drawers; built-in template components including the metric card; Theme Roller dark-mode colours, live preview and template options on items and report columns; report selection across pages; instance settings; fourteen built-in languages; 0.28.0: AI with Claude and OpenAI; 0.27.0: Data Reporter; sample data generator; create application from several sheets, pasted data or existing tables; 0.26.0: working copies with a three-way merge; theme, library and boilerplate applications with subscriptions; 0.25.0: Gantt, pyramid and polar charts; map layers, clustering and server-side spatial filters; REST data sources that write back from forms and grids and synchronise into tables, OAuth2 password and refresh-token grants; debug messages per request and an install/upgrade log; `meta.web_request()` and `meta.parse_data()`; Theme Roller style variants and template options; 0.24.0: automations with several actions, error handling per row and runs from SQL; invoke-API workflow steps; Unload Data in the Data Workshop; create page wizards for cards, calendar, chart, map, faceted search, form and master-detail; create an application from a file; 0.23.0: interactive grid aggregates, frozen/moved/resized columns, layouts and saved grid reports, master-detail, row actions and copy/paste; download processes, execution chains in the background, workflow processes, branches to a function's URL or another app, the Dialog Closed event; number format masks, automatic time zone, runtime messages in German, French and Spanish; SQL scripts, Quick SQL, a query builder, XML loading and data load definitions; custom authentication, lists, page and application locks with developer comments, supporting objects; 0.22.0: keyset paging, report PDFs and REST collections streamed from a cursor, database-account authentication; 0.21.0: HTTP-header authentication behind a reverse proxy; 0.20.0: a popup list of values with server-side search; 0.19.0: row-range pagination, maximum row counts and row limits, lazy loading and caching of regions, streamed CSV/Excel downloads; 0.18.0: calendar week/day/list views with drag and drop, bubble/gauge/funnel/radar charts and drill-down, smart filters, a region display selector and more facet types, computations, conditional branches, build options, menu buttons, REST data sources and web credentials, rich text/Markdown, rating, combobox, date range and QR code items; 0.17.0: an App Builder home, workspace dashboard and utilities like APEX's; 0.16.0: a Page Designer with drag-and-drop layout, a code editor with autocomplete, template components and plug-ins, parallel branches and versions of workflows, the `pgkiln` command line with one file per component; 0.15.0: drop and paste files, heat maps, filtering a report by the map area; 0.14.0: stacked, combo, scatter and pie charts, several files per upload item; 0.13.0: map and tree regions; 0.12.0: REST modules in the builder; 0.11.0: workflows and Progressive Web Apps; 0.10.0: LDAP, SAML, "Keep me signed in", document templates, JSON loading, approvals and the task list; pgkiln installs no application, HR is an example).
11
11
 
12
12
  ## At a glance
13
13
 
@@ -85,7 +85,7 @@ the 26.2 comparison.)
85
85
  | Pagination of large tables (row ranges, maximum row count) | ✅ | Reports and grids page in the database; `"pagination": "range"` shows APEX's "row ranges X to Y" without a total (it reads one row more for *Next*); `max_rows` is a maximum row count (the total is counted over at most max+1 rows: "of more than N"; downloads are capped too); page numbers and sizes are clamped on the server. Cards (500), charts (1000), dynamic content (1000) and lists of values (5000) have configurable row limits. Keyset ("seek") paging on an indexed sort (`"keyset"`), with signed positions |
86
86
  | Lazy loading of regions | ✅ | `"lazy": true` on report, chart, cards, dynamic, tree and template component regions: a placeholder, fetched after the page shows (page, condition and authorization re-checked); without JavaScript a link shows the region ([chapter 4](guide/04-pages-and-regions.md#large-tables)) |
87
87
  | Region caching (per user, per session, for a duration) | ✅ | `"cache": {"scope": "user" \| "session" \| "all", "seconds": N}` keeps rendered regions in server memory, keyed by app, region, roles, language, the query string and the item values the region uses; a page submit empties it. CSRF tokens and per-user links are never shared. Per server process |
88
- | Search configurations and the search region (22.2) | ✅ | (sprint 50) Shared Components → **Search configurations**: a table, a query (binds allowed) or a list, the searchable columns, and each result's title, subtitle, description, badge, icon and link. A **search** region searches the ones it names with one field (or a page item): every word in one of the columns, as the app's role with the text as a parameter; results grouped per configuration with the words marked; searches as you type, works without JavaScript; exported; checked by the Advisor ([chapter 4](guide/04-pages-and-regions.md#search-search-region)). No Oracle Text, no REST data sources or APEX-application search as sources, no custom result templates |
88
+ | Search configurations and the search region (22.2) | ✅ | (sprint 50) Shared Components → **Search configurations**: a table, a query (binds allowed) or a list, the searchable columns, and each result's title, subtitle, description, badge, icon and link. A **search** region searches the ones it names with one field (or a page item): every word in one of the columns, as the app's role with the text as a parameter; results grouped per configuration with the words marked; searches as you type, works without JavaScript; exported; checked by the Advisor ([chapter 4](guide/04-pages-and-regions.md#search-search-region)). (sprint 135) Search type **fulltext** for APEX's *Oracle Text*: a tsvector column or expression (or built from the searchable columns), a text search configuration per application language (`simple` as fallback), `websearch_to_tsquery` for the user's text (phrases, or, -word), the best match first (`ts_rank_cd`) and matched words highlighted (`ts_headline`, escaped); *Did you mean* with `pg_trgm`; SQL Workshop › Table Properties › *Add full-text search* (generated tsvector column and GIN index, previewed, recorded in the Change Log); report search boxes use a tsvector column too; the Advisor warns about a missing GIN index ([full-text search](guide/04-pages-and-regions.md#full-text-search)). No vector search yet (AP-1), no REST data sources or APEX-application search as sources, no custom result templates |
89
89
  | URL region | ✅ | (sprint 50) A **url** region frames another site's page (or one of this server) in a sandboxed iframe: the origin is written out (an item can never choose the host), `&ITEM.` values after it are URL-encoded, and the page's CSP gets `frame-src` for exactly the origins of its visible URL regions; height, sandbox flags and full screen in the attributes; an *Open in a new tab* link ([chapter 4](guide/04-pages-and-regions.md#url-url-region)) |
90
90
  | Template components and template directives | ✅ | Shared Components → Template components: `#PLACEHOLDERS#` (always escaped), `{if}`, `{case}` and `{loop}` directives, custom attributes, a wrapper; as a region type and as report column templates, with a preview. Templates are checked against an allow-list (no scripts, styles, event handlers or `javascript:` links) ([chapter 4](guide/04-pages-and-regions.md#template-components)) |
91
91
 
@@ -1853,6 +1853,13 @@ the SQL Workshop's connection may use. Without JavaScript the header's field ope
1853
1853
  transaction (nothing changes when one fails, and the error shows under *Output*); **Open in the worksheet** puts
1854
1854
  them there instead. *Use as template* starts a new table with the same columns and keys (names with `_copy`).
1855
1855
  Defaults, checks and index expressions are single expressions (no `;` or comments).
1856
+ (sprint 135) **Add full-text search…** (a table's Table Properties, or its ⋯ menu): pick the text columns, a
1857
+ weight for each (A counts most in the ranking, D least: A for a title, B for a body), the **text search
1858
+ configuration** (the languages PostgreSQL has, `english`, `dutch`, `german`…, or `simple` without stemming) and the
1859
+ names of the column and the index. **Preview DDL** shows a stored generated `tsvector` column and a GIN index on it;
1860
+ **Apply** runs both in one transaction and records them in the **Change Log**; **Open in the worksheet** puts them
1861
+ there. The column keeps itself up to date; adding it rewrites the table. Name the column in a search
1862
+ configuration of search type *fulltext*, or in a report's `fulltext` attribute.
1856
1863
  - (sprint 119) **Security** (the Object Browser's *Security* button on a table, *Security…* in its ⋯ menu): the
1857
1864
  table's row level security policies, a matrix of database roles × columns with *read* and *update*, and masked
1858
1865
  columns per application role in a masking view; Preview SQL, Apply (one transaction, recorded in the Change Log)
@@ -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,10 @@ 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)). The type is the place where later types (vector search, and a hybrid of both) will go |
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 |
1642
1648
 
1643
1649
  **How it searches.** Each word of the search text must be found (case-insensitively, anywhere in
1644
1650
  the value) in one of the searchable columns, so `clerk mil` finds MILLER the clerk. `%` and `_` are
@@ -1655,6 +1661,45 @@ select e.empno, initcap(e.ename) as name, initcap(e.job) as job, d.dname as depa
1655
1661
  with searchable columns `name, job, department`, title `name`, subtitle `job`, badge `department`
1656
1662
  and link `{"page": 3, "items": {"P3_EMPNO": "#EMPNO#"}}`.
1657
1663
 
1664
+ #### Full-text search
1665
+
1666
+ A configuration of search type **fulltext** (APEX: the search type *Oracle Text*) uses PostgreSQL's full-text
1667
+ search instead of matching the words letter for letter:
1668
+
1669
+ - **Stemming**: in the `english` configuration *running shoes* finds a row with *run* and *shoe*. The text is
1670
+ read with `websearch_to_tsquery`, so users can write `"exact phrase"`, `shoes or boots` and `shoes -polish`.
1671
+ - **Ranking**: the best match first (`ts_rank_cd`; words close together and in a column with weight A count
1672
+ more), then by the title.
1673
+ - **Highlights**: the title and subtitle come back with every matched word marked, the description as the
1674
+ fragments around the matches (`ts_headline`). Everything is escaped: only ts_headline's own marks become
1675
+ `<mark>`, the data's characters are always text.
1676
+ - **The tsvector**: name a `tsvector` column of the source. The quickest way to make one is SQL Workshop ›
1677
+ Table Properties › **Add full-text search** ([chapter 3](03-builder.md)): a generated column over the text
1678
+ columns, weighted, and a GIN index on it. An expression works too (a GIN index on the same expression helps);
1679
+ with neither, the searchable columns are turned into a tsvector on every search, which reads the whole table.
1680
+ - **The text search configuration** is checked against `pg_ts_config` and passed as a query parameter; use the
1681
+ one the tsvector was made with. Empty: the configuration of the application's primary language (`en` →
1682
+ `english`, `nl` → `dutch`, `de` → `german`, `fr`, `es`, `it`, `pt`, `sv`, `da`, `nb`, `fi`, `ru`, `tr`, `el`,
1683
+ `ar`…), else `simple` (no stemming; for Japanese, Chinese and Korean).
1684
+ - **Did you mean**: with `pg_trgm` installed (`create extension pg_trgm`), a configuration that asks for it
1685
+ suggests, when nothing at all is found, the search text with each unknown word replaced by the most similar
1686
+ word of its searched columns (as the user: row level security applies). Without `pg_trgm` nothing is
1687
+ suggested and nothing fails.
1688
+
1689
+ ```sql
1690
+ -- what Add full-text search writes (HR example part 71, hr.policy)
1691
+ alter table hr.policy
1692
+ add column search_vector tsvector generated always as (
1693
+ setweight(to_tsvector('english'::regconfig, coalesce(title, '')), 'A') ||
1694
+ setweight(to_tsvector('english'::regconfig, coalesce(body, '')), 'B')) stored;
1695
+ create index policy_search_vector_idx on hr.policy using gin (search_vector);
1696
+ ```
1697
+
1698
+ with search type `fulltext`, tsvector `search_vector`, configuration `english`, title `title` and description
1699
+ `body`. The Advisor warns when a fulltext configuration's column has no GIN index (or its tsvector is built on
1700
+ every search), reports a configuration name that doesn't exist, and a column that isn't a tsvector. A report's
1701
+ search box can use the same engine: see its `fulltext` attribute.
1702
+
1658
1703
  **The region**: attributes
1659
1704
 
1660
1705
  | Key | Meaning |
@@ -1669,7 +1714,7 @@ The region's own field is a GET form (`?s<region id>=text`, so a search can be b
1669
1714
  without JavaScript; with JavaScript it searches while the user types and replaces only the results.
1670
1715
  The Advisor plans every configuration with the columns it names and reports names a region uses
1671
1716
  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.
1717
+ searches employees, departments, the HR policies (full-text, part 71) and the shortcuts list.
1673
1718
 
1674
1719
  ### `url` (URL region)
1675
1720
 
@@ -266,6 +266,10 @@ src/
266
266
  pdf.ts report PDFs with report layouts (pdfkit); rows from a cursor in batches (tablePdf takes batches)
267
267
  search.ts search region and search configurations (migration 080): words as ILIKE parameters, table/sql/list
268
268
  sources as the app role in savepoints, marked results, configColumns/searchConfigProblems for the Advisor
269
+ (sprint 135, migration 135) search type fulltext: tsvectorSql (column, expression or built from the
270
+ searchable columns), websearch_to_tsquery / ts_rank_cd / ts_headline between private-use markers
271
+ (headlined() escapes, only the markers become <mark>), textSearchConfig() (pg_ts_config names only,
272
+ passed as $1::regconfig; tsConfigFor(language)), trigramSchema() and "did you mean" with pg_trgm
269
273
  url-region.ts URL region (migration 078): literal origins only (urlTarget), &ITEM. URL-encoded, sandbox and height,
270
274
  frameOrigins() for the CSP's frame-src (security.ts)
271
275
  ajax.ts Ajax callbacks (migration 085: POST …/ajax/:name, point "ajax" processes, x01 … x10 binds) and the
@@ -353,6 +357,9 @@ src/
353
357
  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
358
  table-editor.ts (sprint 95) Table Properties: GET/POST /sql/objects/table (create, edit ?o=, ?template=), fromForm(), Preview / Apply
355
359
  (one transaction) / Open in the worksheet
360
+ fulltext.ts (sprint 135, AP-2) Table Properties › Add full-text search: GET/POST /sql/objects/fulltext?o= (text columns
361
+ with weights A–D, a configuration from pg_ts_config, fulltextDdl(): a generated tsvector column and a GIN
362
+ index), Preview / Apply (one transaction, recorded in the Change Log) / Open in the worksheet
356
363
  data-security.ts (sprint 119) a table's Security: GET/POST /sql/objects/security?o= (policies, roles × columns, masks), Preview / Apply
357
364
  object-actions.ts (sprint 94) an object's ⋯ menu (objectMenu) in the worksheet and the Object Browser: Quick DDL (objectDdl), Drop with
358
365
  dependents, Add index / trigger / policy as drafts; GET /sql/objects/action, /sql/objects/panel (Open in a panel)
@@ -495,6 +502,9 @@ test/
495
502
  changelog.test.ts (sprint 106) DDL recognition and names, recording from Table Properties, the worksheet and Compare,
496
503
  downloads (Flyway, Liquibase, SQL), stable numbers, the latest application export
497
504
  table-editor.test.ts (sprint 95) the Table Properties page: preview, apply, a failing apply rolls back, template, worksheet
505
+ fulltext-search.test.ts (sprint 135) full-text search configurations: stemming, rank order, highlights, websearch syntax, the
506
+ configuration fallback, did you mean (pg_trgm when it can be installed), the report search on a tsvector
507
+ column, the Advisor's GIN check, Add full-text search (preview, apply, change log), export/import
498
508
  object-actions.test.ts (sprint 94) object menus: Quick DDL, Drop with dependents, Add index / trigger / policy, the panel, not-found
499
509
  data-modeler.test.ts (sprint 98) Data Modeler: "hr core" with its FK lines, saved positions, SVG, objects, DDL, dictionary, geometry
500
510
  worksheet.test.ts (sprint 91) the SQL Worksheet: statement at the cursor, Run Script log, plans, notices, history, navigator, Run as
@@ -42,6 +42,7 @@ 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 |
45
46
 
46
47
  The files run in order, each once (recorded in `public.pgkiln_seed`), so new ones can be added
47
48
  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;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.68.0",
3
+ "version": "0.69.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
@@ -11,7 +11,7 @@ import { handlerProblems } from '../runtime/rest.ts';
11
11
  import { plpgsqlBlock } from '../runtime/rest-handlers.ts';
12
12
  import { processProblems } from '../runtime/processes.ts';
13
13
  import { urlConfigProblem, urlTarget } from '../runtime/url-region.ts';
14
- import { configColumns, searchConfigProblems } from '../runtime/search.ts';
14
+ import { configColumns, searchConfigProblems, textSearchConfig, tsvectorSql } from '../runtime/search.ts';
15
15
  import { COMPONENTS } from './components.ts';
16
16
  import { appEntries, type Entry } from './search.ts';
17
17
  import { appHeader, BASE, developer, region, send, shell, type Req } from './ui.ts';
@@ -240,10 +240,46 @@ export async function advise(appId: number, tx?: pg.ClientBase, opts: { db?: Db
240
240
  findings.push({ severity: 'error', entry, field: 'Source', message: problems.join(' ') });
241
241
  continue;
242
242
  }
243
- const cols = configColumns(row).map((x) => `t.${pg.escapeIdentifier(x)}`).join(', ') || '*';
243
+ let cols = configColumns(row).map((x) => `t.${pg.escapeIdentifier(x)}`).join(', ') || '*';
244
+ // (135) a full-text configuration: its tsvector matched with its text search configuration
245
+ const fulltext = row.search_type === 'fulltext';
246
+ if (fulltext) {
247
+ const cfg = await textSearchConfig(c, row.fts_config, app.language);
248
+ if (row.fts_config && cfg !== row.fts_config)
249
+ findings.push({ severity: 'error', entry, field: 'Text search configuration', message: `Text search configuration ${row.fts_config} doesn't exist (pg_ts_config): ${cfg} is used instead.` });
250
+ const lit = `${pg.escapeLiteral(cfg)}::regconfig`;
251
+ // (ts_rank_cd takes only a tsvector: text @@ tsquery would pass)
252
+ cols = `${cols === '*' ? 't.*' : cols}, ts_rank_cd(${tsvectorSql(row, lit)}, websearch_to_tsquery(${lit}, 'x'))`;
253
+ }
244
254
  checked++;
245
255
  const problem = await checkSql(c, `select ${cols} from ${row.source_type === 'table' ? row.source.trim() : `(${row.source})`} t`, 'select');
246
- if (problem) findings.push({ ...problem, entry, field: 'Source' });
256
+ if (problem) findings.push({ ...problem, entry, field: fulltext ? 'Source / tsvector' : 'Source' });
257
+ // (135) a full-text search on a table without a GIN index on its tsvector reads every row
258
+ if (fulltext && !problem && row.source_type === 'table') {
259
+ const vector = String(row.fts_vector ?? '').trim();
260
+ const column = /^[A-Za-z_][A-Za-z0-9_$]{0,62}$/.test(vector) ? vector : null;
261
+ await c.query('savepoint advisor_fts');
262
+ const ix = (await c.query<{ checkable: boolean; gin: boolean }>(
263
+ `select cl.relkind in ('r', 'p', 'm') as checkable,
264
+ exists (select from pg_catalog.pg_index i join pg_catalog.pg_class ic on ic.oid = i.indexrelid join pg_catalog.pg_am am on am.oid = ic.relam
265
+ where i.indrelid = cl.oid and am.amname = 'gin'
266
+ and case when $2::text is not null
267
+ then (select a.attnum from pg_catalog.pg_attribute a where a.attrelid = cl.oid and a.attname = $2 and not a.attisdropped) = any (i.indkey::int2[])
268
+ else i.indexprs is not null and pg_catalog.pg_get_indexdef(i.indexrelid) ~* 'to_tsvector' end) as gin
269
+ from pg_catalog.pg_class cl where cl.oid = $1::regclass`, [row.source.trim(), column]).catch(async () => {
270
+ await c.query('rollback to savepoint advisor_fts');
271
+ return { rows: [] };
272
+ })).rows[0];
273
+ if (ix?.checkable && (!ix.gin || !vector))
274
+ findings.push({
275
+ severity: 'warning', entry, field: 'tsvector column or expression',
276
+ message: column
277
+ ? `${row.source.trim()}.${column} has no GIN index: every search reads the whole table (create index … on ${row.source.trim()} using gin (${pg.escapeIdentifier(column)})).`
278
+ : vector
279
+ ? `${row.source.trim()} has no GIN index on a to_tsvector expression: every search reads the whole table.`
280
+ : `The tsvector is built from the searchable columns on every search, so no index can help: add a tsvector column with a GIN index (SQL Workshop › Table Properties › Add full-text search) and name it here.`,
281
+ });
282
+ }
247
283
  }
248
284
  // the search regions name existing configurations
249
285
  for (const { kind, row } of all.filter((x) => x.kind === 'region' && x.row.type === 'search')) {
@@ -722,8 +722,13 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
722
722
  plural: 'Search configurations',
723
723
  icon: 'search',
724
724
  summary: (c) => `${c.name} (${c.source_type})`,
725
- defaults: { source_type: 'table', seq: 10, max_results: 10, searchable_columns: [] },
725
+ defaults: { source_type: 'table', search_type: 'standard', seq: 10, max_results: 10, searchable_columns: [], did_you_mean: false },
726
726
  validate: (v) => searchConfigProblems(v as never).join(' ') || null,
727
+ // (135) empty means: built from the searchable columns / the application language's configuration
728
+ beforeSave: async (v) => {
729
+ for (const k of ['fts_vector', 'fts_config'] as const) if (typeof v[k] === 'string' && !(v[k] as string).trim()) v[k] = null;
730
+ if (typeof v.fts_config === 'string') v.fts_config = v.fts_config.trim().toLowerCase();
731
+ },
727
732
  fields: [
728
733
  { name: 'name', label: 'Name (static ID)', kind: 'upper', group: 'Identification', help: 'e.g. CUSTOMERS; a search region names it in {"configs": ["CUSTOMERS"]}.' },
729
734
  { name: 'label', label: 'Label', kind: 'text', group: 'Identification', help: 'The heading of its results, e.g. Customers.' },
@@ -733,6 +738,14 @@ export const COMPONENTS: Record<string, ComponentSpec> = {
733
738
  { name: 'source', label: 'Source', kind: 'code', wide: true, group: 'Source', help: 'table: sales.customers · sql: select id, name, city from sales.customers where active · list: the list\'s name.' },
734
739
  { name: 'searchable_columns', label: 'Searchable columns', kind: 'list', group: 'Source',
735
740
  help: 'Comma-separated columns the search text is looked for in (each word must be found in one of them). Empty: the title, subtitle and description columns.' },
741
+ { name: 'search_type', label: 'Search type', kind: 'select', options: ['standard', 'fulltext'], group: 'Search',
742
+ help: 'standard: each word anywhere in one of the searchable columns (case-insensitive) · fulltext: PostgreSQL full-text search: stemmed words ("running" finds "run"), "quoted phrases", or, -word; the best match first, the matched words highlighted (table, sql).' },
743
+ { name: 'fts_vector', label: 'tsvector column or expression', kind: 'code', wide: true, group: 'Search',
744
+ help: 'fulltext: a tsvector column of the source (e.g. search_vector, with a GIN index: SQL Workshop › Table Properties › Add full-text search), or an expression, e.g. to_tsvector(\'english\', title || \' \' || body). Empty: to_tsvector over the searchable columns (no index can help).' },
745
+ { name: 'fts_config', label: 'Text search configuration', kind: 'text', group: 'Search',
746
+ help: 'fulltext: english, dutch, german, french, simple … (pg_ts_config); the same one the tsvector was made with. Empty: the application\'s primary language\'s, else simple.' },
747
+ { name: 'did_you_mean', label: 'Did you mean', kind: 'bool', group: 'Search',
748
+ help: 'With the pg_trgm extension installed: when nothing is found, suggest the closest words from the searched columns (reads up to 10,000 rows). Without pg_trgm nothing is suggested.' },
736
749
  { name: 'title_column', label: 'Title column', kind: 'text', group: 'Result', help: 'Required (table, sql); results are ordered by it.' },
737
750
  { name: 'subtitle_column', label: 'Subtitle column', kind: 'text', group: 'Result' },
738
751
  { name: 'description_column', label: 'Description column', kind: 'text', group: 'Result' },
@@ -0,0 +1,160 @@
1
+ import type { FastifyInstance, FastifyReply } from 'fastify';
2
+ import pg from 'pg';
3
+ import { html, type Raw } from '../html.ts';
4
+ import { icon } from '../icons.ts';
5
+ import { clientIp, logActivity } from '../session.ts';
6
+ import { joinStatements, recordChanges } from '../changelog.ts';
7
+ import { readTable } from '../tabledef.ts';
8
+ import { tsConfigFor } from '../runtime/search.ts';
9
+ import { builderLanguage, tr } from './i18n.ts';
10
+ import { SYSTEM_SCHEMAS } from './object-browser.ts';
11
+ import { clearCompletions } from './code-editor.ts';
12
+ import { back, BASE, csrf, developer, flash, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
13
+ import { currentWorkspace, usableSchemas, workshopDb } from './workspaces.ts';
14
+
15
+ // (sprint 135, AP-2) SQL Workshop › Table Properties › Add full-text search: a stored generated tsvector column over
16
+ // the text columns the developer picks (each with a weight A–D) in a text search configuration from pg_ts_config,
17
+ // and a GIN index on it. Preview DDL shows the statements; Apply runs them in one transaction with the workshop's
18
+ // connection and records them in the Change Log; Open in the worksheet puts them there. The table, its columns and
19
+ // the configuration are read from the catalog on every request; only names found there reach the DDL (quoted).
20
+
21
+ type Db = Awaited<ReturnType<typeof workshopDb>>;
22
+ const NAME = /^[a-z_][a-z0-9_]{0,62}$/;
23
+ const WEIGHTS = ['A', 'B', 'C', 'D'] as const;
24
+ const ident = (n: string) => (/^[a-z_][a-z0-9_$]*$/.test(n) ? n : pg.escapeIdentifier(n));
25
+
26
+ interface Col { name: string; type: string; texty: boolean; tsv: boolean; gin: boolean }
27
+
28
+ async function columnsOf(db: Db, oid: number): Promise<Col[]> {
29
+ return (await db.query<Col>(
30
+ `select a.attname as name, format_type(a.atttypid, a.atttypmod) as type,
31
+ t.typcategory = 'S' and a.attgenerated = '' as texty, a.atttypid = 'tsvector'::regtype as tsv,
32
+ exists (select from pg_index i join pg_class ic on ic.oid = i.indexrelid join pg_am am on am.oid = ic.relam
33
+ where i.indrelid = a.attrelid and am.amname = 'gin' and a.attnum = any (i.indkey::int2[])) as gin
34
+ from pg_attribute a join pg_type t on t.oid = a.atttypid
35
+ where a.attrelid = $1 and a.attnum > 0 and not a.attisdropped order by a.attnum`, [oid])).rows;
36
+ }
37
+
38
+ /** The text search configurations the connection sees by name (english, dutch, simple…). */
39
+ async function tsConfigs(db: Db) {
40
+ return (await db.query<{ n: string }>(`select cfgname as n from pg_ts_config where pg_ts_config_is_visible(oid) order by 1`)).rows.map((r) => r.n);
41
+ }
42
+
43
+ /** The statements: the generated column and its GIN index. */
44
+ export function fulltextDdl(table: string, column: string, index: string, config: string, cols: { name: string; weight: string }[]) {
45
+ const cfg = `${pg.escapeLiteral(config)}::regconfig`;
46
+ const parts = cols.map((c) => `setweight(to_tsvector(${cfg}, coalesce(${ident(c.name)}::text, '')), '${c.weight}')`);
47
+ return [
48
+ `alter table ${table}\n add column ${ident(column)} tsvector generated always as (\n ${parts.join(' ||\n ')}) stored`,
49
+ `create index ${ident(index)} on ${table} using gin (${ident(column)})`,
50
+ ];
51
+ }
52
+
53
+ async function page(req: Req, reply: FastifyReply) {
54
+ const s = await developer(req, reply);
55
+ if (!s) return;
56
+ const tx = tr(s);
57
+ const db = await workshopDb(s);
58
+ const isPost = req.method === 'POST';
59
+ const b: Body = (isPost ? req.body : req.query) ?? {};
60
+ const ref = String(b.o ?? '');
61
+ const t = ref ? await readTable(db, ref) : null;
62
+ const schemas = (await usableSchemas(db)).filter((n) => !SYSTEM_SCHEMAS.includes(n));
63
+ if (!t || !schemas.includes(t.schema)) return reply.code(404).send('Not found');
64
+ const table = `${ident(t.schema)}.${ident(t.name)}`;
65
+ const oid = (await db.one<{ oid: number }>('select $1::regclass::oid as oid', [table]))!.oid;
66
+ const cols = await columnsOf(db, oid);
67
+ const texty = cols.filter((c) => c.texty);
68
+ const configs = await tsConfigs(db);
69
+ const defaultConfig = configs.includes(tsConfigFor(builderLanguage(s))) ? tsConfigFor(builderLanguage(s)) : configs.includes('simple') ? 'simple' : configs[0] ?? 'simple';
70
+ // the form's values (defaults: every text column, A for the first, B for the rest)
71
+ const picked = new Map<string, string>();
72
+ const column = isPost ? String(b.column ?? '').trim() : 'search_vector';
73
+ const index = isPost ? String(b.index ?? '').trim() : `${t.name.replace(/[^a-z0-9_]+/gi, '_').toLowerCase().slice(0, 40)}_search_vector_idx`;
74
+ const config = isPost ? String(b.config ?? '') : defaultConfig;
75
+ if (isPost) {
76
+ for (const c of texty) {
77
+ const i = texty.indexOf(c);
78
+ if (b[`c.${i}.use`] === 'on' && b[`c.${i}.name`] === c.name) picked.set(c.name, WEIGHTS.find((w) => w === b[`c.${i}.w`]) ?? 'B');
79
+ }
80
+ } else texty.forEach((c, i) => picked.set(c.name, i === 0 ? 'A' : 'B'));
81
+
82
+ const errors: string[] = [];
83
+ let ddl: string[] = [];
84
+ let output: Raw | '' = '';
85
+ if (isPost) {
86
+ if (!picked.size) errors.push(tx('fts.pick'));
87
+ for (const n of [column, index]) if (!NAME.test(n)) errors.push(tx('fts.bad_name', { name: n }));
88
+ if (NAME.test(column) && cols.some((c) => c.name === column)) errors.push(tx('fts.exists', { name: column }));
89
+ if (NAME.test(index) && (await db.one('select 1 as x from pg_class where relnamespace = $1::regnamespace and relname = $2', [ident(t.schema), index]))) errors.push(tx('fts.exists', { name: index }));
90
+ if (!configs.includes(config)) errors.push(tx('fts.bad_config', { name: config }));
91
+ if (!errors.length) ddl = fulltextDdl(table, column, index, config, [...picked].map(([name, weight]) => ({ name, weight })));
92
+ if (ddl.length && b.mode === 'worksheet') {
93
+ s.state.__SQLDRAFT = `${ddl.join(';\n\n')};`;
94
+ flash(s, tx('te.review'));
95
+ return back(reply, s, `${BASE}/sql/worksheet`);
96
+ }
97
+ if (ddl.length && b.mode === 'apply') {
98
+ const c = await db.pool.connect();
99
+ let failed = '';
100
+ let at = '';
101
+ try {
102
+ await c.query('begin');
103
+ for (const x of ddl) {
104
+ at = x;
105
+ await c.query(x);
106
+ }
107
+ await c.query('commit');
108
+ } catch (e) {
109
+ failed = (e as Error).message;
110
+ await c.query('rollback').catch(() => {});
111
+ } finally {
112
+ c.release(true);
113
+ }
114
+ await logActivity({ username: s.username, event: 'sql_command', ip: clientIp(req), detail: ddl.join(';\n').slice(0, 2000) });
115
+ clearCompletions();
116
+ if (!failed) {
117
+ await recordChanges({ workspaceId: currentWorkspace(s), author: s.username ?? '?', source: 'table', defaultSchema: t.schema, statements: [joinStatements(ddl)] });
118
+ flash(s, tx('fts.added', { table, column, index }));
119
+ return back(reply, s, `${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}`);
120
+ }
121
+ output = html`<div class="alert alert-error" role="alert"><p><strong>${tx('te.nothing_changed')}</strong>: ${tx('te.rolled_back')}</p><p>${failed}</p><pre class="source" data-code-view="sql" tabindex="0" aria-label="${tx('te.failed_stmt')}">${at}</pre></div>`;
122
+ }
123
+ }
124
+
125
+ const title = tx('fts.title', { table });
126
+ const existing = cols.filter((c) => c.tsv);
127
+ const sel = (a: string, v: string) => (a === v ? ' selected' : '');
128
+ const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}
129
+ <p class="muted">${tx('fts.intro')}</p>
130
+ ${existing.length ? html`<p>${tx('fts.existing')} ${existing.map((c) => html`<code>${c.name}</code> (${c.gin ? tx('fts.gin_yes') : tx('fts.gin_no')}) `)}</p>` : ''}
131
+ ${errors.length ? html`<div class="alert alert-error" role="alert"><ul>${errors.map((e) => html`<li>${e}</li>`)}</ul></div>` : ''}
132
+ ${output}
133
+ ${texty.length ? html`<form method="post" action="${BASE}/sql/objects/fulltext" class="fts-form">${csrf(s)}
134
+ <input type="hidden" name="o" value="${table}">
135
+ <fieldset class="prop-group"><legend>${tx('fts.columns')}</legend>
136
+ <div class="table-wrap"><table class="report"><thead><tr><th>${tx('fts.include')}</th><th>${tx('te.name')}</th><th>${tx('te.type')}</th><th>${tx('fts.weight')}</th></tr></thead><tbody>
137
+ ${texty.map((c, i) => html`<tr><td class="te-c"><input type="hidden" name="c.${i}.name" value="${c.name}"><input type="checkbox" id="fts-c${i}" name="c.${i}.use"${picked.has(c.name) ? ' checked' : ''}></td>
138
+ <td><label for="fts-c${i}">${c.name}</label></td><td><code>${c.type}</code></td>
139
+ <td><select name="c.${i}.w" aria-label="${tx('fts.weight')} ${c.name}">${WEIGHTS.map((w) => html`<option${sel(w, picked.get(c.name) ?? 'B')}>${w}</option>`)}</select></td></tr>`)}
140
+ </tbody></table></div>
141
+ <small class="help">${tx('fts.weight_help')}</small></fieldset>
142
+ <div class="form-grid u-mt1">
143
+ <div class="field"><label class="label" for="fts-config">${tx('fts.config')}</label><select id="fts-config" name="config">${configs.map((n) => html`<option${sel(n, config)}>${n}</option>`)}</select><small class="help">${tx('fts.config_help')}</small></div>
144
+ <div class="field"><label class="label" for="fts-column">${tx('fts.column')}</label><input id="fts-column" name="column" value="${column}" required autocomplete="off" spellcheck="false" class="code"></div>
145
+ <div class="field"><label class="label" for="fts-index">${tx('fts.index')}</label><input id="fts-index" name="index" value="${index}" required autocomplete="off" spellcheck="false" class="code"></div>
146
+ </div>
147
+ ${ddl.length ? html`<h2>DDL</h2><pre class="source te-ddl" data-code-view="sql" tabindex="0" aria-label="DDL">${ddl.map((x) => `${x};`).join('\n\n')}</pre>` : ''}
148
+ <div class="buttons te-buttons">
149
+ <button class="btn" name="mode" value="preview">${icon('eye')} ${tx('te.preview')}</button>
150
+ <button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
151
+ <button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
152
+ <a class="btn" href="${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}">${tx('common.cancel')}</a>
153
+ </div></form>` : html`<p class="alert alert-info">${tx('fts.no_text')}</p>`}`;
154
+ return send(reply, s, shell(s, title, [['SQL Workshop', `${BASE}/sql`], [tx('ob.title'), `${BASE}/sql/objects`], [tx('te.title_table', { table }), `${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}`], [tx('fts.add')]], main, 'sql'));
155
+ }
156
+
157
+ export async function fulltextRoutes(app: FastifyInstance) {
158
+ app.get(`${BASE}/sql/objects/fulltext`, page);
159
+ app.post(`${BASE}/sql/objects/fulltext`, page);
160
+ }