temporalio/temporal · error
error reading contents: unmatched `END IF` keyword
Error message
error reading contents: unmatched `END IF` keyword
What it means
LoadAndSplitQueryFromReaders parses schema SQL/CQL containing BEGIN/IF/LOOP blocks and splits statements. This error means the parser hit an `END IF` while its block stack was empty or its top was not an `IF`, i.e. the schema file has an END IF with no matching IF. It is a parse-time validation of the schema script.
Source
Thrown at common/persistence/query_util.go:134
if !hasWordAt(contentStr, sqlLoopKeyword, j) {
continue
}
st = append(st, sqlLoopKeyword[0])
j += len(sqlLoopKeyword) - 1
case sqlBeginKeyword[0]:
if hasWordAt(contentStr, sqlBeginKeyword, j) {
st = append(st, sqlBeginKeyword[0])
j += len(sqlBeginKeyword) - 1
}
case sqlEndKeyword[0]:
if !hasWordAt(contentStr, sqlEndKeyword, j) {
continue
}
if ok, after := hasWordAfter(contentStr, sqlIfKeyword, j+len(sqlEndKeyword)); ok {
if len(st) == 0 || st[len(st)-1] != sqlIfKeyword[0] {
return nil, errors.New("error reading contents: unmatched `END IF` keyword")
}
st = st[:len(st)-1]
j = after + len(sqlIfKeyword) - 1
} else if ok, after := hasWordAfter(contentStr, sqlLoopKeyword, j+len(sqlEndKeyword)); ok {
//nolint:revive
if len(st) == 0 || st[len(st)-1] != sqlLoopKeyword[0] {
return nil, errors.New("error reading contents: unmatched `END LOOP` keyword")
}
st = st[:len(st)-1]
j = after + len(sqlLoopKeyword) - 1
} else {
if len(st) == 0 || st[len(st)-1] != sqlBeginKeyword[0] {
return nil, errors.New("error reading contents: unmatched `END` keyword")
}
st = st[:len(st)-1]
j += len(sqlEndKeyword) - 1
}
View on GitHub (pinned to bde624efd1)
Solutions
- Open the schema file and pair every `IF ... THEN` with its `END IF`; fix or remove the unmatched END IF
- Check whether the opening IF is commented out or inside a string; restore it
- Diff the file against the upstream schema to spot a dropped/renumbered block
- If content is generated, fix the generator so IF/END IF are emitted together
Example fix
// before (schema.cql) DROP INDEX IF EXISTS idx; END IF; // after IF x THEN DROP INDEX idx; END IF;
Defensive patterns
Strategy: validation
Validate before calling
func checkIfEndIfBalance(content string) error {
depth := 0
words := strings.Fields(strings.ToUpper(content))
for _, w := range words {
if w == "IF" { depth++ }
if w == "END IF" || (w == "END") { depth-- } // approximate; use the library's tokenizer for exactness
if depth < 0 { return errors.New("END IF without matching IF") }
}
if depth != 0 { return errors.New("unclosed IF") }
return nil
} Prevention
- Keep IF/END IF pairs adjacent and indented so mismatches are visually obvious
- Never leave block keywords inside comments/strings in a way that mimics real syntax
- Validate schema files with LoadAndSplitQueryFromReaders in CI before shipping schema updates
- Diff hand-edits against upstream schema files
When it happens
Trigger: Calling LoadAndSplitQuery/LoadAndSplitQueryFromReaders (or parseSQLStmts/Run) on a schema file whose text contains `END IF` without a preceding `IF ... THEN` at the same nesting level, e.g. comments or strings containing the literal keywords confusing nesting, or a deleted/misplaced IF block.
Common situations: Hand-edited schema CQL where an IF block was removed but its END IF remained; keywords appearing inside string literals/comments in an unexpected order; a file concatenated in the wrong order; a mistakenly uppercase/lowercase variant that skips IF detection while still matching END IF.
Related errors
- error reading contents: unmatched `END LOOP` keyword
- error reading contents: unmatched `END` keyword
- unable to insert workflow execution: %w
- unable to insert custom search attributes: %w
- unable to insert chasm search attributes: %w
AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01).
Data as JSON: /api/errors/682821a1cef6145c.
Report an issue: GitHub.