Summary
CREATE INDEX and CREATE UNIQUE INDEX produce no nodes. In the same file, CREATE TABLE,
CREATE VIEW and CREATE FUNCTION all land correctly, so the file is not skipped and the grammar
is loading — indexes specifically are lost.
This is the same shape as #3401 (CREATE POLICY), and I could not find an existing issue for the
index case, so filing separately. If you consider them one bug, feel free to close this into #3401 —
but note that PR #3456 addresses policies only, so merging it would leave this half open.
Reproducer
One file, 19 lines of plain PostgreSQL. No project, no config.
-- schema.sql
CREATE TABLE public.profiles (
id uuid PRIMARY KEY,
owner uuid NOT NULL
);
CREATE INDEX profiles_owner_idx ON public.profiles (owner);
CREATE UNIQUE INDEX profiles_id_uniq ON public.profiles (id);
CREATE VIEW public.active_profiles AS SELECT * FROM public.profiles;
CREATE FUNCTION public.is_owner(u uuid) RETURNS boolean AS $$
SELECT u = auth.uid();
$$ LANGUAGE sql;
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;
CREATE POLICY profiles_select_own ON public.profiles
FOR SELECT USING (public.is_owner(owner));
CREATE POLICY profiles_update_own ON public.profiles
FOR UPDATE USING (public.is_owner(owner));
$ graphify extract . --code-only
[graphify extract] AST extraction on 1 code files...
[graphify extract] wrote graphify-out/graph.json: 4 nodes, 4 edges, 2 communities
Expected vs actual
Every node in graph.json:
'schema.sql'
'public.is_owner()'
'public.active_profiles'
'public.profiles'
| declaration |
node |
CREATE TABLE public.profiles |
present |
CREATE VIEW public.active_profiles |
present |
CREATE FUNCTION public.is_owner |
present |
CREATE INDEX profiles_owner_idx |
absent |
CREATE UNIQUE INDEX profiles_id_uniq |
absent |
CREATE POLICY profiles_select_own |
absent (#3401) |
CREATE POLICY profiles_update_own |
absent (#3401) |
There is no warning and no error. graphify extract exits 0.
Why this matters beyond the toy case
Measured on a production Next.js + Supabase codebase, 166 tracked .sql files, comments and
single-quoted strings stripped before counting, then matching each declaration name against node
labels in the same source file:
| category |
declared |
in graph |
CREATE TABLE |
139 |
136 |
CREATE VIEW |
42 |
38 |
CREATE FUNCTION |
188 |
188 |
CREATE TRIGGER |
122 |
113 |
CREATE INDEX |
218 |
0 |
CREATE POLICY |
440 |
0 |
The part that makes this more than a coverage gap: per-file coverage reads as a clean pass. All
166 .sql files produced at least one node, so every file-level coverage check says SQL is fully
mapped, while two entire entity classes are absent. Nothing in the output signals the hole.
And the graph does not go quiet when asked about the missing entities — it answers from whatever
else it has. graphify query "which RLS policies protect the profiles table" on that repo returned
394 nodes drawn from test files, a Drizzle schema symbol, UI components and prose documentation,
citing zero .sql sources. A plausible substitute answer is harder to catch than an empty one.
Environment
- graphify 0.9.57 (
uv tool install 'graphifyy[sql,leiden,mcp,watch,svg]')
- Python 3.12.14, macOS (arm64)
tree_sitter_sql imports cleanly in the tool env — verified before filing, since without that
extra extract_sql returns zero nodes for a different reason
Possible cause
graphify/extractors/sql.py handles create_table and create_view node types explicitly, and the
ERROR-recovery regex _ROUTINE_RECOVERY_RX covers only FUNCTION/PROCEDURE. Searching that file
for index turns up no rule, the same way searching it for policy turns up none. If the grammar
emits an ERROR node for CREATE INDEX, the recovery path has nothing to catch it.
Summary
CREATE INDEXandCREATE UNIQUE INDEXproduce no nodes. In the same file,CREATE TABLE,CREATE VIEWandCREATE FUNCTIONall land correctly, so the file is not skipped and the grammaris loading — indexes specifically are lost.
This is the same shape as #3401 (
CREATE POLICY), and I could not find an existing issue for theindex case, so filing separately. If you consider them one bug, feel free to close this into #3401 —
but note that PR #3456 addresses policies only, so merging it would leave this half open.
Reproducer
One file, 19 lines of plain PostgreSQL. No project, no config.
Expected vs actual
Every node in
graph.json:CREATE TABLE public.profilesCREATE VIEW public.active_profilesCREATE FUNCTION public.is_ownerCREATE INDEX profiles_owner_idxCREATE UNIQUE INDEX profiles_id_uniqCREATE POLICY profiles_select_ownCREATE POLICY profiles_update_ownThere is no warning and no error.
graphify extractexits 0.Why this matters beyond the toy case
Measured on a production Next.js + Supabase codebase, 166 tracked
.sqlfiles, comments andsingle-quoted strings stripped before counting, then matching each declaration name against node
labels in the same source file:
CREATE TABLECREATE VIEWCREATE FUNCTIONCREATE TRIGGERCREATE INDEXCREATE POLICYThe part that makes this more than a coverage gap: per-file coverage reads as a clean pass. All
166
.sqlfiles produced at least one node, so every file-level coverage check says SQL is fullymapped, while two entire entity classes are absent. Nothing in the output signals the hole.
And the graph does not go quiet when asked about the missing entities — it answers from whatever
else it has.
graphify query "which RLS policies protect the profiles table"on that repo returned394 nodes drawn from test files, a Drizzle schema symbol, UI components and prose documentation,
citing zero
.sqlsources. A plausible substitute answer is harder to catch than an empty one.Environment
uv tool install 'graphifyy[sql,leiden,mcp,watch,svg]')tree_sitter_sqlimports cleanly in the tool env — verified before filing, since without thatextra
extract_sqlreturns zero nodes for a different reasonPossible cause
graphify/extractors/sql.pyhandlescreate_tableandcreate_viewnode types explicitly, and theERROR-recovery regex
_ROUTINE_RECOVERY_RXcovers onlyFUNCTION/PROCEDURE. Searching that filefor
indexturns up no rule, the same way searching it forpolicyturns up none. If the grammaremits an ERROR node for
CREATE INDEX, the recovery path has nothing to catch it.