query #
Run read-only SQL over a repo's code-understanding index and get back just the answer — a project-wide question (e.g. "what names does this project use for error indicators?") answered as a small table instead of a dump of rows.
Synopsis #
lvkit query <path> "<SELECT …>"
lvkit query <path> --schema
lvkit query <path> "<SELECT …>" --format json<path> is any file or directory inside the repo — a directory, .lvproj,
.lvlib, .lvclass, or .vi. Its enclosing project is queried, so the
index always covers the whole repo. Build the index first with lvkit index
(or let ordinary describe/render/generate runs warm it as you work);
querying an unindexed project fails with a clear message.
Options #
| Option | Description |
|---|---|
--schema | List the queryable views and their columns, then exit (ignores the SQL argument). |
--format {table,json} | Output format. table (default) prints an aligned text table; json prints {columns, rows, row_count, truncated}. |
The SQL argument is optional only when --schema is given; otherwise it is
required.
Building & refreshing the index #
query reads a persisted index. You rarely build it by hand — query (and the
callers/callees/blast-radius commands) build it on first use and
incrementally refresh it before each read, and ordinary describe/render/
generate/docs runs warm it as you work. To build or refresh it explicitly:
lvkit index <path> # build (or, with --refresh, incrementally update)
lvkit index <path> --refresh # rebuild only content-changed/added VIs and controls; drop deleted.ctl controls are indexed with the VIs. A VI that uses a control is rebuilt when
that control changes, even if the VI file did not.
<path> resolves to its enclosing project, and the whole repo is indexed
(path-keyed, so same-named VIs like setUp.vi ×17 never collide). A refresh is
keyed by each VI's content hash, so it only re-parses what actually changed.
Pass --no-refresh to query/callers/… to skip the pre-read refresh (faster,
but results may be stale if a VI changed since the last build).
The views #
Query these curated views (run lvkit query <path> --schema for the exact
columns of each):
| View | One row per | Key columns |
|---|---|---|
vi | indexed VI | path, name, qualified_name, library, is_stub, impact_score, callers_count |
terminal | connector-pane terminal | vi_path, name, direction, is_indicator, type_descriptor, type_kind, field_names, type_id |
constant | block-diagram constant | vi_path, value, label, type_descriptor, type_kind, wired_to, type_id |
node | block-diagram node | vi_path, kind, name, prim_id, qualified_name, callee_path, parent_uid, frame |
type_use | class / typedef name a VI references | vi_path, type_key |
type | distinct type, by structure | type_id, kind, descriptor, name, dimensions, element_type_id |
type_field | cluster field | type_id, seq, name, field_type_id |
type_item | enum / ring item | type_id, seq, name, value |
vi_used_type | type a VI uses (its own and everything nested in it) | vi_path, type_id |
typedef | indexed .ctl control | path, name, library, type_id, kind, default_text, is_stub, stub_reason |
typedef_field | field of a control's cluster | typedef_path, seq, parent_seq, depth, name, type_id, default_text |
typedef_ref | file a control uses / is owned by | typedef_path, ref_path, ref_name, ref_kind, rel |
typedef_use | control a VI depends on (by path) | vi_path, typedef_path |
typedef_type | type a control uses (its own and everything nested in it) | typedef_path, type_id |
class_fact | class-member VI | vi_path, owning_class, parent, scope, is_accessor, accessor_field |
lvproj | .lvproj member | lvproj_name, member_name, member_type, resolved_path, is_in_repo |
Types and controls #
Every type — an enum, a cluster, an array, a typedef, inline or named — has a
structural id (type_id): the same shape gets the same id in every VI and
.ctl, and a same-named type with a different structure gets a different one (a
name is not identity). terminal.type_id and constant.type_id join straight to
type. vi_used_type and typedef_type hold each VI's / control's type and everything
nested inside it, so "who uses this type, directly or nested" is a plain filter,
no recursion:
-- every VI that uses (or nests) an enum containing the item 'Stop'
SELECT DISTINCT vt.vi_path FROM vi_used_type vt
JOIN type t USING (type_id) JOIN type_item i USING (type_id)
WHERE t.kind = 'enum' AND i.name = 'Stop'
-- clusters that have both fields 'mode' and 'gain'
SELECT t.type_id, t.name FROM type t WHERE t.kind = 'cluster'
AND EXISTS (SELECT 1 FROM type_field f WHERE f.type_id = t.type_id AND f.name = 'mode')
AND EXISTS (SELECT 1 FROM type_field f WHERE f.type_id = t.type_id AND f.name = 'gain')
-- the VIs that depend on a given .ctl (by path, so same-named controls stay distinct)
SELECT vi_path FROM typedef_use WHERE typedef_path = '<path to the .ctl>'.ctl files are indexed alongside the VIs (typedef* views), incrementally on
content hash; a control that cannot be read appears with is_stub = 1 and its stub_reason.
Reachability questions ("what calls this?", "what breaks if I change it?") are
answerable two ways: as SQL over node's callee_path column (direct callers
are SELECT DISTINCT vi_path FROM node WHERE callee_path='<path>', direct
callees are SELECT callee_path FROM node WHERE vi_path='<X>' AND kind='vi',
transitive blast radius a WITH RECURSIVE over callee_path), or as the
typed lvkit callers / lvkit callees / lvkit blast-radius
CLI commands — graph walks, not SQL, with no MCP tool twin of their own. For a
quick count without the full list, impact_score (transitive) and
callers_count (direct, in-degree) on the vi view are precomputed.
Example #
The driving question — the names a project uses for error indicators, as a histogram:
lvkit query MyRepo \
"SELECT name, COUNT(*) AS n FROM terminal
WHERE type_descriptor='Error' AND direction='output'
GROUP BY name ORDER BY n DESC"name n
---------------------- ---
error out 382
control_100 3
control_101 3
Error out 2
Test Method Error 2
…The GROUP BY returns the answer — a handful of rows — rather than every
matching terminal. --format json gives the same data for piping into another
tool.
Read-only by construction #
query opens the index database read-only and rejects anything that isn't a
single SELECT/WITH:
- writes (
INSERT/UPDATE/DELETE/DROP/CREATE),PRAGMA, andATTACHare refused; - a second, stacked statement (
SELECT …; DROP …) is refused; - a long-running query is cut off by a time limit, and results are row-capped
(
truncatedreports when the cap was hit).
A rejected or failing query prints query error: … to stderr and exits 2.
Notes #
- The index is stored per project root under
~/.lvkit/cache/index/projects/<slug>/index.db(SQLite/WAL), rebuilt cheaply from the content-hash-keyed extraction cache. - The views are the interface; you never query the tables. They are a
curated layer that decouples callers from the physical schema, so the tables
can change underneath without breaking your SQL. Pre-1.0, the views
themselves may still evolve — but they are the intentional, documented seam,
and
--schemaalways reports the current shape. The SQL dialect is SQLite's. - The MCP server exposes the same surface as its
query/query_schematools — see mcp.
See also #
- CLI reference — the map of every
lvkitcommand (lvkit indexbuilds the indexqueryreads). - mcp — the same query surface for an AI agent.
- describe — deep, single-VI inspection when SQL isn't the right grain.