@@ -278,3 +278,99 @@ func (p *Parser) supportsTableFunction() bool {
278278 }
279279 return false
280280}
281+
282+ // parseSnowflakeTimeTravel parses the Snowflake time-travel / change-tracking
283+ // modifier attached to a table reference. The current token must be one of
284+ // AT / BEFORE / CHANGES. Returns the head clause with any additional clauses
285+ // appended to Chained (e.g. CHANGES (...) AT (...)).
286+ func (p * Parser ) parseSnowflakeTimeTravel () (* ast.TimeTravelClause , error ) {
287+ head , err := p .parseOneTimeTravelClause ()
288+ if err != nil {
289+ return nil , err
290+ }
291+ // Allow additional clauses: CHANGES (...) AT (...) is legal.
292+ for p .isSnowflakeTimeTravelStart () {
293+ next , err := p .parseOneTimeTravelClause ()
294+ if err != nil {
295+ return nil , err
296+ }
297+ head .Chained = append (head .Chained , next )
298+ }
299+ return head , nil
300+ }
301+
302+ func (p * Parser ) parseOneTimeTravelClause () (* ast.TimeTravelClause , error ) {
303+ pos := p .currentLocation ()
304+ kind := strings .ToUpper (p .currentToken .Token .Value )
305+ p .advance () // Consume AT / BEFORE / CHANGES
306+ if ! p .isType (models .TokenTypeLParen ) {
307+ return nil , p .expectedError ("( after " + kind )
308+ }
309+ p .advance () // Consume (
310+
311+ clause := & ast.TimeTravelClause {
312+ Kind : kind ,
313+ Named : map [string ]ast.Expression {},
314+ Pos : pos ,
315+ }
316+
317+ // Parse comma-separated named arguments: name => expr [, name => expr]...
318+ // Snowflake uses TIMESTAMP, OFFSET, STATEMENT, INFORMATION as argument
319+ // names; these tokenize as dedicated keyword types, not identifiers.
320+ // Accept any non-punctuation token with a non-empty value as the name.
321+ for {
322+ argName := strings .ToUpper (p .currentToken .Token .Value )
323+ if argName == "" || p .isType (models .TokenTypeRParen ) ||
324+ p .isType (models .TokenTypeComma ) || p .isType (models .TokenTypeLParen ) {
325+ return nil , p .expectedError ("argument name in " + kind )
326+ }
327+ p .advance ()
328+ if p .currentToken .Token .Type != models .TokenTypeRArrow {
329+ return nil , p .expectedError ("=> after " + argName )
330+ }
331+ p .advance () // =>
332+ // Values are typically literal expressions, but may also be bare
333+ // keywords like DEFAULT or APPEND_ONLY for CHANGES (INFORMATION => …).
334+ var value ast.Expression
335+ if v , err := p .parseExpression (); err == nil {
336+ value = v
337+ } else if p .currentToken .Token .Value != "" &&
338+ ! p .isType (models .TokenTypeRParen ) && ! p .isType (models .TokenTypeComma ) {
339+ value = & ast.Identifier {Name : p .currentToken .Token .Value }
340+ p .advance ()
341+ } else {
342+ return nil , err
343+ }
344+ clause .Named [argName ] = value
345+ if p .isType (models .TokenTypeComma ) {
346+ p .advance ()
347+ continue
348+ }
349+ break
350+ }
351+
352+ if ! p .isType (models .TokenTypeRParen ) {
353+ return nil , p .expectedError (")" )
354+ }
355+ p .advance () // Consume )
356+ return clause , nil
357+ }
358+
359+ // isSnowflakeTimeTravelStart returns true when the current token begins an
360+ // AT / BEFORE / CHANGES time-travel clause in the Snowflake dialect.
361+ func (p * Parser ) isSnowflakeTimeTravelStart () bool {
362+ if p .dialect != string (keywords .DialectSnowflake ) {
363+ return false
364+ }
365+ // BEFORE / CHANGES: plain identifier or keyword
366+ val := strings .ToUpper (p .currentToken .Token .Value )
367+ if val == "BEFORE" || val == "CHANGES" {
368+ // Must be followed by '(' to disambiguate from other uses.
369+ return p .peekToken ().Token .Type == models .TokenTypeLParen
370+ }
371+ // AT: either TokenTypeAt (@) or an identifier-token "AT" followed by '('.
372+ if val == "AT" && p .peekToken ().Token .Type == models .TokenTypeLParen {
373+ return true
374+ }
375+ return false
376+ }
0 commit comments