git.iko.soy/rowtype-yoga/purescript-yoga-postgres
on main help sign in
commit443f054978a395ce9cd607d9f7c69b8e96387001 authorMark <mark.eibes@gmail.com> dateSat 14 Feb 2026, 00:14 (8 months ago) parent60ed26c
6 files changed+172−58
spago.lock+62 spago.yaml+1 src/Yoga/Postgres/Client.purs+52−54 src/Yoga/Postgres/Schema.purs+25 test/EdgeCases.purs+14−4 test/Schema.purs+18
spago.lock+62
@@ -53,6 +53,9 @@
5353 {
5454 "yoga-json": ">=5.2.0 <6.0.0"
5555 },
56+ {
57+ "yoga-om": ">=1.2.2 <2.0.0"
58+ },
5659 {
5760 "yoga-sql-types": ">=0.1.0 <0.2.0"
5861 }
@@ -750,6 +753,19 @@
750753 "unsafe-coerce"
751754 ]
752755 },
756+ "record-studio": {
757+ "type": "registry",
758+ "version": "1.0.4",
759+ "integrity": "sha256-CRnFvSXpDDzio+buibUxFU9zPZDd7/lzrYC1Ya3FuIw=",
760+ "dependencies": [
761+ "heterogeneous",
762+ "lists",
763+ "prelude",
764+ "record",
765+ "typelevel-prelude",
766+ "unsafe-coerce"
767+ ]
768+ },
753769 "refs": {
754770 "type": "registry",
755771 "version": "6.0.0",
@@ -898,6 +914,23 @@
898914 "type-equality"
899915 ]
900916 },
917+ "uncurried-transformers": {
918+ "type": "registry",
919+ "version": "1.1.0",
920+ "integrity": "sha256-AqqBMzVLzozVoDorE3QcVVRDkLd7O3624mRHCDnPhaI=",
921+ "dependencies": [
922+ "control",
923+ "effect",
924+ "either",
925+ "functions",
926+ "identity",
927+ "prelude",
928+ "safe-coerce",
929+ "tailrec",
930+ "transformers",
931+ "tuples"
932+ ]
933+ },
901934 "unfoldable": {
902935 "type": "registry",
903936 "version": "6.0.0",
@@ -971,6 +1004,35 @@
9711004 "yoga-tree"
9721005 ]
9731006 },
1007+ "yoga-om": {
1008+ "type": "registry",
1009+ "version": "1.2.2",
1010+ "integrity": "sha256-uDV3/o8mUOTIgSEQFqIATPyladzL8BcjURvfS901hgI=",
1011+ "dependencies": [
1012+ "aff",
1013+ "avar",
1014+ "console",
1015+ "control",
1016+ "datetime",
1017+ "effect",
1018+ "either",
1019+ "exceptions",
1020+ "functions",
1021+ "maybe",
1022+ "newtype",
1023+ "parallel",
1024+ "prelude",
1025+ "record",
1026+ "record-studio",
1027+ "tailrec",
1028+ "transformers",
1029+ "tuples",
1030+ "typelevel-prelude",
1031+ "uncurried-transformers",
1032+ "unsafe-coerce",
1033+ "variant"
1034+ ]
1035+ },
9741036 "yoga-sql-types": {
9751037 "type": "registry",
9761038 "version": "0.1.0",
spago.yaml+1
@@ -25,6 +25,7 @@ package:
2525 - prelude: ">=6.0.2 <7.0.0"
2626 - unsafe-coerce: ">=6.0.0 <7.0.0"
2727 - yoga-json: ">=5.2.0 <6.0.0"
28+ - yoga-om: ">=1.2.2 <2.0.0"
2829 - yoga-sql-types: ">=0.1.0 <0.2.0"
2930 test:
3031 main: Test.Postgres.Main
src/Yoga/Postgres/Client.purs+52−54
@@ -5,7 +5,6 @@ import Prelude
55import Data.Array (intercalate, mapWithIndex)
66import Data.Maybe (Maybe)
77import Data.Symbol (class IsSymbol, reflectSymbol)
8−import Effect.Aff (Aff)
98import Prim.Boolean (True, False)
109import Prim.Row (class Cons, class Lacks, class Union) as Row
1110import Prim.RowList as RL
@@ -14,6 +13,7 @@ import Prim.TypeError (class Fail, Text)
1413import Type.Proxy (Proxy(..))
1514import Type.RowList (class ListToRow)
1615import Yoga.JSON (class ReadForeign)
16+import Yoga.Om (Om, ask)
1717import Yoga.Postgres as PG
1818import Yoga.Postgres.Schema
1919 ( class ColumnCountRL, class ColumnNamesRL, class ExtractType
@@ -26,6 +26,12 @@ import Yoga.Postgres.Schema
2626 , unsafeDecodeRow, unsafeDecodeRows
2727 )
2828
29+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
30+-- Context type for Om
31+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
32+
33+type PgCtx r = (pgConnection :: PG.Connection | r)
34+
2935-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3036-- Type extraction from tables
3137-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -156,21 +162,20 @@ instance
156162-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157163
158164findAll
159− :: forall name cols row
165+ :: forall name cols row ctx errs
160166 . IsSymbol name
161167 => TableRow (Table name cols) row
162168 => ReadForeign { | row }
163169 => Proxy (Table name cols)
164− -> PG.Connection
165− -> Aff (Array { | row })
166−findAll _ conn = do
167− result <- PG.query (PG.SQL sql) [] conn
168− pure (unsafeDecodeRows result.rows)
170+ -> Om { | PgCtx ctx } errs (Array { | row })
171+findAll _ = do
172+ { pgConnection } <- ask
173+ PG.query (PG.SQL sql) [] pgConnection # liftAff <#> \r -> unsafeDecodeRows r.rows
169174 where
170175 sql = "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name)
171176
172177findById
173− :: forall name cols row pkCol pkType
178+ :: forall name cols row pkCol pkType ctx errs
174179 . IsSymbol name
175180 => IsSymbol pkCol
176181 => TableRow (Table name cols) row
@@ -178,19 +183,18 @@ findById
178183 => FieldToPGValue pkType
179184 => ReadForeign { | row }
180185 => Proxy (Table name cols)
181− -> PG.Connection
182186 -> pkType
183− -> Aff (Maybe { | row })
184−findById _ conn pk = do
185− result <- PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] conn
186− pure (result <#> unsafeDecodeRow)
187+ -> Om { | PgCtx ctx } errs (Maybe { | row })
188+findById _ pk = do
189+ { pgConnection } <- ask
190+ PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] pgConnection # liftAff <#> \r -> r <#> unsafeDecodeRow
187191 where
188192 sql = "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name)
189193 <> " WHERE " <> reflectSymbol (Proxy :: Proxy pkCol) <> " = $1"
190194
191195create
192196 :: forall name cols colsRL insertableRL insertable requiredRL required
193− optionalProvided missing userRow userRowRL row
197+ optionalProvided missing userRow userRowRL row ctx errs
194198 . IsSymbol name
195199 => RowToList cols colsRL
196200 => InsertableColumnsRL colsRL insertableRL
@@ -205,12 +209,11 @@ create
205209 => TableRow (Table name cols) row
206210 => ReadForeign { | row }
207211 => Proxy (Table name cols)
208− -> PG.Connection
209212 -> { | userRow }
210− -> Aff (Maybe { | row })
211−create _ conn rec = do
212− result <- PG.queryOne (PG.SQL sql) values conn
213− pure (result <#> unsafeDecodeRow)
213+ -> Om { | PgCtx ctx } errs (Maybe { | row })
214+create _ rec = do
215+ { pgConnection } <- ask
216+ PG.queryOne (PG.SQL sql) values pgConnection # liftAff <#> \r -> r <#> unsafeDecodeRow
214217 where
215218 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
216219 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
@@ -221,7 +224,7 @@ create _ conn rec = do
221224 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
222225
223226updateById
224− :: forall name cols row pkCol pkType setRow setRL
227+ :: forall name cols row pkCol pkType setRow setRL ctx errs
225228 . IsSymbol name
226229 => IsSymbol pkCol
227230 => TableRow (Table name cols) row
@@ -234,13 +237,12 @@ updateById
234237 => FieldToPGValue pkType
235238 => ReadForeign { | row }
236239 => Proxy (Table name cols)
237− -> PG.Connection
238240 -> pkType
239241 -> { | setRow }
240− -> Aff (Maybe { | row })
241−updateById _ conn pk rec = do
242− result <- PG.queryOne (PG.SQL sql) values conn
243− pure (result <#> unsafeDecodeRow)
242+ -> Om { | PgCtx ctx } errs (Maybe { | row })
243+updateById _ pk rec = do
244+ { pgConnection } <- ask
245+ PG.queryOne (PG.SQL sql) values pgConnection # liftAff <#> \r -> r <#> unsafeDecodeRow
244246 where
245247 setClauses = setClauseRL (Proxy :: Proxy setRL) 1
246248 setCount = columnCountRL (Proxy :: Proxy setRL)
@@ -251,17 +253,17 @@ updateById _ conn pk rec = do
251253 values = recordValuesRL (Proxy :: Proxy setRL) rec <> [ fieldToPGValue pk ]
252254
253255deleteById
254− :: forall name cols pkCol pkType
256+ :: forall name cols pkCol pkType ctx errs
255257 . IsSymbol name
256258 => IsSymbol pkCol
257259 => FindPrimaryKey (Table name cols) pkCol pkType
258260 => FieldToPGValue pkType
259261 => Proxy (Table name cols)
260− -> PG.Connection
261262 -> pkType
262− -> Aff Int
263−deleteById _ conn pk =
264− PG.execute (PG.SQL sql) [ fieldToPGValue pk ] conn
263+ -> Om { | PgCtx ctx } errs Int
264+deleteById _ pk = do
265+ { pgConnection } <- ask
266+ PG.execute (PG.SQL sql) [ fieldToPGValue pk ] pgConnection # liftAff
265267 where
266268 sql = "DELETE FROM " <> reflectSymbol (Proxy :: Proxy name)
267269 <> " WHERE " <> reflectSymbol (Proxy :: Proxy pkCol) <> " = $1"
@@ -272,7 +274,7 @@ deleteById _ conn pk =
272274
273275findWith
274276 :: forall childName childCols parentName parentCols
275− childRow parentRow localCol parentCol rest result
277+ childRow parentRow localCol parentCol rest result ctx errs
276278 . IsSymbol childName
277279 => IsSymbol parentName
278280 => IsSymbol localCol
@@ -285,11 +287,10 @@ findWith
285287 => ReadForeign { | result }
286288 => Proxy (Table childName childCols)
287289 -> Proxy (Table parentName parentCols)
288− -> PG.Connection
289− -> Aff (Array { | result })
290−findWith _ _ conn = do
291− result <- PG.query (PG.SQL sql) [] conn
292− pure (unsafeDecodeRows result.rows)
290+ -> Om { | PgCtx ctx } errs (Array { | result })
291+findWith _ _ = do
292+ { pgConnection } <- ask
293+ PG.query (PG.SQL sql) [] pgConnection # liftAff <#> \r -> unsafeDecodeRows r.rows
293294 where
294295 childTbl = reflectSymbol (Proxy :: Proxy childName)
295296 parentTbl = reflectSymbol (Proxy :: Proxy parentName)
@@ -302,7 +303,7 @@ findWith _ _ conn = do
302303
303304findByIdWith
304305 :: forall childName childCols parentName parentCols
305− childRow parentRow localCol parentCol pkCol pkType rest result
306+ childRow parentRow localCol parentCol pkCol pkType rest result ctx errs
306307 . IsSymbol childName
307308 => IsSymbol parentName
308309 => IsSymbol localCol
@@ -318,12 +319,11 @@ findByIdWith
318319 => ReadForeign { | result }
319320 => Proxy (Table childName childCols)
320321 -> Proxy (Table parentName parentCols)
321− -> PG.Connection
322322 -> pkType
323− -> Aff (Maybe { | result })
324−findByIdWith _ _ conn pk = do
325− result <- PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] conn
326− pure (result <#> unsafeDecodeRow)
323+ -> Om { | PgCtx ctx } errs (Maybe { | result })
324+findByIdWith _ _ pk = do
325+ { pgConnection } <- ask
326+ PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] pgConnection # liftAff <#> \r -> r <#> unsafeDecodeRow
327327 where
328328 childTbl = reflectSymbol (Proxy :: Proxy childName)
329329 parentTbl = reflectSymbol (Proxy :: Proxy parentName)
@@ -342,7 +342,7 @@ findByIdWith _ _ conn pk = do
342342
343343findIncluding
344344 :: forall @label parentName parentCols childName childCols
345− parentRow childRow localCol parentCol result
345+ parentRow childRow localCol parentCol result ctx errs
346346 . IsSymbol label
347347 => IsSymbol parentName
348348 => IsSymbol childName
@@ -356,11 +356,10 @@ findIncluding
356356 => ReadForeign { | result }
357357 => Proxy (Table parentName parentCols)
358358 -> Proxy (Table childName childCols)
359− -> PG.Connection
360− -> Aff (Array { | result })
361−findIncluding _ _ conn = do
362− result <- PG.query (PG.SQL sql) [] conn
363− pure (unsafeDecodeRows result.rows)
359+ -> Om { | PgCtx ctx } errs (Array { | result })
360+findIncluding _ _ = do
361+ { pgConnection } <- ask
362+ PG.query (PG.SQL sql) [] pgConnection # liftAff <#> \r -> unsafeDecodeRows r.rows
364363 where
365364 parentTbl = reflectSymbol (Proxy :: Proxy parentName)
366365 childTbl = reflectSymbol (Proxy :: Proxy childName)
@@ -374,7 +373,7 @@ findIncluding _ _ conn = do
374373
375374findByIdIncluding
376375 :: forall @label parentName parentCols childName childCols
377− parentRow childRow localCol parentCol pkCol pkType result
376+ parentRow childRow localCol parentCol pkCol pkType result ctx errs
378377 . IsSymbol label
379378 => IsSymbol parentName
380379 => IsSymbol childName
@@ -391,12 +390,11 @@ findByIdIncluding
391390 => ReadForeign { | result }
392391 => Proxy (Table parentName parentCols)
393392 -> Proxy (Table childName childCols)
394− -> PG.Connection
395393 -> pkType
396− -> Aff (Maybe { | result })
397−findByIdIncluding _ _ conn pk = do
398− result <- PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] conn
399− pure (result <#> unsafeDecodeRow)
394+ -> Om { | PgCtx ctx } errs (Maybe { | result })
395+findByIdIncluding _ _ pk = do
396+ { pgConnection } <- ask
397+ PG.queryOne (PG.SQL sql) [ fieldToPGValue pk ] pgConnection # liftAff <#> \r -> r <#> unsafeDecodeRow
400398 where
401399 parentTbl = reflectSymbol (Proxy :: Proxy parentName)
402400 childTbl = reflectSymbol (Proxy :: Proxy childName)
src/Yoga/Postgres/Schema.purs+25
@@ -1058,6 +1058,23 @@ else instance FlushWhereWord "ALL" currentType tables paramsIn (Array currentTyp
10581058else instance FlushWhereWord "CAST" currentType tables paramsIn currentType paramsIn
10591059else instance FlushWhereWord "AS" currentType tables paramsIn currentType paramsIn
10601060else instance FlushWhereWord "EXISTS" currentType tables paramsIn currentType paramsIn
1061+-- Lowercase SQL keywords
1062+else instance FlushWhereWord "and" currentType tables paramsIn currentType paramsIn
1063+else instance FlushWhereWord "or" currentType tables paramsIn currentType paramsIn
1064+else instance FlushWhereWord "not" currentType tables paramsIn currentType paramsIn
1065+else instance FlushWhereWord "is" currentType tables paramsIn currentType paramsIn
1066+else instance FlushWhereWord "null" currentType tables paramsIn currentType paramsIn
1067+else instance FlushWhereWord "like" currentType tables paramsIn currentType paramsIn
1068+else instance FlushWhereWord "ilike" currentType tables paramsIn currentType paramsIn
1069+else instance FlushWhereWord "in" currentType tables paramsIn (Array currentType) paramsIn
1070+else instance FlushWhereWord "true" currentType tables paramsIn currentType paramsIn
1071+else instance FlushWhereWord "false" currentType tables paramsIn currentType paramsIn
1072+else instance FlushWhereWord "between" currentType tables paramsIn currentType paramsIn
1073+else instance FlushWhereWord "any" currentType tables paramsIn (Array currentType) paramsIn
1074+else instance FlushWhereWord "all" currentType tables paramsIn (Array currentType) paramsIn
1075+else instance FlushWhereWord "cast" currentType tables paramsIn currentType paramsIn
1076+else instance FlushWhereWord "as" currentType tables paramsIn currentType paramsIn
1077+else instance FlushWhereWord "exists" currentType tables paramsIn currentType paramsIn
10611078-- Aggregate functions (for HAVING support)
10621079else instance FlushWhereWord "COUNT" currentType tables paramsIn Int paramsIn
10631080else instance FlushWhereWord "SUM" currentType tables paramsIn currentType paramsIn
@@ -1066,6 +1083,14 @@ else instance FlushWhereWord "MIN" currentType tables paramsIn currentType param
10661083else instance FlushWhereWord "MAX" currentType tables paramsIn currentType paramsIn
10671084else instance FlushWhereWord "ARRAY_AGG" currentType tables paramsIn currentType paramsIn
10681085else instance FlushWhereWord "STRING_AGG" currentType tables paramsIn String paramsIn
1086+-- Lowercase aggregate functions
1087+else instance FlushWhereWord "count" currentType tables paramsIn Int paramsIn
1088+else instance FlushWhereWord "sum" currentType tables paramsIn currentType paramsIn
1089+else instance FlushWhereWord "avg" currentType tables paramsIn Number paramsIn
1090+else instance FlushWhereWord "min" currentType tables paramsIn currentType paramsIn
1091+else instance FlushWhereWord "max" currentType tables paramsIn currentType paramsIn
1092+else instance FlushWhereWord "array_agg" currentType tables paramsIn currentType paramsIn
1093+else instance FlushWhereWord "string_agg" currentType tables paramsIn String paramsIn
10691094else instance FlushWhereWord "*" currentType tables paramsIn currentType paramsIn
10701095-- Postgres type names (for ::type casts)
10711096else instance FlushWhereWord "text" currentType tables paramsIn currentType paramsIn
test/EdgeCases.purs+14−4
@@ -82,13 +82,23 @@ overClauseNotValidated = from usersTable
8282-- "IS NULL" works but "is null" would fail (tries to resolve "is" as column)
8383-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
8484
85−-- This works:
8685uppercaseIsNull :: Q _ _ () _
8786uppercaseIsNull = from usersTable # selectAll # where_ @"age IS NULL"
8887
89−-- This would FAIL to compile (uncomment to verify):
90−-- lowercaseIsNull :: Q _ _ () _
91−-- lowercaseIsNull = from usersTable # selectAll # where_ @"age is null"
88+lowercaseIsNull :: Q _ _ () _
89+lowercaseIsNull = from usersTable # selectAll # where_ @"age is null"
90+
91+lowercaseNotIn :: Q _ _ (ids :: Array Int) _
92+lowercaseNotIn = from usersTable # selectAll # where_ @"id not in $ids"
93+
94+lowercaseLike :: Q _ _ (pat :: String) _
95+lowercaseLike = from usersTable # selectAll # where_ @"name like $pat"
96+
97+lowercaseBetween :: Q _ _ (lo :: Int, hi :: Int) _
98+lowercaseBetween = from usersTable # selectAll # where_ @"age between $lo and $hi"
99+
100+lowercaseAndOr :: Q _ _ (minAge :: Int, namePat :: String) _
101+lowercaseAndOr = from usersTable # selectAll # where_ @"age > $minAge or name like $namePat"
92102
93103-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
94104-- EDGE CASE 6: DISTINCT ORDER BY on non-selected column
test/Schema.purs+18
@@ -610,6 +610,18 @@ typedIsNotNull
610610 :: Q _ _ () _
611611typedIsNotNull = from usersTable # selectAll # where_ @"age IS NOT NULL"
612612
613+typedIsNullLowercase
614+ :: Q _ _ () _
615+typedIsNullLowercase = from usersTable # selectAll # where_ @"age is null"
616+
617+typedBetweenLowercase
618+ :: Q _ _ (lo :: Int, hi :: Int) _
619+typedBetweenLowercase = from usersTable # selectAll # where_ @"age between $lo and $hi"
620+
621+typedLikeLowercase
622+ :: Q _ _ (pat :: String) _
623+typedLikeLowercase = from usersTable # selectAll # where_ @"name like $pat"
624+
613625typedNestedParens
614626 :: Q _ _ (a :: Int, b :: Int, name :: String) _
615627typedNestedParens = from usersTable # selectAll
@@ -787,6 +799,12 @@ spec = do
787799 (typedIsNull # toSQL) `shouldEqual` "SELECT * FROM users WHERE age IS NULL"
788800 it "builds IS NOT NULL" do
789801 (typedIsNotNull # toSQL) `shouldEqual` "SELECT * FROM users WHERE age IS NOT NULL"
802+ it "builds lowercase is null" do
803+ (typedIsNullLowercase # toSQL) `shouldEqual` "SELECT * FROM users WHERE age is null"
804+ it "builds lowercase between" do
805+ (typedBetweenLowercase # toSQL) `shouldEqual` "SELECT * FROM users WHERE age between $lo and $hi"
806+ it "builds lowercase like" do
807+ (typedLikeLowercase # toSQL) `shouldEqual` "SELECT * FROM users WHERE name like $pat"
790808 it "builds nested parentheses" do
791809 (typedNestedParens # toSQL) `shouldEqual` "SELECT * FROM users WHERE (age > $a OR age < $b) AND name = $name"
792810