pgkiln 0.69.0 → 0.71.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 (63) hide show
  1. package/db/migrations/136_vector_search.sql +252 -0
  2. package/db/migrations/137_create_page_types.sql +904 -0
  3. package/db/migrations/138_app_utilities.sql +153 -0
  4. package/docs/apex-feature-parity.md +10 -8
  5. package/docs/guide/03-builder.md +176 -4
  6. package/docs/guide/04-pages-and-regions.md +45 -3
  7. package/docs/guide/05-items.md +1 -1
  8. package/docs/guide/06-processing.md +18 -0
  9. package/docs/guide/09-reference.md +34 -1
  10. package/docs/guide/12-development.md +53 -3
  11. package/docs/guide/15-extensions.md +1 -1
  12. package/docs/guide/16-files.md +1 -1
  13. package/examples/hr/README.md +1 -0
  14. package/examples/hr/hr_72_vector_search.sql +158 -0
  15. package/examples/hr/hr_73_configuration_options.sql +17 -0
  16. package/examples/hr/hr_74_app_utilities.sql +22 -0
  17. package/package.json +1 -1
  18. package/public/app.css +9 -0
  19. package/public/builder.css +48 -0
  20. package/src/ai/embeddings.ts +197 -0
  21. package/src/ai/service.ts +13 -9
  22. package/src/app.ts +13 -0
  23. package/src/builder/advisor.ts +43 -3
  24. package/src/builder/ai-agents.ts +13 -5
  25. package/src/builder/app-updates.ts +3 -0
  26. package/src/builder/components.ts +24 -4
  27. package/src/builder/create-page.ts +259 -0
  28. package/src/builder/dependencies.ts +311 -0
  29. package/src/builder/features.ts +313 -0
  30. package/src/builder/home.ts +3 -0
  31. package/src/builder/i18n/de.ts +38 -0
  32. package/src/builder/i18n/en.ts +38 -0
  33. package/src/builder/i18n/es.ts +38 -0
  34. package/src/builder/i18n/fr.ts +38 -0
  35. package/src/builder/i18n/it.ts +38 -0
  36. package/src/builder/i18n/ja.ts +38 -0
  37. package/src/builder/i18n/ko.ts +38 -0
  38. package/src/builder/i18n/nl.ts +38 -0
  39. package/src/builder/i18n/pt.ts +38 -0
  40. package/src/builder/i18n/zh-cn.ts +38 -0
  41. package/src/builder/i18n/zh-tw.ts +38 -0
  42. package/src/builder/login-page.ts +17 -14
  43. package/src/builder/newapp.ts +41 -23
  44. package/src/builder/object-actions.ts +1 -0
  45. package/src/builder/routes.ts +13 -2
  46. package/src/builder/shared.ts +1 -1
  47. package/src/builder/sql.ts +3 -0
  48. package/src/builder/table-editor.ts +1 -0
  49. package/src/builder/ui-defaults.ts +161 -0
  50. package/src/builder/ui.ts +2 -1
  51. package/src/builder/upgrade.ts +154 -0
  52. package/src/builder/utilities.ts +181 -0
  53. package/src/builder/vector-column.ts +229 -0
  54. package/src/builder/vector-providers.ts +286 -0
  55. package/src/builder/wizards.ts +211 -26
  56. package/src/cli/replace.ts +11 -1
  57. package/src/history.ts +155 -0
  58. package/src/runtime/ai-agents.ts +53 -2
  59. package/src/runtime/builtin-components.ts +23 -2
  60. package/src/runtime/items.ts +4 -2
  61. package/src/runtime/search.ts +116 -12
  62. package/src/server.ts +3 -0
  63. package/src/vector.ts +288 -0
@@ -0,0 +1,252 @@
1
+ -- =====================================================================
2
+ -- 136: vector providers, vector and hybrid search configurations,
3
+ -- vector columns kept current, Retrieve tools (sprint 136, AP-1;
4
+ -- APEX: Vector Providers, APEX_AI.GET_VECTOR_EMBEDDINGS, a search
5
+ -- configuration of type "Oracle Vector Search")
6
+ --
7
+ -- Everything here installs without pgvector: no meta table or function
8
+ -- uses the `vector` type. Vectors cross meta.* as real[] (pgvector casts
9
+ -- real[] to vector and back); the DDL that adds a vector column to an
10
+ -- application's table is written by the builder (SQL Workshop › Table
11
+ -- Properties › Add vector column) only when the extension is installed.
12
+ --
13
+ -- meta.app.vector_providers: the application's vector providers, a JSON
14
+ -- list (exported with the application in app.json, like ai_agents, 121):
15
+ -- {"name": "POLICY_EMBEDDINGS", "description": "…",
16
+ -- "type": "ai_service", "service": "EMBEDDINGS",
17
+ -- "model": "text-embedding-3-small", "dimensions": 1536,
18
+ -- "batch_size": 64, "per_minute": 60}
19
+ -- {"name": "TOY", "type": "function", "function": "sales.embed", "dimensions": 8}
20
+ -- ai_service: an AI service of the installation (OpenAI, Gemini or an
21
+ -- OpenAI-compatible endpoint such as Ollama) and an embedding model; its
22
+ -- key stays in meta.ai_service (encrypted, never exported). function: a
23
+ -- SQL / PL/pgSQL function (text) returning vector, real[] or float8[].
24
+ -- The server checks the list (src/ai/embeddings.ts).
25
+ --
26
+ -- meta.vector_provider(name, app) returns one definition (none are secret).
27
+ -- meta.embed(text, provider, app) returns the embedding as real[] for a
28
+ -- function provider, called as the caller's role (security invoker). An
29
+ -- AI service is called by the pgkiln server, not from SQL: meta.embed
30
+ -- says so for such a provider (vector search, Retrieve tools and the
31
+ -- vector column queue use it).
32
+ --
33
+ -- meta.search_config: search types `vector` (nearest rows by a vector
34
+ -- column, the user's text embedded by vector_provider; distance cosine,
35
+ -- l2 or inner_product; at least min_similarity) and `hybrid` (full-text
36
+ -- and vector rankings merged by reciprocal rank fusion, rows matched by
37
+ -- key_column). The new columns travel with the configuration
38
+ -- (to_jsonb of the row); the defaults trigger fills them for older exports.
39
+ --
40
+ -- Keeping a vector column current (triggers the builder creates on the
41
+ -- application's table; their arguments: app id, provider, vector column,
42
+ -- key column, source columns…):
43
+ -- meta.embedding_set() BEFORE INSERT OR UPDATE OF the source columns:
44
+ -- a function provider's embedding, at once;
45
+ -- meta.embedding_enqueue() AFTER INSERT OR UPDATE OF the source columns:
46
+ -- the row's key into meta.embedding_queue; the
47
+ -- server embeds queued rows in batches, within
48
+ -- the provider's requests per minute and the
49
+ -- application's daily AI limits (src/embeddings.ts).
50
+ -- meta.embedding_queue: installation data, not exported (like
51
+ -- meta.process_job); only the owner reads it.
52
+ -- =====================================================================
53
+
54
+ alter table meta.app add column vector_providers jsonb not null default '[]'
55
+ check (jsonb_typeof(vector_providers) = 'array');
56
+ comment on column meta.app.vector_providers is 'Vector providers (136): an AI service with an embedding model, or a function returning vector; exported with the application';
57
+
58
+ -- an export made before 136 has no vector_providers (nor one before 121 ai_tools / ai_agents)
59
+ create or replace function meta.app_ai_default() returns trigger
60
+ language plpgsql set search_path = pg_catalog as $$
61
+ begin
62
+ new.ai_tools := coalesce(new.ai_tools, '[]');
63
+ new.ai_agents := coalesce(new.ai_agents, '[]');
64
+ new.vector_providers := coalesce(new.vector_providers, '[]');
65
+ return new;
66
+ end $$;
67
+
68
+ -- ---------------------------------------------------------------- providers and meta.embed
69
+
70
+ -- One vector provider of an application (the current one, or one the login may see).
71
+ create function meta.vector_provider(p_name text, p_app int default null) returns jsonb
72
+ language plpgsql stable security definer set search_path = meta, pg_catalog as $$
73
+ declare
74
+ v_app int := coalesce(p_app, meta.app_id());
75
+ begin
76
+ if v_app is null then
77
+ raise exception 'meta.vector_provider: no current application (pass the application''s id)';
78
+ end if;
79
+ if not meta.app_visible(v_app) then
80
+ raise exception 'Application % is not in this workspace.', v_app using errcode = '42501';
81
+ end if;
82
+ return (select e from meta.app a, jsonb_array_elements(a.vector_providers) e
83
+ where a.id = v_app and e->>'name' = upper(p_name) limit 1);
84
+ end
85
+ $$;
86
+
87
+ -- The embedding of a text by a function provider, as real[] (cast it: meta.embed(…)::vector).
88
+ -- Runs the provider's function as the caller (security invoker): its grants apply. Stable, so
89
+ -- "order by embedding <=> meta.embed(…)::vector" embeds the text once (an index scan's argument).
90
+ create function meta.embed(p_text text, p_provider text, p_app int default null) returns real[]
91
+ language plpgsql stable set search_path = meta, pg_catalog as $$
92
+ declare
93
+ v jsonb := meta.vector_provider(p_provider, p_app);
94
+ fn text;
95
+ r real[];
96
+ begin
97
+ if v is null then
98
+ raise exception 'meta.embed: there is no vector provider %', upper(p_provider);
99
+ end if;
100
+ if p_text is null then
101
+ return null;
102
+ end if;
103
+ if v->>'type' is distinct from 'function' then
104
+ raise exception 'meta.embed: vector provider % uses AI service %, which the pgkiln server calls: use it in a vector search configuration, a Retrieve tool or a vector column (Table Properties › Add vector column); in SQL only function providers embed',
105
+ v->>'name', v->>'service';
106
+ end if;
107
+ fn := v->>'function';
108
+ if fn !~ '^[A-Za-z_][A-Za-z0-9_]{0,62}\.[A-Za-z_][A-Za-z0-9_]{0,62}$' then
109
+ raise exception 'meta.embed: the function of vector provider % is schema.name', v->>'name';
110
+ end if;
111
+ execute format('select (%I.%I($1))::real[]', split_part(fn, '.', 1), split_part(fn, '.', 2)) into r using p_text;
112
+ if r is not null and cardinality(r) is distinct from (v->>'dimensions')::int then
113
+ raise exception 'meta.embed: % returned % dimensions, vector provider % has %', fn, cardinality(r), v->>'name', v->>'dimensions';
114
+ end if;
115
+ return r;
116
+ end
117
+ $$;
118
+
119
+ -- ---------------------------------------------------------------- keeping vector columns current
120
+
121
+ create table meta.embedding_queue (
122
+ id bigserial primary key,
123
+ app_id int not null references meta.app on delete cascade,
124
+ provider text not null,
125
+ -- the table whose trigger queued the row (pg_class oid)
126
+ tbl oid not null,
127
+ vector_column text not null,
128
+ key_column text not null,
129
+ source_columns text[] not null,
130
+ key_value text not null,
131
+ queued_at timestamptz not null default now(),
132
+ next_at timestamptz not null default now(),
133
+ attempts int not null default 0,
134
+ last_error text,
135
+ unique (tbl, vector_column, key_value)
136
+ );
137
+ create index on meta.embedding_queue (next_at);
138
+ create index on meta.embedding_queue (app_id, provider);
139
+ revoke all on meta.embedding_queue from public;
140
+ comment on table meta.embedding_queue is 'Rows whose vector column the pgkiln server re-embeds (136): queued by meta.embedding_enqueue() triggers and Backfill';
141
+
142
+ -- the trigger arguments: app id, provider, vector column, key column, source columns
143
+ create function meta.embedding_args_ok(p_args text[]) returns boolean
144
+ language sql immutable set search_path = pg_catalog as $$
145
+ select cardinality(p_args) between 5 and 24
146
+ and p_args[1] ~ '^[0-9]{1,10}$'
147
+ and p_args[2] ~ '^[A-Za-z][A-Za-z0-9_]{0,59}$'
148
+ and not exists (select from unnest(p_args[3:]) c where c !~ '^[a-z_][a-z0-9_$]{0,62}$')
149
+ $$;
150
+
151
+ -- the text a row is embedded from: its source columns, one per line
152
+ create function meta.embedding_text(p_row jsonb, p_columns text[]) returns text
153
+ language sql immutable set search_path = pg_catalog as $$
154
+ select nullif(concat_ws(E'\n', variadic array(select p_row->>c from unnest(p_columns) with ordinality u(c, n) order by n)), '')
155
+ $$;
156
+
157
+ -- BEFORE INSERT OR UPDATE OF …: a function provider's embedding, set at once (as the role that writes the row)
158
+ create function meta.embedding_set() returns trigger
159
+ language plpgsql volatile set search_path = meta, pg_catalog as $$
160
+ declare
161
+ v_args text[] := array(select unnest(tg_argv));
162
+ v_cols text[];
163
+ v_vec real[];
164
+ begin
165
+ if not meta.embedding_args_ok(v_args) then
166
+ raise exception 'meta.embedding_set: the arguments are app id, provider, vector column, key column, source columns';
167
+ end if;
168
+ v_cols := v_args[5:];
169
+ if tg_op = 'UPDATE' and meta.embedding_text(to_jsonb(old), v_cols) is not distinct from meta.embedding_text(to_jsonb(new), v_cols)
170
+ and to_jsonb(new)->>v_args[3] is not null then
171
+ return new;
172
+ end if;
173
+ v_vec := meta.embed(meta.embedding_text(to_jsonb(new), v_cols), v_args[2], v_args[1]::int);
174
+ new := jsonb_populate_record(new, jsonb_build_object(v_args[3], translate(v_vec::text, '{}', '[]')));
175
+ return new;
176
+ end
177
+ $$;
178
+
179
+ -- AFTER INSERT OR UPDATE OF …: the row's key into the queue (the server embeds it)
180
+ create function meta.embedding_enqueue() returns trigger
181
+ language plpgsql volatile security definer set search_path = meta, pg_catalog as $$
182
+ declare
183
+ v_args text[] := array(select unnest(tg_argv));
184
+ v_app int;
185
+ v_key text;
186
+ begin
187
+ if not meta.embedding_args_ok(v_args) then
188
+ raise exception 'meta.embedding_enqueue: the arguments are app id, provider, vector column, key column, source columns';
189
+ end if;
190
+ v_app := v_args[1]::int;
191
+ if not meta.app_visible(v_app) then
192
+ raise exception 'Application % is not in this workspace.', v_app using errcode = '42501';
193
+ end if;
194
+ if tg_op = 'UPDATE' and meta.embedding_text(to_jsonb(old), v_args[5:]) is not distinct from meta.embedding_text(to_jsonb(new), v_args[5:]) then
195
+ return null;
196
+ end if;
197
+ v_key := to_jsonb(new)->>v_args[4];
198
+ if v_key is null then
199
+ return null;
200
+ end if;
201
+ insert into meta.embedding_queue (app_id, provider, tbl, vector_column, key_column, source_columns, key_value)
202
+ values (v_app, upper(v_args[2]), tg_relid, v_args[3], v_args[4], v_args[5:], v_key)
203
+ on conflict (tbl, vector_column, key_value) do update
204
+ set app_id = excluded.app_id, provider = excluded.provider, key_column = excluded.key_column,
205
+ source_columns = excluded.source_columns, queued_at = now(), next_at = now(), attempts = 0, last_error = null;
206
+ return null;
207
+ end
208
+ $$;
209
+
210
+ grant execute on function meta.vector_provider(text, int), meta.embed(text, text, int), meta.embedding_args_ok(text[]),
211
+ meta.embedding_text(jsonb, text[]), meta.embedding_set(), meta.embedding_enqueue() to public;
212
+
213
+ -- ---------------------------------------------------------------- vector and hybrid search configurations
214
+
215
+ alter table meta.search_config
216
+ add column vector_provider text,
217
+ add column vector_column text,
218
+ add column key_column text,
219
+ add column distance text not null default 'cosine',
220
+ add column min_similarity real;
221
+
222
+ alter table meta.search_config drop constraint search_config_search_type_check;
223
+ alter table meta.search_config
224
+ add constraint search_config_search_type_check check (search_type in ('standard', 'fulltext', 'vector', 'hybrid')),
225
+ add constraint search_config_vector_provider_check check (vector_provider is null or vector_provider ~ '^[A-Z][A-Z0-9_]{0,59}$'),
226
+ -- column names only (quoted as identifiers by the server)
227
+ add constraint search_config_vector_column_check check (vector_column is null or vector_column ~ '^[A-Za-z_][A-Za-z0-9_$]{0,62}$'),
228
+ add constraint search_config_key_column_check check (key_column is null or key_column ~ '^[A-Za-z_][A-Za-z0-9_$]{0,62}$'),
229
+ add constraint search_config_distance_check check (distance in ('cosine', 'l2', 'inner_product')),
230
+ add constraint search_config_min_similarity_check check (min_similarity is null or min_similarity between -1 and 1),
231
+ add constraint search_config_vector_check check (search_type not in ('vector', 'hybrid') or (vector_provider is not null and vector_column is not null)),
232
+ add constraint search_config_hybrid_key_check check (search_type <> 'hybrid' or key_column is not null);
233
+
234
+ comment on column meta.search_config.search_type is 'standard (ILIKE per word), fulltext (websearch_to_tsquery, ts_rank_cd, ts_headline), vector (nearest by an embedding, 136) or hybrid (fulltext and vector by reciprocal rank fusion, 136)';
235
+ comment on column meta.search_config.vector_provider is 'vector, hybrid: the vector provider (static ID) that embeds the search text';
236
+ comment on column meta.search_config.vector_column is 'vector, hybrid: the source''s vector column';
237
+ comment on column meta.search_config.key_column is 'hybrid: the key that matches full-text and vector results';
238
+ comment on column meta.search_config.distance is 'vector, hybrid: cosine (<=>), l2 (<->) or inner_product (<#>)';
239
+ comment on column meta.search_config.min_similarity is 'vector, hybrid: at least this similarity (cosine: 1 - distance; inner product: the product; l2: 1 / (1 + distance))';
240
+
241
+ create or replace function meta.search_config_defaults() returns trigger
242
+ language plpgsql set search_path = pg_catalog as $$
243
+ begin
244
+ new.seq := coalesce(new.seq, 10);
245
+ new.source_type := coalesce(new.source_type, 'table');
246
+ new.searchable_columns := coalesce(new.searchable_columns, '{}');
247
+ new.max_results := coalesce(new.max_results, 10);
248
+ new.search_type := coalesce(new.search_type, 'standard');
249
+ new.did_you_mean := coalesce(new.did_you_mean, false);
250
+ new.distance := coalesce(new.distance, 'cosine');
251
+ return new;
252
+ end $$;