Skip to content

Commit 4466b5c

Browse files
authored
Add support for table functions in CREATE TABLE AS statements (#214)
This commit adds support for using table functions like remoteSecure() and remote() in CREATE TABLE AS statements when column definitions are present, and fixes support for AS SELECT without parentheses. The parser now supports table creations such as: - CREATE TABLE (...) AS remoteSecure('host', 'db', 'table', 'user', 'pass') - CREATE TABLE (...) AS remote('host', 'db', 'table') - CREATE TABLE AS SELECT ... (without parentheses) - CREATE TABLE (...) AS SELECT ... (without parentheses)
1 parent 8ca8a97 commit 4466b5c

25 files changed

+427
-58
lines changed

parser/ast.go

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1770,18 +1770,19 @@ func (c *CreateDatabase) Accept(visitor ASTVisitor) error {
17701770
}
17711771

17721772
type CreateTable struct {
1773-
CreatePos Pos // position of CREATE|ATTACH keyword
1774-
StatementEnd Pos
1775-
OrReplace bool
1776-
Name *TableIdentifier
1777-
IfNotExists bool
1778-
UUID *UUID
1779-
OnCluster *ClusterClause
1780-
TableSchema *TableSchemaClause
1781-
Engine *EngineExpr
1782-
SubQuery *SubQuery
1783-
HasTemporary bool
1784-
Comment *StringLiteral
1773+
CreatePos Pos // position of CREATE|ATTACH keyword
1774+
StatementEnd Pos
1775+
OrReplace bool
1776+
Name *TableIdentifier
1777+
IfNotExists bool
1778+
UUID *UUID
1779+
OnCluster *ClusterClause
1780+
TableSchema *TableSchemaClause
1781+
Engine *EngineExpr
1782+
SubQuery *SubQuery
1783+
TableFunction *TableFunctionExpr
1784+
HasTemporary bool
1785+
Comment *StringLiteral
17851786
}
17861787

17871788
func (c *CreateTable) Pos() Pos {
@@ -1829,6 +1830,10 @@ func (c *CreateTable) String() string {
18291830
builder.WriteString(" AS ")
18301831
builder.WriteString(c.SubQuery.String())
18311832
}
1833+
if c.TableFunction != nil {
1834+
builder.WriteString(" AS ")
1835+
builder.WriteString(c.TableFunction.String())
1836+
}
18321837
if c.Comment != nil {
18331838
builder.WriteString(" COMMENT ")
18341839
builder.WriteString(c.Comment.String())
@@ -1867,6 +1872,11 @@ func (c *CreateTable) Accept(visitor ASTVisitor) error {
18671872
return err
18681873
}
18691874
}
1875+
if c.TableFunction != nil {
1876+
if err := c.TableFunction.Accept(visitor); err != nil {
1877+
return err
1878+
}
1879+
}
18701880
return visitor.VisitCreateTable(c)
18711881
}
18721882

@@ -3098,7 +3108,7 @@ func (t *TableSchemaClause) String() string {
30983108
builder.WriteString(t.AliasTable.String())
30993109
}
31003110
if t.TableFunction != nil {
3101-
builder.WriteByte(' ')
3111+
builder.WriteString(" AS ")
31023112
builder.WriteString(t.TableFunction.String())
31033113
}
31043114
return builder.String()

parser/parser_table.go

Lines changed: 74 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -233,12 +233,39 @@ func (p *Parser) parseCreateTable(pos Pos, orReplace bool) (*CreateTable, error)
233233
}
234234

235235
if p.tryConsumeKeywords(KeywordAs) {
236-
subQuery, err := p.parseSubQuery(p.Pos())
237-
if err != nil {
238-
return nil, err
236+
// After AS, we can have: SELECT/WITH (with or without parens), or table_function(...)
237+
// Check if it's a SELECT/WITH query (explicitly check keywords/paren before ident)
238+
if p.matchKeyword(KeywordSelect) || p.matchKeyword(KeywordWith) || p.matchTokenKind(TokenKindLParen) {
239+
// It's a SELECT or WITH query (with or without parentheses)
240+
subQuery, err := p.parseSubQuery(p.Pos())
241+
if err != nil {
242+
return nil, err
243+
}
244+
createTable.SubQuery = subQuery
245+
createTable.StatementEnd = subQuery.End()
246+
} else if p.matchTokenKind(TokenKindIdent) {
247+
// It's a table function: remote(...), remoteSecure(...), etc.
248+
ident, err := p.parseIdent()
249+
if err != nil {
250+
return nil, err
251+
}
252+
if p.matchTokenKind(TokenKindLParen) {
253+
argsExpr, err := p.parseTableArgList(p.Pos())
254+
if err != nil {
255+
return nil, err
256+
}
257+
tableFunc := &TableFunctionExpr{
258+
Name: ident,
259+
Args: argsExpr,
260+
}
261+
createTable.TableFunction = tableFunc
262+
createTable.StatementEnd = tableFunc.End()
263+
} else {
264+
return nil, fmt.Errorf("expected ( after identifier in AS clause, got %q", p.lastTokenKind())
265+
}
266+
} else {
267+
return nil, fmt.Errorf("expected SELECT, WITH or identifier after AS, got %q", p.lastTokenKind())
239268
}
240-
createTable.SubQuery = subQuery
241-
createTable.StatementEnd = subQuery.End()
242269
}
243270

244271
comment, err := p.tryParseComment()
@@ -382,51 +409,53 @@ func (p *Parser) parseTableSchemaClause(pos Pos) (*TableSchemaClause, error) {
382409
SchemaEnd: rightParenPos,
383410
Columns: columns,
384411
}, nil
385-
case p.tryConsumeKeywords(KeywordAs):
412+
case p.matchKeyword(KeywordAs) && !p.peekKeyword(KeywordSelect) && !p.peekKeyword(KeywordWith) && !p.peekTokenKind(TokenKindLParen):
413+
// Handle AS only if followed by identifier (not SELECT/WITH/LPAREN)
414+
// This handles: AS ident, AS ident.ident, AS ident(...)
415+
// CREATE TABLE will handle: AS SELECT, AS WITH, AS (SELECT ...)
416+
p.tryConsumeKeywords(KeywordAs)
417+
418+
ident, err := p.parseIdent()
419+
if err != nil {
420+
return nil, err
421+
}
386422
switch {
387-
case p.matchTokenKind(TokenKindIdent):
388-
ident, err := p.parseIdent()
423+
case p.matchTokenKind(TokenKindDot):
424+
// it's a database.table
425+
dotIdent, err := p.tryParseDotIdent(p.Pos())
389426
if err != nil {
390427
return nil, err
391428
}
392-
switch {
393-
case p.matchTokenKind(TokenKindDot):
394-
// it's a database.table
395-
dotIdent, err := p.tryParseDotIdent(p.Pos())
396-
if err != nil {
397-
return nil, err
398-
}
399-
return &TableSchemaClause{
400-
SchemaPos: pos,
401-
SchemaEnd: dotIdent.End(),
402-
AliasTable: &TableIdentifier{
403-
Database: ident,
404-
Table: dotIdent,
405-
},
406-
}, nil
407-
case p.matchTokenKind(TokenKindLParen):
408-
// it's a table function
409-
argsExpr, err := p.parseTableArgList(pos)
410-
if err != nil {
411-
return nil, err
412-
}
413-
return &TableSchemaClause{
414-
SchemaPos: pos,
415-
SchemaEnd: p.End(),
416-
TableFunction: &TableFunctionExpr{
417-
Name: ident,
418-
Args: argsExpr,
419-
},
420-
}, nil
421-
default:
422-
return &TableSchemaClause{
423-
SchemaPos: pos,
424-
SchemaEnd: p.End(),
425-
AliasTable: &TableIdentifier{
426-
Table: ident,
427-
},
428-
}, nil
429+
return &TableSchemaClause{
430+
SchemaPos: pos,
431+
SchemaEnd: dotIdent.End(),
432+
AliasTable: &TableIdentifier{
433+
Database: ident,
434+
Table: dotIdent,
435+
},
436+
}, nil
437+
case p.matchTokenKind(TokenKindLParen):
438+
// it's a table function
439+
argsExpr, err := p.parseTableArgList(pos)
440+
if err != nil {
441+
return nil, err
429442
}
443+
return &TableSchemaClause{
444+
SchemaPos: pos,
445+
SchemaEnd: p.End(),
446+
TableFunction: &TableFunctionExpr{
447+
Name: ident,
448+
Args: argsExpr,
449+
},
450+
}, nil
451+
default:
452+
return &TableSchemaClause{
453+
SchemaPos: pos,
454+
SchemaEnd: p.End(),
455+
AliasTable: &TableIdentifier{
456+
Table: ident,
457+
},
458+
}, nil
430459
}
431460
}
432461
// no schema is ok for MATERIALIZED VIEW
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
-- CREATE TABLE with columns AS table function (remoteSecure)
2+
CREATE TABLE test_remote
3+
(
4+
id UInt64,
5+
name String,
6+
value Int32
7+
)
8+
AS remoteSecure('host.example.com', 'source_db', 'source_table', 'user', 'password');
9+
10+
-- Simpler test case with remote()
11+
CREATE TABLE test_table (id UInt64, name String) AS remote('localhost', 'db', 'source_table');
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
-- Origin SQL:
2+
-- CREATE TABLE with columns AS table function (remoteSecure)
3+
CREATE TABLE test_remote
4+
(
5+
id UInt64,
6+
name String,
7+
value Int32
8+
)
9+
AS remoteSecure('host.example.com', 'source_db', 'source_table', 'user', 'password');
10+
11+
-- Simpler test case with remote()
12+
CREATE TABLE test_table (id UInt64, name String) AS remote('localhost', 'db', 'source_table');
13+
14+
15+
-- Format SQL:
16+
CREATE TABLE test_remote (id UInt64, name String, value Int32) AS remoteSecure('host.example.com', 'source_db', 'source_table', 'user', 'password');
17+
CREATE TABLE test_table (id UInt64, name String) AS remote('localhost', 'db', 'source_table');

parser/testdata/ddl/output/attach_table_basic.sql.golden.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -480,6 +480,7 @@
480480
}
481481
},
482482
"SubQuery": null,
483+
"TableFunction": null,
483484
"HasTemporary": false,
484485
"Comment": null
485486
}

parser/testdata/ddl/output/create_distributed_table.sql.golden.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@
140140
"OrderBy": null
141141
},
142142
"SubQuery": null,
143+
"TableFunction": null,
143144
"HasTemporary": false,
144145
"Comment": null
145146
}

parser/testdata/ddl/output/create_or_replace.sql.golden.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@
317317
}
318318
},
319319
"SubQuery": null,
320+
"TableFunction": null,
320321
"HasTemporary": false,
321322
"Comment": {
322323
"LiteralPos": 372,

0 commit comments

Comments
 (0)