Legend: [S/M/L] = size · [P0/P1/P2] = priority · [cat] = category
-
[S][P1][binding]Column type accessors:column_int,column_double,column_type,column_count,column_name -
[M][P2][binding]More PRAGMA introspection:index_list,index_info,foreign_key_list,table_xinfo -
[S][P2][binding]Bindsqlite3_errmsgfor human-readable error messages -
[S][P1][data]Arena allocator for schema data lifetime — load once, free on reload (implemented viaSchema.arena+Dynamic_Arena) -
[M][P1][data]Second pass to resolve FKto_table/to_columnstrings toGlobalColumnIndexafter all tables are loaded -
[S][P2][data]Read column properties (type,not_null,pk) frompragma_table_info -
[S][P2][data]Remove@Todoondatabase_name— currently cloned from filename, confirm that's the right lifetime -
[S][P2][data]Avoid duplicate schema load in GUI double-click handler (print_database_informationre-callsextract_database_information) — double-click now setsschema_dirtyand the main loop loads once;print_database_informationis CLI-only -
[L][P1][data]Abstract SQLite schema extraction behind a backend interface —schema_load.odindefinesBackend(detect/open/close + neutral row listers) and sharedload_schema; SQLite backend uses the PRAGMA queries. GUI load, CLI dump, and the file-dialog sniff all go through it (known_database_format), so MSSQL/Postgres are drop-inBACKENDSentries -
[L][P2][data]MSSQL backend — catalog queries (INFORMATION_SCHEMA/sys), connection string opens instead of a file -
[L][P2][data]Postgres backend — catalog queries, connection string opens; note SQL-backend assumption: row lists must be grouped by table in table order -
[S][P2][data]Revisitload_schemakeying if a future backend needs it (only matters if a backend returns rows ungrouped)
Things we don't know if we'll need, kept here so we don't forget them but don't spend time on them prematurely.
- [~]
[S][P3][data]Consider string interning (esp. column types) if memory usage ever becomes a concern — storestringdirectly for now - [~]
[S][P3][data]Storereferenced_byslice onTablefor O(1) reverse FK lookups (currently scanforeign_keysarray on demand) - [~]
[S][P3][data]Addtable_indexfield toColumnfor O(1) column→table lookup (currently scantables[]range check) - [~]
[S][P3][data]Cache diagram layout positions per seed table so revisiting a sub-diagram remembers node positions after drag — drags persist within a view (positions mirrored intoDiagramState.layoutevery frame), but a retarget re-centres the seed and re-lays-out the visible set. Per-seed position memory across retargets would need a layout cache keyed by (seed, visible set) - [~]
[S][P2][data]Remove temporaryfrom_column/to_table/to_columnstring fields fromForeignKeynow that index resolution works - [~]
[S][P2][data]Removeresolved_to_indexbool fromForeignKeyonce display code checks resolution (currently set but never read) - [~]
[M][P2][prof]Integrate Tracy profiler for frame timing, zone instrumentation, and allocation tracking — can't optimise blind -
[M][P2][data]Dump schema to a custom snapshot format so exploration doesn't need repeated DB hits -
[S][P2][cli]CLI mode:schema_spelunker dump something.dbto produce the snapshot - [~]
[S][P3][perf]Pre-filter magic bytes check by.db/.sqliteextension first to avoid opening every file in large directories - [~]
[M][P3][settings]File dialog filter model (settings-raft item): filter mode (All files / SQLite extensions / custom), "verify SQLite magic on open" toggle — sniff the one file being opened instead of every file in the listing (corp AV / network share safe) - [~]
[S][P3][perf]Replacefmt.ctprintf+clone_to_cstringwithig.TextUnformattedin schema/node display loops to avoid per-frame allocs - [~]
[S][P3][perf]Batch SQLite introspection queries instead of onePRAGMAper table (2000+ round trips at scale) - [~]
[S][P3][build]Odin git-master: ranging directly over a map-stored[dynamic]with a missing key segfaults (reproduced standalone intest/repro_layout.odin). Workaround inlayout_visible_tables: copy the map value into a local before ranging. Drop the workaround when a fixed compiler is pinned.
-
[M][P0][gui]SDL3 + ImGui + OpenGL 3.3 application loop -
[S][P0][gui]Dockspace viaDockSpaceOverViewport -
[S][P0][gui]Roboto TTF font loading -
[S][P1][gui]Menu bar with File > Open and Theme > Light/Dark switching -
[S][P1][gui]File dialog: custom ImGui window with file list, path navigation, open/cancel-
[S][P1]Cancel button wired (closes or resets) -
[S][P1]Open button — opens the selected file, wiring the same path as double-click viaopen_database_file(2026-08-23) -
[S][P1]Double-click detection wired viaAllowDoubleClick+IsMouseDoubleClicked - [~]
[S][P1]Double-click navigation into subdirectories (path_buffer set on click, but takes two frames to take effect sinceos.openalready ran this frame) -
[S][P1]Keyboard shortcuts (Enter to confirm, Esc to cancel) -
[S][P2]Path bar showing current directory (InputText, works now that buffer isn't zeroed every frame) -
[S][P2]File type filter — magic bytes check on file open -
[S][P1]Fix stability bug:or_continueonshow_file_dialogerror skipsig.Render()— fixed withdefer ig.Render()block -
[S][P1]Fix path buffer lifecycle: don't zeropath_bufferevery frame beforeos.open— no longer zeroed on every frame -
[S][P1]File dialog: directory re-read every frame even when unchanged — adddirtyflag toFileDialog, only rebuild listing on navigation -
[S][P2]File dialog: sort directories before files in the list -
[S][P2]File dialog:os.openerror silently swallowed when directory can't be opened — show error message or return error
-
-
[S][P1][gui]File dialog: use arena allocator for per-frame directory listing (reset each frame, no per-element delete) -
[S][P1][gui]Basic table-list schema viewer (listbox in schema window shows table names) -
[M][P1][gui]Schema detail view: when a table is selected, show its columns in an ImGuiBeginTablewith columns Name, Type, Nullable, PK -
[S][P1][gui]Foreign-key panel in schema detail view: show FK rows (from column, to table.to column) below the column table -
[S][P1][gui]Schema extractor errors surfaced to user: storelast_error: stringonAppState, show as status bar or notification
Phased implementation of the filtered sub-diagram view with FK link lanes.
-
[S][P0][gui]AddDiagramStatetoAppStatewithseed_table: u32andshow_from_seed_table: bool— zero value = show all tables -
[S][P0][gui]Writefind_table_by_column :: proc(tables: []Table, col: GlobalColumnIndex) -> int— scansfrom_column..to_columnrange, returns -1 if not found -
[S][P0][gui]Wire "One Degree" / "Two Degrees" / "Show All" buttons to setdiagram_state.degreesandshow_from_seed_table -
[M][P0][gui]BFS over FK graph from seed table, collecting visible table indices within N degrees. Handle N=0/all as full set — wired 2026-08-21: degree buttons and schema load callrefresh_diagram_visible; out-of-range seed falls back to the full set;DiagramState.dirtyremoved (refresh is immediate, no deferral needed) -
[M][P0][gui]Render only visible nodes inshow_node_editor— node loop iteratesdiagram_state.visible_tables -
[M][P0][gui]Draw FK links: loop the visible tables' FK ranges,imn.Linkwhere the referenced table is also visible. Link ids are the FK's index into the schema; pin ids are rawGlobalColumnIndexvalues — FK-endpoint columns are registered as pins (BeginInputAttributefor referencing,BeginOutputAttributefor referenced) so links anchor to the correct column line
-
[S][P1][gui]Compute each visible table's hop distance from the seed (BFS through both FK directions, restricted to the visible set) -
[S][P1][gui]Place the seed at the origin and every other table on the ring matching its hop count — ring radius scales with the ring's table count so dense rings stay readable; deeper rings are ordered around their parent's angle to keep links short (unreachable tables go on one outermost ring) -
[S][P1][gui]Callimn.SetNodeGridSpacePoson each node before rendering to place it on its ring
Layout runs automatically on every view change (schema load, degree button,
seed retarget, Show All): the seed must land in the centre, so the visible
set is re-laid-out each time — that's the whole point of a centred layout.
Two guards keep it cheap on huge databases: positions are cached in
DiagramState.layout and mirrored back from ImNodes every frame (drags
included), and a refresh whose seed and visible set are unchanged
(layout_key) is skipped entirely, so re-clicking the active table or Show
All twice costs nothing. Show All on huge.db is a single radial pass per
click (O(V) placement, no per-frame cost).
-
[S][P2][gui]Pin attributes on column rows (imn.BeginPin/imn.EndPin) so FK links attach to the correct column line — FK-endpoint pins + anchoring done in Phase 1; remaining: pins on every column row -
[S][P2][gui]Pin side: derived from the linked tables' relative position in the current layout (node on the left = output pin / right edge, node on the right = input pin / left edge), never the FK's one/many semantics — done for FK endpoints (2026-08-23); remaining: apply to all column rows -
[S][P2][gui]Highlight active seed table in the diagram — seed node gets the accent-coloured title bar (reads theme accent from the ImNodes Link colour, no extra state) -
[S][P2][gui]Degree buttons show current state as selected/active (One Degree / Two Degrees / Show All all show their active state; row shared between the Diagram and schema windows) -
[M][P2][gui]Schema window links to diagram: selecting a table in the schema list sets it as the diagram seed and shows One/Two Degree buttons that operate on the list selection; clicking a table node in the diagram retargets the seed back (both sync the node selection and the list highlight; selection is deferred a frame viapending_seed_selectionbecause ImNodes assertsSelectNodeon an unrendered node) -
[S][P1][gui]Retarget happens on a click, not a drag: release without mouse movement past the drag threshold on a table retargets; dragging a node just moves it -
[S][P2][gui]Cardinality symbols on FK links: the referencing (input) end is a filled triangle — the "many" side; the referenced (output) end is a filled circle — the "one" side. Shape contrast keeps both ends of every link readable even when one column is the endpoint of many links — superseded below by crow's-foot glyphs drawn on the link itself; the per-pin triangle/circle are now fully transparent (ImNodesCol.Pin/PinHoveredalpha 0) rather than merely shrunk, since a shrunk-but-opaque marker was still a visible dot up close or against a high-contrast theme -
[L][P1][gui]Replace the seed-centred radial ring layout with a layered (Sugiyama-style) layout: tables ranked by hop distance from the seed (relaxed by FK direction so a same-rank FK never loops sideways), ranks stack left-to-right (matches ImNodes' fixed left-input/ right-output link curve shape), ordered within each rank by barycenter + transpose crossing-reduction sweeps, then packed using each table's real ImNodes-rendered size (node_size, mirrored back every frame) so nothing overlaps.test/repro_layout.odinvalidates it standalone at scale (no overlapping tables, crossing/link-node-overlap metrics, rank-span histogram) against real.dbfiles. -
[S][P0][gui]Fixed a rank-relaxation bug that could strand part of the layout tens of ranks away from the rest of it, with nothing but a wide empty gap between them (auto-pan then centres on that gap, making the diagram look completely empty until you scroll). Cause: a self-referencing FK (cards.parent_id -> cards.idin the seed schema) hasfrom_t == to_t, socompute_layer_order's relaxation conditionfrom_hop <= to_hopreads the same map entry on both sides and is trivially true forever — the table's own rank climbed by exactly one on every single pass with no way to converge, until the 64-iteration cap stopped it 64 ranks out. Fixed by excluding self-referencing FKs from rank relaxation (main.odinand its standalone port intest/repro_layout.odin) — confirmed withrepro_layout.exe seed.db 0 2, rank count dropped from 70 to 7. -
[L][P1][gui]Custom obstacle-avoiding FK link routing + ER cardinality glyphs (link_routing.odin): ImNodes' ownLink()draws a fixed two-point bezier with no obstacle awareness, which is the real ceiling on how clean the diagram looks regardless of layout quality. Replaced with our own link drawing, built entirely on ImNodes' existing Odin bindings (DrawList_*, node/pin item rects — no vendor edits, see AGENTS.md's "Vendoring rules") — every visible table's screen rect and FK pin position is captured fresh each frame straight from what ImNodes just drew, so a routed link tracks a drag or pan for free. Each link samples the plain direct bezier, detects which other visible tables it actually crosses, and bumps around them with a couple of waypoints. Every segment is still the same cubic bezier family (never a different spline style, so a routed link and a plain one never look like two different kinds of line): a two-point link uses ImNodes' own fixed-horizontal-tangent construction unchanged, while a multi-waypoint link blends each interior waypoint's tangent Catmull-Rom style (direction from the waypoint before to the one after) so a detour reads as one smooth curve instead of a chain of near-straight facets — the two true endpoints still keep the fixed horizontal tangent the cardinality glyphs rely on. The per-segment tangent offset is capped (MAX_TANGENT_OFFSET) so several links sharing one pin (e.g. every table with acompany_idreferencing the samecompanies.id) diverge from it much sooner instead of travelling in lockstep on a long near-straight run that reads as an extra, fake crow's foot swallowing the real glyph. Also draws crow's-foot notation at each end (crow's foot + optional hollow circle for a nullable referencing column on the "many" end, a single tick on the "one" end), oriented by the pin's fixed side (always exactly horizontal — input pins are always a node's left edge, output always right — never derived from the routed path's local shape, which an earlier version tried and which could flip the glyph to the wrong side when a waypoint bent the curve sharply close to a pin). A self-referencing FK (one_table == many_table) skips obstacle routing entirely — there's no useful direct path between two pins on the same node to test — and instead always loops from each pin straight down to just below the node's own bottom edge and back up into the other pin.test/repro_link_routing.odinchecks the routing math standalone. -
[S][P1][gui]Link colour and thickness moved into the theme system:ThemeData.link_color/link_thickness, parsed from a[diagram_links]section (colour/colorandthicknesskeys) in each.ssThemefile, applied inapply_theme. All four themes tuned: Paper & Ink Light/Dark use blueprint blue variants, OLED Dark uses a vivid bright blue to read on pure black, and the new Blueprint theme (see below) uses its own white ink colour so links match its monochrome cyanotype look. -
[M][P2][theme]Added a fourth theme, Blueprint (assets/themes/blueprint.ssTheme): cyanotype print style, white/pale linework on deep Prussian-blue paper, monochrome by design (no separate accent hue — separation comes from value and alpha, same approach as OLED Dark but inverted to a blue field instead of black). -
[M][P3][gui]The link router's obstacle detour is a single greedy pass (seeroute_link_waypointsinlink_routing.odin): a dense cluster of overlapping obstacles (e.g. a heavily-shared hub table's whole neighbourhood) can still leave a residual close call after routing around the first one. Revisit if that shows up in practice — an iterative re-check pass, or dummy-node rank reservation in the layout itself, are the two standard fixes. -
[S][P3][gui]A self-referencing FK's loop (see above) doesn't run obstacle avoidance against other visible tables — only against its own node, which it excludes by construction. In practice the loop stays within a small margin below its own card, so a collision with an unrelated table would need one positioned immediately below at that exact margin; hasn't been observed, but worth a real obstacle check if it ever shows up. -
[S][P3][gui]Several links leaving the same shared pin still briefly overlap right at the pin itself before diverging (the capped tangent offset above reduces how far they travel together, but can't eliminate the overlap outright — every link is required to leave a pin on an exactly horizontal tangent, which is what makes the cardinality glyph orientation guarantee possible). Revisit only if this is still a legibility problem in practice. -
[M][P1][gui]Fixed link curves/glyphs breaking (curve launching into the node, cardinality glyph pointing the wrong way or floating off the visible line) once a table was dragged past its FK partner. Cause:main.odindeliberately reassigns which edge a pin renders on based on the two tables' current relative x position — "the left table's pin is an output/right edge, the right table's pin an input/left edge" (see thepin_is_inputcomment) — so links always flow left-to-right visually, regardless of the FK's one/many role.link_routing.odin's curve construction and cardinality-glyph code never knew about this: they assumed the "one" pin always opens +x and the "many" pin always opens -x, which only holds in the default/unmoved layout. Fixed by threading each pin's actual outward direction (derived frompin_is_inputat thedraw_fk_linkcall site) throughsample_direct_curve,sample_chained_curve, anddraw_cardinality_glyphsinstead of assuming a fixed side. Also fixed while in there: a cardinality glyph's outer vertex (and, worse, the optional hollow nullable-circle beyond it) was positioned by a fixed offset along the outward direction, not by anything on the actual curve — fine when the curve stayed near-straight that far out, but visibly detached from the line whenever routing bent the curve away before reaching that distance (reported as "what is the circle … why is that not on the link line?"). Fixed by reintroducing a bounded arc-length walk along the real path (point_at_arc_distance) to place the vertex — but only for position; orientation (which way the fan/tick opens) still always comes from the known-correct outward vector, never from that walk, which is what keeps this from reintroducing the earlier "wrong side" bug a pure-secant approach had. -
[S][P2][gui]Default view on schema load: One Degree around the first table (seed table 0, list selection 0, node selected) -
[S][P2][gui]Centre the seed in the editor viewport whenever the view re-lays-out:EditorContextResetPanningexported through the dcimnodes wrapper (pan = seed pos − window size / 2, the same canvas convention ImNodes uses for its minimap); skips the pan when nothing was re-laid-out, and never fights manual panning -
[S][P2][gui]Schema snapshot viewer (load from file, no DB needed)
Migrate themes from hard-coded Odin procedures to .ssTheme files on
disk. See docs/THEME_MIGRATION.md for detailed plan.
-
[L][P1][theme]Step 1: Write the two.ssThemefiles (paper_and_ink_light.ssTheme,paper_and_ink_dark.ssTheme) translating current colour assignments into[text],[background],[controls],[title_bar],[table_card],[border],[diagram_grid],[accent],[layout]sections -
[M][P1][theme]Step 2: Create theme loader —ThemeDatastruct,parse_ssThemeparser,apply_themewriter, hex/axis-pair helpers. Originally inlined inmain.odin, later extracted tostyle.odin. -
[S][P2][theme]Step 3: Removeset_common_elements(layout values now served as defaults inThemeData) -
[S][P2][theme]Step 4: Remove_imnodes_light_themeand_imnodes_dark_themestubs (already dead code, confirmed gone) -
[S][P2][theme]Step 5: RemoveThemeenum, replace references withThemeData -
[M][P2][theme]Step 6: Replaceset_themecalls withparse_ssTheme("assets/themes/paper_and_ink_light.ssTheme")+apply_theme -
[M][P1][theme]Step 7: Build Theme menu dynamically fromdiscover_themes()scan ofassets/themes/*.ssTheme— removes hardcoded Light/Dark entries, user can drop custom.ssThemefiles in the themes folder (done 2026-08-22) -
[S][P2][theme]Step 8: Delete unused helpers —imn_colkept (used byapply_theme),Themeenum removed, dead stubs removed,set_common_elementsremoved -
[M][P2][theme]Step 9: Test — visual parity with current themes, fallback when file missing, custom.ssThemewith partial overrides
-
[S][P0][cli]CLI mode: extract + print schema to stdout -
[S][P0][cli]Parameterised introspection queries viapragma_table_info(?) -
[S][P2][cli]schema_spelunker dump <file>output snapshot
-
[L][P0][build]Amalgamation build: compile SQLite fromsqlite3.c+sqlite3.hinstead of vendoring binary DLL -
[L][P0][build]Vendor ImGui docking branch source + dcimgui C wrapper + Odin bindings -
[M][P0][build]Vendor SDL3 headers for dcimgui backend compilation -
[S][P0][build]Vendor OpenGL 3.3 core bindings -
[S][P0][build]Fix foreign lib link-prefix split in imgui.odin -
[S][P0][build]Post-processing script for binding regen -
[M][P1][build]Cross-platform: test the build on Linux and macOS (confirmed working on macOS ARM and Linux x64 — ImGui + SQLite) -
[L][P1][build]Linux support:setup.shfor fetching/building deps — not needed, existing_compile_libs.sh+build.shworks on Linux x64 and macOS ARM out of the box -
[S][P2][build]Linux: providesqlite3.aandimgui.afor non-Windows targets -
[S][P2][build]Documentdebugargument inbuild.bat(AGENTS.md lists[run|release|clean]butdebugexists) -
[S][P2][build]Adddebug+ two-position arg support tobuild.shto matchbuild.batsemantics -
[S][P2][build]Replace hardcoded two-position arg checks inbuild.batandbuild.shwith ashiftloop for arbitrary flag ordering -
[S][P2][project]Icon generator:tools/gen_icons.odinrasterises Segoe MDL2 window-control glyphs toassets/icons/*.png(committed). Ported from C on 2026-08-06: the dev-2026-07 foreign-call ABI crash on consecutive stb_truetype rasterise calls is fixed on dev-2026-08 (verified with a 6-glyph repro). Glyph source remains Windows-only (Segoe MDL2 font); committed PNGs keep the runtime cross-platform. -
[S][P2][build]Cross-platform smoke test for the owner-drawn titlebar on Linux/macOS (Nix shell): drag fallback path, vendor:stb/image PNG decode, borderless window — Windows verified only so far -
[S][P2][project]Zed debugger wiring:.zed/debug.jsonwith CodeLLDB launch configs for Windows + Linux;buildfield references the per-OS Debug task so the-o:none -debugbuild runs before launch — 2026-08-21 -
[S][P2][project]Add huge_seed tool (test/huge_seed.odin) for generating large test databases (2000 tables, random FKs). Build viaseed.bat huge. -
[S][P2][project]Update "Current state" section in AGENTS.md (was still saying "empty dockspace")
-
[M][P1][perf]Frame pacing ramp-up:multiplenow increases when 30-frame avg exceeds 95% of target andmultiple < max_multiple. FPS can recover after throttling down (was stuck at lowest throttle forever). -
[S][P1][perf]Refresh rate re-query onWINDOW_DISPLAY_CHANGED:display_id,refresh_rate, and targets recalculated when window moves to another monitor. -
[S][P2][perf]Time build phases with-show-timingsflag in both debug and release -
[M][P2][perf]Time app phases (DB open, introspection queries) usingcore:timeto measure and compare debug vs release performance -
[S][P2][perf]Establish baseline numbers and track regressions across changes
-
[S][P2][gui]Add theme state enum to track current theme -
[S][P2][project]Update AGENTS.md "Current state" section — (was still saying "empty dockspace" and "demo window removed") -
[S][P3][gui]BumpBUF_LENfrom 1024 to 4096 for long Windows paths -
[M][P2][refactor]Extract theme/styling code frommain.odinintostyle.odin(~440 lines moved) -
[S][P2][refactor]Extractrebuild_directory_listingandrender_file_dialogfromshow_file_dialogto reduce nesting depth -
[M][P1][gui]OS-level glass blur: transparent SDL3 window (.TRANSPARENT+ alpha-0 clear +GL_ALPHA_SIZE=8) with per-pixel alpha viaDWMWA_REDIRECTIONBITMAP_ALPHA(Win11 24H2+) + empty-regionDwmEnableBlurBehindWindowfallback, and theme-configurable backdrop ([glass] backdrop = "mica"|"acrylic"|"none"->DWMWA_SYSTEMBACKDROP_TYPE). Implemented inblur_windows.odin, applied after window is shown. Tuned: both themes Mica — lightbackdrop_alpha 0.65(subtle paper tint, effective ~0.87 after dock-host + child alpha stacking), darkbackdrop_alpha 1.0(pixel-exact opaque). Original acrylic @ 0.35 let the desktop show through 65%, washing out the light paper and muddying dark; Mica reads through reliably on both light and dark systems. -
[M][P1][gui]Custom title bar: borderless window (SDL_SetWindowBordered(false)) with client-side min/max/close + drag (WM_NCHITTEST HTCAPTION) — superseded by the owner-drawn titlebar entry below, done viaSDL_SetWindowHitTestDRAGGABLE instead -
[M][P1][gui]Owner-drawn titlebar:.BORDERLESSwindow, title + File/Theme menus + min/max-restore/close buttons (Segoe MDL2 glyphs rasterised toassets/icons/*.pngbytools/gen_icons.odin, decoded at runtime viavendor:stb/image) in the main menu bar. Native drag/snap viaSDL_SetWindowHitTest(returns DRAGGABLE for the titlebar, resize edges at the window border), manualSetWindowPositionfallback when the platform lacks hit-test. Dockspace host offset below the titlebar. Verified on Windows: min/max/restore/close + drag all work.