@@ -28,6 +28,7 @@ import (
2828 goerrors "github.com/ajitpratap0/GoSQLX/pkg/errors"
2929 "github.com/ajitpratap0/GoSQLX/pkg/models"
3030 "github.com/ajitpratap0/GoSQLX/pkg/sql/ast"
31+ "github.com/ajitpratap0/GoSQLX/pkg/sql/keywords"
3132)
3233
3334// parseExpression parses an expression with OR operators (lowest precedence)
@@ -213,6 +214,17 @@ func (p *Parser) parseJSONExpression() (ast.Expression, error) {
213214 return nil , err
214215 }
215216
217+ // Snowflake VARIANT path: `expr:field[.field|[idx]]*`. Must run before
218+ // the `::` cast loop so that `col:a.b::int` casts the full path rather
219+ // than treating the path as a bare expression.
220+ if p .dialect == string (keywords .DialectSnowflake ) && p .isType (models .TokenTypeColon ) {
221+ vp , err := p .parseSnowflakeVariantPath (left )
222+ if err != nil {
223+ return nil , err
224+ }
225+ left = vp
226+ }
227+
216228 // Handle type casting (::) with highest precedence
217229 // PostgreSQL: expr::type (e.g., '123'::integer, column::text)
218230 for p .isType (models .TokenTypeDoubleColon ) {
@@ -380,3 +392,81 @@ func (p *Parser) isJSONOperator() bool {
380392 }
381393 return false
382394}
395+
396+ // parseSnowflakeVariantPath parses the tail of a Snowflake VARIANT path
397+ // expression. The current token must be `:`. Returns a VariantPath with
398+ // the given root and the parsed segments.
399+ //
400+ // Grammar:
401+ //
402+ // path := ':' step ( '.' field | '[' expr ']' )*
403+ // step := field | '"' quoted '"'
404+ // field := identifier
405+ func (p * Parser ) parseSnowflakeVariantPath (root ast.Expression ) (* ast.VariantPath , error ) {
406+ pos := p .currentLocation ()
407+ if ! p .isType (models .TokenTypeColon ) {
408+ return nil , p .expectedError (":" )
409+ }
410+ p .advance () // Consume leading :
411+
412+ vp := & ast.VariantPath {Root : root , Pos : pos }
413+
414+ // First segment must be a field name (identifier or string literal).
415+ name , err := p .parseVariantFieldName ()
416+ if err != nil {
417+ return nil , err
418+ }
419+ vp .Segments = append (vp .Segments , ast.VariantPathSegment {Name : name })
420+
421+ // Subsequent segments: `.field` | `[expr]` | `:field` (rare).
422+ for {
423+ switch {
424+ case p .isType (models .TokenTypePeriod ):
425+ p .advance ()
426+ n , err := p .parseVariantFieldName ()
427+ if err != nil {
428+ return nil , err
429+ }
430+ vp .Segments = append (vp .Segments , ast.VariantPathSegment {Name : n })
431+ case p .isType (models .TokenTypeColon ):
432+ p .advance ()
433+ n , err := p .parseVariantFieldName ()
434+ if err != nil {
435+ return nil , err
436+ }
437+ vp .Segments = append (vp .Segments , ast.VariantPathSegment {Name : n })
438+ case p .isType (models .TokenTypeLBracket ):
439+ p .advance () // Consume [
440+ idx , err := p .parseExpression ()
441+ if err != nil {
442+ return nil , err
443+ }
444+ if ! p .isType (models .TokenTypeRBracket ) {
445+ return nil , p .expectedError ("]" )
446+ }
447+ p .advance () // Consume ]
448+ vp .Segments = append (vp .Segments , ast.VariantPathSegment {Index : idx })
449+ default :
450+ return vp , nil
451+ }
452+ }
453+ }
454+
455+ // parseVariantFieldName consumes one VARIANT path field name, which may be
456+ // a bare identifier or a double-quoted string. Returns the name and
457+ // advances past it.
458+ func (p * Parser ) parseVariantFieldName () (string , error ) {
459+ tok := p .currentToken .Token
460+ switch {
461+ case p .isIdentifier (), p .isType (models .TokenTypeDoubleQuotedString ):
462+ name := tok .Value
463+ p .advance ()
464+ return name , nil
465+ case p .isType (models .TokenTypeKeyword ):
466+ // Keywords may appear as field names (e.g. TYPE, VALUE, STATUS).
467+ name := tok .Value
468+ p .advance ()
469+ return name , nil
470+ }
471+ return "" , p .expectedError ("field name after `:` or `.`" )
472+ }
0 commit comments