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
4 changes: 4 additions & 0 deletions oracle/ast/loc.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,8 @@ func NodeLoc(n Node) Loc {
return v.Loc
case *FuncCallExpr:
return v.Loc
case *ExtractExpr:
return v.Loc
case *CaseExpr:
return v.Loc
case *CaseWhen:
Expand All @@ -241,6 +243,8 @@ func NodeLoc(n Node) Loc {
return v.Loc
case *IntervalExpr:
return v.Loc
case *DateTimeLiteral:
return v.Loc
case *ParenExpr:
return v.Loc
case *DecodeExpr:
Expand Down
22 changes: 22 additions & 0 deletions oracle/ast/outfuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ func writeNode(sb *strings.Builder, node Node) {
writeBoolExpr(sb, n)
case *FuncCallExpr:
writeFuncCallExpr(sb, n)
case *ExtractExpr:
writeExtractExpr(sb, n)
case *CaseExpr:
writeCaseExpr(sb, n)
case *CaseWhen:
Expand All @@ -112,6 +114,11 @@ func writeNode(sb *strings.Builder, node Node) {
writeTreatExpr(sb, n)
case *IntervalExpr:
writeIntervalExpr(sb, n)
case *DateTimeLiteral:
sb.WriteString("{DATETIMELIT")
sb.WriteString(fmt.Sprintf(" :typeName %q :val %q", n.TypeName, n.Val))
sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End))
sb.WriteString("}")
case *BetweenExpr:
writeBetweenExpr(sb, n)
case *InExpr:
Expand Down Expand Up @@ -727,6 +734,17 @@ func writeFuncCallExpr(sb *strings.Builder, n *FuncCallExpr) {
sb.WriteString("}")
}

func writeExtractExpr(sb *strings.Builder, n *ExtractExpr) {
sb.WriteString("{EXTRACT")
sb.WriteString(fmt.Sprintf(" :field %q", n.Field))
if n.Expr != nil {
sb.WriteString(" :expr ")
writeNode(sb, n.Expr)
}
sb.WriteString(fmt.Sprintf(" :loc_start %d :loc_end %d", n.Loc.Start, n.Loc.End))
sb.WriteString("}")
}

func writeCaseExpr(sb *strings.Builder, n *CaseExpr) {
sb.WriteString("{CASE")
if n.Arg != nil {
Expand Down Expand Up @@ -2063,6 +2081,10 @@ func writePartitionClause(sb *strings.Builder, n *PartitionClause) {
sb.WriteString(" :columns ")
writeNode(sb, n.Columns)
}
if n.Interval != nil {
sb.WriteString(" :interval ")
writeNode(sb, n.Interval)
}
if n.Partitions != nil {
sb.WriteString(" :partitions ")
writeNode(sb, n.Partitions)
Expand Down
21 changes: 21 additions & 0 deletions oracle/ast/parsenodes.go
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,16 @@ type FuncCallExpr struct {
func (n *FuncCallExpr) nodeTag() {}
func (n *FuncCallExpr) exprNode() {}

// ExtractExpr represents EXTRACT(datetime_field FROM expr).
type ExtractExpr struct {
Field string // YEAR, MONTH, DAY, HOUR, MINUTE, SECOND
Expr ExprNode // source datetime or interval expression
Loc Loc
}

func (n *ExtractExpr) nodeTag() {}
func (n *ExtractExpr) exprNode() {}

// CaseExpr represents a CASE expression.
type CaseExpr struct {
Arg ExprNode // test expression for simple CASE (nil for searched CASE)
Expand Down Expand Up @@ -606,6 +616,16 @@ type StringLiteral struct {
func (n *StringLiteral) nodeTag() {}
func (n *StringLiteral) exprNode() {}

// DateTimeLiteral represents ANSI datetime literals such as DATE '2020-01-01'.
type DateTimeLiteral struct {
TypeName string // DATE, TIMESTAMP
Val string // literal value without quotes
Loc Loc
}

func (n *DateTimeLiteral) nodeTag() {}
func (n *DateTimeLiteral) exprNode() {}

// NumberLiteral represents a numeric literal.
type NumberLiteral struct {
Val string // raw numeric text
Expand Down Expand Up @@ -1537,6 +1557,7 @@ func (n *StorageClause) nodeTag() {}
type PartitionClause struct {
Type PartitionType // RANGE/LIST/HASH
Columns *List // partition columns
Interval ExprNode // INTERVAL (expr), for range interval partitioning
Partitions *List // list of *PartitionDef
Subpartition *PartitionClause // subpartition template
Loc Loc // start location
Expand Down
14 changes: 14 additions & 0 deletions oracle/parser/create_index_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,20 @@ func TestP2CreateIndexMalformedStorageOption(t *testing.T) {
ParseShouldFail(t, "CREATE INDEX ix_sales ON sales (sale_date) STORAGE")
}

func TestCreateIndexFunctionBasedExtract(t *testing.T) {
tests := []string{
"CREATE INDEX idx_sales_month_year ON sales_data(EXTRACT(YEAR FROM sale_date), EXTRACT(MONTH FROM sale_date))",
"CREATE INDEX i_extract_year ON t(EXTRACT(YEAR FROM d))",
"CREATE INDEX i_extract_month ON t(EXTRACT(MONTH FROM d))",
}

for _, sql := range tests {
t.Run(sql, func(t *testing.T) {
ParseAndCheck(t, sql)
})
}
}

func parseCreateIndexForP2(t *testing.T, sql string) *ast.CreateIndexStmt {
t.Helper()
result := ParseAndCheck(t, sql)
Expand Down
25 changes: 25 additions & 0 deletions oracle/parser/create_table.go
Original file line number Diff line number Diff line change
Expand Up @@ -2047,6 +2047,27 @@ func (p *Parser) parsePartitionClause() (*nodes.PartitionClause, error) {
}
}

// Optional INTERVAL (expr) for range-interval partitioned tables.
if p.cur.Type == kwINTERVAL {
p.advance()
if p.cur.Type != '(' {
return nil, p.syntaxErrorAtCur()
}
p.advance()
interval, parseErr560 := p.parseExpr()
if parseErr560 != nil {
return nil, parseErr560
}
if interval == nil {
return nil, p.syntaxErrorAtCur()
}
if p.cur.Type != ')' {
return nil, p.syntaxErrorAtCur()
}
p.advance()
clause.Interval = interval
}

// Optional SUBPARTITION BY
if p.isIdentLike() && p.cur.Str == "SUBPARTITION" {
p.advance()
Expand Down Expand Up @@ -2083,6 +2104,10 @@ func (p *Parser) parsePartitionClause() (*nodes.PartitionClause, error) {
}
}

if clause.Interval != nil && (clause.Partitions == nil || clause.Partitions.Len() == 0) {
return nil, p.syntaxErrorAtCur()
}

clause.Loc.End = p.prev.End
return clause, nil
}
Expand Down
17 changes: 17 additions & 0 deletions oracle/parser/create_table_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,23 @@ func TestP2CreateTableMalformedComplexOption(t *testing.T) {
ParseShouldFail(t, "CREATE TABLE docs (id NUMBER, doc CLOB) LOB STORE AS lob_seg")
}

func TestCreateTableIntervalPartitioning(t *testing.T) {
ParseAndCheck(t, `CREATE TABLE t_interval_full (d DATE)
PARTITION BY RANGE (d)
INTERVAL (NUMTOYMINTERVAL(1,'MONTH'))
(
PARTITION p1 VALUES LESS THAN (TO_DATE('2012-01-01', 'YYYY-MM-DD'))
)`)
}

func TestCreateTableIntervalPartitioningRequiresRangePartition(t *testing.T) {
ParseShouldFail(t, "CREATE TABLE t_interval_min (d DATE) PARTITION BY RANGE (d) INTERVAL (NUMTOYMINTERVAL(1,'MONTH'))")
}

func TestCreateTableDateLiteralPartitionBound(t *testing.T) {
ParseAndCheck(t, "CREATE TABLE t_date_bound (d DATE) PARTITION BY RANGE (d) (PARTITION p1 VALUES LESS THAN (DATE '2020-01-01'))")
}

func parseCreateTableForP2(t *testing.T, sql string) *ast.CreateTableStmt {
t.Helper()
result := ParseAndCheck(t, sql)
Expand Down
65 changes: 65 additions & 0 deletions oracle/parser/expr.go
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,12 @@ func (p *Parser) parsePrimary() (nodes.ExprNode, error) {
case kwCAST:
return p.parseCastExpr()

case kwDATE, kwTIMESTAMP:
if p.peekNext().Type == tokSCONST {
return p.parseDateTimeLiteral()
}
return p.parseIdentExpr()

case kwDECODE:
return p.parseDecodeExpr()

Expand Down Expand Up @@ -413,6 +419,21 @@ func (p *Parser) parsePrimary() (nodes.ExprNode, error) {
}
}

// parseDateTimeLiteral parses ANSI datetime literals such as DATE '2020-01-01'.
func (p *Parser) parseDateTimeLiteral() (nodes.ExprNode, error) {
start := p.pos()
typeTok := p.advance()
if p.cur.Type != tokSCONST {
return nil, p.syntaxErrorAtCur()
}
valueTok := p.advance()
return &nodes.DateTimeLiteral{
TypeName: typeTok.Str,
Val: valueTok.Str,
Loc: nodes.Loc{Start: start, End: p.prev.End},
}, nil
}

// isIntervalField returns true if the current token is an interval date field keyword.
func (p *Parser) isIntervalField() bool {
if !p.isIdentLike() {
Expand Down Expand Up @@ -509,6 +530,9 @@ func (p *Parser) parseIdentExpr() (nodes.ExprNode, error) {
Loc: nodes.Loc{Start: start, End: p.prev.End},
})
}
if name1 == "EXTRACT" {
return p.parseExtractExpr(start)
}
// But first check if this looks like a schema-qualified function: name.name(
// No — just name( is the function call case
return p.parseFuncCall(name1, "", start)
Expand Down Expand Up @@ -598,6 +622,47 @@ func (p *Parser) parseIdentExpr() (nodes.ExprNode, error) {
}, nil
}

// parseExtractExpr parses Oracle EXTRACT(datetime_field FROM expr).
//
// Ref: https://docs.oracle.com/en/database/oracle/oracle-database/23/sqlrf/EXTRACT-datetime.html
//
// EXTRACT({ YEAR | MONTH | DAY | HOUR | MINUTE | SECOND } FROM expr)
func (p *Parser) parseExtractExpr(start int) (nodes.ExprNode, error) {
p.advance() // consume '('

field, parseErr713 := p.parseIdentifier()
if parseErr713 != nil {
return nil, parseErr713
}
if field == "" {
return nil, p.syntaxErrorAtCur()
}

if p.cur.Type != kwFROM {
return nil, p.syntaxErrorAtCur()
}
p.advance()

expr, parseErr714 := p.parseExpr()
if parseErr714 != nil {
return nil, parseErr714
}
if expr == nil {
return nil, p.syntaxErrorAtCur()
}

if p.cur.Type != ')' {
return nil, p.syntaxErrorAtCur()
}
p.advance()

return &nodes.ExtractExpr{
Field: field,
Expr: expr,
Loc: nodes.Loc{Start: start, End: p.prev.End},
}, nil
}

// parseSubscriptIfPresent checks if the current token is '[' and parses a
// MODEL cell reference subscript: expr[dim1, dim2, ...].
// Returns the original expression if no '[' is found.
Expand Down
2 changes: 2 additions & 0 deletions oracle/parser/testdata/coverage/loc_node_coverage.tsv
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ node_type family status fixture notes debt_class approval next_action
*nodes.BoolExpr expression covered SELECT 1 FROM dual WHERE NOT (a = 1 AND b = 2) boolean expression span none covered keep_regression_guard
*nodes.ColumnRef expression covered SELECT a FROM t column reference span none covered keep_regression_guard
*nodes.FuncCallExpr expression covered SELECT f(1) FROM dual function call span none covered keep_regression_guard
*nodes.ExtractExpr expression covered SELECT EXTRACT(YEAR FROM d) FROM t EXTRACT datetime expression span none covered keep_regression_guard
*nodes.CaseExpr expression covered SELECT CASE WHEN a = 1 THEN 2 ELSE 3 END FROM dual case expression span none covered keep_regression_guard
*nodes.CaseWhen ast covered SELECT CASE WHEN a = 1 THEN 2 ELSE 3 END FROM dual case expression span none covered keep_regression_guard
*nodes.CastExpr expression covered SELECT CAST(1 AS NUMBER) FROM dual cast expression span none covered keep_regression_guard
Expand All @@ -124,6 +125,7 @@ node_type family status fixture notes debt_class approval next_action
*nodes.ContainersExpr expression deferred Loc-bearing AST node classified; fixture expansion deferred loc_fixture_deferred oracle-parser-strict-audit-2026-04-28 add_direct_fixture_or_reclassify_node_support
*nodes.NumberLiteral expression covered SELECT 1 FROM dual number literal span none covered keep_regression_guard
*nodes.StringLiteral expression covered SELECT 'x' FROM dual string literal span none covered keep_regression_guard
*nodes.DateTimeLiteral expression covered SELECT DATE '2020-01-01' FROM dual ANSI datetime literal span none covered keep_regression_guard
*nodes.NullLiteral expression covered SELECT NULL FROM dual null literal span none covered keep_regression_guard
*nodes.Star expression covered SELECT * FROM t star span none covered keep_regression_guard
*nodes.BindVariable expression covered SELECT :id FROM dual bind variable span none covered keep_regression_guard
Expand Down
Loading