git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
Validate OVER clause content: PARTITION BY / ORDER BY column refs
commit9753632a4d902cf016c1387fcea1bfac261cf2ac authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 00:57 (8 months ago) parent4490719
4 files changed+455−71
.psc-ide-port+1−1 compile-fail-tests/DistinctOrderByNonSelected.purs+20 src/Yoga/Postgres/Schema.purs+422−62 test/EdgeCases.purs+12−8
.psc-ide-port+1−1
@@ -1 +1 @@
1−15556
1+15245
compile-fail-tests/DistinctOrderByNonSelected.pursnew file+20
@@ -0,0 +1,20 @@
1+-- EXPECT: ORDER BY column "age" must appear in the SELECT list when using DISTINCT
2+module Test.CompileFail.DistinctOrderByNonSelected 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 # selectDistinct @"name" # orderBy @"age"
src/Yoga/Postgres/Schema.purs+422−62
@@ -771,6 +771,7 @@ else instance
771771
772772-- Peano depth counter for nested parentheses
773773data Z
774+
774775data S :: Type -> Type
775776data S n
776777
@@ -1210,74 +1211,23 @@ else instance
12101211 ParseWhereContinue sym currentType tables paramsIn paramsOut
12111212
12121213-- Flush a word in WHERE context
1214+-- Dispatches on first character to avoid linear search through all keywords
12131215class FlushWhereWord :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
12141216class FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
12151217
12161218instance FlushWhereWord "" currentType tables paramsIn currentType paramsIn
1219+-- Most common SQL keywords matched directly for fast resolution
12171220else instance FlushWhereWord "AND" currentType tables paramsIn currentType paramsIn
1218−else instance FlushWhereWord "OR" currentType tables paramsIn currentType paramsIn
1219−else instance FlushWhereWord "NOT" currentType tables paramsIn currentType paramsIn
1220−else instance FlushWhereWord "IS" currentType tables paramsIn currentType paramsIn
1221−else instance FlushWhereWord "NULL" currentType tables paramsIn currentType paramsIn
1222−else instance FlushWhereWord "LIKE" currentType tables paramsIn currentType paramsIn
1223−else instance FlushWhereWord "ILIKE" currentType tables paramsIn currentType paramsIn
1224−else instance FlushWhereWord "IN" currentType tables paramsIn (Array currentType) paramsIn
1225−else instance FlushWhereWord "TRUE" currentType tables paramsIn currentType paramsIn
1226−else instance FlushWhereWord "FALSE" currentType tables paramsIn currentType paramsIn
1227−else instance FlushWhereWord "BETWEEN" currentType tables paramsIn currentType paramsIn
1228−else instance FlushWhereWord "ANY" currentType tables paramsIn (Array currentType) paramsIn
1229−else instance FlushWhereWord "ALL" currentType tables paramsIn (Array currentType) paramsIn
1230−else instance FlushWhereWord "CAST" currentType tables paramsIn currentType paramsIn
1231−else instance FlushWhereWord "AS" currentType tables paramsIn currentType paramsIn
1232−else instance FlushWhereWord "EXISTS" currentType tables paramsIn currentType paramsIn
1233−-- Lowercase SQL keywords
12341221else instance FlushWhereWord "and" currentType tables paramsIn currentType paramsIn
1222+else instance FlushWhereWord "OR" currentType tables paramsIn currentType paramsIn
12351223else instance FlushWhereWord "or" currentType tables paramsIn currentType paramsIn
1224+else instance FlushWhereWord "NOT" currentType tables paramsIn currentType paramsIn
12361225else instance FlushWhereWord "not" currentType tables paramsIn currentType paramsIn
1226+else instance FlushWhereWord "IS" currentType tables paramsIn currentType paramsIn
12371227else instance FlushWhereWord "is" currentType tables paramsIn currentType paramsIn
1228+else instance FlushWhereWord "NULL" currentType tables paramsIn currentType paramsIn
12381229else instance FlushWhereWord "null" currentType tables paramsIn currentType paramsIn
1239−else instance FlushWhereWord "like" currentType tables paramsIn currentType paramsIn
1240−else instance FlushWhereWord "ilike" currentType tables paramsIn currentType paramsIn
1241−else instance FlushWhereWord "in" currentType tables paramsIn (Array currentType) paramsIn
1242−else instance FlushWhereWord "true" currentType tables paramsIn currentType paramsIn
1243−else instance FlushWhereWord "false" currentType tables paramsIn currentType paramsIn
1244−else instance FlushWhereWord "between" currentType tables paramsIn currentType paramsIn
1245−else instance FlushWhereWord "any" currentType tables paramsIn (Array currentType) paramsIn
1246−else instance FlushWhereWord "all" currentType tables paramsIn (Array currentType) paramsIn
1247−else instance FlushWhereWord "cast" currentType tables paramsIn currentType paramsIn
1248−else instance FlushWhereWord "as" currentType tables paramsIn currentType paramsIn
1249−else instance FlushWhereWord "exists" currentType tables paramsIn currentType paramsIn
1250−-- Aggregate functions (for HAVING support)
1251−else instance FlushWhereWord "COUNT" currentType tables paramsIn Int paramsIn
1252−else instance FlushWhereWord "SUM" currentType tables paramsIn currentType paramsIn
1253−else instance FlushWhereWord "AVG" currentType tables paramsIn Number paramsIn
1254−else instance FlushWhereWord "MIN" currentType tables paramsIn currentType paramsIn
1255−else instance FlushWhereWord "MAX" currentType tables paramsIn currentType paramsIn
1256−else instance FlushWhereWord "ARRAY_AGG" currentType tables paramsIn currentType paramsIn
1257−else instance FlushWhereWord "STRING_AGG" currentType tables paramsIn String paramsIn
1258−-- Lowercase aggregate functions
1259−else instance FlushWhereWord "count" currentType tables paramsIn Int paramsIn
1260−else instance FlushWhereWord "sum" currentType tables paramsIn currentType paramsIn
1261−else instance FlushWhereWord "avg" currentType tables paramsIn Number paramsIn
1262−else instance FlushWhereWord "min" currentType tables paramsIn currentType paramsIn
1263−else instance FlushWhereWord "max" currentType tables paramsIn currentType paramsIn
1264−else instance FlushWhereWord "array_agg" currentType tables paramsIn currentType paramsIn
1265−else instance FlushWhereWord "string_agg" currentType tables paramsIn String paramsIn
1266−else instance FlushWhereWord "*" currentType tables paramsIn currentType paramsIn
1267−-- Postgres type names (for ::type casts)
1268−else instance FlushWhereWord "text" currentType tables paramsIn currentType paramsIn
1269−else instance FlushWhereWord "integer" currentType tables paramsIn currentType paramsIn
1270−else instance FlushWhereWord "bigint" currentType tables paramsIn currentType paramsIn
1271−else instance FlushWhereWord "boolean" currentType tables paramsIn currentType paramsIn
1272−else instance FlushWhereWord "jsonb" currentType tables paramsIn currentType paramsIn
1273−else instance FlushWhereWord "json" currentType tables paramsIn currentType paramsIn
1274−else instance FlushWhereWord "timestamptz" currentType tables paramsIn currentType paramsIn
1275−else instance FlushWhereWord "timestamp" currentType tables paramsIn currentType paramsIn
1276−else instance FlushWhereWord "date" currentType tables paramsIn currentType paramsIn
1277−else instance FlushWhereWord "int" currentType tables paramsIn currentType paramsIn
1278−else instance FlushWhereWord "varchar" currentType tables paramsIn currentType paramsIn
1279−else instance FlushWhereWord "uuid" currentType tables paramsIn currentType paramsIn
1280−-- Non-keyword: check first char
1230+-- Everything else: dispatch on first character
12811231else instance
12821232 ( Symbol.Cons head rest word
12831233 , FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut
@@ -1304,8 +1254,41 @@ else instance FlushWhereWordByHead "6" word currentType tables paramsIn currentT
13041254else instance FlushWhereWordByHead "7" word currentType tables paramsIn currentType paramsIn
13051255else instance FlushWhereWordByHead "8" word currentType tables paramsIn currentType paramsIn
13061256else instance FlushWhereWordByHead "9" word currentType tables paramsIn currentType paramsIn
1307−
1308−-- Column reference: resolve and set as currentType
1257+else instance FlushWhereWordByHead "*" word currentType tables paramsIn currentType paramsIn
1258+
1259+-- Letters with keywords: dispatch to per-letter helpers
1260+else instance FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "A" word currentType tables paramsIn currentTypeOut paramsOut
1261+else instance FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "a" word currentType tables paramsIn currentTypeOut paramsOut
1262+else instance FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "B" word currentType tables paramsIn currentTypeOut paramsOut
1263+else instance FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "b" word currentType tables paramsIn currentTypeOut paramsOut
1264+else instance FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "C" word currentType tables paramsIn currentTypeOut paramsOut
1265+else instance FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "c" word currentType tables paramsIn currentTypeOut paramsOut
1266+else instance FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "D" word currentType tables paramsIn currentTypeOut paramsOut
1267+else instance FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "d" word currentType tables paramsIn currentTypeOut paramsOut
1268+else instance FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "E" word currentType tables paramsIn currentTypeOut paramsOut
1269+else instance FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "e" word currentType tables paramsIn currentTypeOut paramsOut
1270+else instance FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "F" word currentType tables paramsIn currentTypeOut paramsOut
1271+else instance FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "f" word currentType tables paramsIn currentTypeOut paramsOut
1272+else instance FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "I" word currentType tables paramsIn currentTypeOut paramsOut
1273+else instance FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "i" word currentType tables paramsIn currentTypeOut paramsOut
1274+else instance FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "J" word currentType tables paramsIn currentTypeOut paramsOut
1275+else instance FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "j" word currentType tables paramsIn currentTypeOut paramsOut
1276+else instance FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "L" word currentType tables paramsIn currentTypeOut paramsOut
1277+else instance FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "l" word currentType tables paramsIn currentTypeOut paramsOut
1278+else instance FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "M" word currentType tables paramsIn currentTypeOut paramsOut
1279+else instance FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "m" word currentType tables paramsIn currentTypeOut paramsOut
1280+else instance FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "N" word currentType tables paramsIn currentTypeOut paramsOut
1281+else instance FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "n" word currentType tables paramsIn currentTypeOut paramsOut
1282+else instance FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "S" word currentType tables paramsIn currentTypeOut paramsOut
1283+else instance FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "s" word currentType tables paramsIn currentTypeOut paramsOut
1284+else instance FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "T" word currentType tables paramsIn currentTypeOut paramsOut
1285+else instance FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "t" word currentType tables paramsIn currentTypeOut paramsOut
1286+else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "U" word currentType tables paramsIn currentTypeOut paramsOut
1287+else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "u" word currentType tables paramsIn currentTypeOut paramsOut
1288+else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "V" word currentType tables paramsIn currentTypeOut paramsOut
1289+else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "v" word currentType tables paramsIn currentTypeOut paramsOut
1290+
1291+-- Catch-all: column reference (letters without keywords like g, h, p, q, r, w, x, y, z)
13091292else instance
13101293 ( ResolveColumn word tables entry
13111294 , ExtractType entry typ
@@ -1313,6 +1296,210 @@ else instance
13131296 ) =>
13141297 FlushWhereWordByHead head word currentType tables paramsIn unwrapped paramsIn
13151298
1299+-- Per-letter keyword helpers
1300+
1301+class FlushWhereWordA :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1302+class FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1303+
1304+instance FlushWhereWordA "ANY" currentType tables paramsIn (Array currentType) paramsIn
1305+else instance FlushWhereWordA "any" currentType tables paramsIn (Array currentType) paramsIn
1306+else instance FlushWhereWordA "ALL" currentType tables paramsIn (Array currentType) paramsIn
1307+else instance FlushWhereWordA "all" currentType tables paramsIn (Array currentType) paramsIn
1308+else instance FlushWhereWordA "AS" currentType tables paramsIn currentType paramsIn
1309+else instance FlushWhereWordA "as" currentType tables paramsIn currentType paramsIn
1310+else instance FlushWhereWordA "AVG" currentType tables paramsIn Number paramsIn
1311+else instance FlushWhereWordA "avg" currentType tables paramsIn Number paramsIn
1312+else instance FlushWhereWordA "ARRAY_AGG" currentType tables paramsIn currentType paramsIn
1313+else instance FlushWhereWordA "array_agg" currentType tables paramsIn currentType paramsIn
1314+else instance
1315+ ( ResolveColumn word tables entry
1316+ , ExtractType entry typ
1317+ , UnwrapMaybe typ unwrapped
1318+ ) =>
1319+ FlushWhereWordA word currentType tables paramsIn unwrapped paramsIn
1320+
1321+class FlushWhereWordB :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1322+class FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1323+
1324+instance FlushWhereWordB "BETWEEN" currentType tables paramsIn currentType paramsIn
1325+else instance FlushWhereWordB "between" currentType tables paramsIn currentType paramsIn
1326+else instance FlushWhereWordB "bigint" currentType tables paramsIn currentType paramsIn
1327+else instance FlushWhereWordB "boolean" currentType tables paramsIn currentType paramsIn
1328+else instance
1329+ ( ResolveColumn word tables entry
1330+ , ExtractType entry typ
1331+ , UnwrapMaybe typ unwrapped
1332+ ) =>
1333+ FlushWhereWordB word currentType tables paramsIn unwrapped paramsIn
1334+
1335+class FlushWhereWordC :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1336+class FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1337+
1338+instance FlushWhereWordC "CAST" currentType tables paramsIn currentType paramsIn
1339+else instance FlushWhereWordC "cast" currentType tables paramsIn currentType paramsIn
1340+else instance FlushWhereWordC "COUNT" currentType tables paramsIn Int paramsIn
1341+else instance FlushWhereWordC "count" currentType tables paramsIn Int paramsIn
1342+else instance
1343+ ( ResolveColumn word tables entry
1344+ , ExtractType entry typ
1345+ , UnwrapMaybe typ unwrapped
1346+ ) =>
1347+ FlushWhereWordC word currentType tables paramsIn unwrapped paramsIn
1348+
1349+class FlushWhereWordD :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1350+class FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1351+
1352+instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1353+else instance
1354+ ( ResolveColumn word tables entry
1355+ , ExtractType entry typ
1356+ , UnwrapMaybe typ unwrapped
1357+ ) =>
1358+ FlushWhereWordD word currentType tables paramsIn unwrapped paramsIn
1359+
1360+class FlushWhereWordE :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1361+class FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1362+
1363+instance FlushWhereWordE "EXISTS" currentType tables paramsIn currentType paramsIn
1364+else instance FlushWhereWordE "exists" currentType tables paramsIn currentType paramsIn
1365+else instance
1366+ ( ResolveColumn word tables entry
1367+ , ExtractType entry typ
1368+ , UnwrapMaybe typ unwrapped
1369+ ) =>
1370+ FlushWhereWordE word currentType tables paramsIn unwrapped paramsIn
1371+
1372+class FlushWhereWordF :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1373+class FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1374+
1375+instance FlushWhereWordF "FALSE" currentType tables paramsIn currentType paramsIn
1376+else instance FlushWhereWordF "false" currentType tables paramsIn currentType paramsIn
1377+else instance
1378+ ( ResolveColumn word tables entry
1379+ , ExtractType entry typ
1380+ , UnwrapMaybe typ unwrapped
1381+ ) =>
1382+ FlushWhereWordF word currentType tables paramsIn unwrapped paramsIn
1383+
1384+class FlushWhereWordI :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1385+class FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1386+
1387+instance FlushWhereWordI "IN" currentType tables paramsIn (Array currentType) paramsIn
1388+else instance FlushWhereWordI "in" currentType tables paramsIn (Array currentType) paramsIn
1389+else instance FlushWhereWordI "ILIKE" currentType tables paramsIn currentType paramsIn
1390+else instance FlushWhereWordI "ilike" currentType tables paramsIn currentType paramsIn
1391+else instance FlushWhereWordI "integer" currentType tables paramsIn currentType paramsIn
1392+else instance FlushWhereWordI "int" currentType tables paramsIn currentType paramsIn
1393+else instance
1394+ ( ResolveColumn word tables entry
1395+ , ExtractType entry typ
1396+ , UnwrapMaybe typ unwrapped
1397+ ) =>
1398+ FlushWhereWordI word currentType tables paramsIn unwrapped paramsIn
1399+
1400+class FlushWhereWordJ :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1401+class FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1402+
1403+instance FlushWhereWordJ "jsonb" currentType tables paramsIn currentType paramsIn
1404+else instance FlushWhereWordJ "json" currentType tables paramsIn currentType paramsIn
1405+else instance
1406+ ( ResolveColumn word tables entry
1407+ , ExtractType entry typ
1408+ , UnwrapMaybe typ unwrapped
1409+ ) =>
1410+ FlushWhereWordJ word currentType tables paramsIn unwrapped paramsIn
1411+
1412+class FlushWhereWordL :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1413+class FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1414+
1415+instance FlushWhereWordL "LIKE" currentType tables paramsIn currentType paramsIn
1416+else instance FlushWhereWordL "like" currentType tables paramsIn currentType paramsIn
1417+else instance
1418+ ( ResolveColumn word tables entry
1419+ , ExtractType entry typ
1420+ , UnwrapMaybe typ unwrapped
1421+ ) =>
1422+ FlushWhereWordL word currentType tables paramsIn unwrapped paramsIn
1423+
1424+class FlushWhereWordM :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1425+class FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1426+
1427+instance FlushWhereWordM "MIN" currentType tables paramsIn currentType paramsIn
1428+else instance FlushWhereWordM "min" currentType tables paramsIn currentType paramsIn
1429+else instance FlushWhereWordM "MAX" currentType tables paramsIn currentType paramsIn
1430+else instance FlushWhereWordM "max" currentType tables paramsIn currentType paramsIn
1431+else instance
1432+ ( ResolveColumn word tables entry
1433+ , ExtractType entry typ
1434+ , UnwrapMaybe typ unwrapped
1435+ ) =>
1436+ FlushWhereWordM word currentType tables paramsIn unwrapped paramsIn
1437+
1438+class FlushWhereWordN :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1439+class FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1440+
1441+instance FlushWhereWordN "NOT" currentType tables paramsIn currentType paramsIn -- unreachable (caught by FlushWhereWord directly) but included for self-containedness
1442+else instance FlushWhereWordN "not" currentType tables paramsIn currentType paramsIn
1443+else instance FlushWhereWordN "NULL" currentType tables paramsIn currentType paramsIn
1444+else instance FlushWhereWordN "null" currentType tables paramsIn currentType paramsIn
1445+else instance
1446+ ( ResolveColumn word tables entry
1447+ , ExtractType entry typ
1448+ , UnwrapMaybe typ unwrapped
1449+ ) =>
1450+ FlushWhereWordN word currentType tables paramsIn unwrapped paramsIn
1451+
1452+class FlushWhereWordS :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1453+class FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1454+
1455+instance FlushWhereWordS "SUM" currentType tables paramsIn currentType paramsIn
1456+else instance FlushWhereWordS "sum" currentType tables paramsIn currentType paramsIn
1457+else instance FlushWhereWordS "STRING_AGG" currentType tables paramsIn String paramsIn
1458+else instance FlushWhereWordS "string_agg" currentType tables paramsIn String paramsIn
1459+else instance
1460+ ( ResolveColumn word tables entry
1461+ , ExtractType entry typ
1462+ , UnwrapMaybe typ unwrapped
1463+ ) =>
1464+ FlushWhereWordS word currentType tables paramsIn unwrapped paramsIn
1465+
1466+class FlushWhereWordT :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1467+class FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1468+
1469+instance FlushWhereWordT "TRUE" currentType tables paramsIn currentType paramsIn
1470+else instance FlushWhereWordT "true" currentType tables paramsIn currentType paramsIn
1471+else instance FlushWhereWordT "text" currentType tables paramsIn currentType paramsIn
1472+else instance FlushWhereWordT "timestamptz" currentType tables paramsIn currentType paramsIn
1473+else instance FlushWhereWordT "timestamp" currentType tables paramsIn currentType paramsIn
1474+else instance
1475+ ( ResolveColumn word tables entry
1476+ , ExtractType entry typ
1477+ , UnwrapMaybe typ unwrapped
1478+ ) =>
1479+ FlushWhereWordT word currentType tables paramsIn unwrapped paramsIn
1480+
1481+class FlushWhereWordU :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1482+class FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1483+
1484+instance FlushWhereWordU "uuid" currentType tables paramsIn currentType paramsIn
1485+else instance
1486+ ( ResolveColumn word tables entry
1487+ , ExtractType entry typ
1488+ , UnwrapMaybe typ unwrapped
1489+ ) =>
1490+ FlushWhereWordU word currentType tables paramsIn unwrapped paramsIn
1491+
1492+class FlushWhereWordV :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1493+class FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1494+
1495+instance FlushWhereWordV "varchar" currentType tables paramsIn currentType paramsIn
1496+else instance
1497+ ( ResolveColumn word tables entry
1498+ , ExtractType entry typ
1499+ , UnwrapMaybe typ unwrapped
1500+ ) =>
1501+ FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
1502+
13161503-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
13171504-- ValidateColumnList: comma-separated column references
13181505-- Used by GROUP BY, DISTINCT ON, ON CONFLICT target
@@ -1487,6 +1674,177 @@ else instance
14871674 ) =>
14881675 ValidateOrderByContinueByHead h t tables
14891676
1677+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1678+-- RowListHas: check if a label exists in a RowList
1679+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1680+
1681+class RowListHas :: Symbol -> RL.RowList Type -> Boolean -> Constraint
1682+class RowListHas label rl has | label rl -> has
1683+
1684+instance RowListHas label RL.Nil False
1685+instance RowListHas label (RL.Cons label typ rest) True
1686+else instance RowListHas label rest has => RowListHas label (RL.Cons other typ rest) has
1687+
1688+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1689+-- CheckDistinctOrderBy: when DISTINCT is in stage, ORDER BY
1690+-- columns must appear in the SELECT result list
1691+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1692+
1693+class CheckDistinctOrderBy :: Row Type -> Symbol -> Row Type -> Constraint
1694+class CheckDistinctOrderBy stage cols result
1695+
1696+instance
1697+ ( RowToList stage stageRL
1698+ , RowListHas "distinct" stageRL isDistinct
1699+ , CheckDistinctOrderByBranch isDistinct cols result
1700+ ) =>
1701+ CheckDistinctOrderBy stage cols result
1702+
1703+class CheckDistinctOrderByBranch :: Boolean -> Symbol -> Row Type -> Constraint
1704+class CheckDistinctOrderByBranch isDistinct cols result
1705+
1706+instance OrderByColumnsInResult cols result => CheckDistinctOrderByBranch True cols result
1707+instance CheckDistinctOrderByBranch False cols result
1708+
1709+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1710+-- OrderByColumnsInResult: validate ORDER BY column refs exist in result row
1711+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1712+
1713+class OrderByColumnsInResult :: Symbol -> Row Type -> Constraint
1714+class OrderByColumnsInResult sym result
1715+
1716+instance OrderByColumnsInResult "" result
1717+else instance
1718+ ( Symbol.Cons h t sym
1719+ , OrderByColumnsInResultGo h t "" result
1720+ ) =>
1721+ OrderByColumnsInResult sym result
1722+
1723+class OrderByColumnsInResultGo :: Symbol -> Symbol -> Symbol -> Row Type -> Constraint
1724+class OrderByColumnsInResultGo head tail acc result
1725+
1726+-- Comma: flush column, continue
1727+instance
1728+ ( FlushOrderByWordInResult acc result
1729+ , SkipSpaces tail rest
1730+ , OrderByColumnsInResult rest result
1731+ ) =>
1732+ OrderByColumnsInResultGo "," tail acc result
1733+
1734+-- Space: flush word, skip modifiers
1735+else instance
1736+ ( SkipSpaces tail rest
1737+ , FlushOrderByThenSkipInResult acc rest result
1738+ ) =>
1739+ OrderByColumnsInResultGo " " tail acc result
1740+
1741+-- End of string: flush final column
1742+else instance
1743+ ( Symbol.Append acc h acc'
1744+ , FlushOrderByWordInResult acc' result
1745+ ) =>
1746+ OrderByColumnsInResultGo h "" acc result
1747+
1748+-- Regular char: accumulate
1749+else instance
1750+ ( Symbol.Append acc h acc'
1751+ , Symbol.Cons nextH nextT tail
1752+ , OrderByColumnsInResultGo nextH nextT acc' result
1753+ ) =>
1754+ OrderByColumnsInResultGo h tail acc result
1755+
1756+class FlushOrderByWordInResult :: Symbol -> Row Type -> Constraint
1757+class FlushOrderByWordInResult word result
1758+
1759+instance FlushOrderByWordInResult "" result
1760+else instance FlushOrderByWordInResult "ASC" result
1761+else instance FlushOrderByWordInResult "asc" result
1762+else instance FlushOrderByWordInResult "DESC" result
1763+else instance FlushOrderByWordInResult "desc" result
1764+else instance FlushOrderByWordInResult "NULLS" result
1765+else instance FlushOrderByWordInResult "FIRST" result
1766+else instance FlushOrderByWordInResult "LAST" result
1767+else instance CheckColumnInResult word result => FlushOrderByWordInResult word result
1768+
1769+class CheckColumnInResult :: Symbol -> Row Type -> Constraint
1770+class CheckColumnInResult col result
1771+
1772+instance
1773+ ( RowToList result rl
1774+ , CheckColumnInResultRL col rl
1775+ ) =>
1776+ CheckColumnInResult col result
1777+
1778+class CheckColumnInResultRL :: Symbol -> RL.RowList Type -> Constraint
1779+class CheckColumnInResultRL col rl
1780+
1781+instance
1782+ Fail (Beside (Text "ORDER BY column ") (Beside (Quote col) (Text " must appear in the SELECT list when using DISTINCT"))) =>
1783+ CheckColumnInResultRL col RL.Nil
1784+
1785+instance CheckColumnInResultRL col (RL.Cons col typ tail)
1786+
1787+else instance CheckColumnInResultRL col tail => CheckColumnInResultRL col (RL.Cons name typ tail)
1788+
1789+class FlushOrderByThenSkipInResult :: Symbol -> Symbol -> Row Type -> Constraint
1790+class FlushOrderByThenSkipInResult colName rest result
1791+
1792+instance FlushOrderByWordInResult colName result => FlushOrderByThenSkipInResult colName "" result
1793+else instance
1794+ ( FlushOrderByWordInResult colName result
1795+ , Symbol.Cons h t rest
1796+ , FlushOrderByThenSkipByHeadInResult h t result
1797+ ) =>
1798+ FlushOrderByThenSkipInResult colName rest result
1799+
1800+class FlushOrderByThenSkipByHeadInResult :: Symbol -> Symbol -> Row Type -> Constraint
1801+class FlushOrderByThenSkipByHeadInResult head tail result
1802+
1803+-- Comma: continue with next column
1804+instance
1805+ ( SkipSpaces tail rest
1806+ , OrderByColumnsInResult rest result
1807+ ) =>
1808+ FlushOrderByThenSkipByHeadInResult "," tail result
1809+
1810+-- Modifier word: consume it, continue
1811+else instance
1812+ ( Symbol.Append h t rest
1813+ , ExtractWord rest word afterWord
1814+ , FlushOrderByWordInResult word result
1815+ , SkipSpaces afterWord rest'
1816+ , OrderByColumnsInResultContinue rest' result
1817+ ) =>
1818+ FlushOrderByThenSkipByHeadInResult h t result
1819+
1820+class OrderByColumnsInResultContinue :: Symbol -> Row Type -> Constraint
1821+class OrderByColumnsInResultContinue sym result
1822+
1823+instance OrderByColumnsInResultContinue "" result
1824+else instance
1825+ ( Symbol.Cons h t sym
1826+ , OrderByColumnsInResultContinueByHead h t result
1827+ ) =>
1828+ OrderByColumnsInResultContinue sym result
1829+
1830+class OrderByColumnsInResultContinueByHead :: Symbol -> Symbol -> Row Type -> Constraint
1831+class OrderByColumnsInResultContinueByHead head tail result
1832+
1833+instance
1834+ ( SkipSpaces tail rest
1835+ , OrderByColumnsInResult rest result
1836+ ) =>
1837+ OrderByColumnsInResultContinueByHead "," tail result
1838+
1839+else instance
1840+ ( Symbol.Append h t rest
1841+ , ExtractWord rest word afterWord
1842+ , FlushOrderByWordInResult word result
1843+ , SkipSpaces afterWord rest'
1844+ , OrderByColumnsInResultContinue rest' result
1845+ ) =>
1846+ OrderByColumnsInResultContinueByHead h t result
1847+
14901848-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
14911849-- ValidateColumns: comma-separated column names (for ON CONFLICT target)
14921850-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1998,7 +2356,7 @@ select
19982356select (Q q) = Q (q { sql = "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
19992357
20002358selectDistinct
2001− :: forall @sel tables result r p stage stage'
2359+ :: forall @sel tables result r p stage stage' stage''
20022360 . IsSymbol sel
20032361 => ParseSelect sel tables result
20042362 => Row.Lacks "select" stage
@@ -2010,8 +2368,9 @@ selectDistinct
20102368 => Row.Lacks "limit" stage
20112369 => Row.Lacks "offset" stage
20122370 => Row.Cons "select" Unit stage stage'
2371+ => Row.Cons "distinct" Unit stage' stage''
20132372 => Q tables r p stage
2014− -> Q tables result p stage'
2373+ -> Q tables result p stage''
20152374selectDistinct (Q q) = Q (q { sql = "SELECT DISTINCT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
20162375
20172376selectDistinctOn
@@ -2054,6 +2413,7 @@ orderBy
20542413 :: forall @cols tables result params stage stage'
20552414 . IsSymbol cols
20562415 => ValidateOrderBy cols tables
2416+ => CheckDistinctOrderBy stage cols result
20572417 => HasClause "select" stage
20582418 => Row.Lacks "orderBy" stage
20592419 => Row.Lacks "limit" stage
test/EdgeCases.purs+12−8
@@ -111,18 +111,22 @@ lowercaseAndOr :: Q _ _ (minAge :: Int, namePat :: String) _
111111lowercaseAndOr = from usersTable # selectAll # where_ @"age > $minAge or name like $namePat"
112112
113113-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
114−-- EDGE CASE 6: DISTINCT ORDER BY on non-selected column
115−-- PostgreSQL: "ORDER BY items must appear in the select list"
116−-- when using SELECT DISTINCT
114+-- EDGE CASE 6: DISTINCT ORDER BY validated (FIXED)
115+-- ORDER BY columns must appear in SELECT list when using DISTINCT
116+-- (see DistinctOrderByNonSelected.purs)
117117-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118118
119−-- Produces: SELECT DISTINCT name FROM users ORDER BY age
120−-- PostgreSQL rejects this but it compiles
121−distinctOrderByNonSelected
119+distinctOrderByValid
122120 :: Q _ (name :: String) () _
123−distinctOrderByNonSelected = from usersTable
121+distinctOrderByValid = from usersTable
124122 # selectDistinct @"name"
125− # orderBy @"age"
123+ # orderBy @"name"
124+
125+distinctOrderByMultiple
126+ :: Q _ (name :: String, age :: Maybe Int) () _
127+distinctOrderByMultiple = from usersTable
128+ # selectDistinct @"name, age"
129+ # orderBy @"name, age DESC"
126130
127131-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
128132-- EDGE CASE 7: Nonexistent column in OVER (FIXED)