Skip to content

Add opt-in IsComplete callback to UninterpretedConfig - #11

Open
codeaucafe wants to merge 7 commits into
dolthub:mainfrom
codeaucafe:codeaucafe/feat/10866-add-iscomplete-hook
Open

codeaucafe wants to merge 7 commits into
dolthub:mainfrom
codeaucafe:codeaucafe/feat/10866-add-iscomplete-hook

Conversation

@codeaucafe

@codeaucafe codeaucafe commented Aug 29, 2026 •

Copy link
Copy Markdown

Summary

Add an opt-in IsComplete callback to UninterpretedConfig so consumers can replace the built-in per-line LineTerminator suffix check with their own cumulative-buffer-aware logic. Refactor readUninterpreted and its accumulation helpers to eliminate duplicate terminator-check code and a duplicate accumulation loop.

This is a prerequisite for dolthub/dolt#10866, which fixes four interactive SQL shell bugs that all stem from the per-line suffix check having no awareness of SQL quotes, comments, or escapes.

Consumed by dolthub/dolt#11202, which pins this branch through a temporary go.mod replace until this PR merges. Replaces #8, which was auto-closed by the master to main branch rename.

Changes

IsComplete callback (ishell.go)

  • Add IsComplete func(accumulated, delimiter string) bool to UninterpretedConfig. When set, it replaces the HasSuffix(line, lineTerminator) test in readUninterpreted. The callback receives the full cumulative buffer (all lines typed so far, joined by "\n") and the delimiter currently in effect, and returns true when accumulation should stop.
  • Store the value on the Shell struct as isComplete and copy it in NewUninterpreted.
  • Consumers that leave IsComplete nil see no behavior change; the legacy suffix check runs as before.

lineIsComplete helper (ishell.go)

  • Extract the "should this line stop accumulation?" decision into a new lineIsComplete(line, accumulated string) bool method. Special terminators (\g, \G) and backslash commands always end input. Otherwise it defers to isComplete when set, or falls back to the original HasSuffix check. This removes the two duplicate terminator-check loops that previously existed in the isComplete != nil and isComplete == nil branches.

Accumulation loop refactor (ishell.go)

  • Add readMultiLinesAccumFunc whose predicate receives both the latest line and the cumulative buffer, enabling the IsComplete callback to reason over all input so far.
  • Add readMultiLinesAccumFromReader, the inner loop parameterized on the line-reader and multi-mode toggle functions so it can be driven by unit tests without a real readline instance.
  • Make the existing readMultiLinesFunc delegate to readMultiLinesAccumFunc (ignoring the accumulated argument), removing the previously duplicated loop body.

Tests (uninterpreted_test.go, new file)

Four table-driven tests drive readMultiLinesAccumFromReader directly with canned input lines:

  • TestReadMultiLinesAccumFromReaderTerminator — verifies a cumulative-buffer predicate continues past per-line ; and stops on a custom marker.
  • TestReadMultiLinesAccumFromReaderSingleLine — single line that immediately stops; multi-mode is never entered.
  • TestReadMultiLinesAccumFromReaderEmpty — empty line that immediately stops; multi-mode is never entered. Mirrors the empty-Enter behavior in Dolt shell expects more input after empty line whereas MySQL shell returns nothing dolt#10865.
  • TestReadMultiLinesAccumFromReaderEOFMidStream — predicate keeps reading but EOF arrives; surfaces the error with the buffer collected so far.

…rminations

Add optional IsComplete func(string) bool field to UninterpretedConfig.
When non-nil, readUninterpreted dispatches accumulation through a new
sibling readMultiLinesAccumFunc that exposes the cumulative buffer to
the predicate, enabling consumers (such as the dolt SQL shell) to
implement quote-, comment-, and escape-aware termination instead of
the legacy per-line suffix check.

The legacy line-suffix path is preserved for any consumer that leaves
IsComplete nil, so existing consumers see no behavior change.

Refs: dolthub/dolt#10866
…nterpreted

Change UninterpretedConfig.IsComplete to func(accumulated, delimiter
string) bool so the callback receives the delimiter currently in effect
without the consumer having to capture the shell. Collapse the duplicated
terminator logic in readUninterpreted into a single predicate plus a
lineIsComplete helper, and make readMultiLinesFunc delegate to
readMultiLinesAccumFunc to remove the duplicated accumulation loop.

Refs: dolthub/dolt#10866
The IsComplete callback and supporting lineIsComplete helper were
originally added to allow the accumulation loop to be tested w/ a
standalone function (i.e., readMultiLinesAccumFromReader). Decided
during PR review that this testing approach is unnecessary since
the behavior is covered by Dolt BATS tests.

Remove UninterpretedConfig.IsComplete, Shell.isComplete, and
lineIsComplete. The SQL branch predicate in readUninterpreted now
inlines the special-terminator, backslash-command, and line-terminator
checks directly. Delete the standalone accumulation loop unit tests.
Fix ReadMultiLinesFunc in actions.go to adapt to the two-argument
readMultiLinesFunc predicate.

Refs: dolthub/dolt#10866
Replace per-line readMultiLinesFunc predicate loop with a single
streaming scan of uninterpreted-shell input. Before, each new line
re-evaluated a completeness predicate over the whole accumulated
buffer, and the consumer re-scanned the assembled command, an O(n^2)
re-scan plus a redundant second scan.

Relocate the SQL statement scanner into ishell as StreamScanner and add
a line-at-a-time to io.Reader adapter so the scanner consumes terminal
input directly, blocking for continuation lines only while
mid-statement, and stopping at a special terminator. Parsed statements
are stashed on the shell and delivered to handlers via the new Context
.Statements field, so consumers execute them without scanning again.

- Add StreamScanner: per-byte scan of quotes, comments, escapes, and
multi-byte DELIMITER; relocated from dolt
- Add uninterpretedReader: adapts shell's line reader into an io.Reader,
strips \g and \G terminators from the scan feed while preserving them in
Raw, and switches to the continuation prompt for pulled lines
- Add readUninterpretedCommand: resolve line-level cases (empty,
DELIMITER, --, quit keywords, backslash commands) then scan the rest
- Add Context.Statements to deliver the pre-scanned statements to
handlers
- IgnoreDelimiterStatements avoids a blocking lookahead read on short
complete statements
- Add unit tests for the scanner and the reader adapter

Refs: #10866
Ctrl-D at the continuation prompt was executing whatever had been
typed so far instead of cancelling it. The new uninterpretedReader
feeds a live, blocking readLine directly into the StreamScanner, so
when readLine returns EOF mid-statement the scanner treated the
buffered bytes as a normal EOF-terminated token and ran it. A user
that aborts a half typed statement (e.g. an unterminated DELETE) would
have it executed instead of cancelled, which diverges from the
previous line-accumulation behavior and the mysql client.

Add an aborted flag to uninterpretedReader, and set only when a genuine
EOF (not a \g and \G special terminator, which also ends the reader via
EOF) is reached while pulling a continuation line. When the flag is
set, readUninterpretedCommand discards the collected statements and
returns io.EOF so the shell's run loop routes to its EOF handler
instead of executing the fragment.

- Add uninterpretedReader.aborted: set in Read on a genuine EOF from
readLine while continuing a command
- Check it in readUninterpretedCommand: clear s.statements and
return io.EOF instead of the partial command
- Add TestUninterpretedReaderAbortedOnContinuationEOF and
TestUninterpretedReaderNotAbortedOnSpecialTerminator to pin the
discard-on-Ctrl-D behavior and its \g and \G exception

Refs: dolthub/dolt#10866
Trim each statement before collecting it into Context.Statements so that
entries carry no trailing whitespace or newlines. The scan loop
already computed strings.TrimSpace to skip whitespace-only tokens, but
then stored the untrimmed token. Store the trimmed value instead.

Refs: dolthub/dolt#10866
Restore readMultiLinesFunc to its original func(string) bool predicate
and revert its three callers (ReadMultiLinesFunc, read, and the heredoc
path in readUninterpreted) to match. The accumulated-buffer argument
was left over from the abandoned IsComplete design; after the move to a
single streaming scan in readUninterpretedCommand no caller reads it,
yet building it called lines.String() on every line, copying the whole
buffer each time (quadratic in the number of input lines).

The function and its callers are now identical to upstream master,
which also shrinks the review surface of the PR. No test updates are
needed: no test references the old signature, and go vet and
go test ./... pass. Addresses the 2026-08-19 review comment on
dolthub#8 about repeated lines.String() calls.

Refs: dolthub/dolt#10866
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.

1 participant