temporalio/temporal · error

error reading contents: unmatched `END LOOP` keyword

Error message

error reading contents: unmatched `END LOOP` keyword

What it means

During statement splitting, the parser maintains a stack of BEGIN/IF/LOOP blocks. Reaching `END LOOP` when the stack is empty or the top entry is not LOOP means the schema text has an END LOOP without a matching LOOP, so LoadAndSplitQueryFromReaders aborts with this error.

Source

Thrown at common/persistence/query_util.go:141

					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
					}

				case sqlSingleQuote, sqlDoubleQuote:
					quote := contentStr[j]
					j++
					for j < n && contentStr[j] != quote {
						j++
					}
					if j == n {

View on GitHub (pinned to bde624efd1)

Solutions

  1. Locate the END LOOP in the schema file and ensure a matching LOOP appears earlier at the same nesting depth
  2. Restore the missing LOOP block or delete the stray END LOOP
  3. Verify nesting order (BEGIN > IF/LOOP) is properly interleaved; reorder END statements so each END matches the innermost open block

Example fix

// before
BEGIN
  INSERT ...
END LOOP;
// after
BEGIN
  FOR r IN list LOOP
    INSERT ...;
  END LOOP;
END;
Defensive patterns

Strategy: validation

Validate before calling

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

Prevention

When it happens

Trigger: Calling LoadAndSplitQuery/LoadAndSplitQueryFromReaders on a schema file containing `END LOOP` whose corresponding `LOOP` was never opened (or was closed early/nested incorrectly).

Common situations: Copied CQL from another database dialect where LOOP syntax differs; a LOOP removed during editing leaving its END LOOP; nested BEGIN/LOOP pairs closed in the wrong order causing stack mismatch.

Related errors


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