pgkiln 0.67.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 +5 -5
- package/docs/guide/03-builder.md +12 -2
- package/docs/guide/04-pages-and-regions.md +46 -1
- package/docs/guide/12-development.md +17 -4
- package/examples/hr/README.md +1 -0
- package/examples/hr/hr_71_fulltext_search.sql +43 -0
- package/package.json +1 -1
- package/public/builder.css +15 -0
- 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 +22 -1
- package/src/builder/i18n/es.ts +20 -0
- package/src/builder/i18n/fr.ts +20 -0
- package/src/builder/i18n/it.ts +1914 -0
- package/src/builder/i18n/ja.ts +1914 -0
- package/src/builder/i18n/ko.ts +1914 -0
- package/src/builder/i18n/nl.ts +20 -0
- package/src/builder/i18n/pt.ts +20 -0
- package/src/builder/i18n/zh-cn.ts +1914 -0
- package/src/builder/i18n/zh-tw.ts +1914 -0
- package/src/builder/i18n.ts +13 -3
- 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,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
|
+
}
|
package/src/builder/i18n/de.ts
CHANGED
|
@@ -1045,6 +1045,26 @@ export const de: BuilderMessages = {
|
|
|
1045
1045
|
'te.failed_stmt': 'Fehlgeschlagene Anweisung',
|
|
1046
1046
|
'te.title_table': 'Tabelle {table}',
|
|
1047
1047
|
'te.title_create': 'Tabelle erstellen',
|
|
1048
|
+
'fts.add': "Volltextsuche hinzufügen",
|
|
1049
|
+
'fts.title': "Volltextsuche für {table}",
|
|
1050
|
+
'fts.intro': "Fügt eine gespeicherte generierte tsvector-Spalte über die gewählten Textspalten hinzu, mit einem GIN-Index darauf. Eine Suchkonfiguration vom Suchtyp fulltext (Gemeinsame Komponenten › Suchkonfigurationen) oder die Suche eines Berichts findet dann Wortformen (\"running\" findet \"run\") schnell. Die Spalte aktualisiert sich selbst; das Hinzufügen schreibt die Tabelle neu.",
|
|
1051
|
+
'fts.columns': "Textspalten",
|
|
1052
|
+
'fts.include': "Einbeziehen",
|
|
1053
|
+
'fts.weight': "Gewichtung",
|
|
1054
|
+
'fts.weight_help': "A zählt in der Rangfolge am meisten, D am wenigsten: z. B. A für einen Titel, B für eine Beschreibung.",
|
|
1055
|
+
'fts.config': "Textsuchkonfiguration",
|
|
1056
|
+
'fts.config_help': "Die Sprache, deren Wortstämme verwendet werden (german: Läufer → läuf); simple: keine Stammformen. Mit derselben Konfiguration suchen.",
|
|
1057
|
+
'fts.column': "tsvector-Spalte",
|
|
1058
|
+
'fts.index': "Indexname",
|
|
1059
|
+
'fts.no_text': "Diese Tabelle hat keine Textspalten zum Durchsuchen.",
|
|
1060
|
+
'fts.pick': "Wählen Sie mindestens eine Textspalte.",
|
|
1061
|
+
'fts.bad_name': "\"{name}\" ist kein gültiger Name: Kleinbuchstaben, Ziffern und _.",
|
|
1062
|
+
'fts.exists': "{name} existiert bereits.",
|
|
1063
|
+
'fts.bad_config': "{name} ist keine Textsuchkonfiguration.",
|
|
1064
|
+
'fts.existing': "Vorhandene tsvector-Spalten:",
|
|
1065
|
+
'fts.gin_yes': "GIN-Index",
|
|
1066
|
+
'fts.gin_no': "kein GIN-Index",
|
|
1067
|
+
'fts.added': "Volltextsuche zu {table} hinzugefügt: Spalte {column} mit Index {index}.",
|
|
1048
1068
|
// ------------------------------------------------------------ Shared Components (sprint 124)
|
|
1049
1069
|
'comp.nav_entry': 'Navigationseintrag',
|
|
1050
1070
|
'comp.nav_entry.plural': 'Navigationsmenü',
|
package/src/builder/i18n/en.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// The builder's own texts in English (sprint 121, WS-7): the keys of every builder message file (i18n.ts).
|
|
2
2
|
// A key is <screen>.<what>; {name} is a placeholder (filled by tx(key, { name })). Keep the other languages
|
|
3
|
-
// (de, es, fr, nl, pt) in step: TypeScript and test/builder-i18n.test.ts refuse a missing or an extra key.
|
|
3
|
+
// (de, es, fr, it, nl, pt, zh-cn, zh-tw, ja, ko) in step: TypeScript and test/builder-i18n.test.ts refuse a missing or an extra key.
|
|
4
4
|
// Keys made at run time (gallery.*, pos.*, slot.*, comp.*, pd.new.* …, sql.tool.*, sql.group.*) are listed here
|
|
5
5
|
// too; the test checks that every region type, item type, button action, position, slot and SQL Workshop tool has one.
|
|
6
6
|
|
|
@@ -1048,6 +1048,27 @@ export const en = {
|
|
|
1048
1048
|
'te.failed_stmt': 'Failed statement',
|
|
1049
1049
|
'te.title_table': 'Table {table}',
|
|
1050
1050
|
'te.title_create': 'Create table',
|
|
1051
|
+
// (sprint 135, AP-2) Table Properties › Add full-text search
|
|
1052
|
+
'fts.add': "Add full-text search",
|
|
1053
|
+
'fts.title': "Full-text search on {table}",
|
|
1054
|
+
'fts.intro': "Adds a stored generated tsvector column over the text columns you choose, and a GIN index on it. A search configuration of search type fulltext (Shared Components › Search configurations) or a report search then finds stemmed words (\"running\" finds \"run\") fast. The column keeps itself up to date; adding it rewrites the table.",
|
|
1055
|
+
'fts.columns': "Text columns",
|
|
1056
|
+
'fts.include': "Include",
|
|
1057
|
+
'fts.weight': "Weight",
|
|
1058
|
+
'fts.weight_help': "A counts most in the ranking, D least: e.g. A for a title, B for a description.",
|
|
1059
|
+
'fts.config': "Text search configuration",
|
|
1060
|
+
'fts.config_help': "The language whose stemming is used (english: running → run); simple: no stemming. Search with the same configuration.",
|
|
1061
|
+
'fts.column': "tsvector column",
|
|
1062
|
+
'fts.index': "Index name",
|
|
1063
|
+
'fts.no_text': "This table has no text columns to search.",
|
|
1064
|
+
'fts.pick': "Choose at least one text column.",
|
|
1065
|
+
'fts.bad_name': "\"{name}\" is not a valid name: lowercase letters, digits and _.",
|
|
1066
|
+
'fts.exists': "{name} already exists.",
|
|
1067
|
+
'fts.bad_config': "{name} is not a text search configuration.",
|
|
1068
|
+
'fts.existing': "tsvector columns now:",
|
|
1069
|
+
'fts.gin_yes': "GIN index",
|
|
1070
|
+
'fts.gin_no': "no GIN index",
|
|
1071
|
+
'fts.added': "Added full-text search to {table}: column {column} with index {index}.",
|
|
1051
1072
|
// ------------------------------------------------------------ Shared Components (sprint 124)
|
|
1052
1073
|
'comp.nav_entry': 'Navigation entry',
|
|
1053
1074
|
'comp.nav_entry.plural': 'Navigation menu',
|
package/src/builder/i18n/es.ts
CHANGED
|
@@ -1045,6 +1045,26 @@ export const es: BuilderMessages = {
|
|
|
1045
1045
|
'te.failed_stmt': 'Sentencia con error',
|
|
1046
1046
|
'te.title_table': 'Tabla {table}',
|
|
1047
1047
|
'te.title_create': 'Crear tabla',
|
|
1048
|
+
'fts.add': "Añadir búsqueda de texto completo",
|
|
1049
|
+
'fts.title': "Búsqueda de texto completo en {table}",
|
|
1050
|
+
'fts.intro': "Añade una columna tsvector generada y almacenada sobre las columnas de texto que elija, con un índice GIN. Una configuración de búsqueda de tipo fulltext (Componentes compartidos › Configuraciones de búsqueda) o la búsqueda de un informe encuentra entonces rápidamente las formas de las palabras (\"running\" encuentra \"run\"). La columna se mantiene al día; añadirla reescribe la tabla.",
|
|
1051
|
+
'fts.columns': "Columnas de texto",
|
|
1052
|
+
'fts.include': "Incluir",
|
|
1053
|
+
'fts.weight': "Peso",
|
|
1054
|
+
'fts.weight_help': "A cuenta más en la clasificación, D menos: p. ej. A para un título, B para una descripción.",
|
|
1055
|
+
'fts.config': "Configuración de búsqueda de texto",
|
|
1056
|
+
'fts.config_help': "El idioma cuya lematización se usa (spanish: corriendo → corr); simple: sin lematización. Busque con la misma configuración.",
|
|
1057
|
+
'fts.column': "Columna tsvector",
|
|
1058
|
+
'fts.index': "Nombre del índice",
|
|
1059
|
+
'fts.no_text': "Esta tabla no tiene columnas de texto en las que buscar.",
|
|
1060
|
+
'fts.pick': "Elija al menos una columna de texto.",
|
|
1061
|
+
'fts.bad_name': "\"{name}\" no es un nombre válido: minúsculas, dígitos y _.",
|
|
1062
|
+
'fts.exists': "{name} ya existe.",
|
|
1063
|
+
'fts.bad_config': "{name} no es una configuración de búsqueda de texto.",
|
|
1064
|
+
'fts.existing': "Columnas tsvector actuales:",
|
|
1065
|
+
'fts.gin_yes': "índice GIN",
|
|
1066
|
+
'fts.gin_no': "sin índice GIN",
|
|
1067
|
+
'fts.added': "Búsqueda de texto completo añadida a {table}: columna {column} con el índice {index}.",
|
|
1048
1068
|
// ------------------------------------------------------------ Shared Components (sprint 124)
|
|
1049
1069
|
'comp.nav_entry': 'Entrada de navegación',
|
|
1050
1070
|
'comp.nav_entry.plural': 'Menú de navegación',
|
package/src/builder/i18n/fr.ts
CHANGED
|
@@ -1045,6 +1045,26 @@ export const fr: BuilderMessages = {
|
|
|
1045
1045
|
'te.failed_stmt': 'Instruction en échec',
|
|
1046
1046
|
'te.title_table': 'Table {table}',
|
|
1047
1047
|
'te.title_create': 'Créer une table',
|
|
1048
|
+
'fts.add': "Ajouter la recherche plein texte",
|
|
1049
|
+
'fts.title': "Recherche plein texte sur {table}",
|
|
1050
|
+
'fts.intro': "Ajoute une colonne tsvector générée et stockée sur les colonnes de texte choisies, avec un index GIN. Une configuration de recherche de type fulltext (Composants partagés › Configurations de recherche) ou la recherche d'un rapport trouve alors rapidement les formes des mots (\"running\" trouve \"run\"). La colonne se tient à jour ; l'ajouter réécrit la table.",
|
|
1051
|
+
'fts.columns': "Colonnes de texte",
|
|
1052
|
+
'fts.include': "Inclure",
|
|
1053
|
+
'fts.weight': "Poids",
|
|
1054
|
+
'fts.weight_help': "A compte le plus dans le classement, D le moins : par ex. A pour un titre, B pour une description.",
|
|
1055
|
+
'fts.config': "Configuration de recherche de texte",
|
|
1056
|
+
'fts.config_help': "La langue dont la racinisation est utilisée (french : courir → cour) ; simple : sans racinisation. Recherchez avec la même configuration.",
|
|
1057
|
+
'fts.column': "Colonne tsvector",
|
|
1058
|
+
'fts.index': "Nom de l'index",
|
|
1059
|
+
'fts.no_text': "Cette table n'a pas de colonnes de texte à rechercher.",
|
|
1060
|
+
'fts.pick': "Choisissez au moins une colonne de texte.",
|
|
1061
|
+
'fts.bad_name': "« {name} » n'est pas un nom valide : minuscules, chiffres et _.",
|
|
1062
|
+
'fts.exists': "{name} existe déjà.",
|
|
1063
|
+
'fts.bad_config': "{name} n'est pas une configuration de recherche de texte.",
|
|
1064
|
+
'fts.existing': "Colonnes tsvector actuelles :",
|
|
1065
|
+
'fts.gin_yes': "index GIN",
|
|
1066
|
+
'fts.gin_no': "pas d'index GIN",
|
|
1067
|
+
'fts.added': "Recherche plein texte ajoutée à {table} : colonne {column} avec l'index {index}.",
|
|
1048
1068
|
// ------------------------------------------------------------ Shared Components (sprint 124)
|
|
1049
1069
|
'comp.nav_entry': 'Entrée de navigation',
|
|
1050
1070
|
'comp.nav_entry.plural': 'Menu de navigation',
|