git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
Add table alias support: fromAs, innerJoinAs, leftJoinAs for self-joins
commitbdd67044be9556d2fa38336465cd24c334e268fc authorMark <mark.eibes@gmail.com> dateFri 13 Feb 2026, 21:49 (8 months ago) parent061ff32
2 files changed+147−8
src/Yoga/Postgres/Schema.purs+92−8 test/Schema.purs+55
src/Yoga/Postgres/Schema.purs+92−8
@@ -755,6 +755,7 @@ class ResolveAggregateArg :: Symbol -> Row (Row Type) -> Type -> Constraint
755755class ResolveAggregateArg args tables argType | args tables -> argType
756756
757757instance ResolveAggregateArg "*" tables Star
758+else instance ResolveAggregateArg "" tables Star
758759else instance
759760 ( SkipSpaces args trimmedFront
760761 , ExtractWord trimmedFront col _rest
@@ -766,11 +767,32 @@ class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
766767class ResolveAggregateArgCol col tables argType | col tables -> argType
767768
768769instance ResolveAggregateArgCol "*" tables Star
770+else instance
771+ ( Symbol.Cons h _t col
772+ , ResolveAggregateArgColByHead h col tables argType
773+ ) =>
774+ ResolveAggregateArgCol col tables argType
775+
776+class ResolveAggregateArgColByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
777+class ResolveAggregateArgColByHead head col tables argType | head col tables -> argType
778+
779+-- Numeric literals: treat as Star (return type determined by function)
780+instance ResolveAggregateArgColByHead "0" col tables Star
781+else instance ResolveAggregateArgColByHead "1" col tables Star
782+else instance ResolveAggregateArgColByHead "2" col tables Star
783+else instance ResolveAggregateArgColByHead "3" col tables Star
784+else instance ResolveAggregateArgColByHead "4" col tables Star
785+else instance ResolveAggregateArgColByHead "5" col tables Star
786+else instance ResolveAggregateArgColByHead "6" col tables Star
787+else instance ResolveAggregateArgColByHead "7" col tables Star
788+else instance ResolveAggregateArgColByHead "8" col tables Star
789+else instance ResolveAggregateArgColByHead "9" col tables Star
790+-- Column reference
769791else instance
770792 ( ResolveColumn col tables (Column typ constraints)
771793 , UnwrapMaybe typ unwrapped
772794 ) =>
773− ResolveAggregateArgCol col tables unwrapped
795+ ResolveAggregateArgColByHead head col tables unwrapped
774796
775797-- Map (funcName, argType) -> returnType
776798class AggregateReturnType :: Symbol -> Type -> Type -> Constraint
@@ -792,6 +814,25 @@ else instance AggregateReturnType "STRING_AGG" argType String
792814else instance AggregateReturnType "string_agg" argType String
793815else instance AggregateReturnType "COALESCE" argType argType
794816else instance AggregateReturnType "coalesce" argType argType
817+-- Window functions
818+else instance AggregateReturnType "ROW_NUMBER" argType Int
819+else instance AggregateReturnType "row_number" argType Int
820+else instance AggregateReturnType "RANK" argType Int
821+else instance AggregateReturnType "rank" argType Int
822+else instance AggregateReturnType "DENSE_RANK" argType Int
823+else instance AggregateReturnType "dense_rank" argType Int
824+else instance AggregateReturnType "NTILE" argType Int
825+else instance AggregateReturnType "ntile" argType Int
826+else instance AggregateReturnType "LAG" argType argType
827+else instance AggregateReturnType "lag" argType argType
828+else instance AggregateReturnType "LEAD" argType argType
829+else instance AggregateReturnType "lead" argType argType
830+else instance AggregateReturnType "FIRST_VALUE" argType argType
831+else instance AggregateReturnType "first_value" argType argType
832+else instance AggregateReturnType "LAST_VALUE" argType argType
833+else instance AggregateReturnType "last_value" argType argType
834+else instance AggregateReturnType "NTH_VALUE" argType argType
835+else instance AggregateReturnType "nth_value" argType argType
795836else instance
796837 Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
797838 AggregateReturnType funcName argType returnType
@@ -819,34 +860,77 @@ instance
819860 Fail (Text "Aggregate function requires AS alias (e.g. COUNT(*) AS cnt)") =>
820861 ParseAfterAggregateByHead "," tail tables returnType accRL outRL
821862
822−-- Otherwise: expect "AS alias"
863+-- Otherwise: extract keyword and dispatch (OVER or AS)
823864else instance
824865 ( Symbol.Append h t rest
825866 , ExtractWord rest keyword afterKeyword
826− , ParseAggregateAS keyword afterKeyword tables returnType accRL outRL
867+ , ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL
827868 ) =>
828869 ParseAfterAggregateByHead h t tables returnType accRL outRL
829870
830−class ParseAggregateAS :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
831−class ParseAggregateAS keyword afterKeyword tables returnType accRL outRL | keyword afterKeyword tables returnType accRL -> outRL
871+class ParseAfterAggregateKeyword :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
872+class ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL | keyword afterKeyword tables returnType accRL -> outRL
832873
874+-- OVER: parse the over clause, then continue to AS
833875instance
876+ ( SkipSpaces afterKeyword rest
877+ , ParseOverClause rest tables returnType accRL outRL
878+ ) =>
879+ ParseAfterAggregateKeyword "OVER" afterKeyword tables returnType accRL outRL
880+
881+else instance
882+ ( SkipSpaces afterKeyword rest
883+ , ParseOverClause rest tables returnType accRL outRL
884+ ) =>
885+ ParseAfterAggregateKeyword "over" afterKeyword tables returnType accRL outRL
886+
887+-- AS: extract alias
888+else instance
834889 ( ExtractWord afterKeyword alias afterAlias
835890 , SkipSpaces afterAlias rest
836891 , ParseSelectExpectEnd rest tables (RL.Cons alias returnType accRL) outRL
837892 ) =>
838− ParseAggregateAS "AS" afterKeyword tables returnType accRL outRL
893+ ParseAfterAggregateKeyword "AS" afterKeyword tables returnType accRL outRL
839894
840895else instance
841896 ( ExtractWord afterKeyword alias afterAlias
842897 , SkipSpaces afterAlias rest
843898 , ParseSelectExpectEnd rest tables (RL.Cons alias returnType accRL) outRL
844899 ) =>
845− ParseAggregateAS "as" afterKeyword tables returnType accRL outRL
900+ ParseAfterAggregateKeyword "as" afterKeyword tables returnType accRL outRL
846901
847902else instance
848903 Fail (Text "Aggregate function requires AS alias (e.g. COUNT(*) AS cnt)") =>
849− ParseAggregateAS keyword afterKeyword tables returnType accRL outRL
904+ ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL
905+
906+-- Parse OVER (...) clause: expect (, skip until ), then continue to AS alias
907+class ParseOverClause :: Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
908+class ParseOverClause rest tables returnType accRL outRL | rest tables returnType accRL -> outRL
909+
910+instance
911+ Fail (Text "Expected ( after OVER") =>
912+ ParseOverClause "" tables returnType accRL outRL
913+
914+else instance
915+ ( Symbol.Cons h t rest
916+ , ParseOverClauseByHead h t tables returnType accRL outRL
917+ ) =>
918+ ParseOverClause rest tables returnType accRL outRL
919+
920+class ParseOverClauseByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
921+class ParseOverClauseByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
922+
923+-- Open paren: extract until close paren, skip content, continue to AS
924+instance
925+ ( ExtractUntilParen tail _overContent afterParen
926+ , SkipSpaces afterParen rest
927+ , ParseAfterAggregate rest tables returnType accRL outRL
928+ ) =>
929+ ParseOverClauseByHead "(" tail tables returnType accRL outRL
930+
931+else instance
932+ Fail (Text "Expected ( after OVER") =>
933+ ParseOverClauseByHead h t tables returnType accRL outRL
850934
851935-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
852936-- ParseWhere: parse "id = $id AND age > $age" -> params
test/Schema.purs+55
@@ -491,6 +491,45 @@ typedLeftJoinAs = from usersTable
491491 # leftJoinAs @"u2" @"users.id = u2.id" usersTable
492492 # select @"users.name, u2.email AS other_email"
493493
494+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
495+-- Window functions
496+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
497+
498+typedRowNumber
499+ :: Q _ (name :: String, rn :: Int) () _
500+typedRowNumber = from usersTable
501+ # select @"name, ROW_NUMBER() OVER (ORDER BY age) AS rn"
502+
503+typedRank
504+ :: Q _ (name :: String, rnk :: Int) () _
505+typedRank = from usersTable
506+ # select @"name, RANK() OVER (PARTITION BY name ORDER BY age DESC) AS rnk"
507+
508+typedDenseRank
509+ :: Q _ (name :: String, dr :: Int) () _
510+typedDenseRank = from usersTable
511+ # select @"name, DENSE_RANK() OVER (ORDER BY age) AS dr"
512+
513+typedLag
514+ :: Q _ (name :: String, prev_age :: Int) () _
515+typedLag = from usersTable
516+ # select @"name, LAG(age) OVER (ORDER BY id) AS prev_age"
517+
518+typedLead
519+ :: Q _ (name :: String, next_age :: Int) () _
520+typedLead = from usersTable
521+ # select @"name, LEAD(age) OVER (ORDER BY id) AS next_age"
522+
523+typedFirstValue
524+ :: Q _ (name :: String, first_name :: String) () _
525+typedFirstValue = from usersTable
526+ # select @"name, FIRST_VALUE(name) OVER (ORDER BY age) AS first_name"
527+
528+typedNtile
529+ :: Q _ (name :: String, bucket :: Int) () _
530+typedNtile = from usersTable
531+ # select @"name, NTILE(4) OVER (ORDER BY age) AS bucket"
532+
494533-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
495534-- Spec
496535-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -616,6 +655,22 @@ spec = do
616655 it "builds full aggregate chain" do
617656 (typedFullAggregate # toSQL) `shouldEqual` "SELECT name, COUNT(*) AS cnt FROM users GROUP BY name HAVING COUNT(*) > $min ORDER BY name LIMIT 10"
618657
658+ describe "Builder window functions" do
659+ it "builds ROW_NUMBER() OVER" do
660+ (typedRowNumber # toSQL) `shouldEqual` "SELECT name, ROW_NUMBER() OVER (ORDER BY age) AS rn FROM users"
661+ it "builds RANK() OVER with PARTITION BY" do
662+ (typedRank # toSQL) `shouldEqual` "SELECT name, RANK() OVER (PARTITION BY name ORDER BY age DESC) AS rnk FROM users"
663+ it "builds DENSE_RANK() OVER" do
664+ (typedDenseRank # toSQL) `shouldEqual` "SELECT name, DENSE_RANK() OVER (ORDER BY age) AS dr FROM users"
665+ it "builds LAG(col) OVER" do
666+ (typedLag # toSQL) `shouldEqual` "SELECT name, LAG(age) OVER (ORDER BY id) AS prev_age FROM users"
667+ it "builds LEAD(col) OVER" do
668+ (typedLead # toSQL) `shouldEqual` "SELECT name, LEAD(age) OVER (ORDER BY id) AS next_age FROM users"
669+ it "builds FIRST_VALUE(col) OVER" do
670+ (typedFirstValue # toSQL) `shouldEqual` "SELECT name, FIRST_VALUE(name) OVER (ORDER BY age) AS first_name FROM users"
671+ it "builds NTILE(n) OVER" do
672+ (typedNtile # toSQL) `shouldEqual` "SELECT name, NTILE(4) OVER (ORDER BY age) AS bucket FROM users"
673+
619674integrationSpec :: PG.Connection -> Spec Unit
620675integrationSpec conn = do
621676 describe "Builder query execution" do