Skip to content

fix: table-level CHECK constraints with IS NOT NULL silently omitted from dump (#396)#398

Merged
tianzhou merged 1 commit intomainfrom
fix/issue-396-check-constraint-is-not-null
Apr 10, 2026
Merged

fix: table-level CHECK constraints with IS NOT NULL silently omitted from dump (#396)#398
tianzhou merged 1 commit intomainfrom
fix/issue-396-check-constraint-is-not-null

Conversation

@tianzhou
Copy link
Copy Markdown
Contributor

Summary

  • Table-level CHECK constraints containing IS NOT NULL in complex expressions were silently dropped from pgschema dump output
  • Root cause: strings.Contains(checkClause, "IS NOT NULL") in ir/inspector.go was too broad — it skipped ANY check constraint mentioning IS NOT NULL, not just simple system-generated NOT NULL checks
  • Fix narrows the filter to only skip simple CHECK ((identifier IS NOT NULL)) patterns by stripping the CHECK wrapper/parentheses and verifying the prefix is a single word

Fixes #396

Test plan

  • Added dump test issue_396_check_constraint_is_not_null with a table containing a multi-condition CHECK using IS NOT NULL
  • Verified existing constraint tests pass (issue_78, issue_83, online/, create_table/)
  • Run: go test -v ./cmd/dump -run TestDumpCommand_Issue396

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings April 10, 2026 15:02
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 10, 2026

Greptile Summary

This PR fixes a bug where table-level CHECK constraints containing IS NOT NULL in complex expressions were silently dropped from pgschema dump. The root cause was an overly broad strings.Contains check in ir/inspector.go; the fix narrows it to only skip simple single-identifier CHECK ((col IS NOT NULL)) patterns by stripping the CHECK wrapper/parentheses and confirming the prefix is a single token with no whitespace.

Confidence Score: 5/5

Safe to merge — the fix correctly resolves the reported bug and all remaining findings are P2 robustness notes.

The fix addresses the root cause precisely and is well-tested with a new dump fixture. The one P2 note (function-call parens being stripped before the space-check) is a theoretical edge case that PostgreSQL never triggers with its auto-generated NOT NULL constraints, so it does not affect real-world correctness.

ir/inspector.go — the paren-stripping logic has a theoretical edge case worth hardening in a follow-up.

Important Files Changed

Filename Overview
ir/inspector.go Core fix: replaces broad strings.Contains with a structured parse; works correctly for all normal cases but strips all parentheses before the identifier check, which could misclassify function-call expressions like lower(col) as simple identifiers
cmd/dump/dump_integration_test.go Adds integration test entry for the new issue_396 dump fixture; follows the existing pattern correctly
testdata/dump/issue_396_check_constraint_is_not_null/raw.sql Test input with a multi-condition OR check using IS NOT NULL in two branches — representative of the reported bug
testdata/dump/issue_396_check_constraint_is_not_null/pgschema.sql Expected pgschema dump output; correctly includes the CHECK constraint that was previously silently dropped

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[checkClause from pg_get_constraintdef] --> B["TrimPrefix 'CHECK '"]
    B --> C[Strip ALL parentheses]
    C --> D{"Ends with ' IS NOT NULL'?"}
    D -- No --> G[Keep constraint]
    D -- Yes --> E{"Prefix has spaces or tabs?"}
    E -- Yes --> G
    E -- No --> F[Skip constraint as simple NOT NULL]
    F -.->|"Edge case: lower(col) becomes lowercol, no spaces, wrongly skipped"| H[Theoretical gap]
Loading

Reviews (1): Last reviewed commit: "fix: table-level CHECK constraints with ..." | Re-trigger Greptile

ir/inspector.go Outdated
// expression is a simple "identifier IS NOT NULL" — not complex expressions
// that happen to contain IS NOT NULL (e.g., "status = 'x' OR reason IS NOT NULL").
inner := strings.TrimPrefix(strings.TrimSpace(checkClause), "CHECK ")
inner = strings.TrimSpace(strings.NewReplacer("(", "", ")", "").Replace(inner))
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Parenthesis stripping can fuse function-call names into a single token

strings.NewReplacer("(", "", ")", "").Replace(inner) removes every paren character from the whole string before the space-check. For an expression like CHECK ((lower(col) IS NOT NULL)) the call becomes lowercol IS NOT NULL — prefix lowercol has no spaces, so the constraint is silently skipped even though it is a user-defined constraint, not a system-generated NOT NULL check.

A regex anchored to the canonical double-paren form PostgreSQL emits (CHECK ((ident IS NOT NULL))) would be more precise and avoids this class of false positives:

var simpleNotNullRe = regexp.MustCompile(
    `^CHECK\s*\(\s*\(\s*[A-Za-z_][A-Za-z0-9_$]*\s+IS NOT NULL\s*\)\s*\)$`,
)
// …
if simpleNotNullRe.MatchString(strings.TrimSpace(checkClause)) {
    continue
}

In practice, PostgreSQL does not auto-generate NOT NULL checks that involve function calls, so the current code is safe for the described issue. This is a robustness note for future-proofing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Already addressed — the pushed commit (ad88724) replaced the blanket strings.NewReplacer approach with balanced outer-paren peeling using isBalancedParentheses, which only strips matched outer parentheses and leaves inner parens (like function calls) intact. So CHECK ((lower(col) IS NOT NULL)) correctly peels to lower(col) IS NOT NULL, and the space in lower(col) prevents it from being skipped.

…from dump (#396)

The inspector was using strings.Contains(checkClause, "IS NOT NULL") to skip
system-generated NOT NULL constraints, but this incorrectly filtered out any
CHECK constraint whose expression contained IS NOT NULL — including complex
user-defined constraints like CHECK (status = 'active' OR reason IS NOT NULL).

Narrowed the filter to only skip simple single-identifier NOT NULL checks
(e.g., CHECK ((value IS NOT NULL))) by stripping the CHECK wrapper and
parentheses and verifying the prefix is a single word with no spaces.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@tianzhou tianzhou force-pushed the fix/issue-396-check-constraint-is-not-null branch from 51deaab to ad88724 Compare April 10, 2026 15:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes a schema-dump correctness bug where table-level CHECK constraints containing IS NOT NULL inside more complex boolean expressions were being incorrectly filtered out during inspection, and adds an integration test fixture for issue #396.

Changes:

  • Narrowed the inspector’s “skip redundant NOT NULL CHECK constraints” logic to avoid dropping complex CHECK expressions that merely contain IS NOT NULL.
  • Added a new dump integration test case + fixture dataset reproducing issue #396.
  • Registered the new integration test in cmd/dump’s test suite.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
ir/inspector.go Updates the CHECK constraint filtering logic intended to skip redundant NOT NULL checks.
cmd/dump/dump_integration_test.go Adds a new integration test to run the issue #396 fixture.
testdata/dump/issue_396_check_constraint_is_not_null/raw.sql Defines the reproduction schema (complex CHECK with IS NOT NULL).
testdata/dump/issue_396_check_constraint_is_not_null/pgdump.sql Captures the pg_dump input used by the integration test harness.
testdata/dump/issue_396_check_constraint_is_not_null/pgschema.sql Captures the expected pgschema dump output asserting the CHECK is preserved.
testdata/dump/issue_396_check_constraint_is_not_null/manifest.json Documents the fixture and links it to GitHub issue #396.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +524 to +528
inner = strings.TrimSpace(inner)
for len(inner) > 2 && inner[0] == '(' && inner[len(inner)-1] == ')' && isBalancedParentheses(inner[1:len(inner)-1]) {
inner = strings.TrimSpace(inner[1 : len(inner)-1])
}
if prefix, ok := strings.CutSuffix(inner, " IS NOT NULL"); ok {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The NOT NULL CHECK skipping logic is still too permissive: removing all parentheses and then only checking for whitespace means expressions like CHECK (lower(col) IS NOT NULL) or CHECK (foo(bar) IS NOT NULL) become lowercol IS NOT NULL / foobar IS NOT NULL and will be incorrectly treated as a “simple identifier IS NOT NULL”, causing legitimate CHECK constraints to be omitted again. Consider stripping only the outer CHECK wrapper/parentheses (balanced), then matching the remaining expression against a strict identifier regex (quoted or unquoted) + IS NOT NULL, rather than deleting all parentheses in the expression.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This concern applied to the first commit but is already fixed in the current code. The pushed version uses isBalancedParentheses to peel only matched outer parentheses — it does not remove all parens. After peeling, CHECK ((lower(col) IS NOT NULL)) becomes lower(col) IS NOT NULL, the prefix lower(col) contains a space, and the constraint is correctly preserved.

@tianzhou tianzhou merged commit 194f72e into main Apr 10, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Table-level CHECK constraints omitted from schema dump

2 participants