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

  1. Open the schema file and pair every `IF ... THEN` with its `END IF`; fix or remove the unmatched END IF
  2. Check whether the opening IF is commented out or inside a string; restore it
  3. Diff the file against the upstream schema to spot a dropped/renumbered block
  4. 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

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


AI-assisted analysis of temporalio/temporal@bde624efd1 (2026-09-01). Data as JSON: /api/errors/682821a1cef6145c. Report an issue: GitHub.