posthog/bug-verification
Rigorously verify suspected bugs before filing issues. Determines provenance (upstream vs DuckHog), severity, and whether a bug is real. Use whenever a test failure, unexpected behavior, or suspected regression needs to be classified before opening an issue or adding to RELATED_BUGS.md.
npx skills add https://github.com/PostHog/duckhog --skill bug-verification
Prevent spurious issue filings by systematically verifying every suspected bug.
A bug is not confirmed until it has been reproduced through an independent path
and attributed to a specific component with evidence.
0/1 might be a real type conversion bug or just the wrong format specifier in your test.For each suspected bug, execute all steps in order. Do not skip steps.
Write a minimal, focused SQLLogicTest that isolates the behavior. The test
should do exactly one thing and have clear expected output.
# Minimal reproduction — one table, one operation, one assertion
statement ok
CREATE TABLE remote_catalog.schema.t(col TYPE);
statement ok
INSERT INTO remote_catalog.schema.t VALUES (...);
query I
SELECT col FROM remote_catalog.schema.t;
----
expected_value
If the test passes, the bug is not real. Stop here.
If the test fails, record the actual vs expected output and continue.
Connect directly to Duckgres via the Postgres wire protocol and run the
equivalent SQL. This isolates whether the bug is in DuckHog or upstream.
eval "$(./scripts/test-servers.sh env)"
psql -h $PGHOST -p $PGPORT -U $PGUSER <<'SQL'
-- Run the same operations directly, no DuckHog involved
CREATE TABLE ducklake.schema.t(col TYPE);
INSERT INTO ducklake.schema.t VALUES (...);
SELECT col FROM ducklake.schema.t;
DROP TABLE ducklake.schema.t;
SQL
Decision point:
If the bug reproduced via psql, test the same operation on the memory
catalog to determine if it's DuckLake-specific or a broader Duckgres/Arrow issue.
-- Via psql, use memory catalog instead of ducklake
CREATE TABLE memory.main.t(col TYPE);
INSERT INTO memory.main.t VALUES (...);
SELECT col FROM memory.main.t;
Decision point:
Before filing, search all relevant repos:
# DuckLake issues
gh search issues --repo duckdb/ducklake "keyword" --state open
gh search issues --repo duckdb/ducklake "keyword" --state closed
# Duckgres issues
gh search issues --repo PostHog/duckgres "keyword" --state open
# DuckDB issues (for binder/Arrow bugs)
gh search issues --repo duckdb/duckdb "keyword" --state open
Also check recent PRs — the fix may already be merged but unreleased:
gh search prs --repo duckdb/ducklake "keyword" --state merged
Identify the exact code path. For DuckHog bugs, find the file and line:
| Symptom | Likely location |
|---------|-----------------|
| SELECT returns wrong data | arrow_stream.cpp:Produce() — SQL generation |
| INSERT loses/corrupts data | posthog_insert.cpp — chunk iteration, posthog_sql_utils.cpp:BuildInsertSQL |
| RETURNING fails | posthog_dml_rewriter.cpp — CTE wrapping, arrow_value.cpp:ArrowScalarToValue |
| Identifier/quoting errors | arrow_stream.cpp:Produce(), posthog_sql_utils.cpp:QuoteIdent |
| Transaction errors | posthog_transaction_manager.cpp |
| DDL errors | posthog_schema_entry.cpp:RenderAlterTableSQL or CreateTable/CreateIndex etc. |
| Catalog errors | posthog_catalog.cpp, posthog_schema_set.cpp |
For upstream bugs, identify the upstream function from stack traces or error messages.
| Severity | Criteria |
|----------|----------|
| Crash | Server crashes, database invalidated, process killed |
| Silent data loss | Data inserted but not retrievable, rows lost, values corrupted to NULL |
| Feature broken | Operation errors when it should work; clean error message |
| Type fidelity | Values technically accessible but wrong type/representation |
| Limitation | Expected behavior gap, documented |
Create a standalone .test_slow file that proves the bug exists. This test
should be self-contained and deterministic.
For confirmed bugs, use statement error with the expected error substring.
For data bugs where the wrong value is returned, assert the wrong value with
a comment explaining what the correct value should be.
# BUG: arrow_stream.cpp does not escape " in column names (D1)
# Expected: SELECT succeeds and returns 2
# Actual: syntax error because SELECT "has"dq" is invalid SQL
statement error
SELECT "has""dq" FROM remote_catalog.schema.t;
----
syntax error
Add the bug to docs/RELATED_BUGS.md using the established format:
U# prefixD# prefixL# prefixInclude: component, severity, reproduction SQL, root cause analysis,
verification method, workaround, and test file reference.
When retiring roadmap items, closing issues, or documenting limitations, always
verify which catalog the test or reproduction uses. Capabilities differ:
| Catalog | PK/UNIQUE | RETURNING | ON CONFLICT | STRUCT/MAP |
|---------|-----------|-----------|-------------|------------|
| hog:memory | Yes (server-side) | No | Blocked by L2 | TBD |
| hog:ducklake | Never (ducklake#66) | No | Never | TBD |
A feature blocked on hog:ducklake may still be viable on hog:memory (or vice
versa). Attribute the blocker to the correct layer:
Example of getting this wrong: RM16 was retired as "DuckLake will never support
PK/UNIQUE" but the test used hog:memory, where PK/UNIQUE works server-side. The
real blocker was L2 (constraint metadata not synced to local binder).
These patterns frequently look like bugs but aren't:
| Symptom | Likely cause |
|---------|-------------|
| BOOLEAN shows as 0/1 | SQLLogicTest query II (integer specifier) instead of query TI (text) |
| Wrong row count in test | DuckLake metadata accumulation after DROP/CREATE cycles — use MAX(table_id) |
| Table already exists | Memory catalog tables persist across test runs — add DROP TABLE IF EXISTS |
| Env vars not substituted | Missing require-env directives in test file |
| INTERNAL Error in test | SQLLogicTest treats INTERNAL errors as failures even with statement error |
| Wrong sort order | Verify expected values match actual alphabetical/numeric order |
references/provenance-tree.mddocs/RELATED_BUGS.mdCLAUDE.mdTake posthog/bug-verification from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.