temporalio/temporal · error

error reading contents: unmatched `END` keyword

Error message

error reading contents: unmatched `END` keyword

What it means

A bare `END` must close an open `BEGIN` block. When the parser encounters `END` (not followed by IF/LOOP) while its block stack is empty or topped by something other than BEGIN, it returns this error, since the schema script has unbalanced block keywords.

Source

Thrown at common/persistence/query_util.go:147

					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
					}

				case sqlSingleQuote, sqlDoubleQuote:
					quote := contentStr[j]
					j++
					for j < n && contentStr[j] != quote {
						j++
					}
					if j == n {
						return nil, fmt.Errorf("error reading contents: unmatched quotes")
					}

				case sqlLineComment[0]:
					if j+len(sqlLineComment) <= n && contentStr[j:j+len(sqlLineComment)] == sqlLineComment {
						_, _ = stmtBuilder.Write(bytes.TrimRight(content[i:j], " "))

View on GitHub (pinned to bde624efd1)

Solutions

  1. Fix any earlier unbalanced IF/LOOP blocks in the file first — a single early mismatch cascades into this error at the bare END
  2. Ensure the standalone END has a matching BEGIN above it; add or remove accordingly
  3. Diff against the original upstream schema file to locate the lost/extra BEGIN

Example fix

// before
IF x THEN
  UPDATE t SET c = 1;
END;
// after
IF x THEN
  UPDATE t SET c = 1;
END IF;
Defensive patterns

Strategy: validation

Validate before calling

beginDepth := 0
for _, tok := range tokenizeUpper(content) {
    switch tok {
    case "BEGIN":
        beginDepth++
    case "END":
        beginDepth--
        if beginDepth < 0 {
            return errors.New("bare END without matching BEGIN")
        }
    }
}

Prevention

When it happens

Trigger: Calling LoadAndSplitQuery/LoadAndSplitQueryFromReaders/parseSQLStmts on a schema file with a standalone `END;` that has no enclosing `BEGIN`, or where an earlier IF/END mismatch shifted the stack so BEGIN is not on top.

Common situations: An earlier unmatched END IF/END LOOP popping the wrong entry (or an IF closed by bare END), desynchronizing the stack so a legitimate END appears unmatched; truncation dropping a BEGIN; dialects like T-SQL where END pairs with BEGIN differently.

Related errors


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