Skip to main content

inillucent_cli/command/
registry.rs

1//! Every command, as data.
2//!
3//! Invariant: **this array is the only list.** `inillucent --help`,
4//! `inillucent <verb>`'s argument parsing, `inillucent-mcp`'s `tools/list` and
5//! every JSON Schema it publishes are derived from it, and
6//! `command_parity.rs` fails the build if any of them stops agreeing.
7//!
8//! The descriptions are written for two readers at once. A person runs
9//! `inillucent help query`; a 27B model reads the same sentence as a tool
10//! description and has one attempt at getting the call right. That is why they
11//! say what a parameter *is for* rather than restating its name, and why the
12//! ones with a trap in them - `limit` cutting the rows handed back but not the
13//! count, `params` being positional - say so.
14
15use super::verbs;
16use super::{Command, Kind, Param, Writes, DB, FORMAT, LIMIT};
17
18/// The parameters `query` takes.
19const QUERY_PARAMS: &[Param] = &[
20    Param {
21        name: "sql",
22        kind: Kind::Text,
23        required: true,
24        positional: true,
25        description: "The SQL to run. One statement. Use ?1, ?2 ... for values you pass in \
26                      'params' rather than pasting them into the text.",
27    },
28    Param {
29        name: "params",
30        kind: Kind::Values,
31        required: false,
32        positional: false,
33        description: "The values for ?1, ?2 ... in order, as a JSON array of strings, numbers, \
34                      booleans or nulls. An array of numbers is a vector and \
35                      {\"blob\":\"<hex>\"} is bytes. Binding is how you avoid quoting mistakes \
36                      and SQL injection.",
37    },
38    Param {
39        name: "params-file",
40        kind: Kind::Text,
41        required: false,
42        positional: false,
43        description: "A file holding the JSON array for 'params', or - for standard input. What \
44                      a wrapper that spawns this binary uses: a command line has a length \
45                      ceiling, about 32 KB on Windows, and a parameter past it fails outright.",
46    },
47    LIMIT,
48    DB,
49    FORMAT,
50];
51
52/// The parameters `exec` takes.
53const EXEC_PARAMS: &[Param] = &[
54    Param {
55        name: "sql",
56        kind: Kind::Text,
57        required: true,
58        positional: true,
59        description: "One statement to run for its effect: INSERT, UPDATE, DELETE, CREATE, DROP, \
60                      ALTER or a PRAGMA that sets something.",
61    },
62    Param {
63        name: "params",
64        kind: Kind::Values,
65        required: false,
66        positional: false,
67        description: "The values for ?1, ?2 ... in order, as a JSON array. An array of numbers \
68                      is a vector and {\"blob\":\"<hex>\"} is bytes.",
69    },
70    Param {
71        name: "params-file",
72        kind: Kind::Text,
73        required: false,
74        positional: false,
75        description: "A file holding the JSON array for 'params', or - for standard input. What \
76                      a wrapper that spawns this binary uses: a command line has a length \
77                      ceiling, about 32 KB on Windows, and a parameter past it fails outright.",
78    },
79    DB,
80    FORMAT,
81];
82
83/// The parameters `batch` takes.
84const BATCH_PARAMS: &[Param] = &[
85    Param {
86        name: "sql",
87        kind: Kind::Text,
88        required: true,
89        positional: true,
90        description: "Several statements separated by semicolons. They run as one transaction: \
91                      either all of them take effect or none of them do.",
92    },
93    DB,
94    FORMAT,
95];
96
97/// The parameters `run` takes.
98const RUN_PARAMS: &[Param] = &[
99    Param {
100        name: "input",
101        kind: Kind::Text,
102        required: true,
103        positional: true,
104        description: "Shell input, exactly as you would type it: SQL statements ending in a \
105                      semicolon, and dot commands such as '.schema' or '.mode box' on their own \
106                      lines. Everything the interactive shell can do is reachable here.",
107    },
108    DB,
109];
110
111/// The parameters `create` takes.
112const CREATE_PARAMS: &[Param] = &[
113    Param {
114        name: "path",
115        kind: Kind::Text,
116        required: true,
117        positional: true,
118        description: "Where to make the new database file. It refuses a path that already \
119                      exists rather than overwriting it.",
120    },
121    FORMAT,
122];
123
124/// The parameters a pattern-filtered listing takes.
125const PATTERN_PARAMS: &[Param] = &[
126    Param {
127        name: "pattern",
128        kind: Kind::Text,
129        required: false,
130        positional: true,
131        description: "A LIKE pattern to filter the names by, such as 'user%'. Omit it for all \
132                      of them.",
133    },
134    DB,
135    FORMAT,
136];
137
138/// The parameters `schema` takes.
139const SCHEMA_PARAMS: &[Param] = &[
140    Param {
141        name: "pattern",
142        kind: Kind::Text,
143        required: false,
144        positional: true,
145        description: "A LIKE pattern naming which objects to show. Omit it for the whole schema.",
146    },
147    Param {
148        name: "indent",
149        kind: Kind::Boolean,
150        required: false,
151        positional: false,
152        description: "Pretty-print each CREATE statement over several lines instead of one.",
153    },
154    DB,
155    FORMAT,
156];
157
158/// The parameters `describe` takes.
159const DESCRIBE_PARAMS: &[Param] = &[
160    Param {
161        name: "table",
162        kind: Kind::Text,
163        required: true,
164        positional: true,
165        description: "The table or view to describe, by its exact name. Use 'tables' first if \
166                      you are not sure what it is called.",
167    },
168    DB,
169    FORMAT,
170];
171
172/// The parameters the database-only commands take.
173const DB_ONLY: &[Param] = &[DB, FORMAT];
174
175/// The parameters `explain` takes.
176const EXPLAIN_PARAMS: &[Param] = &[
177    Param {
178        name: "sql",
179        kind: Kind::Text,
180        required: true,
181        positional: true,
182        description: "The statement whose plan you want. It is not run - this only shows which \
183                      indexes and scans the planner chose.",
184    },
185    DB,
186    FORMAT,
187];
188
189/// The parameters `import` takes.
190const IMPORT_PARAMS: &[Param] = &[
191    Param {
192        name: "file",
193        kind: Kind::Text,
194        required: true,
195        positional: true,
196        description: "The delimited file to read.",
197    },
198    Param {
199        name: "table",
200        kind: Kind::Text,
201        required: true,
202        positional: false,
203        description: "The table to load into. If it does not exist it is created, taking its \
204                      column names from the file's first row.",
205    },
206    Param {
207        name: "format",
208        kind: Kind::Text,
209        required: false,
210        positional: false,
211        description: "'csv' (the default, RFC 4180 quoting), 'tabs', or 'ascii' for \\037 and \
212                      \\036 separated input.",
213    },
214    Param {
215        name: "skip",
216        kind: Kind::Integer,
217        required: false,
218        positional: false,
219        description: "How many leading rows to ignore, for a file with a preamble above its \
220                      header.",
221    },
222    DB,
223];
224
225/// The parameters `embed` takes.
226const EMBED_PARAMS: &[Param] = &[
227    Param {
228        name: "table",
229        kind: Kind::Text,
230        required: true,
231        positional: false,
232        description:
233            "The table to fill. An ordinary table with a VECTOR(768) or BLOB column, or an \
234                      inillucent_search table, whose vector column can be updated by rowid.",
235    },
236    Param {
237        name: "text",
238        kind: Kind::Text,
239        required: true,
240        positional: false,
241        description: "The column holding the text to embed. A row whose text is NULL or empty is \
242                      skipped and counted.",
243    },
244    Param {
245        name: "vector",
246        kind: Kind::Text,
247        required: true,
248        positional: false,
249        description:
250            "The column to write the vectors into. Only rows where it IS NULL are embedded, \
251                      so a stopped run continues where it ended.",
252    },
253    Param {
254        name: "prefix",
255        kind: Kind::Text,
256        required: false,
257        positional: false,
258        description: "Text put in front of every value before it is embedded, exactly as \
259                      embed('search_document: ' || body) does. For nomic-embed-text-v1.5 use \
260                      'search_document: '. The default is no prefix.",
261    },
262    Param {
263        name: "device",
264        kind: Kind::Text,
265        required: false,
266        positional: false,
267        description:
268            "The processor: 'cpu', 'cuda' or 'cuda:N' for card N. Without it the machine's \
269                      setting applies (INILLUCENT_EMBED_DEVICE, then what setup-embeddings \
270                      recorded, then the processor). A cuda device that will not start is an error \
271                      naming 'inillucent setup-embeddings runtime --gpu', and never runs on the \
272                      processor.",
273    },
274    Param {
275        name: "threads",
276        kind: Kind::Integer,
277        required: false,
278        positional: false,
279        description: "Threads ONNX Runtime uses inside one operator, per session. Without it the \
280                      machine's setting applies.",
281    },
282    Param {
283        name: "sessions",
284        kind: Kind::Integer,
285        required: false,
286        positional: false,
287        description: "How many model sessions to open on the device and run at once. The default \
288                      is 1. Two sessions on one card were measured 2.1 times faster than one.",
289    },
290    Param {
291        name: "batch-size",
292        kind: Kind::Integer,
293        required: false,
294        positional: false,
295        description: "Most texts in one call to the model. The default is 16. A batch is also \
296                      limited by a memory ceiling that shrinks it when the texts are long.",
297    },
298    Param {
299        name: "commit-every",
300        kind: Kind::Integer,
301        required: false,
302        positional: false,
303        description: "How many rows to embed and write in one transaction. The default is 1024. A \
304                      crash loses at most one transaction.",
305    },
306    Param {
307        name: "all",
308        kind: Kind::Boolean,
309        required: false,
310        positional: false,
311        description: "Embed every row again, including rows that already have a vector.",
312    },
313    DB,
314    FORMAT,
315];
316
317/// The parameters `export` takes.
318const EXPORT_PARAMS: &[Param] = &[
319    Param {
320        name: "table",
321        kind: Kind::Text,
322        required: false,
323        positional: true,
324        description: "The table to write out whole. Give this or 'sql', not both.",
325    },
326    Param {
327        name: "sql",
328        kind: Kind::Text,
329        required: false,
330        positional: false,
331        description: "A query whose rows to write out, when you want less than a whole table.",
332    },
333    Param {
334        name: "format",
335        kind: Kind::Text,
336        required: false,
337        positional: false,
338        description: "csv, json, tabs, markdown, insert, quote, line or html. Defaults to csv.",
339    },
340    Param {
341        name: "out",
342        kind: Kind::Text,
343        required: false,
344        positional: false,
345        description: "A file to write to. Omitted, the rows come back in the result instead.",
346    },
347    DB,
348];
349
350/// The parameters `dump` takes.
351const DUMP_PARAMS: &[Param] = &[
352    Param {
353        name: "objects",
354        kind: Kind::Text,
355        required: false,
356        positional: true,
357        description: "A LIKE pattern for the tables, indexes, triggers or views to dump. Omit \
358                      it for the whole database.",
359    },
360    Param {
361        name: "data_only",
362        kind: Kind::Boolean,
363        required: false,
364        positional: false,
365        description: "Write only the INSERT statements, leaving out the CREATE statements.",
366    },
367    DB,
368];
369
370/// The parameters `backup` takes.
371const BACKUP_PARAMS: &[Param] = &[
372    Param {
373        name: "file",
374        kind: Kind::Text,
375        required: true,
376        positional: true,
377        description: "Where to write the copy.",
378    },
379    DB,
380];
381
382/// The parameters `encrypt` and `decrypt` take.
383const COPY_PARAMS: &[Param] = &[
384    Param {
385        name: "file",
386        kind: Kind::Text,
387        required: true,
388        positional: true,
389        description: "Where to write the copy. A file already there is refused, not replaced.",
390    },
391    DB,
392];
393
394/// The parameters `rekey` takes.
395const REKEY_PARAMS: &[Param] = &[
396    Param {
397        name: "new-key-file",
398        kind: Kind::Text,
399        required: false,
400        positional: false,
401        description: "A file holding the new key. Without it the new key is read from the                       INILLUCENT_NEW_KEY environment variable. A key is never taken as a word                       on the command line, where every user of the machine can read it.",
402    },
403    DB,
404];
405
406/// The parameters `restore` takes.
407const RESTORE_PARAMS: &[Param] = &[
408    Param {
409        name: "file",
410        kind: Kind::Text,
411        required: true,
412        positional: true,
413        description: "The copy to read back. It replaces what is in the open database.",
414    },
415    DB,
416];
417
418/// The parameters `analyze` takes.
419const ANALYZE_PARAMS: &[Param] = &[
420    Param {
421        name: "table",
422        kind: Kind::Text,
423        required: false,
424        positional: true,
425        description: "One table to gather statistics for. Omit it for every table.",
426    },
427    DB,
428    FORMAT,
429];
430
431/// The parameters `migrate` takes.
432const MIGRATE_PARAMS: &[Param] = &[
433    Param {
434        name: "source",
435        kind: Kind::Text,
436        required: false,
437        positional: true,
438        description: "The SQLite database file to read, or a postgres:// or mysql:// connection \
439                      URL. The source is never written to. A connection URL holds a password, \
440                      and an argument is visible in the process list for the whole run - so it \
441                      may be left out and given in INILLUCENT_SOURCE_URL instead, or written as \
442                      '-' to read one line from standard input.",
443    },
444    Param {
445        name: "destination",
446        kind: Kind::Text,
447        required: true,
448        positional: false,
449        description: "The .rdb file to build. It refuses to overwrite an existing file.",
450    },
451    Param {
452        name: "kind",
453        kind: Kind::Text,
454        required: false,
455        positional: false,
456        description: "'sqlite' (the default for a path), 'postgres' or 'mysql' (the default for \
457                      a URL written with that scheme), or 'index' for a legacy retrieval index.",
458    },
459    Param {
460        name: "batch",
461        kind: Kind::Integer,
462        required: false,
463        positional: false,
464        description: "Rows per destination transaction while copying from a server. Default \
465                      10000. It changes how long the migration takes and nothing about what it \
466                      produces.",
467    },
468    Param {
469        name: "insecure-plaintext",
470        kind: Kind::Boolean,
471        required: false,
472        positional: false,
473        description: "Permit an unencrypted connection to a server. A migration to a host that \
474                      is not a loopback address uses verified TLS, and refuses rather than \
475                      falling back; this permits plaintext, and only together with \
476                      sslmode=disable in the URL. Both are needed because either one alone is \
477                      something people type without meaning it. The choice is recorded in the \
478                      migration report.",
479    },
480];
481
482/// The parameters `search` takes.
483const SEARCH_PARAMS: &[Param] = &[
484    Param {
485        name: "query",
486        kind: Kind::Text,
487        required: true,
488        positional: true,
489        description: "What to search for, in FTS5 query syntax: bare words are ANDed, \
490                      \"a phrase\" is quoted, and OR and NOT are available.",
491    },
492    Param {
493        name: "table",
494        kind: Kind::Text,
495        required: true,
496        positional: false,
497        description: "The full-text table to search - one created with USING fts5(...) or \
498                      USING inillucent_search(...).",
499    },
500    Param {
501        name: "k",
502        kind: Kind::Integer,
503        required: false,
504        positional: false,
505        description: "How many results to return, best first. Defaults to 10.",
506    },
507    Param {
508        name: "rerank",
509        kind: Kind::Boolean,
510        required: false,
511        positional: false,
512        description: "Reorder the rows the search found with the reranker, which reads the query \
513                      and each row together. The query text is passed as the question, so write \
514                      it as plain words. Needs an inillucent_search table and the reranker, \
515                      installed with 'inillucent setup-embeddings reranker'. On the processor \
516                      60 rows take about 10 seconds; on a graphics card about a tenth of one.",
517    },
518    DB,
519    FORMAT,
520];
521
522/// The parameters `vector-search` takes.
523const VECTOR_PARAMS: &[Param] = &[
524    Param {
525        name: "table",
526        kind: Kind::Text,
527        required: true,
528        positional: true,
529        description: "The table holding the vectors.",
530    },
531    Param {
532        name: "column",
533        kind: Kind::Text,
534        required: true,
535        positional: false,
536        description: "The VECTOR(N) column to measure against.",
537    },
538    Param {
539        name: "vector",
540        kind: Kind::Values,
541        required: true,
542        positional: false,
543        description: "The query vector, as a JSON array of numbers with exactly N elements.",
544    },
545    Param {
546        name: "k",
547        kind: Kind::Integer,
548        required: false,
549        positional: false,
550        description: "How many nearest rows to return. Defaults to 10.",
551    },
552    Param {
553        name: "measure",
554        kind: Kind::Text,
555        required: false,
556        positional: false,
557        description: "'cos' for cosine distance (the default), 'l2' for Euclidean, or 'dot' \
558                      for the inner product.",
559    },
560    DB,
561    FORMAT,
562];
563
564/// The parameters `capabilities` takes.
565const CAPABILITY_PARAMS: &[Param] = &[
566    Param {
567        name: "name",
568        kind: Kind::Text,
569        required: false,
570        positional: true,
571        description: "One capability to ask about, such as 'triggers'. Omit it for the whole \
572                      table. A name that is not in the table answers no, not yes.",
573    },
574    FORMAT,
575];
576
577/// The parameters `help` takes.
578const HELP_PARAMS: &[Param] = &[
579    Param {
580        name: "topic",
581        kind: Kind::Text,
582        required: false,
583        positional: true,
584        description: "A command to explain in full. Omit it for the list of every command.",
585    },
586    FORMAT,
587];
588
589/// The parameters `setup-embeddings` takes.
590const SETUP_PARAMS: &[Param] = &[
591    Param {
592        name: "component",
593        kind: Kind::Text,
594        required: false,
595        positional: true,
596        description: "What to install: 'all' for both halves, 'runtime' for the ONNX Runtime \
597                      shared library on its own, 'model' for the embedding weights on their own, or \
598                      'reranker' for the cross encoder that rerank() and a search naming question \
599                      use (about 600 MB, and not part of 'all'). Omit it to report what is \
600                      installed and download nothing.",
601    },
602    Param {
603        name: "status",
604        kind: Kind::Boolean,
605        required: false,
606        positional: false,
607        description: "Say what is installed, where, and which residency profile is in force, and \
608                      download nothing.",
609    },
610    Param {
611        name: "residency",
612        kind: Kind::Text,
613        required: false,
614        positional: false,
615        description: "When the model is in memory: 'resident' keeps it for the life of the process, \
616                      'on-demand' loads it per call and drops it, 'idle' or 'idle:90s' loads it on \
617                      use and drops it after a quiet period. Recorded for this machine; \
618                      INILLUCENT_EMBED_RESIDENCY overrides it for one process.",
619    },
620    Param {
621        name: "threads",
622        kind: Kind::Integer,
623        required: false,
624        positional: false,
625        description: "How many threads ONNX Runtime uses inside one operator, for embed(), \
626                      rerank() and a reranked search. Recorded for this machine; \
627                      INILLUCENT_EMBED_THREADS overrides it for one process. Without it ONNX \
628                      Runtime picks its own count. Four was the fastest single process setting \
629                      measured for the embedding model.",
630    },
631    Param {
632        name: "device",
633        kind: Kind::Text,
634        required: false,
635        positional: false,
636        description: "The processor the sessions run on: 'cpu', 'cuda' or 'cuda:N' for card N. \
637                      Recorded for this machine; INILLUCENT_EMBED_DEVICE overrides it for one \
638                      process. A cuda device that will not start is an error and never runs on \
639                      the processor. It needs the runtime installed with '--gpu'.",
640    },
641    Param {
642        name: "gpu",
643        kind: Kind::Boolean,
644        required: false,
645        positional: false,
646        description: "Install the ONNX Runtime build carrying the CUDA execution provider, which \
647                      exists for Windows and Linux on x86-64 only. It is a much larger download and \
648                      it needs a CUDA install of its own to be usable.",
649    },
650    Param {
651        name: "force",
652        kind: Kind::Boolean,
653        required: false,
654        positional: false,
655        description: "Fetch and install again even when the files are already there and their \
656                      digests match.",
657    },
658    Param {
659        name: "onnxruntime-version",
660        kind: Kind::Text,
661        required: false,
662        positional: false,
663        description: "The ONNX Runtime version to install. Defaults to the one this build pins a \
664                      digest for; any other version is fetched and reported as unverified.",
665    },
666    Param {
667        name: "dir",
668        kind: Kind::Text,
669        required: false,
670        positional: false,
671        description: "Install somewhere other than the per-user directory. INILLUCENT_HOME does the \
672                      same thing for every command at once.",
673    },
674    FORMAT,
675];
676
677/// The parameters `version` takes.
678const VERSION_PARAMS: &[Param] = &[FORMAT];
679
680/// Every command inillucent has, in the order `help` lists them.
681pub static COMMANDS: &[Command] = &[
682    Command {
683        name: "query",
684        summary: "Run a SELECT and get its rows back.",
685        detail: "Use this for anything that reads. The rows come back as a table, or as typed \
686                 JSON with output=json. 'total' is exact even when 'limit' cut the rows handed \
687                 back, because the engine materialises the whole result - so a limit of 10 on a \
688                 million-row query still costs what the million rows cost. Put a LIMIT in the SQL \
689                 itself when you cannot afford that, where the planner can act on it.",
690        params: QUERY_PARAMS,
691        cli_only: None,
692        writes: Writes::No,
693        run: verbs::query,
694    },
695    Command {
696        name: "exec",
697        summary: "Run one statement that changes something, and get the row count back.",
698        detail: "INSERT, UPDATE, DELETE, CREATE, DROP, ALTER, or a PRAGMA that sets a value. A \
699                 statement with RETURNING gives its rows back as well as its count.",
700        params: EXEC_PARAMS,
701        cli_only: None,
702        writes: Writes::Yes,
703        run: verbs::exec,
704    },
705    Command {
706        name: "batch",
707        summary: "Run several statements as one transaction.",
708        detail: "Separate them with semicolons. Either all of them take effect or none of them \
709                 do, which is what you want when creating a schema or loading related rows.",
710        params: BATCH_PARAMS,
711        cli_only: None,
712        writes: Writes::Yes,
713        run: verbs::batch,
714    },
715    Command {
716        name: "run",
717        summary: "Run shell input, dot commands and all, and get back what it printed.",
718        detail: "The escape hatch, and the reason every dot command is reachable from every front \
719                 end: this drives the same shell 'inillucent shell' does. Use it for the things \
720                 that have no verb of their own - '.eqp on', '.parameter set', '.testcase', \
721                 '.archive'. Run 'inillucent run \".help\"' for the full list of dot commands.",
722        params: RUN_PARAMS,
723        cli_only: None,
724        // **The verb gate stands aside and the shell refuses the writes.**
725        // `writes: true` here refused `--readonly run "SELECT count(*) FROM t;"`
726        // and the `inillucent_run` MCP tool with it, although `run` is how a
727        // read only agent reaches every dot command (task-2066 section 4.2,
728        // item 26).
729        writes: Writes::PerStatement,
730        run: verbs::run_input,
731    },
732    Command {
733        name: "create",
734        summary: "Make a new, empty database file.",
735        detail: "Refuses a path that already exists, so it can never destroy a database by being \
736                 run twice. Every other command opens whatever it is pointed at.",
737        params: CREATE_PARAMS,
738        cli_only: None,
739        writes: Writes::Yes,
740        run: verbs::create,
741    },
742    Command {
743        name: "tables",
744        summary: "List the tables and views.",
745        detail: "The names, with what each one is. System tables whose names begin with sqlite_ \
746                 are left out.",
747        params: PATTERN_PARAMS,
748        cli_only: None,
749        writes: Writes::No,
750        run: verbs::tables,
751    },
752    Command {
753        name: "describe",
754        summary: "Everything about one table: columns, types, keys, indexes, row count and DDL.",
755        detail: "Call this before writing SQL against a table you did not create. It answers in \
756                 one call what four separate pragmas would, which matters because a caller that \
757                 has to make four usually makes three and writes its query from an incomplete \
758                 picture.",
759        params: DESCRIBE_PARAMS,
760        cli_only: None,
761        writes: Writes::No,
762        run: verbs::describe,
763    },
764    Command {
765        name: "schema",
766        summary: "Show the CREATE statements for the whole database or for what a pattern names.",
767        detail: "The schema as SQL, which is the form you can paste into another database.",
768        params: SCHEMA_PARAMS,
769        cli_only: None,
770        writes: Writes::No,
771        run: verbs::schema,
772    },
773    Command {
774        name: "indexes",
775        summary: "List the indexes and the table each one is on.",
776        detail: "Including the ones a UNIQUE constraint or a primary key created, which is why \
777                 an index you did not write may appear here.",
778        params: PATTERN_PARAMS,
779        cli_only: None,
780        writes: Writes::No,
781        run: verbs::indexes,
782    },
783    Command {
784        name: "databases",
785        summary: "List the attached databases and the file behind each.",
786        detail: "'main' is the one that was opened; others come from ATTACH. 'temp' is the \
787                 session's own scratch database and has no file.",
788        params: DB_ONLY,
789        cli_only: None,
790        writes: Writes::No,
791        run: verbs::databases,
792    },
793    Command {
794        name: "explain",
795        summary: "Show the query plan for a statement without running it.",
796        detail: "Which indexes are used, which scans are full, and in what order the tables are \
797                 joined. This is how you find out why a query is slow before making it faster.",
798        params: EXPLAIN_PARAMS,
799        cli_only: None,
800        writes: Writes::No,
801        run: verbs::explain,
802    },
803    Command {
804        name: "import",
805        summary: "Load a CSV or tab-separated file into a table.",
806        detail: "The table is created from the file's first row if it does not exist. Quoting is \
807                 RFC 4180 unless a different format is asked for.",
808        params: IMPORT_PARAMS,
809        cli_only: None,
810        writes: Writes::Yes,
811        run: verbs::import,
812    },
813    Command {
814        name: "embed",
815        summary: "Fill a vector column with embeddings, on the processor or a graphics card.",
816        detail: "Reads every row of --table whose --vector column IS NULL, embeds the --text column \
817                 with the nomic-embed-text-v1.5 model, and writes the vectors in the layout embed() \
818                 returns. It sorts the texts by length, groups them under a memory ceiling, and \
819                 commits every --commit-every rows, so a stopped run continues where it ended. A \
820                 corpus of 558,429 chunks took 22 minutes on a graphics card, where embed() in SQL, \
821                 one row at a time on the processor, would take about 12 hours. It prints how many \
822                 rows were embedded, how many were skipped because the text was NULL or empty, and \
823                 how many were cut at the model's token limit, with the rowid of up to 20 of them. \
824                 Needs the model: run 'inillucent setup-embeddings all' first, and \
825                 'inillucent setup-embeddings runtime --gpu' for a card.",
826        params: EMBED_PARAMS,
827        cli_only: None,
828        writes: Writes::Yes,
829        run: crate::bulk_embed::embed_table,
830    },
831    Command {
832        name: "export",
833        summary: "Write a table or a query's rows out as CSV, JSON or one of six other formats.",
834        detail: "With 'out' the rows go to a file and the result says so; without it they come \
835                 back in the result, which is usually what an agent wants.",
836        params: EXPORT_PARAMS,
837        cli_only: None,
838        writes: Writes::No,
839        run: verbs::export,
840    },
841    Command {
842        name: "dump",
843        summary: "Render the database as the SQL that would rebuild it.",
844        detail: "Schema and data, in dependency order, inside a transaction. This is the \
845                 portable form: it is text, and another SQLite-speaking database will read it.",
846        params: DUMP_PARAMS,
847        cli_only: None,
848        writes: Writes::No,
849        run: verbs::dump,
850    },
851    Command {
852        name: "backup",
853        summary: "Write a copy of the database to another file.",
854        detail: "A consistent copy taken while the database is open. The copy is a database, not \
855                 a text dump.",
856        params: BACKUP_PARAMS,
857        cli_only: None,
858        writes: Writes::No,
859        run: verbs::backup,
860    },
861    Command {
862        name: "encrypt",
863        summary: "Write an encrypted copy of a plaintext database.",
864        detail: "The copy is encrypted with the key from --key-file or INILLUCENT_KEY, and                  holds every row, index, view and trigger of the database --db names, which is                  read without the key. The source is left as it was: replace it with the copy                  once you have checked the copy opens.",
865        params: COPY_PARAMS,
866        cli_only: Some(
867            "it takes its key from the program's own --key-file or environment, and an MCP              client has neither. Encrypt a database before serving it.",
868        ),
869        writes: Writes::No,
870        run: verbs::encrypt,
871    },
872    Command {
873        name: "decrypt",
874        summary: "Write a plaintext copy of an encrypted database.",
875        detail: "Opens the database --db names with the key from --key-file or INILLUCENT_KEY                  and writes every row, index, view and trigger to a file that is not                  encrypted. The encrypted database is left as it was.",
876        params: COPY_PARAMS,
877        cli_only: Some(
878            "it writes every row of an encrypted database to disk unprotected, which is a              decision for the person who holds the key and not for an agent the database              is served to.",
879        ),
880        writes: Writes::No,
881        run: verbs::decrypt,
882    },
883    Command {
884        name: "rekey",
885        summary: "Change the key an encrypted database is encrypted with.",
886        detail: "Opens the database with the key from --key-file or INILLUCENT_KEY and moves it                  to the new key from --new-key-file or INILLUCENT_NEW_KEY. Only the header of                  each file is rewritten, so it takes the same time whatever the database's                  size, and the old key stops opening the database when it returns. PRAGMA                  rekey does the same from SQL.",
887        params: REKEY_PARAMS,
888        cli_only: Some(
889            "an agent that could change the key could lock the database's owner out of it.",
890        ),
891        writes: Writes::Yes,
892        run: verbs::rekey,
893    },
894    Command {
895        name: "restore",
896        summary: "Point this session at a backup file, in place of the database it opened.",
897        detail: "The opposite of 'backup', and it does not overwrite anything: this engine's \
898                 databases are whole files, so restoring is opening the other file rather than \
899                 writing its pages over the one you are in. That means it lasts as long as the \
900                 session does - useful from the shell and from 'run', where the statements after \
901                 it read the restored file, and of no effect on its own, because a one-shot \
902                 process ends immediately after. To replace a file, copy the backup over it. A \
903                 backup file that is not there is refused rather than created empty.",
904        params: RESTORE_PARAMS,
905        cli_only: None,
906        writes: Writes::Yes,
907        run: verbs::restore,
908    },
909    Command {
910        name: "checkpoint",
911        summary: "Fold the write-ahead log back into the database file.",
912        detail: "Writes go to a log first and are folded in later. Doing it now shrinks the log \
913                 and is what you want before copying the file by hand.",
914        params: DB_ONLY,
915        cli_only: None,
916        writes: Writes::Yes,
917        run: verbs::checkpoint,
918    },
919    Command {
920        name: "integrity-check",
921        summary: "Read every page and report whether the database holds together.",
922        detail: "Answers 'ok' on a healthy database. Anything else names what is wrong. It reads \
923                 the whole file, so it costs what the file costs.",
924        params: DB_ONLY,
925        cli_only: None,
926        writes: Writes::No,
927        run: verbs::integrity_check,
928    },
929    Command {
930        name: "analyze",
931        summary: "Gather the statistics the query planner reads.",
932        detail: "Run it after loading a lot of data. Without statistics the planner guesses at \
933                 how selective an index is, and a wrong guess is the usual reason a query that \
934                 should use an index does not.",
935        params: ANALYZE_PARAMS,
936        cli_only: None,
937        writes: Writes::Yes,
938        run: verbs::analyze,
939    },
940    Command {
941        name: "stats",
942        summary: "Report the page cache, the pool size and the shape of the file.",
943        detail: "Cache hits and misses, how many pages the file holds and how many are free. \
944                 This is where you look when a workload is slower than it should be.",
945        params: DB_ONLY,
946        cli_only: None,
947        writes: Writes::No,
948        run: verbs::stats,
949    },
950    Command {
951        name: "search",
952        summary: "Full-text search over an FTS5 or inillucent_search table.",
953        detail: "Writes the MATCH ... ORDER BY rank idiom for you, which is the part nobody \
954                 remembers. The table has to be a full-text one; 'describe' will show you \
955                 whether it is.",
956        params: SEARCH_PARAMS,
957        cli_only: None,
958        writes: Writes::No,
959        run: verbs::search,
960    },
961    Command {
962        name: "vector-search",
963        summary: "Find the rows whose vector is nearest to one you supply.",
964        detail: "Over a VECTOR(N) column, by cosine distance unless you ask for another measure. \
965                 If there is an HNSW index on the column the planner uses it; if there is not, \
966                 this is an exhaustive scan and is still correct.",
967        params: VECTOR_PARAMS,
968        cli_only: None,
969        writes: Writes::No,
970        run: verbs::vector_search,
971    },
972    Command {
973        name: "capabilities",
974        summary: "Ask what this engine can do before composing a statement.",
975        detail: "Every row is checked against the running engine by a test, in both directions - \
976                 a claim of support that fails and a claim of absence that now works each turn \
977                 the build red. So this is worth trusting in a way a hand-maintained feature \
978                 list is not. A name that is not in the table answers no, because a capability \
979                 that was never declared was never checked.",
980        params: CAPABILITY_PARAMS,
981        cli_only: None,
982        writes: Writes::No,
983        run: verbs::capabilities,
984    },
985    Command {
986        name: "functions",
987        summary: "List the SQL functions this engine answers.",
988        detail: "From the engine's own register, which is compared against the reference \
989                 library's on every build - so this is what actually exists rather than what was \
990                 documented once.",
991        params: PATTERN_PARAMS,
992        cli_only: None,
993        writes: Writes::No,
994        run: verbs::functions,
995    },
996    Command {
997        name: "migrate",
998        summary: "Build an inillucent database from a SQLite file, PostgreSQL or MySQL.",
999        detail: "Reads the source and writes a new .rdb with the same rows. A path is a SQLite \
1000                 database file; a postgres:// or mysql:// URL is a running server, read inside \
1001                 one repeatable-read snapshot so that every table is as of one instant. The \
1002                 source is never written to and the destination is never overwritten: the new \
1003                 file is staged under another name and published by a rename, so a half-written \
1004                 database never sits where an application would open it. A server migration is \
1005                 verified per table by row count and by an order-independent digest, and nothing \
1006                 that fails a check is published.",
1007        params: MIGRATE_PARAMS,
1008        cli_only: None,
1009        writes: Writes::Yes,
1010        run: verbs::migrate,
1011    },
1012    Command {
1013        name: "setup-embeddings",
1014        summary: "Download and install the embedding model and the runtime it needs.",
1015        detail: "One command, on Windows, macOS and Linux. It fetches ONNX Runtime and the \
1016                 nomic-embed-text-v1.5 weights into a per-user directory, checks every byte \
1017                 against a digest pinned in this build, and leaves the engine able to answer \
1018                 embed(TEXT) with nothing exported by hand - so a mismatch is a refusal that \
1019                 names both digests rather than a shared library that loads and misbehaves. \
1020                 Name what you want: 'all' installs both halves, 'runtime' and 'model' one \
1021                 each. Run with no component at all and it reports what is installed and \
1022                 downloads nothing, which is what stops a 620 MB fetch being a surprise; \
1023                 '--status' does the same explicitly. About 620 MB the first time and \
1024                 nothing on a later run. '--residency' chooses when the model is in memory: \
1025                 'resident' keeps it, which is about 1.9 GB held and 12 to 36 ms a query; \
1026                 'on-demand' loads it per call, which holds nothing and costs about 0.8 s a \
1027                 query; 'idle' or 'idle:90s' loads it on use and drops it after a quiet \
1028                 period, which is the default and pays the load once for a burst of \
1029                 questions.",
1030        params: SETUP_PARAMS,
1031        cli_only: None,
1032        writes: Writes::Yes,
1033        run: crate::setup::setup_embeddings,
1034    },
1035    Command {
1036        name: "version",
1037        summary: "Report the engine, the dialect and the driver versions.",
1038        detail: "The SQLite version named here is the dialect this engine implements, not a \
1039                 library it links. There is no SQLite in this binary.",
1040        params: VERSION_PARAMS,
1041        cli_only: None,
1042        writes: Writes::No,
1043        run: verbs::version,
1044    },
1045    Command {
1046        name: "help",
1047        summary: "List every command, or explain one in full.",
1048        detail: "With no topic it prints the table. With one it prints that command's usage, \
1049                 what it is for, and every parameter it takes.",
1050        params: HELP_PARAMS,
1051        cli_only: None,
1052        writes: Writes::No,
1053        run: verbs::help,
1054    },
1055    Command {
1056        name: "shell",
1057        summary: "Start the interactive shell.",
1058        detail: "The sqlite3-shaped REPL, with all 63 dot commands. Everything it can do is also \
1059                 reachable non-interactively through 'run'.",
1060        params: &[],
1061        cli_only: Some(
1062            "it is a terminal REPL: it reads a keyboard and writes a screen, and neither exists \
1063             at the other end of an MCP call. Use 'run' instead, which drives the same shell.",
1064        ),
1065        writes: Writes::Yes,
1066        run: verbs::shell_placeholder,
1067    },
1068    Command {
1069        name: "mcp",
1070        summary: "Serve these commands to an agent over MCP on standard input and output.",
1071        detail: "Every command in this table that is not marked cli-only becomes a tool named \
1072                 inillucent_<command>, with this same description and these same parameters.",
1073        params: &[],
1074        cli_only: Some(
1075            "it is the server that would be exposing the tools, so offering it as one of them \
1076             would let a client ask the server to serve itself.",
1077        ),
1078        writes: Writes::Yes,
1079        run: verbs::mcp_placeholder,
1080    },
1081];