git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
Fix nested parens in ExtractUntilParen with Peano depth counter
4490719Mark
compile-fail-tests/OverClauseInvalidContent.pursnew file+20
@@ -0,0 +1,20 @@
1+-- EXPECT: Expected PARTITION BY or ORDER BY inside OVER clause
2+module Test.CompileFail.OverClauseInvalidContent where
3+
4+import Prelude
5+import Data.Maybe (Maybe)
6+import Type.Function (type (#))
7+import Type.Proxy (Proxy(..))
8+import Yoga.Postgres.Schema
9+
10+type UsersTable = Table "users"
11+ ( id :: Int # PrimaryKey # AutoIncrement
12+ , name :: String
13+ , email :: String # Unique
14+ , age :: Maybe Int
15+ )
16+
17+usersTable :: Proxy UsersTable
18+usersTable = Proxy
19+
20+bad = from usersTable # select @"name, ROW_NUMBER() OVER (THIS IS TOTALLY INVALID) AS rn"
compile-fail-tests/OverClauseNonexistentColumn.pursnew file+20
@@ -0,0 +1,20 @@
1+-- EXPECT: not found in any table
2+module Test.CompileFail.OverClauseNonexistentColumn where
3+
4+import Prelude
5+import Data.Maybe (Maybe)
6+import Type.Function (type (#))
7+import Type.Proxy (Proxy(..))
8+import Yoga.Postgres.Schema
9+
10+type UsersTable = Table "users"
11+ ( id :: Int # PrimaryKey # AutoIncrement
12+ , name :: String
13+ , email :: String # Unique
14+ , age :: Maybe Int
15+ )
16+
17+usersTable :: Proxy UsersTable
18+usersTable = Proxy
19+
20+bad = from usersTable # select @"name, ROW_NUMBER() OVER (ORDER BY nonexistent_column) AS rn"
src/Yoga/Postgres/Schema.purs+150−2
@@ -982,9 +982,10 @@ else instance
982class ParseOverClauseByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
982class ParseOverClauseByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
983class ParseOverClauseByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
983class ParseOverClauseByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
984
984
985−-- Open paren: extract until close paren, skip content, continue to AS
985+-- Open paren: extract until close paren, validate content, continue to AS
986instance
986instance
987− ( ExtractUntilParen tail _overContent afterParen
987+ ( ExtractUntilParen tail overContent afterParen
988+ , ValidateOverContent overContent tables
988 , SkipSpaces afterParen rest
989 , SkipSpaces afterParen rest
989 , ParseAfterAggregate rest tables returnType accRL outRL
990 , ParseAfterAggregate rest tables returnType accRL outRL
990 ) =>
991 ) =>
@@ -994,6 +995,153 @@ else instance
994 Fail (Text "Expected ( after OVER") =>
995 Fail (Text "Expected ( after OVER") =>
995 ParseOverClauseByHead h t tables returnType accRL outRL
996 ParseOverClauseByHead h t tables returnType accRL outRL
996
997
998+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
999+-- ValidateOverContent: validates PARTITION BY / ORDER BY inside OVER(...)
1000+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1001+
1002+class ValidateOverContent :: Symbol -> Row (Row Type) -> Constraint
1003+class ValidateOverContent content tables
1004+
1005+instance ValidateOverContent "" tables
1006+
1007+else instance
1008+ ( SkipSpaces content content'
1009+ , ValidateOverContentTrimmed content' tables
1010+ ) =>
1011+ ValidateOverContent content tables
1012+
1013+class ValidateOverContentTrimmed :: Symbol -> Row (Row Type) -> Constraint
1014+class ValidateOverContentTrimmed content tables
1015+
1016+instance ValidateOverContentTrimmed "" tables
1017+
1018+else instance
1019+ ( ExtractWord content keyword rest
1020+ , ValidateOverKeyword keyword rest tables
1021+ ) =>
1022+ ValidateOverContentTrimmed content tables
1023+
1024+class ValidateOverKeyword :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1025+class ValidateOverKeyword keyword rest tables
1026+
1027+instance
1028+ ( ExtractWord rest byWord afterBy
1029+ , AssertIsBy byWord
1030+ , ValidatePartitionByColumns afterBy tables
1031+ ) =>
1032+ ValidateOverKeyword "PARTITION" rest tables
1033+
1034+else instance
1035+ ( ExtractWord rest byWord afterBy
1036+ , AssertIsBy byWord
1037+ , ValidatePartitionByColumns afterBy tables
1038+ ) =>
1039+ ValidateOverKeyword "partition" rest tables
1040+
1041+else instance
1042+ ( ExtractWord rest byWord afterBy
1043+ , AssertIsBy byWord
1044+ , ValidateOrderBy afterBy tables
1045+ ) =>
1046+ ValidateOverKeyword "ORDER" rest tables
1047+
1048+else instance
1049+ ( ExtractWord rest byWord afterBy
1050+ , AssertIsBy byWord
1051+ , ValidateOrderBy afterBy tables
1052+ ) =>
1053+ ValidateOverKeyword "order" rest tables
1054+
1055+else instance
1056+ Fail (Text "Expected PARTITION BY or ORDER BY inside OVER clause") =>
1057+ ValidateOverKeyword keyword rest tables
1058+
1059+class AssertIsBy :: Symbol -> Constraint
1060+class AssertIsBy sym
1061+
1062+instance AssertIsBy "BY"
1063+else instance AssertIsBy "by"
1064+else instance Fail (Text "Expected BY after PARTITION or ORDER") => AssertIsBy sym
1065+
1066+-- ValidatePartitionByColumns: column list that terminates at ORDER keyword
1067+class ValidatePartitionByColumns :: Symbol -> Row (Row Type) -> Constraint
1068+class ValidatePartitionByColumns sym tables
1069+
1070+instance ValidatePartitionByColumns "" tables
1071+
1072+else instance
1073+ ( SkipSpaces sym sym'
1074+ , ValidatePartitionByColumnsTrimmed sym' tables
1075+ ) =>
1076+ ValidatePartitionByColumns sym tables
1077+
1078+class ValidatePartitionByColumnsTrimmed :: Symbol -> Row (Row Type) -> Constraint
1079+class ValidatePartitionByColumnsTrimmed sym tables
1080+
1081+instance ValidatePartitionByColumnsTrimmed "" tables
1082+
1083+else instance
1084+ ( Symbol.Cons h t sym
1085+ , ValidatePartitionByColumnsGo h t "" tables
1086+ ) =>
1087+ ValidatePartitionByColumnsTrimmed sym tables
1088+
1089+class ValidatePartitionByColumnsGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
1090+class ValidatePartitionByColumnsGo head tail acc tables
1091+
1092+-- Comma: flush column, continue
1093+instance
1094+ ( ResolveColumn acc tables typ
1095+ , SkipSpaces tail rest
1096+ , ValidatePartitionByColumnsTrimmed rest tables
1097+ ) =>
1098+ ValidatePartitionByColumnsGo "," tail acc tables
1099+
1100+-- Space: flush word, check if ORDER keyword or column
1101+else instance
1102+ ( SkipSpaces tail rest
1103+ , FlushPartitionWord acc rest tables
1104+ ) =>
1105+ ValidatePartitionByColumnsGo " " tail acc tables
1106+
1107+-- End of string: flush final column
1108+else instance
1109+ ( Symbol.Append acc h acc'
1110+ , ResolveColumn acc' tables typ
1111+ ) =>
1112+ ValidatePartitionByColumnsGo h "" acc tables
1113+
1114+-- Regular char: accumulate
1115+else instance
1116+ ( Symbol.Append acc h acc'
1117+ , Symbol.Cons nextH nextT tail
1118+ , ValidatePartitionByColumnsGo nextH nextT acc' tables
1119+ ) =>
1120+ ValidatePartitionByColumnsGo h tail acc tables
1121+
1122+class FlushPartitionWord :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1123+class FlushPartitionWord word rest tables
1124+
1125+instance
1126+ ( ExtractWord rest byWord afterBy
1127+ , AssertIsBy byWord
1128+ , ValidateOrderBy afterBy tables
1129+ ) =>
1130+ FlushPartitionWord "ORDER" rest tables
1131+
1132+else instance
1133+ ( ExtractWord rest byWord afterBy
1134+ , AssertIsBy byWord
1135+ , ValidateOrderBy afterBy tables
1136+ ) =>
1137+ FlushPartitionWord "order" rest tables
1138+
1139+else instance
1140+ ( ResolveColumn word tables typ
1141+ , ValidatePartitionByColumnsTrimmed rest tables
1142+ ) =>
1143+ FlushPartitionWord word rest tables
1144+
997-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1145-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
998-- ParseWhere: parse "id = $id AND age > $age" -> params
1146-- ParseWhere: parse "id = $id AND age > $age" -> params
999-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1147-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
test/EdgeCases.purs+23−14
@@ -62,16 +62,30 @@ groupByMissingColumn = from usersTable
62 # groupBy @"name"
62 # groupBy @"name"
63
63
64-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
64-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
65−-- EDGE CASE 4: OVER clause content is not validated
65+-- EDGE CASE 4: OVER clause content validated (FIXED)
66−-- Invalid SQL inside OVER() parens is silently accepted.
66+-- Invalid OVER content now fails to compile
67+-- (see OverClauseInvalidContent.purs, OverClauseNonexistentColumn.purs)
67-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
68
69
69−-- OVER clause content is completely skipped by ExtractUntilParen
70+overClauseValid
70−-- "INVALID SQL HERE" is not validated at all
71−overClauseNotValidated
72 :: Q _ (name :: String, rn :: Int) () _
71 :: Q _ (name :: String, rn :: Int) () _
73−overClauseNotValidated = from usersTable
72+overClauseValid = from usersTable
73+ # select @"name, ROW_NUMBER() OVER (ORDER BY age) AS rn"
74+
75+overClausePartitionBy
76+ :: Q _ (name :: String, rnk :: Int) () _
77+overClausePartitionBy = from usersTable
78+ # select @"name, RANK() OVER (PARTITION BY name ORDER BY age DESC) AS rnk"
79+
80+overClausePartitionByOnly
81+ :: Q _ (name :: String, rn :: Int) () _
82+overClausePartitionByOnly = from usersTable
74− # select @"name, ROW_NUMBER() OVER (THIS IS TOTALLY INVALID) AS rn"
83+ # select @"name, ROW_NUMBER() OVER (PARTITION BY name) AS rn"
84+
85+overClauseEmpty
86+ :: Q _ (name :: String, rn :: Int) () _
87+overClauseEmpty = from usersTable
88+ # select @"name, ROW_NUMBER() OVER () AS rn"
75
89
76-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
90-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
77-- EDGE CASE 5: WHERE keywords are case-sensitive (uppercase only)
91-- EDGE CASE 5: WHERE keywords are case-sensitive (uppercase only)
@@ -111,15 +125,10 @@ distinctOrderByNonSelected = from usersTable
111 # orderBy @"age"
125 # orderBy @"age"
112
126
113-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
127-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
114−-- EDGE CASE 7: Aggregate function on nonexistent column in OVER
128+-- EDGE CASE 7: Nonexistent column in OVER (FIXED)
115−-- OVER content is skipped so "ORDER BY nonexistent" compiles
129+-- OVER content is now validated (see OverClauseNonexistentColumn.purs)
116-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
130-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
117
131
118−overNonexistentColumn
119− :: Q _ (name :: String, rn :: Int) () _
120−overNonexistentColumn = from usersTable
121− # select @"name, ROW_NUMBER() OVER (ORDER BY nonexistent_column) AS rn"
122−
123-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
132-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
124-- EDGE CASE 8: Nested parens in aggregate args (FIXED)
133-- EDGE CASE 8: Nested parens in aggregate args (FIXED)
125-- ExtractUntilParen now uses Peano depth counter for nesting
134-- ExtractUntilParen now uses Peano depth counter for nesting