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.
- package/db/migrations/135_fulltext_search.sql +60 -0
- package/docs/apex-feature-parity.md +2 -2
- package/docs/guide/03-builder.md +7 -0
- package/docs/guide/04-pages-and-regions.md +46 -1
- package/docs/guide/12-development.md +10 -0
- package/examples/hr/README.md +1 -0
- package/examples/hr/hr_71_fulltext_search.sql +43 -0
- package/package.json +1 -1
- package/src/builder/advisor.ts +39 -3
- package/src/builder/components.ts +14 -1
- package/src/builder/fulltext.ts +160 -0
- package/src/builder/i18n/de.ts +20 -0
- package/src/builder/i18n/en.ts +21 -0
- package/src/builder/i18n/es.ts +20 -0
- package/src/builder/i18n/fr.ts +20 -0
- package/src/builder/i18n/it.ts +20 -0
- package/src/builder/i18n/ja.ts +20 -0
- package/src/builder/i18n/ko.ts +20 -0
- package/src/builder/i18n/nl.ts +20 -0
- package/src/builder/i18n/pt.ts +20 -0
- package/src/builder/i18n/zh-cn.ts +20 -0
- package/src/builder/i18n/zh-tw.ts +20 -0
- package/src/builder/object-actions.ts +1 -0
- package/src/builder/sql.ts +3 -0
- package/src/builder/table-editor.ts +1 -0
- package/src/i18n/ar.ts +1 -0
- package/src/i18n/cs.ts +1 -0
- package/src/i18n/da.ts +1 -0
- package/src/i18n/de.ts +1 -0
- package/src/i18n/el.ts +1 -0
- package/src/i18n/es.ts +1 -0
- package/src/i18n/fi.ts +1 -0
- package/src/i18n/fr.ts +1 -0
- package/src/i18n/he.ts +1 -0
- package/src/i18n/it.ts +1 -0
- package/src/i18n/ja.ts +1 -0
- package/src/i18n/ko.ts +1 -0
- package/src/i18n/nb.ts +1 -0
- package/src/i18n/pl.ts +1 -0
- package/src/i18n/pt.ts +1 -0
- package/src/i18n/ru.ts +1 -0
- package/src/i18n/sv.ts +1 -0
- package/src/i18n/tr.ts +1 -0
- package/src/i18n/uk.ts +1 -0
- package/src/i18n/zh.ts +1 -0
- package/src/i18n.ts +2 -0
- package/src/runtime/report.ts +26 -2
- 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)).
|
|
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
|
|
package/docs/guide/03-builder.md
CHANGED
|
@@ -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
|
package/examples/hr/README.md
CHANGED
|
@@ -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
package/src/builder/advisor.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
+
}
|