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 #

OptionDescription
--schemaList 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):

ViewOne row perKey columns
viindexed VIpath, name, qualified_name, library, is_stub, impact_score, callers_count
terminalconnector-pane terminalvi_path, name, direction, is_indicator, type_descriptor, type_kind, field_names, type_id
constantblock-diagram constantvi_path, value, label, type_descriptor, type_kind, wired_to, type_id
nodeblock-diagram nodevi_path, kind, name, prim_id, qualified_name, callee_path, parent_uid, frame
type_useclass / typedef name a VI referencesvi_path, type_key
typedistinct type, by structuretype_id, kind, descriptor, name, dimensions, element_type_id
type_fieldcluster fieldtype_id, seq, name, field_type_id
type_itemenum / ring itemtype_id, seq, name, value
vi_used_typetype a VI uses (its own and everything nested in it)vi_path, type_id
typedefindexed .ctl controlpath, name, library, type_id, kind, default_text, is_stub, stub_reason
typedef_fieldfield of a control's clustertypedef_path, seq, parent_seq, depth, name, type_id, default_text
typedef_reffile a control uses / is owned bytypedef_path, ref_path, ref_name, ref_kind, rel
typedef_usecontrol a VI depends on (by path)vi_path, typedef_path
typedef_typetype a control uses (its own and everything nested in it)typedef_path, type_id
class_factclass-member VIvi_path, owning_class, parent, scope, is_accessor, accessor_field
lvproj.lvproj memberlvproj_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, and ATTACH are 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 (truncated reports 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 --schema always reports the current shape. The SQL dialect is SQLite's.
  • The MCP server exposes the same surface as its query / query_schema tools — see mcp.

See also #

  • CLI reference — the map of every lvkit command (lvkit index builds the index query reads).
  • mcp — the same query surface for an AI agent.
  • describe — deep, single-VI inspection when SQL isn't the right grain.