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
7 changes: 7 additions & 0 deletions oracle/ast/outfuncs.go
Original file line number Diff line number Diff line change
Expand Up @@ -1793,9 +1793,16 @@ func writeInlineExternalTable(sb *strings.Builder, n *InlineExternalTable) {
if n.Directory != "" {
sb.WriteString(fmt.Sprintf(" :directory %q", n.Directory))
}
if n.AccessParams != "" {
sb.WriteString(fmt.Sprintf(" :accessParams %q", n.AccessParams))
}
if n.Location != "" {
sb.WriteString(fmt.Sprintf(" :location %q", n.Location))
}
if n.RejectLimit != nil {
sb.WriteString(" :rejectLimit ")
writeNode(sb, n.RejectLimit)
}
if n.Alias != nil {
sb.WriteString(" :alias ")
writeNode(sb, n.Alias)
Expand Down
60 changes: 60 additions & 0 deletions oracle/parser/inline_external_table_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package parser

import (
"testing"

"github.com/bytebase/omni/oracle/ast"
)

func TestParseInlineExternalTable(t *testing.T) {
result := ParseAndCheck(t, `
SELECT ext.id, ext.name
FROM EXTERNAL (
(id NUMBER, name VARCHAR2(100))
TYPE ORACLE_LOADER
DEFAULT DIRECTORY data_dir
ACCESS PARAMETERS (FIELDS TERMINATED BY ',')
LOCATION ('file.csv')
REJECT LIMIT UNLIMITED
) ext`)

raw := result.Items[0].(*ast.RawStmt)
stmt := raw.Stmt.(*ast.SelectStmt)
if stmt.FromClause == nil || stmt.FromClause.Len() != 1 {
t.Fatalf("FromClause len = %d, want 1", stmt.FromClause.Len())
}

ext, ok := stmt.FromClause.Items[0].(*ast.InlineExternalTable)
if !ok {
t.Fatalf("FROM item = %T, want *ast.InlineExternalTable", stmt.FromClause.Items[0])
}
if ext.Columns == nil || ext.Columns.Len() != 2 {
t.Fatalf("Columns len = %d, want 2", ext.Columns.Len())
}
col0 := ext.Columns.Items[0].(*ast.ColumnDef)
if col0.Name != "ID" || col0.TypeName == nil {
t.Fatalf("first column = %#v, want ID NUMBER", col0)
}
col1 := ext.Columns.Items[1].(*ast.ColumnDef)
if col1.Name != "NAME" || col1.TypeName == nil {
t.Fatalf("second column = %#v, want NAME VARCHAR2(100)", col1)
}
if ext.Type != "ORACLE_LOADER" {
t.Fatalf("Type = %q, want ORACLE_LOADER", ext.Type)
}
if ext.Directory != "DATA_DIR" {
t.Fatalf("Directory = %q, want DATA_DIR", ext.Directory)
}
if ext.AccessParams == "" {
t.Fatal("AccessParams is empty, want captured ACCESS PARAMETERS text")
}
if ext.Location != "file.csv" {
t.Fatalf("Location = %q, want file.csv", ext.Location)
}
if _, ok := ext.RejectLimit.(*ast.ColumnRef); !ok {
t.Fatalf("RejectLimit = %T, want *ast.ColumnRef for UNLIMITED", ext.RejectLimit)
}
if ext.Alias == nil || ext.Alias.Name != "EXT" {
t.Fatalf("Alias = %#v, want EXT", ext.Alias)
}
}
217 changes: 217 additions & 0 deletions oracle/parser/select.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,11 @@ func (p *Parser) parseTableRef() (nodes.TableExpr, error) {
return p.parseTableCollectionExpr(start)
}

// EXTERNAL ((columns) TYPE ... DEFAULT DIRECTORY ... LOCATION ...)
if p.isIdentLikeStr("EXTERNAL") && p.peekNext().Type == '(' {
return p.parseInlineExternalTable(start)
}

// CONTAINERS(table) or SHARDS(table)
if p.isIdentLikeStr("CONTAINERS") && p.peekNext().Type == '(' {
return p.parseContainersOrShards(start, false)
Expand Down Expand Up @@ -817,6 +822,218 @@ func (p *Parser) parseSubqueryRef(start int) (nodes.TableExpr, error) {
return ref, nil
}

// parseInlineExternalTable parses Oracle's inline external table source.
//
// inline_external_table:
// EXTERNAL ( ( column_definition [, column_definition ]... )
// TYPE external_table_type
// DEFAULT DIRECTORY directory
// [ ACCESS PARAMETERS { ( opaque_format_spec ) | USING CLOB subquery } ]
// LOCATION ( 'file_uri_list' )
// [ REJECT LIMIT { integer | UNLIMITED } ] ) [ alias ]
func (p *Parser) parseInlineExternalTable(start int) (nodes.TableExpr, error) {
p.advance() // consume EXTERNAL
if p.cur.Type != '(' {
return nil, p.syntaxErrorAtCur()
}
p.advance() // consume outer '('

if p.cur.Type != '(' {
return nil, p.syntaxErrorAtCur()
}
p.advance() // consume column-definition '('

ref := &nodes.InlineExternalTable{
Columns: &nodes.List{},
Loc: nodes.Loc{Start: start},
}
for {
col, err := p.parseColumnDef()
if err != nil {
return nil, err
}
if col == nil {
return nil, p.syntaxErrorAtCur()
}
ref.Columns.Items = append(ref.Columns.Items, col)
if p.cur.Type != ',' {
break
}
p.advance()
}
if p.cur.Type != ')' {
return nil, p.syntaxErrorAtCur()
}
p.advance() // consume column-definition ')'

if p.cur.Type != kwTYPE {
return nil, p.syntaxErrorAtCur()
}
p.advance()
var err error
ref.Type, err = p.parseIdentifier()
if err != nil {
return nil, err
}
if ref.Type == "" {
return nil, p.syntaxErrorAtCur()
}

if p.cur.Type != kwDEFAULT {
return nil, p.syntaxErrorAtCur()
}
p.advance()
if p.cur.Type != kwDIRECTORY {
return nil, p.syntaxErrorAtCur()
}
p.advance()
ref.Directory, err = p.parseIdentifier()
if err != nil {
return nil, err
}
if ref.Directory == "" {
return nil, p.syntaxErrorAtCur()
}

if p.cur.Type == kwACCESS {
p.advance()
if !p.isIdentLikeStr("PARAMETERS") {
return nil, p.syntaxErrorAtCur()
}
p.advance()
ref.AccessParams, err = p.parseInlineExternalAccessParams()
if err != nil {
return nil, err
}
}

if !p.isIdentLikeStr("LOCATION") {
return nil, p.syntaxErrorAtCur()
}
p.advance()
ref.Location, err = p.parseInlineExternalLocation()
if err != nil {
return nil, err
}

if p.cur.Type == kwREJECT {
p.advance()
if p.cur.Type != kwLIMIT {
return nil, p.syntaxErrorAtCur()
}
p.advance()
ref.RejectLimit, err = p.parseExpr()
if err != nil {
return nil, err
}
if ref.RejectLimit == nil {
return nil, p.syntaxErrorAtCur()
}
}

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

if p.cur.Type == kwAS {
p.advance()
ref.Alias, err = p.parseAlias()
if err != nil {
return nil, err
}
} else if p.isTableAliasCandidate() {
ref.Alias, err = p.parseAlias()
if err != nil {
return nil, err
}
}

ref.Loc.End = p.prev.End
return ref, nil
}

func (p *Parser) parseInlineExternalAccessParams() (string, error) {
if p.cur.Type == '(' {
return p.collectBalancedRaw()
}
start := p.pos()
depth := 0
for p.cur.Type != tokEOF {
if depth == 0 && p.isIdentLikeStr("LOCATION") {
break
}
switch p.cur.Type {
case '(':
depth++
case ')':
if depth == 0 {
return "", p.syntaxErrorAtCur()
}
depth--
}
p.advance()
}
if p.cur.Type == tokEOF {
return "", p.syntaxErrorAtCur()
}
return strings.TrimSpace(p.source[start:p.cur.Loc]), nil
}

func (p *Parser) parseInlineExternalLocation() (string, error) {
if p.cur.Type != '(' {
return "", p.syntaxErrorAtCur()
}
p.advance()
start := p.pos()
var files []string
depth := 0
for p.cur.Type != tokEOF {
if depth == 0 && p.cur.Type == ')' {
raw := strings.TrimSpace(p.source[start:p.cur.Loc])
p.advance()
if len(files) == 1 {
return files[0], nil
}
return raw, nil
}
if p.cur.Type == tokSCONST {
files = append(files, p.cur.Str)
}
switch p.cur.Type {
case '(':
depth++
case ')':
depth--
}
p.advance()
}
return "", p.syntaxErrorAtCur()
}

func (p *Parser) collectBalancedRaw() (string, error) {
if p.cur.Type != '(' {
return "", p.syntaxErrorAtCur()
}
p.advance()
start := p.pos()
depth := 1
for p.cur.Type != tokEOF {
if p.cur.Type == '(' {
depth++
} else if p.cur.Type == ')' {
depth--
if depth == 0 {
raw := strings.TrimSpace(p.source[start:p.cur.Loc])
p.advance()
return raw, nil
}
}
p.advance()
}
return "", p.syntaxErrorAtCur()
}

// parseLateralRef parses a LATERAL inline view.
//
// LATERAL ( subquery ) [ alias ]
Expand Down
Loading