pgkiln 0.68.0 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/db/migrations/135_fulltext_search.sql +60 -0
  2. package/db/migrations/136_vector_search.sql +252 -0
  3. package/docs/apex-feature-parity.md +5 -4
  4. package/docs/guide/03-builder.md +76 -0
  5. package/docs/guide/04-pages-and-regions.md +86 -1
  6. package/docs/guide/09-reference.md +19 -0
  7. package/docs/guide/12-development.md +41 -2
  8. package/docs/guide/15-extensions.md +1 -1
  9. package/examples/hr/README.md +2 -0
  10. package/examples/hr/hr_71_fulltext_search.sql +43 -0
  11. package/examples/hr/hr_72_vector_search.sql +158 -0
  12. package/package.json +1 -1
  13. package/public/builder.css +3 -0
  14. package/src/ai/embeddings.ts +197 -0
  15. package/src/ai/service.ts +13 -9
  16. package/src/app.ts +3 -0
  17. package/src/builder/advisor.ts +79 -3
  18. package/src/builder/ai-agents.ts +13 -5
  19. package/src/builder/components.ts +32 -1
  20. package/src/builder/fulltext.ts +160 -0
  21. package/src/builder/home.ts +1 -0
  22. package/src/builder/i18n/de.ts +54 -0
  23. package/src/builder/i18n/en.ts +55 -0
  24. package/src/builder/i18n/es.ts +54 -0
  25. package/src/builder/i18n/fr.ts +54 -0
  26. package/src/builder/i18n/it.ts +54 -0
  27. package/src/builder/i18n/ja.ts +54 -0
  28. package/src/builder/i18n/ko.ts +54 -0
  29. package/src/builder/i18n/nl.ts +54 -0
  30. package/src/builder/i18n/pt.ts +54 -0
  31. package/src/builder/i18n/zh-cn.ts +54 -0
  32. package/src/builder/i18n/zh-tw.ts +54 -0
  33. package/src/builder/object-actions.ts +2 -0
  34. package/src/builder/shared.ts +1 -1
  35. package/src/builder/sql.ts +6 -0
  36. package/src/builder/table-editor.ts +2 -0
  37. package/src/builder/vector-column.ts +229 -0
  38. package/src/builder/vector-providers.ts +286 -0
  39. package/src/cli/replace.ts +3 -1
  40. package/src/i18n/ar.ts +1 -0
  41. package/src/i18n/cs.ts +1 -0
  42. package/src/i18n/da.ts +1 -0
  43. package/src/i18n/de.ts +1 -0
  44. package/src/i18n/el.ts +1 -0
  45. package/src/i18n/es.ts +1 -0
  46. package/src/i18n/fi.ts +1 -0
  47. package/src/i18n/fr.ts +1 -0
  48. package/src/i18n/he.ts +1 -0
  49. package/src/i18n/it.ts +1 -0
  50. package/src/i18n/ja.ts +1 -0
  51. package/src/i18n/ko.ts +1 -0
  52. package/src/i18n/nb.ts +1 -0
  53. package/src/i18n/pl.ts +1 -0
  54. package/src/i18n/pt.ts +1 -0
  55. package/src/i18n/ru.ts +1 -0
  56. package/src/i18n/sv.ts +1 -0
  57. package/src/i18n/tr.ts +1 -0
  58. package/src/i18n/uk.ts +1 -0
  59. package/src/i18n/zh.ts +1 -0
  60. package/src/i18n.ts +2 -0
  61. package/src/runtime/ai-agents.ts +53 -2
  62. package/src/runtime/report.ts +26 -2
  63. package/src/runtime/search.ts +290 -23
  64. package/src/server.ts +3 -0
  65. package/src/vector.ts +288 -0
@@ -1045,6 +1045,60 @@ export const ko: BuilderMessages = {
1045
1045
  'te.failed_stmt': '실패한 문장',
1046
1046
  'te.title_table': '테이블 {table}',
1047
1047
  'te.title_create': '테이블 생성',
1048
+ 'fts.add': "전체 텍스트 검색 추가",
1049
+ 'fts.title': "{table}의 전체 텍스트 검색",
1050
+ 'fts.intro': "선택한 텍스트 열에 대한 저장된 생성 tsvector 열과 그 위의 GIN 인덱스를 추가합니다. 그러면 검색 유형이 fulltext인 검색 구성(공유 구성 요소 › 검색 구성)이나 보고서 검색이 어형 변화(\"running\"으로 \"run\")를 빠르게 찾습니다. 열은 자동으로 최신 상태를 유지하며, 추가할 때 테이블이 다시 작성됩니다.",
1051
+ 'fts.columns': "텍스트 열",
1052
+ 'fts.include': "포함",
1053
+ 'fts.weight': "가중치",
1054
+ 'fts.weight_help': "순위에서 A가 가장 중요하고 D가 가장 덜 중요합니다. 예: 제목은 A, 설명은 B.",
1055
+ 'fts.config': "텍스트 검색 구성",
1056
+ 'fts.config_help': "어간 처리에 사용할 언어(english: running → run). simple: 어간 처리 없음. 검색할 때도 같은 구성을 사용하십시오.",
1057
+ 'fts.column': "tsvector 열",
1058
+ 'fts.index': "인덱스 이름",
1059
+ 'fts.no_text': "이 테이블에는 검색할 텍스트 열이 없습니다.",
1060
+ 'fts.pick': "텍스트 열을 하나 이상 선택하십시오.",
1061
+ 'fts.bad_name': "\"{name}\"은(는) 올바른 이름이 아닙니다: 소문자, 숫자, _만 사용할 수 있습니다.",
1062
+ 'fts.exists': "{name}이(가) 이미 있습니다.",
1063
+ 'fts.bad_config': "{name}은(는) 텍스트 검색 구성이 아닙니다.",
1064
+ 'fts.existing': "현재 tsvector 열:",
1065
+ 'fts.gin_yes': "GIN 인덱스",
1066
+ 'fts.gin_no': "GIN 인덱스 없음",
1067
+ 'fts.added': "{table}에 전체 텍스트 검색을 추가했습니다: 열 {column}, 인덱스 {index}.",
1068
+ // (sprint 136, AP-1) Table Properties › Add vector column
1069
+ 'vec.add': "벡터 열 추가",
1070
+ 'vec.title': "{table}의 벡터 열",
1071
+ 'vec.intro': "임베딩 열 vector(n)(n: 벡터 공급자의 차원 수), 거리용 HNSW 인덱스, 그리고 소스 열이 바뀌면 행을 다시 임베딩하는 트리거를 추가합니다. 그러면 검색 유형 vector 또는 hybrid의 검색 구성이나 AI 에이전트의 Retrieve 도구가 의미로 행을 찾습니다. 지금 있는 행은 바로 임베딩 대기열에 들어갑니다.",
1072
+ 'vec.existing': "현재 벡터 열",
1073
+ 'vec.provider': "벡터 공급자",
1074
+ 'vec.provider_help': "이 작업 영역 애플리케이션의 공유 구성 요소 › 벡터 공급자. 검색할 때도 같은 공급자를 사용하십시오.",
1075
+ 'vec.keep': "최신 상태 유지",
1076
+ 'vec.keep_help': "함수 공급자는 트리거에서 바로 임베딩을 설정할 수 있습니다. AI 서비스는 서버가 호출합니다: 트리거가 행을 대기열에 넣고, 서버가 공급자의 분당 요청 수와 애플리케이션의 일일 AI 한도 안에서 대기열의 행을 일괄로 임베딩합니다.",
1077
+ 'vec.mode_set': "트리거가 바로 설정",
1078
+ 'vec.mode_queue': "서버용 대기열에 넣음",
1079
+ 'vec.sources': "소스 열",
1080
+ 'vec.sources_help': "임베딩할 텍스트: 이 열들, 한 줄에 하나씩. 그중 하나가 바뀌면 행을 다시 임베딩합니다.",
1081
+ 'vec.distance': "거리",
1082
+ 'vec.distance_help': "대부분의 임베딩 모델에는 cosine; 검색 구성도 같은 거리를 사용합니다.",
1083
+ 'vec.column': "벡터 열",
1084
+ 'vec.no_key': "테이블에 열 하나로 된 기본 키가 필요합니다: 대기열은 그것으로 행을 찾습니다.",
1085
+ 'vec.pick_provider': "벡터 공급자를 선택하십시오.",
1086
+ 'vec.no_providers': "이 작업 영역의 애플리케이션에는 아직 벡터 공급자가 없습니다: 공유 구성 요소 › 벡터 공급자에서 추가하십시오.",
1087
+ 'vec.missing': "이 데이터베이스에 pgvector 확장이 설치되어 있지 않아 아직 벡터 열을 추가할 수 없습니다.",
1088
+ 'vec.missing_create': "데이터베이스 서버에 pgvector {version}이(가) 있습니다: 작업 영역 유틸리티 › 벡터 공급자에서 확장을 만드십시오.",
1089
+ 'vec.missing_admin': "데이터베이스 서버에 pgvector {version}이(가) 있습니다: 관리자가 확장을 만듭니다(작업 영역 유틸리티 › 벡터 공급자).",
1090
+ 'vec.missing_package': "데이터베이스 서버에 pgvector 패키지가 없습니다: 설치한 다음(예: apt install postgresql-17-pgvector 또는 Docker 이미지 pgvector/pgvector) 확장을 만드십시오.",
1091
+ 'vec.utilities': "작업 영역 유틸리티 › 벡터 공급자",
1092
+ 'vec.kept_by': "{provider}이(가) {columns}에서 유지",
1093
+ 'vec.not_kept': "최신 상태로 유지하는 트리거가 없음",
1094
+ 'vec.progress': "{column}의 임베딩된 행",
1095
+ 'vec.counts': "{total}행 중 {done}행 임베딩됨, {queued}행 대기 중",
1096
+ 'vec.failing': "{n}건 실패",
1097
+ 'vec.reembed_all': "임베딩이 없는 행만이 아니라 모든 행",
1098
+ 'vec.backfill': "채우기",
1099
+ 'vec.refresh': "새로 고침",
1100
+ 'vec.backfill_queued': "{column}의 {n}행을 임베딩 대기열에 넣었습니다.",
1101
+ 'vec.added': "{table}에 벡터 열 {column}을(를) 추가했습니다. {n}행이 임베딩 대기열에 있습니다.",
1048
1102
  // ------------------------------------------------------------ Shared Components (sprint 124)
1049
1103
  'comp.nav_entry': '탐색 항목',
1050
1104
  'comp.nav_entry.plural': '탐색 메뉴',
@@ -1045,6 +1045,60 @@ export const nl: BuilderMessages = {
1045
1045
  'te.failed_stmt': 'Mislukt statement',
1046
1046
  'te.title_table': 'Tabel {table}',
1047
1047
  'te.title_create': 'Tabel maken',
1048
+ 'fts.add': "Volledige-tekstzoeken toevoegen",
1049
+ 'fts.title': "Volledige-tekstzoeken op {table}",
1050
+ 'fts.intro': "Voegt een opgeslagen gegenereerde tsvector-kolom toe over de tekstkolommen die u kiest, met een GIN-index erop. Een zoekconfiguratie van zoektype fulltext (Gedeelde componenten › Zoekconfiguraties) of het zoeken in een rapport vindt dan snel woorden in al hun vormen (\"running\" vindt \"run\"). De kolom houdt zichzelf bij; toevoegen herschrijft de tabel.",
1051
+ 'fts.columns': "Tekstkolommen",
1052
+ 'fts.include': "Meenemen",
1053
+ 'fts.weight': "Gewicht",
1054
+ 'fts.weight_help': "A telt het zwaarst in de rangorde, D het minst: bijvoorbeeld A voor een titel, B voor een omschrijving.",
1055
+ 'fts.config': "Tekstzoekconfiguratie",
1056
+ 'fts.config_help': "De taal waarvan de woordstammen worden gebruikt (dutch: lopen → loop); simple: geen stammen. Zoek met dezelfde configuratie.",
1057
+ 'fts.column': "tsvector-kolom",
1058
+ 'fts.index': "Indexnaam",
1059
+ 'fts.no_text': "Deze tabel heeft geen tekstkolommen om in te zoeken.",
1060
+ 'fts.pick': "Kies ten minste één tekstkolom.",
1061
+ 'fts.bad_name': "\"{name}\" is geen geldige naam: kleine letters, cijfers en _.",
1062
+ 'fts.exists': "{name} bestaat al.",
1063
+ 'fts.bad_config': "{name} is geen tekstzoekconfiguratie.",
1064
+ 'fts.existing': "tsvector-kolommen nu:",
1065
+ 'fts.gin_yes': "GIN-index",
1066
+ 'fts.gin_no': "geen GIN-index",
1067
+ 'fts.added': "Volledige-tekstzoeken toegevoegd aan {table}: kolom {column} met index {index}.",
1068
+ // (sprint 136, AP-1) Table Properties › Add vector column
1069
+ 'vec.add': "Vectorkolom toevoegen",
1070
+ 'vec.title': "Vectorkolom op {table}",
1071
+ 'vec.intro': "Voegt een embeddingkolom vector(n) toe (n: de dimensies van de vectorprovider), een HNSW-index voor de afstand, en een trigger die een rij opnieuw embedt als de bronkolommen veranderen. Een zoekconfiguratie van het type vector of hybrid, of de Retrieve-tool van een AI-agent, vindt rijen dan op betekenis. De rijen die er nu zijn, komen meteen in de wachtrij voor hun embeddings.",
1072
+ 'vec.existing': "Vectorkolommen nu",
1073
+ 'vec.provider': "Vectorprovider",
1074
+ 'vec.provider_help': "Gedeelde componenten › Vectorproviders van de applicaties in deze werkruimte. Zoek met dezelfde provider.",
1075
+ 'vec.keep': "Actueel houden",
1076
+ 'vec.keep_help': "Een functieprovider kan de embedding meteen in de trigger zetten. Een AI-service wordt door de server aangeroepen: de trigger zet de rij in de wachtrij en de server embedt rijen in batches, binnen de verzoeken per minuut van de provider en de dagelijkse AI-limieten van de applicatie.",
1077
+ 'vec.mode_set': "door de trigger gezet, meteen",
1078
+ 'vec.mode_queue': "in de wachtrij voor de server",
1079
+ 'vec.sources': "Bronkolommen",
1080
+ 'vec.sources_help': "De tekst die wordt ge-embed: deze kolommen, één per regel. Een wijziging in een ervan embedt de rij opnieuw.",
1081
+ 'vec.distance': "Afstand",
1082
+ 'vec.distance_help': "cosine voor de meeste embeddingmodellen; de zoekconfiguratie gebruikt dezelfde afstand.",
1083
+ 'vec.column': "Vectorkolom",
1084
+ 'vec.no_key': "De tabel heeft een primaire sleutel van één kolom nodig: de wachtrij vindt rijen daarmee.",
1085
+ 'vec.pick_provider': "Kies een vectorprovider.",
1086
+ 'vec.no_providers': "Nog geen applicatie in deze werkruimte heeft een vectorprovider: voeg er een toe onder Gedeelde componenten › Vectorproviders.",
1087
+ 'vec.missing': "De extensie pgvector is niet geïnstalleerd in deze database, dus een vectorkolom kan nog niet worden toegevoegd.",
1088
+ 'vec.missing_create': "De databaseserver heeft pgvector {version}: maak de extensie aan onder Werkruimtehulpmiddelen › Vectorproviders.",
1089
+ 'vec.missing_admin': "De databaseserver heeft pgvector {version}: een beheerder maakt de extensie aan (Werkruimtehulpmiddelen › Vectorproviders).",
1090
+ 'vec.missing_package': "De databaseserver heeft het pakket pgvector niet: installeer het (bijv. apt install postgresql-17-pgvector, of het Docker-image pgvector/pgvector) en maak dan de extensie aan.",
1091
+ 'vec.utilities': "Werkruimtehulpmiddelen › Vectorproviders",
1092
+ 'vec.kept_by': "bijgehouden door {provider} uit {columns}",
1093
+ 'vec.not_kept': "geen trigger houdt hem actueel",
1094
+ 'vec.progress': "Ge-embede rijen van {column}",
1095
+ 'vec.counts': "{done} van {total} rijen ge-embed, {queued} in de wachtrij",
1096
+ 'vec.failing': "{n} mislukt",
1097
+ 'vec.reembed_all': "alle rijen, niet alleen die zonder embedding",
1098
+ 'vec.backfill': "Aanvullen",
1099
+ 'vec.refresh': "Vernieuwen",
1100
+ 'vec.backfill_queued': "{n} rijen van {column} in de wachtrij voor hun embeddings.",
1101
+ 'vec.added': "Vectorkolom {column} toegevoegd aan {table}; {n} rijen in de wachtrij voor hun embeddings.",
1048
1102
  // ------------------------------------------------------------ Shared Components (sprint 124)
1049
1103
  'comp.nav_entry': 'Navigatie-item',
1050
1104
  'comp.nav_entry.plural': 'Navigatiemenu',
@@ -1045,6 +1045,60 @@ export const pt: BuilderMessages = {
1045
1045
  'te.failed_stmt': 'Instrução com falha',
1046
1046
  'te.title_table': 'Tabela {table}',
1047
1047
  'te.title_create': 'Criar tabela',
1048
+ 'fts.add': "Adicionar pesquisa de texto integral",
1049
+ 'fts.title': "Pesquisa de texto integral em {table}",
1050
+ 'fts.intro': "Adiciona uma coluna tsvector gerada e armazenada sobre as colunas de texto escolhidas, com um índice GIN. Uma configuração de pesquisa do tipo fulltext (Componentes partilhados › Configurações de pesquisa) ou a pesquisa de um relatório encontra então rapidamente as formas das palavras (\"running\" encontra \"run\"). A coluna mantém-se atualizada; adicioná-la reescreve a tabela.",
1051
+ 'fts.columns': "Colunas de texto",
1052
+ 'fts.include': "Incluir",
1053
+ 'fts.weight': "Peso",
1054
+ 'fts.weight_help': "A conta mais na classificação, D menos: p. ex. A para um título, B para uma descrição.",
1055
+ 'fts.config': "Configuração de pesquisa de texto",
1056
+ 'fts.config_help': "A língua cuja radicalização é usada (portuguese: correndo → corr); simple: sem radicalização. Pesquise com a mesma configuração.",
1057
+ 'fts.column': "Coluna tsvector",
1058
+ 'fts.index': "Nome do índice",
1059
+ 'fts.no_text': "Esta tabela não tem colunas de texto para pesquisar.",
1060
+ 'fts.pick': "Escolha pelo menos uma coluna de texto.",
1061
+ 'fts.bad_name': "\"{name}\" não é um nome válido: minúsculas, dígitos e _.",
1062
+ 'fts.exists': "{name} já existe.",
1063
+ 'fts.bad_config': "{name} não é uma configuração de pesquisa de texto.",
1064
+ 'fts.existing': "Colunas tsvector atuais:",
1065
+ 'fts.gin_yes': "índice GIN",
1066
+ 'fts.gin_no': "sem índice GIN",
1067
+ 'fts.added': "Pesquisa de texto integral adicionada a {table}: coluna {column} com o índice {index}.",
1068
+ // (sprint 136, AP-1) Table Properties › Add vector column
1069
+ 'vec.add': "Adicionar coluna vetorial",
1070
+ 'vec.title': "Coluna vetorial em {table}",
1071
+ 'vec.intro': "Adiciona uma coluna de embedding vector(n) (n: as dimensões do fornecedor de vetores), um índice HNSW para a distância e um gatilho que recalcula o embedding de uma linha quando as colunas de origem mudam. Uma configuração de pesquisa do tipo vector ou hybrid, ou a ferramenta Retrieve de um agente de IA, encontra então as linhas pelo significado. As linhas que já existem entram logo na fila para os seus embeddings.",
1072
+ 'vec.existing': "Colunas vetoriais atuais",
1073
+ 'vec.provider': "Fornecedor de vetores",
1074
+ 'vec.provider_help': "Componentes partilhados › Fornecedores de vetores das aplicações deste espaço de trabalho. Pesquise com o mesmo fornecedor.",
1075
+ 'vec.keep': "Manter atualizada",
1076
+ 'vec.keep_help': "Um fornecedor do tipo função pode definir o embedding no gatilho, de imediato. Um serviço de IA é chamado pelo servidor: o gatilho põe a linha na fila e o servidor calcula os embeddings em lotes, dentro dos pedidos por minuto do fornecedor e dos limites diários de IA da aplicação.",
1077
+ 'vec.mode_set': "definido pelo gatilho, de imediato",
1078
+ 'vec.mode_queue': "na fila para o servidor",
1079
+ 'vec.sources': "Colunas de origem",
1080
+ 'vec.sources_help': "O texto de que se calcula o embedding: estas colunas, uma por linha. Uma alteração numa delas recalcula o embedding da linha.",
1081
+ 'vec.distance': "Distância",
1082
+ 'vec.distance_help': "cosine para a maioria dos modelos de embedding; a configuração de pesquisa usa a mesma distância.",
1083
+ 'vec.column': "Coluna vetorial",
1084
+ 'vec.no_key': "A tabela precisa de uma chave primária de uma só coluna: a fila encontra as linhas por ela.",
1085
+ 'vec.pick_provider': "Escolha um fornecedor de vetores.",
1086
+ 'vec.no_providers': "Nenhuma aplicação deste espaço de trabalho tem ainda um fornecedor de vetores: adicione um em Componentes partilhados › Fornecedores de vetores.",
1087
+ 'vec.missing': "A extensão pgvector não está instalada nesta base de dados, por isso ainda não é possível adicionar uma coluna vetorial.",
1088
+ 'vec.missing_create': "O servidor de base de dados tem o pgvector {version}: crie a extensão em Utilitários do espaço de trabalho › Fornecedores de vetores.",
1089
+ 'vec.missing_admin': "O servidor de base de dados tem o pgvector {version}: um administrador cria a extensão (Utilitários do espaço de trabalho › Fornecedores de vetores).",
1090
+ 'vec.missing_package': "O servidor de base de dados não tem o pacote pgvector: instale-o (p. ex. apt install postgresql-17-pgvector, ou a imagem Docker pgvector/pgvector) e depois crie a extensão.",
1091
+ 'vec.utilities': "Utilitários do espaço de trabalho › Fornecedores de vetores",
1092
+ 'vec.kept_by': "mantida por {provider} a partir de {columns}",
1093
+ 'vec.not_kept': "nenhum gatilho a mantém atualizada",
1094
+ 'vec.progress': "Linhas de {column} com embedding",
1095
+ 'vec.counts': "{done} de {total} linhas com embedding, {queued} na fila",
1096
+ 'vec.failing': "{n} com erro",
1097
+ 'vec.reembed_all': "todas as linhas, não só as que não têm embedding",
1098
+ 'vec.backfill': "Completar",
1099
+ 'vec.refresh': "Atualizar",
1100
+ 'vec.backfill_queued': "{n} linhas de {column} na fila para os seus embeddings.",
1101
+ 'vec.added': "Coluna vetorial {column} adicionada a {table}; {n} linhas na fila para os seus embeddings.",
1048
1102
  // ------------------------------------------------------------ Shared Components (sprint 124)
1049
1103
  'comp.nav_entry': 'Entrada de navegação',
1050
1104
  'comp.nav_entry.plural': 'Menu de navegação',
@@ -1045,6 +1045,60 @@ export const zhCN: BuilderMessages = {
1045
1045
  'te.failed_stmt': '失败的语句',
1046
1046
  'te.title_table': '表 {table}',
1047
1047
  'te.title_create': '创建表',
1048
+ 'fts.add': "添加全文搜索",
1049
+ 'fts.title': "{table} 的全文搜索",
1050
+ 'fts.intro': "在所选的文本列上添加一个存储的生成 tsvector 列,并在其上创建 GIN 索引。之后,搜索类型为 fulltext 的搜索配置(共享组件 › 搜索配置)或报表搜索即可快速找到词形变化(\"running\" 可找到 \"run\")。该列会自动保持最新;添加时会重写该表。",
1051
+ 'fts.columns': "文本列",
1052
+ 'fts.include': "包含",
1053
+ 'fts.weight': "权重",
1054
+ 'fts.weight_help': "A 在排序中最重要,D 最不重要:例如标题用 A,描述用 B。",
1055
+ 'fts.config': "文本搜索配置",
1056
+ 'fts.config_help': "使用其词干规则的语言(english:running → run);simple:不提取词干。搜索时请使用相同的配置。",
1057
+ 'fts.column': "tsvector 列",
1058
+ 'fts.index': "索引名称",
1059
+ 'fts.no_text': "此表没有可搜索的文本列。",
1060
+ 'fts.pick': "请至少选择一个文本列。",
1061
+ 'fts.bad_name': "\"{name}\" 不是有效名称:只能使用小写字母、数字和 _。",
1062
+ 'fts.exists': "{name} 已存在。",
1063
+ 'fts.bad_config': "{name} 不是文本搜索配置。",
1064
+ 'fts.existing': "现有 tsvector 列:",
1065
+ 'fts.gin_yes': "GIN 索引",
1066
+ 'fts.gin_no': "无 GIN 索引",
1067
+ 'fts.added': "已为 {table} 添加全文搜索:列 {column},索引 {index}。",
1068
+ // (sprint 136, AP-1) Table Properties › Add vector column
1069
+ 'vec.add': "添加向量列",
1070
+ 'vec.title': "{table} 的向量列",
1071
+ 'vec.intro': "添加一个嵌入列 vector(n)(n:向量提供者的维数)、一个用于该距离的 HNSW 索引,以及在源列变化时重新嵌入该行的触发器。之后,搜索类型为 vector 或 hybrid 的搜索配置,或 AI 代理的 Retrieve 工具,就能按含义找到行。现有的行会立即进入嵌入队列。",
1072
+ 'vec.existing': "当前的向量列",
1073
+ 'vec.provider': "向量提供者",
1074
+ 'vec.provider_help': "此工作区应用程序的共享组件 › 向量提供者。搜索时使用同一个提供者。",
1075
+ 'vec.keep': "保持最新",
1076
+ 'vec.keep_help': "函数提供者可以在触发器中立即设置嵌入。AI 服务由服务器调用:触发器把行放入队列,服务器在提供者的每分钟请求数和应用程序的每日 AI 限额内批量嵌入队列中的行。",
1077
+ 'vec.mode_set': "由触发器立即设置",
1078
+ 'vec.mode_queue': "放入服务器的队列",
1079
+ 'vec.sources': "源列",
1080
+ 'vec.sources_help': "要嵌入的文本:这些列,每行一个。其中任一列变化时会重新嵌入该行。",
1081
+ 'vec.distance': "距离",
1082
+ 'vec.distance_help': "大多数嵌入模型使用 cosine;搜索配置使用相同的距离。",
1083
+ 'vec.column': "向量列",
1084
+ 'vec.no_key': "该表需要一个单列主键:队列据此查找行。",
1085
+ 'vec.pick_provider': "请选择一个向量提供者。",
1086
+ 'vec.no_providers': "此工作区的应用程序还没有向量提供者:请在共享组件 › 向量提供者中添加。",
1087
+ 'vec.missing': "此数据库未安装 pgvector 扩展,因此暂时无法添加向量列。",
1088
+ 'vec.missing_create': "数据库服务器有 pgvector {version}:请在工作区实用工具 › 向量提供者中创建该扩展。",
1089
+ 'vec.missing_admin': "数据库服务器有 pgvector {version}:由管理员创建该扩展(工作区实用工具 › 向量提供者)。",
1090
+ 'vec.missing_package': "数据库服务器没有 pgvector 软件包:请先安装(例如 apt install postgresql-17-pgvector,或 Docker 镜像 pgvector/pgvector),再创建该扩展。",
1091
+ 'vec.utilities': "工作区实用工具 › 向量提供者",
1092
+ 'vec.kept_by': "由 {provider} 根据 {columns} 维护",
1093
+ 'vec.not_kept': "没有触发器保持其最新",
1094
+ 'vec.progress': "{column} 已嵌入的行",
1095
+ 'vec.counts': "{total} 行中已嵌入 {done} 行,{queued} 行在队列中",
1096
+ 'vec.failing': "{n} 行失败",
1097
+ 'vec.reembed_all': "所有行,而不仅是没有嵌入的行",
1098
+ 'vec.backfill': "回填",
1099
+ 'vec.refresh': "刷新",
1100
+ 'vec.backfill_queued': "已将 {column} 的 {n} 行放入嵌入队列。",
1101
+ 'vec.added': "已为 {table} 添加向量列 {column};{n} 行已进入嵌入队列。",
1048
1102
  // ------------------------------------------------------------ Shared Components (sprint 124)
1049
1103
  'comp.nav_entry': '导航条目',
1050
1104
  'comp.nav_entry.plural': '导航菜单',
@@ -1045,6 +1045,60 @@ export const zhTW: BuilderMessages = {
1045
1045
  'te.failed_stmt': '失敗的敘述句',
1046
1046
  'te.title_table': '表格 {table}',
1047
1047
  'te.title_create': '建立表格',
1048
+ 'fts.add': "新增全文檢索",
1049
+ 'fts.title': "{table} 的全文檢索",
1050
+ 'fts.intro': "在所選的文字欄位上新增一個儲存的產生 tsvector 欄位,並在其上建立 GIN 索引。之後,搜尋類型為 fulltext 的搜尋設定(共用元件 › 搜尋設定)或報表搜尋即可快速找到詞形變化(\"running\" 可找到 \"run\")。該欄位會自動保持最新;新增時會重寫該表格。",
1051
+ 'fts.columns': "文字欄位",
1052
+ 'fts.include': "包含",
1053
+ 'fts.weight': "權重",
1054
+ 'fts.weight_help': "A 在排序中最重要,D 最不重要:例如標題用 A,描述用 B。",
1055
+ 'fts.config': "文字搜尋設定",
1056
+ 'fts.config_help': "使用其詞幹規則的語言(english:running → run);simple:不擷取詞幹。搜尋時請使用相同的設定。",
1057
+ 'fts.column': "tsvector 欄位",
1058
+ 'fts.index': "索引名稱",
1059
+ 'fts.no_text': "此表格沒有可搜尋的文字欄位。",
1060
+ 'fts.pick': "請至少選擇一個文字欄位。",
1061
+ 'fts.bad_name': "\"{name}\" 不是有效名稱:只能使用小寫字母、數字和 _。",
1062
+ 'fts.exists': "{name} 已存在。",
1063
+ 'fts.bad_config': "{name} 不是文字搜尋設定。",
1064
+ 'fts.existing': "現有 tsvector 欄位:",
1065
+ 'fts.gin_yes': "GIN 索引",
1066
+ 'fts.gin_no': "無 GIN 索引",
1067
+ 'fts.added': "已為 {table} 新增全文檢索:欄位 {column},索引 {index}。",
1068
+ // (sprint 136, AP-1) Table Properties › Add vector column
1069
+ 'vec.add': "新增向量欄位",
1070
+ 'vec.title': "{table} 的向量欄位",
1071
+ 'vec.intro': "新增一個嵌入欄位 vector(n)(n:向量提供者的維度數)、一個用於該距離的 HNSW 索引,以及在來源欄位變更時重新嵌入該列的觸發程序。之後,搜尋類型為 vector 或 hybrid 的搜尋組態,或 AI 代理的 Retrieve 工具,就能依含意找到資料列。現有的資料列會立即進入嵌入佇列。",
1072
+ 'vec.existing': "目前的向量欄位",
1073
+ 'vec.provider': "向量提供者",
1074
+ 'vec.provider_help': "此工作區應用程式的共用元件 › 向量提供者。搜尋時使用同一個提供者。",
1075
+ 'vec.keep': "保持最新",
1076
+ 'vec.keep_help': "函式提供者可以在觸發程序中立即設定嵌入。AI 服務由伺服器呼叫:觸發程序將資料列放入佇列,伺服器在提供者的每分鐘請求數與應用程式的每日 AI 上限內,批次嵌入佇列中的資料列。",
1077
+ 'vec.mode_set': "由觸發程序立即設定",
1078
+ 'vec.mode_queue': "放入伺服器的佇列",
1079
+ 'vec.sources': "來源欄位",
1080
+ 'vec.sources_help': "要嵌入的文字:這些欄位,每行一個。其中任一欄位變更時會重新嵌入該列。",
1081
+ 'vec.distance': "距離",
1082
+ 'vec.distance_help': "大多數嵌入模型使用 cosine;搜尋組態使用相同的距離。",
1083
+ 'vec.column': "向量欄位",
1084
+ 'vec.no_key': "此表格需要單一欄位的主索引鍵:佇列以此找出資料列。",
1085
+ 'vec.pick_provider': "請選擇一個向量提供者。",
1086
+ 'vec.no_providers': "此工作區的應用程式還沒有向量提供者:請在共用元件 › 向量提供者中新增。",
1087
+ 'vec.missing': "此資料庫未安裝 pgvector 擴充功能,因此暫時無法新增向量欄位。",
1088
+ 'vec.missing_create': "資料庫伺服器有 pgvector {version}:請在工作區公用程式 › 向量提供者中建立該擴充功能。",
1089
+ 'vec.missing_admin': "資料庫伺服器有 pgvector {version}:由管理員建立該擴充功能(工作區公用程式 › 向量提供者)。",
1090
+ 'vec.missing_package': "資料庫伺服器沒有 pgvector 套件:請先安裝(例如 apt install postgresql-17-pgvector,或 Docker 映像檔 pgvector/pgvector),再建立該擴充功能。",
1091
+ 'vec.utilities': "工作區公用程式 › 向量提供者",
1092
+ 'vec.kept_by': "由 {provider} 依 {columns} 維護",
1093
+ 'vec.not_kept': "沒有觸發程序讓它保持最新",
1094
+ 'vec.progress': "{column} 已嵌入的資料列",
1095
+ 'vec.counts': "{total} 列中已嵌入 {done} 列,{queued} 列在佇列中",
1096
+ 'vec.failing': "{n} 列失敗",
1097
+ 'vec.reembed_all': "所有資料列,而不只是沒有嵌入的資料列",
1098
+ 'vec.backfill': "回填",
1099
+ 'vec.refresh': "重新整理",
1100
+ 'vec.backfill_queued': "已將 {column} 的 {n} 列放入嵌入佇列。",
1101
+ 'vec.added': "已為 {table} 新增向量欄位 {column};{n} 列已進入嵌入佇列。",
1048
1102
  // ------------------------------------------------------------ Shared Components (sprint 124)
1049
1103
  'comp.nav_entry': '導覽項目',
1050
1104
  'comp.nav_entry.plural': '導覽選單',
@@ -32,6 +32,8 @@ export function objectMenu(schema: string, o: MenuObj): Raw {
32
32
  ${table ? item(`${BASE}/sql/objects/table?${new URLSearchParams({ schema, template: o.ref })}`, 'copy', 'Use as template') : ''}
33
33
  ${table ? item(`${BASE}/sql/objects/security?${new URLSearchParams({ o: o.ref })}`, 'shield', 'Security…') : ''}
34
34
  ${table ? item(`${BASE}/sql/load`, 'upload', 'Load data') : ''}
35
+ ${table ? item(`${BASE}/sql/objects/fulltext?${new URLSearchParams({ o: o.ref })}`, 'search', 'Add full-text search…') : ''}
36
+ ${table ? item(`${BASE}/sql/objects/vector?${new URLSearchParams({ o: o.ref })}`, 'search', 'Add vector column…') : ''}
35
37
  ${table ? html`${item(q('index'), 'list', 'Add index')}${item(q('trigger'), 'bolt', 'Add trigger')}${item(q('policy'), 'shield', 'Add policy')}` : ''}
36
38
  ${RELATIONS.has(o.type) ? item(`${BASE}/sql/rest/object?${new URLSearchParams({ schema, name: o.name })}`, 'globe', 'REST › Enable / Disable') : ''}
37
39
  ${item(q('ddl'), 'code', 'Quick DDL to the worksheet')}
@@ -76,7 +76,7 @@ export function sharedGroups(appId: number): { key: string; title: string; icon:
76
76
  { key: 'data', title: 'Data Sources', icon: 'database', items: [t('rest_source', 'REST Data Sources'), t('web_credential', 'Web Credentials'), t('rest_module', 'RESTful Services'), t('data_load_def', 'Data Load Definitions')] },
77
77
  { key: 'workflows', title: 'Workflows and Automations', icon: 'activity', items: [t('workflow_definition', 'Workflows'), t('task_definition', 'Task Definitions'), t('automation', 'Automations')] },
78
78
  { key: 'globalization', title: 'Globalization', icon: 'globe', items: [page('globalization', 'Translations and Text Messages'), t('dynamic_translation', 'Dynamic Translations'), settings('globalization', 'Globalization Attributes')] },
79
- { key: 'ai', title: 'Generative AI', icon: 'lightbulb', items: [page('ai', 'AI Services'), page('ai-agents', 'AI Agents')] },
79
+ { key: 'ai', title: 'Generative AI', icon: 'lightbulb', items: [page('ai', 'AI Services'), page('ai-agents', 'AI Agents'), page('vector-providers', 'Vector Providers')] },
80
80
  ];
81
81
  }
82
82
 
@@ -19,6 +19,8 @@ import { sqlUtilityRoutes } from './sql-utilities.ts';
19
19
  import { createTemplate, objectBrowserRoutes } from './object-browser.ts';
20
20
  import { objectActionRoutes } from './object-actions.ts';
21
21
  import { tableEditorRoutes } from './table-editor.ts';
22
+ import { fulltextRoutes } from './fulltext.ts';
23
+ import { vectorColumnRoutes } from './vector-column.ts';
22
24
  import { dataSecurityRoutes } from './data-security.ts';
23
25
  import { dataModelerRoutes } from './data-modeler.ts';
24
26
  import { autoRestBuilderRoutes } from './autorest.ts';
@@ -134,6 +136,10 @@ export async function sqlRoutes(app: FastifyInstance) {
134
136
  await app.register(objectActionRoutes);
135
137
  // (sprint 95) Table Properties: create, edit or copy a table in a form
136
138
  await app.register(tableEditorRoutes);
139
+ // (sprint 135, AP-2) Table Properties › Add full-text search
140
+ await app.register(fulltextRoutes);
141
+ // (sprint 136, AP-1) Table Properties › Add vector column
142
+ await app.register(vectorColumnRoutes);
137
143
  await app.register(dataSecurityRoutes);
138
144
  // (sprint 98) the Data Modeler: relational diagrams, their DDL and data dictionary
139
145
  await app.register(dataModelerRoutes);
@@ -142,6 +142,8 @@ function form(s: Session, def: TableDef, opts: { editing: TableDef | null; schem
142
142
  <button class="btn" name="mode" value="preview">${icon('eye')} ${tx('te.preview')}</button>
143
143
  <button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
144
144
  <button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
145
+ ${editing ? html`<a class="btn" href="${BASE}/sql/objects/fulltext?${new URLSearchParams({ o: opts.ref })}">${icon('search')} ${tx('fts.add')}…</a>` : ''}
146
+ ${editing ? html`<a class="btn" href="${BASE}/sql/objects/vector?${new URLSearchParams({ o: opts.ref })}">${icon('search')} ${tx('vec.add')}…</a>` : ''}
145
147
  <a class="btn" href="${editing ? `${BASE}/sql/objects?${new URLSearchParams({ schema: def.schema, k: 'table', o: opts.ref })}` : `${BASE}/sql/objects?${new URLSearchParams({ schema: def.schema })}`}">${tx('common.cancel')}</a>
146
148
  </div></form>`;
147
149
  }
@@ -0,0 +1,229 @@
1
+ import type { FastifyInstance, FastifyReply } from 'fastify';
2
+ import pg from 'pg';
3
+ import { HNSW_MAX_DIMENSIONS, vectorProviderProblems, type VectorProvider } from '../ai/embeddings.ts';
4
+ import { joinStatements, recordChanges } from '../changelog.ts';
5
+ import { owner } from '../db.ts';
6
+ import { html, type Raw } from '../html.ts';
7
+ import { icon } from '../icons.ts';
8
+ import { clientIp, logActivity } from '../session.ts';
9
+ import { readTable } from '../tabledef.ts';
10
+ import { backfill, DISTANCES, isDistance, kickEmbeddings, pgvectorStatus, queueState, vectorTriggers, type Distance } from '../vector.ts';
11
+ import { clearCompletions } from './code-editor.ts';
12
+ import { tr } from './i18n.ts';
13
+ import { isAdmin } from './locks.ts';
14
+ import { SYSTEM_SCHEMAS } from './object-browser.ts';
15
+ import { back, BASE, csrf, developer, flash, send, shell, workshopTabs, type Body, type Req } from './ui.ts';
16
+ import { appAllowed, currentWorkspace, inWorkspaceSql, usableSchemas, workshopDb } from './workspaces.ts';
17
+
18
+ // (sprint 136, AP-1) SQL Workshop › Table Properties › Add vector column: an embedding column vector(n) (n: the
19
+ // chosen vector provider's dimensions), an HNSW index for the distance (vector_cosine_ops …), and a trigger that
20
+ // keeps it current when the source columns change:
21
+ // - a function provider: BEFORE INSERT OR UPDATE OF … → meta.embedding_set (the embedding at once);
22
+ // - an AI service (or a function, if chosen): AFTER INSERT OR UPDATE OF … → meta.embedding_enqueue (the row's key
23
+ // queued; the server embeds queued rows in batches, src/vector.ts).
24
+ // Preview DDL shows the statements; Apply runs them in one transaction with the workshop's connection and records
25
+ // them in the Change Log; Open in the worksheet puts them there. The page also lists the table's vector columns
26
+ // with their progress (rows embedded, queued, failing) and a Backfill button that queues the rows without an
27
+ // embedding (or all rows). Names come from the catalog or are checked as plain identifiers before they reach DDL.
28
+
29
+ type Db = Awaited<ReturnType<typeof workshopDb>>;
30
+ const NAME = /^[a-z_][a-z0-9_]{0,62}$/;
31
+ const ident = (n: string) => (/^[a-z_][a-z0-9_$]*$/.test(n) ? n : pg.escapeIdentifier(n));
32
+
33
+ interface Col { name: string; type: string; texty: boolean; dims: number | null; vector: boolean }
34
+
35
+ async function columnsOf(db: Db, oid: number): Promise<Col[]> {
36
+ return (await db.query<Col>(
37
+ `select a.attname as name, format_type(a.atttypid, a.atttypmod) as type, t.typcategory = 'S' and a.attgenerated = '' as texty,
38
+ t.typname = 'vector' as vector, case when t.typname = 'vector' and a.atttypmod > 0 then a.atttypmod end as dims
39
+ from pg_attribute a join pg_type t on t.oid = a.atttypid
40
+ where a.attrelid = $1 and a.attnum > 0 and not a.attisdropped order by a.attnum`, [oid])).rows;
41
+ }
42
+
43
+ /** The single-column primary key of a table (the queue keys rows by it), or null. */
44
+ async function keyOf(db: Db, oid: number) {
45
+ return (await db.one<{ k: string }>(
46
+ `select a.attname as k from pg_index i join pg_attribute a on a.attrelid = i.indrelid and a.attnum = i.indkey[0]
47
+ where i.indrelid = $1 and i.indisprimary and i.indnatts = 1`, [oid]))?.k ?? null;
48
+ }
49
+
50
+ /** The workspace's applications' vector providers, as options "appId:NAME". */
51
+ async function providerChoices(s: Parameters<typeof currentWorkspace>[0]) {
52
+ const apps = (await owner.query<{ id: number; alias: string; vector_providers: VectorProvider[] }>(
53
+ `select id, alias, vector_providers from meta.app a where ${inWorkspaceSql('a', 1)} and jsonb_array_length(vector_providers) > 0 order by alias`, [currentWorkspace(s)])).rows;
54
+ return apps.flatMap((a) => a.vector_providers.filter((p) => !vectorProviderProblems(p).length).map((p) => ({ key: `${a.id}:${p.name}`, appId: a.id, alias: a.alias, p })));
55
+ }
56
+
57
+ /** The statements: the column, its HNSW index (up to 2,000 dimensions) and the trigger that keeps it. */
58
+ export function vectorDdl(o: { table: string; vectorType: string; column: string; index: string; trigger: string; dims: number; distance: Distance; mode: 'set' | 'queue'; appId: number; provider: string; key: string; sources: string[] }) {
59
+ const args = [String(o.appId), o.provider, o.column, o.key, ...o.sources].map((x) => pg.escapeLiteral(x)).join(', ');
60
+ return [
61
+ `alter table ${o.table}\n add column ${ident(o.column)} ${o.vectorType}(${o.dims})`,
62
+ ...(o.dims <= HNSW_MAX_DIMENSIONS ? [`create index ${ident(o.index)} on ${o.table} using hnsw (${ident(o.column)} ${DISTANCES[o.distance].opclass})`] : []),
63
+ o.mode === 'set'
64
+ ? `create trigger ${ident(o.trigger)}\n before insert or update of ${o.sources.map(ident).join(', ')} on ${o.table}\n for each row execute function meta.embedding_set(${args})`
65
+ : `create trigger ${ident(o.trigger)}\n after insert or update of ${o.sources.map(ident).join(', ')} on ${o.table}\n for each row execute function meta.embedding_enqueue(${args})`,
66
+ ];
67
+ }
68
+
69
+ async function page(req: Req, reply: FastifyReply) {
70
+ const s = await developer(req, reply);
71
+ if (!s) return;
72
+ const tx = tr(s);
73
+ const db = await workshopDb(s);
74
+ const isPost = req.method === 'POST';
75
+ const b: Body = (isPost ? req.body : req.query) ?? {};
76
+ const ref = String(b.o ?? '');
77
+ const t = ref ? await readTable(db, ref) : null;
78
+ const schemas = (await usableSchemas(db)).filter((n) => !SYSTEM_SCHEMAS.includes(n));
79
+ if (!t || !schemas.includes(t.schema)) return reply.code(404).send('Not found');
80
+ const table = `${ident(t.schema)}.${ident(t.name)}`;
81
+ const self = `${BASE}/sql/objects/vector?${new URLSearchParams({ o: table })}`;
82
+ const oid = (await db.one<{ oid: number }>('select $1::regclass::oid as oid', [table]))!.oid;
83
+ const st = await pgvectorStatus(db);
84
+ const cols = await columnsOf(db, oid);
85
+ const texty = cols.filter((c) => c.texty);
86
+ const key = await keyOf(db, oid);
87
+ const choices = await providerChoices(s);
88
+ const triggers = st.installed ? await vectorTriggers(db, table) : [];
89
+
90
+ // Backfill: queue the rows of a vector column that have no embedding (or all of them)
91
+ if (isPost && b.mode === 'backfill') {
92
+ const trg = triggers.find((x) => x.vectorColumn === String(b.column ?? ''));
93
+ if (!trg || !(await appAllowed(s, trg.appId))) return reply.code(404).send('Not found');
94
+ const n = await backfill(db, trg, b.all === 'on');
95
+ kickEmbeddings();
96
+ flash(s, tx('vec.backfill_queued', { n: String(n), column: trg.vectorColumn }));
97
+ return back(reply, s, self);
98
+ }
99
+
100
+ const pick = choices.find((c) => c.key === String(b.provider ?? '')) ?? (isPost ? undefined : choices[0]);
101
+ const column = isPost ? String(b.column ?? '').trim() : cols.some((c) => c.name === 'embedding') ? '' : 'embedding';
102
+ const base = t.name.replace(/[^a-z0-9_]+/gi, '_').toLowerCase().slice(0, 40);
103
+ const index = isPost ? String(b.index ?? '').trim() : `${base}_${column || 'embedding'}_idx`;
104
+ const distance: Distance = isDistance(b.distance) ? b.distance : 'cosine';
105
+ const mode: 'set' | 'queue' = pick?.p.type === 'function' && (isPost ? b.keep === 'set' : true) ? 'set' : 'queue';
106
+ const picked = new Set<string>();
107
+ if (isPost) texty.forEach((c, i) => b[`c.${i}.use`] === 'on' && b[`c.${i}.name`] === c.name && picked.add(c.name));
108
+ else texty.forEach((c) => picked.add(c.name));
109
+
110
+ const errors: string[] = [];
111
+ let ddl: string[] = [];
112
+ let output: Raw | '' = '';
113
+ if (isPost && st.installed) {
114
+ if (!pick) errors.push(tx('vec.pick_provider'));
115
+ if (!picked.size) errors.push(tx('fts.pick'));
116
+ if (!key) errors.push(tx('vec.no_key'));
117
+ for (const n of [column, index]) if (!NAME.test(n)) errors.push(tx('fts.bad_name', { name: n }));
118
+ if (NAME.test(column) && cols.some((c) => c.name === column)) errors.push(tx('fts.exists', { name: column }));
119
+ 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 }));
120
+ const trigger = `${column}_embed`.slice(0, 63);
121
+ if (!errors.length && pick && key) {
122
+ ddl = vectorDdl({ table, vectorType: `${ident(st.schema!)}.vector`, column, index, trigger, dims: pick.p.dimensions, distance, mode, appId: pick.appId, provider: pick.p.name, key, sources: [...picked] });
123
+ }
124
+ if (ddl.length && b.mode === 'worksheet') {
125
+ s.state.__SQLDRAFT = `${ddl.join(';\n\n')};`;
126
+ flash(s, tx('te.review'));
127
+ return back(reply, s, `${BASE}/sql/worksheet`);
128
+ }
129
+ if (ddl.length && b.mode === 'apply') {
130
+ const c = await db.pool.connect();
131
+ let failed = '';
132
+ let at = '';
133
+ try {
134
+ await c.query('begin');
135
+ for (const x of ddl) {
136
+ at = x;
137
+ await c.query(x);
138
+ }
139
+ await c.query('commit');
140
+ } catch (e) {
141
+ failed = (e as Error).message;
142
+ await c.query('rollback').catch(() => {});
143
+ } finally {
144
+ c.release(true);
145
+ }
146
+ await logActivity({ username: s.username, event: 'sql_command', ip: clientIp(req), detail: ddl.join(';\n').slice(0, 2000) });
147
+ clearCompletions();
148
+ if (!failed) {
149
+ await recordChanges({ workspaceId: currentWorkspace(s), author: s.username ?? '?', source: 'table', defaultSchema: t.schema, statements: [joinStatements(ddl)] });
150
+ // the rows there are now: queued at once (the server embeds them; a function provider too)
151
+ const trg = (await vectorTriggers(db, table)).find((x) => x.vectorColumn === column);
152
+ const n = trg ? await backfill(db, trg, true) : 0;
153
+ kickEmbeddings();
154
+ flash(s, tx('vec.added', { table, column, n: String(n) }));
155
+ return back(reply, s, self);
156
+ }
157
+ 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>`;
158
+ }
159
+ }
160
+
161
+ // the table's vector columns now, with their progress
162
+ const existing: Raw[] = [];
163
+ for (const c of cols.filter((x) => x.vector)) {
164
+ const trg = triggers.find((x) => x.vectorColumn === c.name);
165
+ const counts = (await db.one<{ total: number; done: number }>(`select count(*)::int as total, count(${ident(c.name)})::int as done from ${table}`))!;
166
+ const q = await queueState(oid, c.name);
167
+ const pct = counts.total ? Math.round((counts.done / counts.total) * 100) : 100;
168
+ existing.push(html`<li class="vec-column"><p><code>${c.name}</code> <code>${c.type}</code>
169
+ ${trg ? html`· ${tx('vec.kept_by', { provider: trg.provider, columns: trg.sourceColumns.join(', ') })} (${trg.mode === 'set' ? tx('vec.mode_set') : tx('vec.mode_queue')})` : html`· <span class="muted">${tx('vec.not_kept')}</span>`}</p>
170
+ <p><progress max="100" value="${pct}" aria-label="${tx('vec.progress', { column: c.name })}">${pct}%</progress>
171
+ ${tx('vec.counts', { done: String(counts.done), total: String(counts.total), queued: String(q.queued) })}${q.failing ? html` · <span class="tag tag-error">${tx('vec.failing', { n: String(q.failing) })}</span>` : ''}</p>
172
+ ${q.message ? html`<p class="muted">${q.message}</p>` : ''}
173
+ ${trg ? html`<form method="post" action="${BASE}/sql/objects/vector" class="buttons">${csrf(s)}<input type="hidden" name="o" value="${table}"><input type="hidden" name="column" value="${c.name}">
174
+ <label class="check"><input type="checkbox" name="all"> ${tx('vec.reembed_all')}</label>
175
+ <button class="btn" name="mode" value="backfill">${icon('refresh')} ${tx('vec.backfill')}</button>
176
+ <a class="btn" href="${self}">${tx('vec.refresh')}</a></form>` : ''}</li>`);
177
+ }
178
+
179
+ const title = tx('vec.title', { table });
180
+ const sel = (a: string, v: string) => (a === v ? ' selected' : '');
181
+ const admin = await isAdmin(s.username);
182
+ const form = !st.installed
183
+ ? html`<div class="alert alert-info" role="status"><p>${tx('vec.missing')}</p>${st.available ? html`<p>${tx(admin && st.canCreate ? 'vec.missing_create' : 'vec.missing_admin', { version: st.availableVersion ?? '' })}</p>` : html`<p>${tx('vec.missing_package')}</p>`}
184
+ <p><a class="btn" href="${BASE}/vector-providers">${icon('search')} ${tx('vec.utilities')}</a></p></div>`
185
+ : !choices.length
186
+ ? html`<p class="alert alert-info">${tx('vec.no_providers')}</p>`
187
+ : !texty.length
188
+ ? html`<p class="alert alert-info">${tx('fts.no_text')}</p>`
189
+ : html`<form method="post" action="${BASE}/sql/objects/vector" class="fts-form">${csrf(s)}
190
+ <input type="hidden" name="o" value="${table}">
191
+ <div class="form-grid">
192
+ <div class="field"><label class="label" for="vec-provider">${tx('vec.provider')}</label><select id="vec-provider" name="provider">${choices.map((c) => html`<option value="${c.key}"${sel(c.key, pick?.key ?? '')}>${c.alias} · ${c.p.name} (${c.p.type === 'ai_service' ? c.p.model : c.p.function}, ${c.p.dimensions})</option>`)}</select>
193
+ <small class="help">${tx('vec.provider_help')}</small></div>
194
+ <div class="field"><label class="label" for="vec-keep">${tx('vec.keep')}</label><select id="vec-keep" name="keep"><option value="set"${sel(mode, 'set')}>${tx('vec.mode_set')}</option><option value="queue"${sel(mode, 'queue')}>${tx('vec.mode_queue')}</option></select>
195
+ <small class="help">${tx('vec.keep_help')}</small></div>
196
+ </div>
197
+ <fieldset class="prop-group"><legend>${tx('vec.sources')}</legend>
198
+ <div class="table-wrap"><table class="report"><thead><tr><th>${tx('fts.include')}</th><th>${tx('te.name')}</th><th>${tx('te.type')}</th></tr></thead><tbody>
199
+ ${texty.map((c, i) => html`<tr><td class="te-c"><input type="hidden" name="c.${i}.name" value="${c.name}"><input type="checkbox" id="vec-c${i}" name="c.${i}.use"${picked.has(c.name) ? ' checked' : ''}></td>
200
+ <td><label for="vec-c${i}">${c.name}</label></td><td><code>${c.type}</code></td></tr>`)}
201
+ </tbody></table></div>
202
+ <small class="help">${tx('vec.sources_help')}</small></fieldset>
203
+ <div class="form-grid u-mt1">
204
+ <div class="field"><label class="label" for="vec-distance">${tx('vec.distance')}</label><select id="vec-distance" name="distance">${(Object.keys(DISTANCES) as Distance[]).map((d) => html`<option${sel(d, distance)}>${d}</option>`)}</select><small class="help">${tx('vec.distance_help')}</small></div>
205
+ <div class="field"><label class="label" for="vec-column">${tx('vec.column')}</label><input id="vec-column" name="column" value="${column}" required autocomplete="off" spellcheck="false" class="code"></div>
206
+ <div class="field"><label class="label" for="vec-index">${tx('fts.index')}</label><input id="vec-index" name="index" value="${index}" required autocomplete="off" spellcheck="false" class="code"></div>
207
+ </div>
208
+ ${key ? '' : html`<p class="alert alert-error" role="alert">${tx('vec.no_key')}</p>`}
209
+ ${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>` : ''}
210
+ <div class="buttons te-buttons">
211
+ <button class="btn" name="mode" value="preview">${icon('eye')} ${tx('te.preview')}</button>
212
+ <button class="btn btn-hot" name="mode" value="apply">${icon('check')} ${tx('te.apply')}</button>
213
+ <button class="btn" name="mode" value="worksheet">${icon('code')} ${tx('te.open_ws')}</button>
214
+ <a class="btn" href="${BASE}/sql/objects/table?${new URLSearchParams({ o: table })}">${tx('common.cancel')}</a>
215
+ </div></form>`;
216
+ const main = html`<h1 class="u-mb1">${title}</h1>${workshopTabs('objects', s)}
217
+ <p class="muted">${tx('vec.intro')}</p>
218
+ ${existing.length ? html`<h2>${tx('vec.existing')}</h2><ul class="vec-columns">${existing}</ul>` : ''}
219
+ ${errors.length ? html`<div class="alert alert-error" role="alert"><ul>${errors.map((e) => html`<li>${e}</li>`)}</ul></div>` : ''}
220
+ ${output}
221
+ <h2>${tx('vec.add')}</h2>
222
+ ${form}`;
223
+ 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('vec.add')]], main, 'sql'));
224
+ }
225
+
226
+ export async function vectorColumnRoutes(app: FastifyInstance) {
227
+ app.get(`${BASE}/sql/objects/vector`, page);
228
+ app.post(`${BASE}/sql/objects/vector`, page);
229
+ }