Debugging

Debugging

Debug database operations, TUI state, tree structures, and performance issues in ModulaCMS.

Use Structured Logging

ModulaCMS uses structured logging throughout the application. The logger outputs to stderr with timestamps and caller information.

Log Levels

utility.DefaultLogger.Debug("Processing node", "node_id", nodeID, "parent_id", parentID)
utility.DefaultLogger.Info("Content tree loaded", "route_id", routeID, "nodes", nodeCount)
utility.DefaultLogger.Warn("Orphaned node detected", "node_id", nodeID)
utility.DefaultLogger.Error("Failed to load tree", "error", err, "route_id", routeID)
utility.DefaultLogger.Fatal("Database connection failed", "error", err)

Use key-value pairs instead of string formatting:

// Structured (preferred)
utility.DefaultLogger.Info("User authenticated", "user_id", userID, "provider", "github")

// Unstructured (avoid)
utility.DefaultLogger.Info(fmt.Sprintf("User %d authenticated via %s", userID, provider))

Enable Debug Output

Set the MODULACMS_DEBUG environment variable to enable debug-level logging:

export MODULACMS_DEBUG=true
modula

Diagnose Database Issues

Reproduce Query Failures

When a query returns unexpected results or fails, log the parameters before execution, then test the same query directly in the database:

# SQLite
sqlite3 modula.db "SELECT * FROM content_data WHERE route_id = 1"

# MySQL
mysql -u root -p -e "SELECT * FROM content_data WHERE route_id = 1"

# PostgreSQL
psql -U postgres -c "SELECT * FROM content_data WHERE route_id = 1"

If the query works in the database but fails in Go, check the sqlc-generated code to verify parameter types and return types match.

Fix Foreign Key Violations

Error: FOREIGN KEY constraint failed

This means you are referencing a parent record that does not exist. Verify all referenced IDs before the insert:

-- Check if the referenced route exists
SELECT route_id FROM routes WHERE route_id = 1;

-- Check if the referenced datatype exists
SELECT datatype_id FROM datatypes WHERE datatype_id = 5;

To see the foreign key definitions on a table:

-- SQLite
PRAGMA foreign_key_list(content_data);

-- MySQL
SHOW CREATE TABLE content_data;

-- PostgreSQL
SELECT conname, confrelid::regclass
FROM pg_constraint
WHERE contype = 'f' AND conrelid = 'content_data'::regclass;

Handle NULL Values

SQLite returns sql.NullString and sql.NullInt64 types. Check .Valid before using the value:

if node.Instance.ParentID.Valid {
    parentID := node.Instance.ParentID.Int64
    // use parentID
} else {
    // this is a root node (no parent)
}

Good to know: Reading .Int64 without checking .Valid returns 0 for NULL, which can look like a valid ID in some contexts.

Investigate Driver-Specific Behavior

A query that works in SQLite may fail in MySQL or PostgreSQL due to:

  • Different placeholder syntax (? vs $1, $2)
  • Different NULL handling
  • Different type widths (int64 vs int32)

Check the sqlc-generated code for each backend to confirm the query syntax is correct.

Diagnose TUI State Issues

Trace Message Flow

The TUI follows the Elm Architecture: messages trigger state changes in Update, which are reflected in View. When the TUI stops responding or shows incorrect state, add logging to the Update function:

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    utility.DefaultLogger.Debug("Update called",
        "msg_type", fmt.Sprintf("%T", msg),
        "current_mode", m.Mode)

    // handle messages
}

Fix Common TUI Problems

Screen is blank or frozen: Check that the SSH session has an active PTY. The TUI requires a terminal -- non-interactive SSH sessions fail. Also verify that terminal dimensions are non-zero.

Commands stop executing: If Update() returns nil for tea.Cmd when it should return a command, the message loop stalls. Every asynchronous operation must return a tea.Cmd that produces a tea.Msg.

State inconsistency: Add validation after state changes to catch cursor out-of-bounds, nil TreeRoot, or other invalid states early.

Diagnose Tree Problems

Find Orphaned Nodes

WARN Orphaned node detected node_id=789 parent_id=999

A node references a parent_id that does not exist in the query result. Find orphaned nodes in the database:

SELECT cd.content_data_id, cd.parent_id
FROM content_data cd
LEFT JOIN content_data parent ON cd.parent_id = parent.content_data_id
WHERE cd.parent_id IS NOT NULL
  AND parent.content_data_id IS NULL;

Fix by setting the orphaned node's parent to NULL (making it a root), or by correcting the parent_id to a valid node.

Break Circular References

ERROR Circular reference detected node_id=123 parent_id=456

A node eventually references itself through its parent chain. Find the cycle with a recursive query:

WITH RECURSIVE chain AS (
    SELECT content_data_id, parent_id, 1 as depth,
           CAST(content_data_id AS TEXT) as path
    FROM content_data
    WHERE content_data_id = 123

    UNION ALL

    SELECT cd.content_data_id, cd.parent_id, c.depth + 1,
           c.path || ' -> ' || cd.content_data_id
    FROM content_data cd
    JOIN chain c ON cd.content_data_id = c.parent_id
    WHERE c.depth < 100
)
SELECT * FROM chain ORDER BY depth;

Break the cycle by setting one node's parent_id to NULL:

UPDATE content_data SET parent_id = NULL WHERE content_data_id = 123;

Fix Missing Root Node

Error: no root node found

The content tree for a route has no node with a NULL parent_id. Every route needs exactly one root node:

SELECT * FROM content_data WHERE route_id = 1 AND parent_id IS NULL;

Debug with Delve

Install the Go debugger:

go install github.com/go-delve/delve/cmd/dlv@latest

Debug a specific test:

dlv test ./internal/model -- -test.run TestTreeLoading

Debug the running application:

dlv debug ./cmd -- serve

Useful commands inside Delve:

Command Action
break file.go:123 Set breakpoint at line
break Model.Update Set breakpoint at function
continue Run to next breakpoint
next Step over
step Step into
stepout Step out of function
print var Print variable value
locals Show local variables
args Show function arguments
stack Show stack trace
goroutines List all goroutines
condition 1 var == value Conditional breakpoint

Profile Performance

CPU Profiling

go test -cpuprofile=cpu.prof ./internal/model
go tool pprof cpu.prof

Inside pprof:

(pprof) top10            # Top 10 functions by CPU time
(pprof) list FunctionName # Source with timing annotations
(pprof) web              # Open graphical view in browser

Memory Profiling

go test -memprofile=mem.prof ./internal/model
go tool pprof mem.prof

Check for memory leaks at runtime:

var m runtime.MemStats
runtime.ReadMemStats(&m)
utility.DefaultLogger.Info("Memory stats",
    "alloc_mb", m.Alloc / 1024 / 1024,
    "goroutines", runtime.NumGoroutine())

Detect Goroutine Leaks

If goroutine count grows continuously, use runtime.NumGoroutine() to track it, or enable the pprof HTTP endpoint:

import _ "net/http/pprof"

go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Then inspect goroutines at http://localhost:6060/debug/pprof/goroutine?debug=2.

Follow the Debugging Checklist

When stuck on an issue:

  1. Add logging at entry and exit of the problematic function
  2. Log all input parameters and return values
  3. Check for nil pointers before dereferencing
  4. Verify database constraints and foreign keys
  5. Test the query directly in the database
  6. Check for race conditions with go test -race
  7. Review recent changes with git diff
  8. Simplify to a minimal reproduction case
  9. Add a unit test that reproduces the issue