@@ -139,6 +139,8 @@ import type {
139139 SimpleSelectCstChildren ,
140140 SnapshotStatementCstChildren ,
141141 SpliceJoinCstChildren ,
142+ HorizonJoinCstChildren ,
143+ HorizonOffsetCstChildren ,
142144 StandardJoinCstChildren ,
143145 StatementCstChildren ,
144146 StatementsCstChildren ,
@@ -659,6 +661,7 @@ class QuestDBVisitor extends BaseVisitor {
659661 if ( ctx . asofLtJoin ) return this . visit ( ctx . asofLtJoin ) as AST . JoinClause
660662 if ( ctx . spliceJoin ) return this . visit ( ctx . spliceJoin ) as AST . JoinClause
661663 if ( ctx . windowJoin ) return this . visit ( ctx . windowJoin ) as AST . JoinClause
664+ if ( ctx . horizonJoin ) return this . visit ( ctx . horizonJoin ) as AST . JoinClause
662665 return this . visit ( ctx . standardJoin ! ) as AST . JoinClause
663666 }
664667
@@ -711,6 +714,60 @@ class QuestDBVisitor extends BaseVisitor {
711714 return result
712715 }
713716
717+ horizonJoin ( ctx : HorizonJoinCstChildren ) : AST . JoinClause {
718+ // Build TableRef from tableName + alias
719+ const tableRef : AST . TableRef = {
720+ type : "tableRef" ,
721+ table : this . visit ( ctx . tableName ) as AST . QualifiedName ,
722+ }
723+ if ( ctx . Identifier ) {
724+ // Implicit alias (bare identifier, not a keyword)
725+ tableRef . alias = ctx . Identifier [ 0 ] . image
726+ } else if ( ctx . identifier && ctx . identifier . length > 1 ) {
727+ // Explicit alias (AS <id>): first identifier is table alias, last is horizon alias
728+ tableRef . alias = (
729+ this . visit ( ctx . identifier [ 0 ] ) as AST . QualifiedName
730+ ) . parts [ 0 ]
731+ }
732+
733+ const result : AST . JoinClause = {
734+ type : "join" ,
735+ joinType : "horizon" ,
736+ table : tableRef ,
737+ }
738+ if ( ctx . expression ) {
739+ result . on = this . visit ( ctx . expression ) as AST . Expression
740+ }
741+ if ( ctx . Range ) {
742+ // RANGE form: horizonOffset[0]=from, [1]=to, [2]=step
743+ const offsets = ctx . horizonOffset !
744+ result . horizonRange = {
745+ from : this . visit ( offsets [ 0 ] ) as string ,
746+ to : this . visit ( offsets [ 1 ] ) as string ,
747+ step : this . visit ( offsets [ 2 ] ) as string ,
748+ }
749+ } else if ( ctx . List ) {
750+ // LIST form: all horizonOffset children are list entries
751+ result . horizonList = ctx . horizonOffset ! . map (
752+ ( o ) => this . visit ( o ) as string ,
753+ )
754+ }
755+ // Horizon alias is always the last identifier
756+ if ( ctx . identifier ) {
757+ const lastId = ctx . identifier [ ctx . identifier . length - 1 ]
758+ result . horizonAlias = ( this . visit ( lastId ) as AST . QualifiedName ) . parts [ 0 ]
759+ }
760+ return result
761+ }
762+
763+ horizonOffset ( ctx : HorizonOffsetCstChildren ) : string {
764+ const sign = ctx . Minus ? "-" : ""
765+ if ( ctx . DurationLiteral ) {
766+ return sign + ctx . DurationLiteral [ 0 ] . image
767+ }
768+ return sign + ctx . NumberLiteral ! [ 0 ] . image
769+ }
770+
714771 standardJoin ( ctx : StandardJoinCstChildren ) : AST . JoinClause {
715772 const result : AST . JoinClause = {
716773 type : "join" ,
0 commit comments