Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 11 additions & 14 deletions oracle/parser/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -233,29 +233,26 @@ func (l *Lexer) lexBlockCommentOrHint() Token {
l.pos++ // skip +
}

// Oracle block comments do not nest: the first */ terminates the
// comment regardless of any /* sequences inside it. Engine-verified in
// both directions on 11gR2: SELECT 1 /* a /* b */ FROM dual succeeds,
// while appending a second */ is ORA-00936 — the shape that would be
// legal under nesting semantics. (PostgreSQL nests; this is a genuine
// per-engine divergence, so the PG lexer must keep its counting.)
var buf strings.Builder
depth := 1
terminated := false

for l.pos < len(l.input) && depth > 0 {
if l.input[l.pos] == '/' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '*' {
depth++
buf.WriteString("/*")
l.pos += 2
continue
}
for l.pos < len(l.input) {
if l.input[l.pos] == '*' && l.pos+1 < len(l.input) && l.input[l.pos+1] == '/' {
depth--
if depth > 0 {
buf.WriteString("*/")
}
l.pos += 2
continue
terminated = true
break
}
buf.WriteByte(l.input[l.pos])
l.pos++
}

if depth > 0 {
if !terminated {
l.Err = fmt.Errorf("unterminated block comment")
return Token{Type: tokEOF, Loc: l.start}
}
Expand Down
103 changes: 103 additions & 0 deletions oracle/parser/nonnested_comment_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package parser

import (
"strings"
"testing"
)

// Oracle block comments do not nest: the first */ terminates the comment
// regardless of /* sequences inside it. Engine-verified on 11gR2 in both
// directions (BYT-9963):
//
// SELECT 1 /* a /* b */ FROM dual -- succeeds (comment ends at first */)
// SELECT 2 /* a /* b */ */ FROM dual -- ORA-00936 (legal only under nesting)
//
// PostgreSQL nests block comments; that is a genuine per-engine divergence,
// so these fences are Oracle-only and the PG lexer keeps its counting.

func TestLexerNonNestedBlockComment(t *testing.T) {
tests := []struct {
name string
sql string
wantErr bool
}{
{"innerOpenIgnored", "SELECT 1 /* a /* b */ FROM dual", false},
{"strayCloseAfterComment", "SELECT 2 /* a /* b */ */ FROM dual", true},
{"plainComment", "SELECT 1 /* plain */ FROM dual", false},
{"unterminated", "SELECT 1 /* never closed", true},
{"unterminatedWithInnerOpen", "SELECT 1 /* a /* b", true},
{"hintEndsAtFirstClose", "SELECT /*+ FULL(t) /* x */ c FROM t", false},
{"division", "SELECT 4 / 2 FROM dual", false},
{"openMarkerInString", "SELECT '/*' FROM dual", false},
{"closeMarkerInString", "SELECT '*/' FROM dual", false},
{"openMarkerInLineComment", "SELECT 1 FROM dual -- /*\n", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
_, err := Parse(tt.sql)
if tt.wantErr && err == nil {
t.Fatalf("want error, got none")
}
if !tt.wantErr && err != nil {
t.Fatalf("want success, got %v", err)
}
})
}
}

// The customer shape (BYT-9963, MBBank): a commented-out code region that
// itself contains a /* marker, inside a package body terminated by a
// standalone SQL*Plus slash. The slash must be recognized as a delimiter,
// not retained inside the SQL segment.
func TestSplitNonNestedCommentSlashDelimiter(t *testing.T) {
sql := "create or replace procedure p as\nbegin\n /* outer /* inner */\n null;\nend;\n/\n"
segs := Split(sql)

// The standalone slash is a pure delimiter: it must not appear in any
// segment (see TestSplitSlashDelimiter for the established contract).
var sqlSegs int
for _, s := range segs {
if s.Kind == SegmentSQLPlusCommand {
continue
}
sqlSegs++
if strings.Contains(s.Text, "\n/") || strings.HasSuffix(strings.TrimSpace(s.Text), "/") {
t.Fatalf("SQL segment retains slash delimiter: %q", s.Text)
}
}
if sqlSegs != 1 {
t.Fatalf("got %d SQL segments, want 1 (all: %#v)", sqlSegs, segs)
}
}

// Two-unit script in the customer file's structure: spec, slash, body whose
// nested-marker comment must not swallow the terminator, slash.
func TestSplitCustomerShapePackagePair(t *testing.T) {
sql := strings.Join([]string{
"create or replace package pkg as\n procedure q;\nend pkg;",
"/",
"create or replace package body pkg as\n procedure q as\n begin\n /*old := 1;\n /*\n if x then\n y;\n end if;*/\n null;\n end;\nend pkg;",
"/",
"",
}, "\n")
segs := Split(sql)

var sqlTexts []string
for _, s := range segs {
if s.Kind == SegmentSQLPlusCommand {
continue
}
sqlTexts = append(sqlTexts, s.Text)
}
if len(sqlTexts) != 2 {
t.Fatalf("got %d SQL segments, want 2", len(sqlTexts))
}
for i, txt := range sqlTexts {
if strings.HasSuffix(strings.TrimSpace(txt), "/") {
t.Fatalf("SQL segment %d retains trailing slash", i)
}
}
if !strings.Contains(sqlTexts[1], "end pkg;") {
t.Fatalf("body segment truncated: %q", sqlTexts[1][:80])
}
}
22 changes: 10 additions & 12 deletions oracle/parser/split.go
Original file line number Diff line number Diff line change
Expand Up @@ -764,20 +764,18 @@ func splitSkipLineComment(sql string, i int) int {
return i
}

// splitSkipBlockComment advances past an Oracle block comment. Oracle block
// comments do not nest — the first */ terminates the comment (engine-verified;
// see lexBlockCommentOrHint) — so the scan must stay byte-identical to the
// lexer's rule or the splitter and parser would disagree on statement
// boundaries around comments containing a stray /*.
func splitSkipBlockComment(sql string, i int) (int, bool) {
i += 2
depth := 1
for i < len(sql) && depth > 0 {
switch {
case sql[i] == '/' && i+1 < len(sql) && sql[i+1] == '*':
depth++
i += 2
case sql[i] == '*' && i+1 < len(sql) && sql[i+1] == '/':
depth--
i += 2
default:
i++
for i < len(sql) {
if sql[i] == '*' && i+1 < len(sql) && sql[i+1] == '/' {
return i + 2, true
}
i++
}
return i, depth == 0
return i, false
}
Loading