git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
Add clause ordering via phantom Row Type stage parameter on Q
commitc1a6a65653bef53715a49a1e5ab05399326a05a8 authorMark <mark.eibes@gmail.com> dateFri 13 Feb 2026, 19:05 (8 months ago) parent2eeb96b
2 files changed+129−59
src/Yoga/Postgres/Schema.purs+112−48 test/Schema.purs+17−11
src/Yoga/Postgres/Schema.purs+112−48
@@ -890,38 +890,67 @@ else instance
890890-- Query builder
891891-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
892892
893−newtype Q :: Symbol -> Row Type -> Row Type -> Row Type -> Type
894−newtype Q name cols result params = Q { sql :: String, values :: Array PG.PGValue }
893+newtype Q :: Symbol -> Row Type -> Row Type -> Row Type -> Row Type -> Type
894+newtype Q name cols result params stage = Q { sql :: String, values :: Array PG.PGValue }
895895
896−from :: forall name cols. Proxy (Table name cols) -> Q name cols () ()
896+class HasClause :: Symbol -> Row Type -> Constraint
897+class HasClause label row
898+
899+instance Row.Cons label Unit rest row => HasClause label row
900+
901+from :: forall name cols. Proxy (Table name cols) -> Q name cols () () ()
897902from _ = Q { sql: "", values: [] }
898903
899904selectAll
900− :: forall name cols result r p
905+ :: forall name cols result r p stage stage'
901906 . IsSymbol name
902907 => StripColumns cols result
903− => Q name cols r p
904− -> Q name cols result p
908+ => Row.Lacks "select" stage
909+ => Row.Lacks "insert" stage
910+ => Row.Lacks "set" stage
911+ => Row.Lacks "delete" stage
912+ => Row.Lacks "where" stage
913+ => Row.Lacks "orderBy" stage
914+ => Row.Lacks "limit" stage
915+ => Row.Lacks "offset" stage
916+ => Row.Cons "select" Unit stage stage'
917+ => Q name cols r p stage
918+ -> Q name cols result p stage'
905919selectAll _ = Q { sql: "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
906920
907921select
908− :: forall @sel name cols result r p
922+ :: forall @sel name cols result r p stage stage'
909923 . IsSymbol name
910924 => IsSymbol sel
911925 => ParseSelect sel cols result
912− => Q name cols r p
913− -> Q name cols result p
926+ => Row.Lacks "select" stage
927+ => Row.Lacks "insert" stage
928+ => Row.Lacks "set" stage
929+ => Row.Lacks "delete" stage
930+ => Row.Lacks "where" stage
931+ => Row.Lacks "orderBy" stage
932+ => Row.Lacks "limit" stage
933+ => Row.Lacks "offset" stage
934+ => Row.Cons "select" Unit stage stage'
935+ => Q name cols r p stage
936+ -> Q name cols result p stage'
914937select _ = Q { sql: "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
915938
916939where_
917− :: forall @whr name cols result params p
940+ :: forall @whr name cols result params p stage stage'
918941 . IsSymbol whr
919942 => ParseWhere whr cols params
920− => Q name cols result p
921− -> Q name cols result params
943+ => Row.Lacks "where" stage
944+ => Row.Lacks "insert" stage
945+ => Row.Lacks "orderBy" stage
946+ => Row.Lacks "limit" stage
947+ => Row.Lacks "offset" stage
948+ => Row.Cons "where" Unit stage stage'
949+ => Q name cols result p stage
950+ -> Q name cols result params stage'
922951where_ (Q q) = Q (q { sql = q.sql <> " WHERE " <> reflectSymbol (Proxy :: Proxy whr) })
923952
924−toSQL :: forall name cols result params. Q name cols result params -> String
953+toSQL :: forall name cols result params stage. Q name cols result params stage -> String
925954toSQL (Q q) = q.sql
926955
927956-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -994,16 +1023,21 @@ instance
9941023-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9951024
9961025insert
997− :: forall name cols colsRL insertableRL insertRow
1026+ :: forall name cols colsRL insertableRL insertRow stage stage'
9981027 . RowToList cols colsRL
9991028 => InsertableColumnsRL colsRL insertableRL
10001029 => ListToRow insertableRL insertRow
10011030 => IsSymbol name
10021031 => InsertColumnsRL colsRL
10031032 => RecordValuesRL insertableRL insertRow
1033+ => Row.Lacks "select" stage
1034+ => Row.Lacks "insert" stage
1035+ => Row.Lacks "set" stage
1036+ => Row.Lacks "delete" stage
1037+ => Row.Cons "insert" Unit stage stage'
10041038 => { | insertRow }
1005− -> Q name cols () ()
1006− -> Q name cols () ()
1039+ -> Q name cols () () stage
1040+ -> Q name cols () () stage'
10071041insert rec _ = Q { sql, values }
10081042 where
10091043 tableName = reflectSymbol (Proxy :: Proxy name)
@@ -1023,11 +1057,14 @@ insert rec _ = Q { sql, values }
10231057-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10241058
10251059returning
1026− :: forall @sel name cols result p
1060+ :: forall @sel name cols result p stage stage'
10271061 . IsSymbol sel
10281062 => ParseSelect sel cols result
1029− => Q name cols () p
1030− -> Q name cols result p
1063+ => Row.Lacks "returning" stage
1064+ => Row.Lacks "select" stage
1065+ => Row.Cons "returning" Unit stage stage'
1066+ => Q name cols () p stage
1067+ -> Q name cols result p stage'
10311068returning (Q q) = Q (q { sql = q.sql <> " RETURNING " <> reflectSymbol (Proxy :: Proxy sel) })
10321069
10331070-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1057,15 +1094,20 @@ instance (IsSymbol name, SetClauseRL tail) => SetClauseRL (RL.Cons name typ tail
10571094 <> setClauseRL (Proxy :: Proxy tail) (idx + 1)
10581095
10591096set
1060− :: forall name cols setRow setRL
1097+ :: forall name cols setRow setRL stage stage'
10611098 . RowToList setRow setRL
10621099 => IsSymbol name
10631100 => ValidateSetColumnsRL setRL cols
10641101 => SetClauseRL setRL
10651102 => RecordValuesRL setRL setRow
1103+ => Row.Lacks "select" stage
1104+ => Row.Lacks "insert" stage
1105+ => Row.Lacks "set" stage
1106+ => Row.Lacks "delete" stage
1107+ => Row.Cons "set" Unit stage stage'
10661108 => { | setRow }
1067− -> Q name cols () ()
1068− -> Q name cols () ()
1109+ -> Q name cols () () stage
1110+ -> Q name cols () () stage'
10691111set rec _ = Q { sql, values }
10701112 where
10711113 tableName = reflectSymbol (Proxy :: Proxy name)
@@ -1078,10 +1120,15 @@ set rec _ = Q { sql, values }
10781120-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10791121
10801122delete
1081− :: forall name cols r p
1123+ :: forall name cols r p stage stage'
10821124 . IsSymbol name
1083− => Q name cols r p
1084− -> Q name cols () ()
1125+ => Row.Lacks "select" stage
1126+ => Row.Lacks "insert" stage
1127+ => Row.Lacks "set" stage
1128+ => Row.Lacks "delete" stage
1129+ => Row.Cons "delete" Unit stage stage'
1130+ => Q name cols r p stage
1131+ -> Q name cols () () stage'
10851132delete _ = Q { sql: "DELETE FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
10861133
10871134-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1089,25 +1136,36 @@ delete _ = Q { sql: "DELETE FROM " <> reflectSymbol (Proxy :: Proxy name), value
10891136-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
10901137
10911138orderBy
1092− :: forall @cols name tableCols result params
1139+ :: forall @cols name tableCols result params stage stage'
10931140 . IsSymbol cols
10941141 => ValidateOrderBy cols tableCols
1095− => Q name tableCols result params
1096− -> Q name tableCols result params
1142+ => HasClause "select" stage
1143+ => Row.Lacks "orderBy" stage
1144+ => Row.Lacks "limit" stage
1145+ => Row.Lacks "offset" stage
1146+ => Row.Cons "orderBy" Unit stage stage'
1147+ => Q name tableCols result params stage
1148+ -> Q name tableCols result params stage'
10971149orderBy (Q q) = Q (q { sql = q.sql <> " ORDER BY " <> reflectSymbol (Proxy :: Proxy cols) })
10981150
10991151limit
1100− :: forall name cols result params
1101− . Int
1102− -> Q name cols result params
1103− -> Q name cols result params
1152+ :: forall name cols result params stage stage'
1153+ . HasClause "select" stage
1154+ => Row.Lacks "limit" stage
1155+ => Row.Cons "limit" Unit stage stage'
1156+ => Int
1157+ -> Q name cols result params stage
1158+ -> Q name cols result params stage'
11041159limit n (Q q) = Q (q { sql = q.sql <> " LIMIT " <> show n })
11051160
11061161offset
1107− :: forall name cols result params
1108− . Int
1109− -> Q name cols result params
1110− -> Q name cols result params
1162+ :: forall name cols result params stage stage'
1163+ . HasClause "select" stage
1164+ => Row.Lacks "offset" stage
1165+ => Row.Cons "offset" Unit stage stage'
1166+ => Int
1167+ -> Q name cols result params stage
1168+ -> Q name cols result params stage'
11111169offset n (Q q) = Q (q { sql = q.sql <> " OFFSET " <> show n })
11121170
11131171-- Validate ORDER BY columns: "name, age DESC" → validate name, age exist
@@ -1333,13 +1391,16 @@ else instance
13331391 MatchSymbol actual expected
13341392
13351393onConflict
1336− :: forall @target @action name cols result params
1394+ :: forall @target @action name cols result params stage stage'
13371395 . IsSymbol target
13381396 => IsSymbol action
13391397 => ValidateColumns target cols
13401398 => ParseConflictAction action cols
1341− => Q name cols result params
1342− -> Q name cols result params
1399+ => HasClause "insert" stage
1400+ => Row.Lacks "conflict" stage
1401+ => Row.Cons "conflict" Unit stage stage'
1402+ => Q name cols result params stage
1403+ -> Q name cols result params stage'
13431404onConflict (Q q) = Q
13441405 ( q
13451406 { sql = q.sql
@@ -1351,11 +1412,14 @@ onConflict (Q q) = Q
13511412 )
13521413
13531414onConflictDoNothing
1354− :: forall @target name cols result params
1415+ :: forall @target name cols result params stage stage'
13551416 . IsSymbol target
13561417 => ValidateColumns target cols
1357− => Q name cols result params
1358− -> Q name cols result params
1418+ => HasClause "insert" stage
1419+ => Row.Lacks "conflict" stage
1420+ => Row.Cons "conflict" Unit stage stage'
1421+ => Q name cols result params stage
1422+ -> Q name cols result params stage'
13591423onConflictDoNothing (Q q) = Q
13601424 ( q
13611425 { sql = q.sql
@@ -1409,13 +1473,13 @@ replaceNamedParams offset entries sql = do
14091473 { sql: sql', values: map (\e -> unsafeCoerce e.value) indexed }
14101474
14111475runQuery
1412− :: forall name cols result params paramsRL
1476+ :: forall name cols result params paramsRL stage
14131477 . RowToList params paramsRL
14141478 => ParamsToArray paramsRL params
14151479 => ReadForeign { | result }
14161480 => PG.Connection
14171481 -> { | params }
1418− -> Q name cols result params
1482+ -> Q name cols result params stage
14191483 -> Aff (Array { | result })
14201484runQuery conn params (Q q) = do
14211485 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
@@ -1425,13 +1489,13 @@ runQuery conn params (Q q) = do
14251489 pure (unsafeDecodeRows result.rows)
14261490
14271491runQueryOne
1428− :: forall name cols result params paramsRL
1492+ :: forall name cols result params paramsRL stage
14291493 . RowToList params paramsRL
14301494 => ParamsToArray paramsRL params
14311495 => ReadForeign { | result }
14321496 => PG.Connection
14331497 -> { | params }
1434− -> Q name cols result params
1498+ -> Q name cols result params stage
14351499 -> Aff (Maybe { | result })
14361500runQueryOne conn params (Q q) = do
14371501 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
@@ -1441,12 +1505,12 @@ runQueryOne conn params (Q q) = do
14411505 pure (result <#> unsafeDecodeRow)
14421506
14431507runExecute
1444− :: forall name cols params paramsRL
1508+ :: forall name cols params paramsRL stage
14451509 . RowToList params paramsRL
14461510 => ParamsToArray paramsRL params
14471511 => PG.Connection
14481512 -> { | params }
1449− -> Q name cols () params
1513+ -> Q name cols () params stage
14501514 -> Aff Int
14511515runExecute conn params (Q q) = do
14521516 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
test/Schema.purs+17−11
@@ -98,36 +98,42 @@ typedSelectAll
9898 :: Q "users" _
9999 (age :: Maybe Int, email :: String, id :: Int, name :: String)
100100 ()
101+ _
101102typedSelectAll = from usersTable # selectAll
102103
103104typedSelectCols
104105 :: Q "users" _
105106 (name :: String, email :: String)
106107 ()
108+ _
107109typedSelectCols = from usersTable # select @"name, email"
108110
109111typedSelectAlias
110112 :: Q "users" _
111113 (name :: String, e :: String)
112114 ()
115+ _
113116typedSelectAlias = from usersTable # select @"name, email AS e"
114117
115118typedWhere
116119 :: Q "users" _
117120 (age :: Maybe Int, email :: String, id :: Int, name :: String)
118121 (id :: Int)
122+ _
119123typedWhere = from usersTable # selectAll # where_ @"id = $id"
120124
121125typedWhereComplex
122126 :: Q "users" _
123127 (age :: Maybe Int, email :: String, id :: Int, name :: String)
124128 (name :: String, age :: Int)
129+ _
125130typedWhereComplex = from usersTable # selectAll # where_ @"name = $name AND age > $age"
126131
127132typedSelectColsWhere
128133 :: Q "users" _
129134 (name :: String)
130135 (age :: Int)
136+ _
131137typedSelectColsWhere = from usersTable # select @"name" # where_ @"age > $age"
132138
133139builderSelectAll :: String
@@ -153,32 +159,32 @@ builderWhereComplex = typedWhereComplex # toSQL
153159-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
154160
155161typedInsert
156− :: Q "users" _ () ()
162+ :: Q "users" _ () () _
157163typedInsert = from usersTable # insert { name: "Alice", email: "alice@example.com", age: Nothing :: Maybe Int }
158164
159165typedInsertReturning
160− :: Q "users" _ (id :: Int, name :: String) ()
166+ :: Q "users" _ (id :: Int, name :: String) () _
161167typedInsertReturning = from usersTable
162168 # insert { name: "Alice", email: "alice@example.com", age: Nothing :: Maybe Int }
163169 # returning @"id, name"
164170
165171typedSet
166− :: Q "users" _ () ()
172+ :: Q "users" _ () () _
167173typedSet = from usersTable # set { name: "Bob" }
168174
169175typedSetWhere
170− :: Q "users" _ () (id :: Int)
176+ :: Q "users" _ () (id :: Int) _
171177typedSetWhere = from usersTable # set { name: "Bob" } # where_ @"id = $id"
172178
173179typedSetReturning
174− :: Q "users" _ (id :: Int, name :: String, email :: String) (id :: Int)
180+ :: Q "users" _ (id :: Int, name :: String, email :: String) (id :: Int) _
175181typedSetReturning = from usersTable
176182 # set { name: "Bob" }
177183 # where_ @"id = $id"
178184 # returning @"id, name, email"
179185
180186typedUpsert
181− :: Q "users" _ () ()
187+ :: Q "users" _ () () _
182188typedUpsert = from usersTable
183189 # insert { name: "Alice", email: "alice@example.com", age: Nothing :: Maybe Int }
184190 # onConflictDoNothing @"email"
@@ -199,23 +205,23 @@ builderUpsert :: String
199205builderUpsert = typedUpsert # toSQL
200206
201207typedDelete
202− :: Q "users" _ () (id :: Int)
208+ :: Q "users" _ () (id :: Int) _
203209typedDelete = from usersTable # delete # where_ @"id = $id"
204210
205211typedDeleteReturning
206− :: Q "users" _ (name :: String, email :: String) (id :: Int)
212+ :: Q "users" _ (name :: String, email :: String) (id :: Int) _
207213typedDeleteReturning = from usersTable # delete # where_ @"id = $id" # returning @"name, email"
208214
209215typedOrderBy
210− :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) ()
216+ :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) () _
211217typedOrderBy = from usersTable # selectAll # orderBy @"name"
212218
213219typedOrderByDesc
214− :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) ()
220+ :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) () _
215221typedOrderByDesc = from usersTable # selectAll # orderBy @"name DESC, age ASC"
216222
217223typedLimitOffset
218− :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) ()
224+ :: Q "users" _ (age :: Maybe Int, email :: String, id :: Int, name :: String) () _
219225typedLimitOffset = from usersTable # selectAll # orderBy @"name" # limit 10 # offset 5
220226
221227builderDelete :: String