Skip to content

Commit 1ddac5f

Browse files
kyleconroyclaude
andcommitted
Add FOR SYSTEM_TIME temporal clause parsing
- Add TemporalClause type to AST with TemporalClauseType, StartTime, EndTime fields - Parse FOR SYSTEM_TIME AS OF, BETWEEN...AND, FROM...TO, CONTAINED IN, and ALL clauses - Support string literals and variables as time values - Add TemporalClause field to NamedTableReference - Add JSON marshaling for TemporalClause Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d8db13f commit 1ddac5f

5 files changed

Lines changed: 164 additions & 5 deletions

File tree

ast/named_table_reference.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,20 @@ package ast
44
type NamedTableReference struct {
55
SchemaObject *SchemaObjectName `json:"SchemaObject,omitempty"`
66
TableSampleClause *TableSampleClause `json:"TableSampleClause,omitempty"`
7+
TemporalClause *TemporalClause `json:"TemporalClause,omitempty"`
78
Alias *Identifier `json:"Alias,omitempty"`
89
TableHints []TableHintType `json:"TableHints,omitempty"`
910
ForPath bool `json:"ForPath,omitempty"`
1011
}
1112

1213
func (*NamedTableReference) node() {}
1314
func (*NamedTableReference) tableReference() {}
15+
16+
// TemporalClause represents a FOR SYSTEM_TIME clause for temporal tables.
17+
type TemporalClause struct {
18+
TemporalClauseType string `json:"TemporalClauseType,omitempty"`
19+
StartTime ScalarExpression `json:"StartTime,omitempty"`
20+
EndTime ScalarExpression `json:"EndTime,omitempty"`
21+
}
22+
23+
func (*TemporalClause) node() {}

parser/marshal.go

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2575,16 +2575,19 @@ func tableReferenceToJSON(ref ast.TableReference) jsonNode {
25752575
if r.SchemaObject != nil {
25762576
node["SchemaObject"] = schemaObjectNameToJSON(r.SchemaObject)
25772577
}
2578-
if r.TableSampleClause != nil {
2579-
node["TableSampleClause"] = tableSampleClauseToJSON(r.TableSampleClause)
2580-
}
25812578
if len(r.TableHints) > 0 {
25822579
hints := make([]jsonNode, len(r.TableHints))
25832580
for i, h := range r.TableHints {
25842581
hints[i] = tableHintToJSON(h)
25852582
}
25862583
node["TableHints"] = hints
25872584
}
2585+
if r.TableSampleClause != nil {
2586+
node["TableSampleClause"] = tableSampleClauseToJSON(r.TableSampleClause)
2587+
}
2588+
if r.TemporalClause != nil {
2589+
node["TemporalClause"] = temporalClauseToJSON(r.TemporalClause)
2590+
}
25882591
if r.Alias != nil {
25892592
node["Alias"] = identifierToJSON(r.Alias)
25902593
}
@@ -3728,6 +3731,22 @@ func tableSampleClauseToJSON(tsc *ast.TableSampleClause) jsonNode {
37283731
return node
37293732
}
37303733

3734+
func temporalClauseToJSON(tc *ast.TemporalClause) jsonNode {
3735+
node := jsonNode{
3736+
"$type": "TemporalClause",
3737+
}
3738+
if tc.TemporalClauseType != "" {
3739+
node["TemporalClauseType"] = tc.TemporalClauseType
3740+
}
3741+
if tc.StartTime != nil {
3742+
node["StartTime"] = scalarExpressionToJSON(tc.StartTime)
3743+
}
3744+
if tc.EndTime != nil {
3745+
node["EndTime"] = scalarExpressionToJSON(tc.EndTime)
3746+
}
3747+
return node
3748+
}
3749+
37313750
func tableHintToJSON(h ast.TableHintType) jsonNode {
37323751
switch th := h.(type) {
37333752
case *ast.TableHint:

parser/parse_select.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2856,6 +2856,15 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
28562856
ForPath: false,
28572857
}
28582858

2859+
// Parse FOR SYSTEM_TIME clause (temporal tables)
2860+
if p.curTok.Type == TokenIdent && strings.ToUpper(p.curTok.Literal) == "FOR" && strings.ToUpper(p.peekTok.Literal) == "SYSTEM_TIME" {
2861+
temporal, err := p.parseTemporalClause()
2862+
if err != nil {
2863+
return nil, err
2864+
}
2865+
ref.TemporalClause = temporal
2866+
}
2867+
28592868
// Check for TABLESAMPLE before alias
28602869
if strings.ToUpper(p.curTok.Literal) == "TABLESAMPLE" {
28612870
tableSample, err := p.parseTableSampleClause()
@@ -2978,6 +2987,127 @@ func (p *Parser) parseNamedTableReferenceWithName(son *ast.SchemaObjectName) (*a
29782987
return ref, nil
29792988
}
29802989

2990+
// parseTemporalClause parses a FOR SYSTEM_TIME clause for temporal tables
2991+
func (p *Parser) parseTemporalClause() (*ast.TemporalClause, error) {
2992+
clause := &ast.TemporalClause{}
2993+
2994+
p.nextToken() // consume FOR
2995+
p.nextToken() // consume SYSTEM_TIME
2996+
2997+
upper := strings.ToUpper(p.curTok.Literal)
2998+
switch upper {
2999+
case "AS":
3000+
// AS OF <time>
3001+
p.nextToken() // consume AS
3002+
if strings.ToUpper(p.curTok.Literal) != "OF" {
3003+
return nil, fmt.Errorf("expected OF after AS, got %s", p.curTok.Literal)
3004+
}
3005+
p.nextToken() // consume OF
3006+
clause.TemporalClauseType = "AsOf"
3007+
startTime, err := p.parseTemporalTimeValue()
3008+
if err != nil {
3009+
return nil, err
3010+
}
3011+
clause.StartTime = startTime
3012+
3013+
case "BETWEEN":
3014+
// BETWEEN <start> AND <end>
3015+
p.nextToken() // consume BETWEEN
3016+
clause.TemporalClauseType = "Between"
3017+
startTime, err := p.parseTemporalTimeValue()
3018+
if err != nil {
3019+
return nil, err
3020+
}
3021+
clause.StartTime = startTime
3022+
if p.curTok.Type != TokenAnd {
3023+
return nil, fmt.Errorf("expected AND, got %s", p.curTok.Literal)
3024+
}
3025+
p.nextToken() // consume AND
3026+
endTime, err := p.parseTemporalTimeValue()
3027+
if err != nil {
3028+
return nil, err
3029+
}
3030+
clause.EndTime = endTime
3031+
3032+
case "FROM":
3033+
// FROM <start> TO <end>
3034+
p.nextToken() // consume FROM
3035+
clause.TemporalClauseType = "FromTo"
3036+
startTime, err := p.parseTemporalTimeValue()
3037+
if err != nil {
3038+
return nil, err
3039+
}
3040+
clause.StartTime = startTime
3041+
if strings.ToUpper(p.curTok.Literal) != "TO" {
3042+
return nil, fmt.Errorf("expected TO, got %s", p.curTok.Literal)
3043+
}
3044+
p.nextToken() // consume TO
3045+
endTime, err := p.parseTemporalTimeValue()
3046+
if err != nil {
3047+
return nil, err
3048+
}
3049+
clause.EndTime = endTime
3050+
3051+
case "CONTAINED":
3052+
// CONTAINED IN (<start>, <end>)
3053+
p.nextToken() // consume CONTAINED
3054+
if strings.ToUpper(p.curTok.Literal) != "IN" {
3055+
return nil, fmt.Errorf("expected IN after CONTAINED, got %s", p.curTok.Literal)
3056+
}
3057+
p.nextToken() // consume IN
3058+
if p.curTok.Type != TokenLParen {
3059+
return nil, fmt.Errorf("expected ( after CONTAINED IN, got %s", p.curTok.Literal)
3060+
}
3061+
p.nextToken() // consume (
3062+
clause.TemporalClauseType = "ContainedIn"
3063+
startTime, err := p.parseTemporalTimeValue()
3064+
if err != nil {
3065+
return nil, err
3066+
}
3067+
clause.StartTime = startTime
3068+
if p.curTok.Type != TokenComma {
3069+
return nil, fmt.Errorf("expected comma, got %s", p.curTok.Literal)
3070+
}
3071+
p.nextToken() // consume ,
3072+
endTime, err := p.parseTemporalTimeValue()
3073+
if err != nil {
3074+
return nil, err
3075+
}
3076+
clause.EndTime = endTime
3077+
if p.curTok.Type != TokenRParen {
3078+
return nil, fmt.Errorf("expected ), got %s", p.curTok.Literal)
3079+
}
3080+
p.nextToken() // consume )
3081+
3082+
case "ALL":
3083+
// ALL
3084+
p.nextToken() // consume ALL
3085+
clause.TemporalClauseType = "TemporalAll"
3086+
3087+
default:
3088+
return nil, fmt.Errorf("unexpected temporal clause type: %s", p.curTok.Literal)
3089+
}
3090+
3091+
return clause, nil
3092+
}
3093+
3094+
// parseTemporalTimeValue parses a time value in a temporal clause (string literal or variable)
3095+
func (p *Parser) parseTemporalTimeValue() (ast.ScalarExpression, error) {
3096+
if p.curTok.Type == TokenString || p.curTok.Type == TokenNationalString {
3097+
lit, err := p.parseStringLiteral()
3098+
if err != nil {
3099+
return nil, err
3100+
}
3101+
return lit, nil
3102+
}
3103+
if p.curTok.Type == TokenIdent && strings.HasPrefix(p.curTok.Literal, "@") {
3104+
varRef := &ast.VariableReference{Name: p.curTok.Literal}
3105+
p.nextToken()
3106+
return varRef, nil
3107+
}
3108+
return nil, fmt.Errorf("expected string literal or variable for temporal time, got %s", p.curTok.Literal)
3109+
}
3110+
29813111
// parseFullTextTableReference parses CONTAINSTABLE or FREETEXTTABLE
29823112
func (p *Parser) parseFullTextTableReference(funcType string) (*ast.FullTextTableReference, error) {
29833113
ref := &ast.FullTextTableReference{
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
{"todo": true}
1+
{}

0 commit comments

Comments
 (0)