@@ -2,48 +2,47 @@ module Yoga.Postgres.Schema where
22
33import Prelude
44
5−import Data.Array as Array
6−import Data.Array (intercalate, mapWithIndex, foldl)
7−import Data.Date (Date)
8−import Data.Enum (toEnum)
9−import Data.DateTime (DateTime(..), date, time)
10−import Data.JSDate as JSDate
11−import Data.Maybe (Maybe(..), fromMaybe, maybe)
12−import Data.Time (Time(..))
13−import Data.Newtype (class Newtype)
14−import Data.Nullable (toNullable)
15−import Data.UUID (UUID)
16−import JS.BigInt (BigInt)
17−import Unsafe.Coerce (unsafeCoerce)
18−import Data.Reflectable (class Reflectable, reflectType)
19−import Data.String.Regex (regex, replace') as Regex
20−import Data.String.Regex (Regex) as Regex
21−import Data.String.Regex.Flags (global) as Regex
225import Control.Monad.Except (except, runExcept)
6+import Data.Array (intercalate, mapWithIndex, foldl)
7+import Data.Array as Array
8+import Data.Date (Date, day, month, year)
9+import Data.DateTime (DateTime(..))
2310import Data.Either (Either(..), note)
11+import Data.Enum (fromEnum, toEnum)
2412import Data.Int as Int
13+import Data.JSDate as JSDate
2514import Data.List.Types (NonEmptyList)
15+import Data.Map as Map
16+import Data.Maybe (Maybe(..), fromMaybe)
17+import Data.Newtype (class Newtype)
18+import Data.Nullable (toNullable)
2619import Data.Number as Number
20+import Data.Reflectable (class Reflectable, reflectType)
21+import Data.String.CodeUnits as SCU
2722import Data.String.Common as Str
2823import Data.String.Pattern (Pattern(..))
29−import Data.String.CodeUnits as SCU
30−import Data.Traversable (traverse)
31−import Effect.Exception (error) as Exception
32−import Data.Map as Map
24+import Data.String.Regex (Regex, regex, replace') as Regex
25+import Data.String.Regex.Flags (global) as Regex
3326import Data.Symbol (class IsSymbol, reflectSymbol)
34−import Type.Function (type (#))
27+import Data.Time (Time(..))
28+import Data.Traversable (traverse)
29+import Data.UUID (UUID)
30+import Data.UUID as UUID
3531import Effect.Aff (Aff, throwError)
36−import Foreign (Foreign, ForeignError(..), unsafeToForeign)
32+import Effect.Exception (error) as Exception
33+import Foreign (Foreign, ForeignError(..))
34+import JS.BigInt (BigInt)
3735import Prim.Boolean (True, False)
3836import Prim.Row (class Cons, class Lacks, class Union, class Nub) as Row
39−import Prim.RowList as RL
4037import Prim.RowList (class RowToList)
38+import Prim.RowList as RL
4139import Prim.Symbol (class Cons, class Append) as Symbol
4240import Prim.TypeError (class Fail, Beside, Text, Quote)
4341import Record (get) as Record
4442import Type.Proxy (Proxy(..))
4543import Type.RowList (class ListToRow)
46−import Yoga.JSON (class ReadForeign, readImpl)
44+import Unsafe.Coerce (unsafeCoerce)
45+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
4746import Yoga.Postgres as PG
4847
4948-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -69,15 +68,15 @@ data Unique a
6968
7069data Default :: forall k. k -> Type -> Type
7170data Default val a
71+data DefaultExpr :: Symbol -> Type -> Type
72+data DefaultExpr expression a
73+
7274
7375data References
7476
7577data ForeignKey :: Symbol -> Type -> Symbol -> Type -> Type
7678data ForeignKey table references col a
7779
78−data DefaultExpr :: Symbol -> Type -> Type
79−data DefaultExpr expr a
80−
8180-- Internal: used by LEFT JOIN to mark columns as nullable
8281data Nullable :: Type -> Type
8382data Nullable a
@@ -93,6 +92,9 @@ derive instance Newtype Jsonb _
9392instance ReadForeign Jsonb where
9493 readImpl = pure <<< Jsonb
9594
95+instance WriteForeign Jsonb where
96+ writeImpl (Jsonb value) = value
97+
9698-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9799-- PGUUID: newtype over Data.UUID.UUID for Postgres integration
98100-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -109,6 +111,9 @@ instance ReadForeign PGUUID where
109111 s :: String <- readImpl f
110112 pure (unsafeCoerce s)
111113
114+instance WriteForeign PGUUID where
115+ writeImpl (PGUUID uuid) = writeImpl (UUID.toString uuid)
116+
112117-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
113118-- PGDate / PGTime: newtypes for DATE and TIME columns
114119-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -128,6 +133,33 @@ instance ReadForeign PGDate where
128133 jsDate :: JSDate.JSDate
129134 jsDate = unsafeCoerce f
130135
136+instance WriteForeign PGDate where
137+ writeImpl (PGDate date) =
138+ writeImpl
139+ ( show (fromEnum (year date))
140+ <> "-"
141+ <> pad2 (fromEnum (month date))
142+ <> "-"
143+ <> pad2 (fromEnum (day date))
144+ )
145+ where
146+ pad2 n = if n < 10 then "0" <> show n else show n
147+
148+newtype TimestampTZ = TimestampTZ DateTime
149+
150+derive instance Newtype TimestampTZ _
151+derive instance Eq TimestampTZ
152+derive instance Ord TimestampTZ
153+instance Show TimestampTZ where
154+ show (TimestampTZ dt) = "(TimestampTZ " <> show dt <> ")"
155+
156+instance ReadForeign TimestampTZ where
157+ readImpl f =
158+ except $ note (pure (ForeignError "Invalid timestamp")) (JSDate.toDateTime jsDate <#> TimestampTZ)
159+ where
160+ jsDate :: JSDate.JSDate
161+ jsDate = unsafeCoerce f
162+
131163newtype PGTime = PGTime Time
132164
133165derive instance Newtype PGTime _
@@ -149,10 +181,34 @@ instance ReadForeign PGTime where
149181 mi <- Int.fromString mStr >>= toEnum
150182 let secParts = Str.split (Pattern ".") sStr
151183 s <- Int.fromString (fromMaybe sStr (Array.head secParts)) >>= toEnum
152− let ms = fromMaybe bottom (Array.index secParts 1 >>= Int.fromString >>= toEnum)
184+ let fraction = fromMaybe "" (Array.index secParts 1)
185+ let milliseconds = case SCU.length fraction of
186+ 0 -> "0"
187+ 1 -> fraction <> "00"
188+ 2 -> fraction <> "0"
189+ _ -> SCU.take 3 fraction
190+ let ms = fromMaybe bottom (Int.fromString milliseconds >>= toEnum)
153191 pure (PGTime (Time h mi s ms))
154192 _ -> Nothing
155193
194+instance WriteForeign PGTime where
195+ writeImpl (PGTime (Time hour minute second millisecond)) =
196+ writeImpl
197+ ( pad2 (fromEnum hour)
198+ <> ":"
199+ <> pad2 (fromEnum minute)
200+ <> ":"
201+ <> pad2 (fromEnum second)
202+ <> "."
203+ <> pad3 (fromEnum millisecond)
204+ )
205+ where
206+ pad2 n = if n < 10 then "0" <> show n else show n
207+ pad3 n
208+ | n < 10 = "00" <> show n
209+ | n < 100 = "0" <> show n
210+ | otherwise = show n
211+
156212-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
157213-- Point: geometric point "(x,y)"
158214-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -231,7 +287,7 @@ instance ExtractType a typ => ExtractType (PrimaryKey a) typ
231287else instance ExtractType a typ => ExtractType (AutoIncrement a) typ
232288else instance ExtractType a typ => ExtractType (Unique a) typ
233289else instance ExtractType a typ => ExtractType (Default val a) typ
234−else instance ExtractType a typ => ExtractType (DefaultExpr expr a) typ
290+else instance ExtractType a typ => ExtractType (DefaultExpr expression a) typ
235291else instance ExtractType a typ => ExtractType (ForeignKey t r c a) typ
236292else instance ExtractType a typ => ExtractType (Nullable a) (Maybe typ)
237293else instance ExtractType a a
@@ -264,6 +320,9 @@ instance PGTypeName BigInt where
264320instance PGTypeName PGUUID where
265321 pgTypeName _ = "UUID"
266322
323+instance PGTypeName TimestampTZ where
324+ pgTypeName _ = "TIMESTAMPTZ"
325+
267326instance PGTypeName PGDate where
268327 pgTypeName _ = "DATE"
269328
@@ -301,8 +360,24 @@ instance Reflectable sym String => RenderDefaultValue sym String where
301360instance Reflectable val Int => RenderDefaultValue val Int where
302361 renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
303362
304−instance Reflectable val Boolean => RenderDefaultValue val Boolean where
305− renderDefaultValue _ _ = if reflectType (Proxy :: Proxy val) then "DEFAULT true" else "DEFAULT false"
363+instance RenderDefaultValue True Boolean where
364+ renderDefaultValue _ _ = "DEFAULT true"
365+
366+instance RenderDefaultValue False Boolean where
367+ renderDefaultValue _ _ = "DEFAULT false"
368+
369+instance RenderDefaultValue "true" Boolean where
370+ renderDefaultValue _ _ = "DEFAULT true"
371+
372+instance RenderDefaultValue "false" Boolean where
373+ renderDefaultValue _ _ = "DEFAULT false"
374+
375+instance RenderDefaultValue "now()" TimestampTZ where
376+ renderDefaultValue _ _ = "DEFAULT now()"
377+
378+instance RenderDefaultValue "gen_random_uuid()" PGUUID where
379+ renderDefaultValue _ _ = "DEFAULT gen_random_uuid()"
380+
306381
307382class RenderConstraint a where
308383 renderConstraint :: Proxy a -> String
@@ -319,11 +394,10 @@ else instance RenderConstraint a => RenderConstraint (Unique a) where
319394else instance (RenderDefaultValue val a, RenderConstraint a) => RenderConstraint (Default val a) where
320395 renderConstraint _ = joinConstraints (renderDefaultValue (Proxy :: Proxy val) (Proxy :: Proxy a)) (renderConstraint (Proxy :: Proxy a))
321396
322−else instance (IsSymbol expr, RenderConstraint a) => RenderConstraint (DefaultExpr expr a) where
397+else instance (IsSymbol expression, RenderConstraint a) => RenderConstraint (DefaultExpr expression a) where
323398 renderConstraint _ = joinConstraints
324− ("DEFAULT " <> reflectSymbol (Proxy :: Proxy expr))
399+ ("DEFAULT " <> reflectSymbol (Proxy :: Proxy expression))
325400 (renderConstraint (Proxy :: Proxy a))
326−
327401else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
328402 renderConstraint _ = joinConstraints
329403 ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
@@ -384,6 +458,11 @@ instance
384458 let columns = renderColumnsRL (Proxy :: Proxy rl)
385459 "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
386460
461+createTableDDLIfNotExists :: forall @a. CreateTableDDL a => String
462+createTableDDLIfNotExists = do
463+ let ddl = createTableDDL @a
464+ "CREATE TABLE IF NOT EXISTS" <> SCU.drop 12 ddl
465+
387466-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
388467-- INSERT SQL generation
389468-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -395,7 +474,7 @@ instance IsAutoGenerated (AutoIncrement a) where
395474 isAutoGenerated _ = true
396475else instance IsAutoGenerated (Default v a) where
397476 isAutoGenerated _ = true
398−else instance IsAutoGenerated (DefaultExpr expr a) where
477+else instance IsAutoGenerated (DefaultExpr expression a) where
399478 isAutoGenerated _ = true
400479else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
401480 isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
@@ -440,15 +519,18 @@ instance
440519 insertSQLFor = do
441520 let tableName = reflectSymbol (Proxy :: Proxy name)
442521 let cols = insertColumnsRL (Proxy :: Proxy rl)
443− let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
444− "INSERT INTO " <> tableName
445− <> " ("
446− <> intercalate ", " cols
447− <> ")"
448− <> " VALUES ("
449− <> intercalate ", " placeholders
450− <> ")"
451− <> " RETURNING *"
522+ if Array.null cols then
523+ "INSERT INTO " <> tableName <> " DEFAULT VALUES RETURNING *"
524+ else do
525+ let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
526+ "INSERT INTO " <> tableName
527+ <> " ("
528+ <> intercalate ", " cols
529+ <> ")"
530+ <> " VALUES ("
531+ <> intercalate ", " placeholders
532+ <> ")"
533+ <> " RETURNING *"
452534
453535-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
454536-- SELECT SQL generation
@@ -903,6 +985,15 @@ else instance
903985 ) =>
904986 ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
905987
988+-- Bare alias (no AS keyword): "name n" means "name AS n"
989+else instance
990+ ( ResolveColumn colRef tables entry
991+ , ExtractType entry typ
992+ , SkipSpaces afterKeyword rest
993+ , ParseSelectExpectEnd rest tables (RL.Cons keyword typ accRL) outRL
994+ ) =>
995+ ParseSelectHandleAS keyword afterKeyword colRef tables accRL outRL
996+
906997class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
907998class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
908999
@@ -2707,7 +2798,7 @@ class IsAutoGeneratedTC constraints result | constraints -> result
27072798
27082799instance IsAutoGeneratedTC (AutoIncrement a) True
27092800else instance IsAutoGeneratedTC (Default s a) True
2710−else instance IsAutoGeneratedTC (DefaultExpr expr a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
27112802else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
27122803else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
27132804else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
@@ -2763,13 +2854,19 @@ class FieldToPGValue a where
27632854 fieldToPGValue :: a -> PG.PGValue
27642855
27652856instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2766− fieldToPGValue = toNullable >>> unsafeCoerce
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
27672860else instance FieldToPGValue DateTime where
27682861 fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
2862+else instance FieldToPGValue TimestampTZ where
2863+ fieldToPGValue (TimestampTZ dt) = JSDate.fromDateTime dt # unsafeCoerce
27692864else instance FieldToPGValue PGDate where
2770− fieldToPGValue (PGDate d) = JSDate.fromDateTime (DateTime d bottom) # unsafeCoerce
2771−else instance FieldToPGValue a where
2865+ fieldToPGValue = writeImpl >>> unsafeCoerce
2866+else instance FieldToPGValue PGUUID where
27722867 fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
27732870
27742871class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
27752872class RecordValuesRL rl row where
@@ -2837,6 +2934,20 @@ else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
28372934else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
28382935else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
28392936
2937+class HasMutation :: Row Type -> Constraint
2938+class HasMutation stage
2939+
2940+instance (RL.RowToList stage rl, HasMutationRL rl) => HasMutation stage
2941+
2942+class HasMutationRL :: RL.RowList Type -> Constraint
2943+class HasMutationRL rl
2944+
2945+instance HasMutationRL (RL.Cons "insert" Unit rest)
2946+else instance HasMutationRL (RL.Cons "set" Unit rest)
2947+else instance HasMutationRL (RL.Cons "delete" Unit rest)
2948+else instance HasMutationRL rest => HasMutationRL (RL.Cons label typ rest)
2949+else instance Fail (Text "RETURNING requires a preceding INSERT, UPDATE (set), or DELETE") => HasMutationRL RL.Nil
2950+
28402951toSQL :: forall tables result params stage. Q tables result params stage -> String
28412952toSQL (Q q) = q.sql
28422953
@@ -2846,6 +2957,12 @@ from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
28462957fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
28472958fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
28482959
2960+into :: forall name cols tables. IsSymbol name => Row.Cons name cols () tables => Proxy (Table name cols) -> Q tables () () (into :: Unit)
2961+into _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2962+
2963+update :: forall name cols tables. IsSymbol name => Row.Cons name cols () tables => Proxy (Table name cols) -> Q tables () () (update :: Unit)
2964+update _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2965+
28492966selectAll
28502967 :: forall tables name cols result r p stage stage'
28512968 . SingleTable tables name cols
@@ -2853,6 +2970,7 @@ selectAll
28532970 => StripColumns cols result
28542971 => Row.Lacks "select" stage
28552972 => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
28562974 => Row.Lacks "set" stage
28572975 => Row.Lacks "delete" stage
28582976 => Row.Lacks "where" stage
@@ -2870,6 +2988,7 @@ select
28702988 => ParseSelect sel tables result
28712989 => Row.Lacks "select" stage
28722990 => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
28732992 => Row.Lacks "set" stage
28742993 => Row.Lacks "delete" stage
28752994 => Row.Lacks "where" stage
@@ -2887,6 +3006,7 @@ selectDistinct
28873006 => ParseSelect sel tables result
28883007 => Row.Lacks "select" stage
28893008 => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
28903010 => Row.Lacks "set" stage
28913011 => Row.Lacks "delete" stage
28923012 => Row.Lacks "where" stage
@@ -2907,6 +3027,7 @@ selectDistinctOn
29073027 => ParseSelect sel tables result
29083028 => Row.Lacks "select" stage
29093029 => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
29103031 => Row.Lacks "set" stage
29113032 => Row.Lacks "delete" stage
29123033 => Row.Lacks "where" stage
@@ -3111,13 +3232,17 @@ insert rec _ = Q { sql, values }
31113232 tableName = reflectSymbol (Proxy :: Proxy name)
31123233 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
31133234 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
3114− sql = "INSERT INTO " <> tableName
3115− <> " ("
3116− <> intercalate ", " colNames
3117− <> ")"
3118− <> " VALUES ("
3119− <> intercalate ", " placeholders
3120− <> ")"
3235+ sql =
3236+ if Array.null colNames then
3237+ "INSERT INTO " <> tableName <> " DEFAULT VALUES"
3238+ else
3239+ "INSERT INTO " <> tableName
3240+ <> " ("
3241+ <> intercalate ", " colNames
3242+ <> ")"
3243+ <> " VALUES ("
3244+ <> intercalate ", " placeholders
3245+ <> ")"
31213246 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
31223247
31233248-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -3129,6 +3254,7 @@ returning
31293254 . SingleTable tables name cols
31303255 => IsSymbol sel
31313256 => ParseSelect sel tables result
3257+ => HasMutation stage
31323258 => Row.Lacks "returning" stage
31333259 => Row.Lacks "select" stage
31343260 => Row.Cons "returning" Unit stage stage'
@@ -3140,6 +3266,7 @@ returningAll
31403266 :: forall tables name cols result p stage stage'
31413267 . SingleTable tables name cols
31423268 => StripColumns cols result
3269+ => HasMutation stage
31433270 => Row.Lacks "returning" stage
31443271 => Row.Lacks "select" stage
31453272 => Row.Cons "returning" Unit stage stage'
@@ -3182,8 +3309,10 @@ set
31823309 => ValidateSetColumnsRL setRL cols
31833310 => SetClauseRL setRL
31843311 => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
31853313 => Row.Lacks "select" stage
31863314 => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
31873316 => Row.Lacks "set" stage
31883317 => Row.Lacks "delete" stage
31893318 => Row.Cons "set" Unit stage stage'
@@ -3207,6 +3336,7 @@ delete
32073336 => IsSymbol name
32083337 => Row.Lacks "select" stage
32093338 => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
32103340 => Row.Lacks "set" stage
32113341 => Row.Lacks "delete" stage
32123342 => Row.Cons "delete" Unit stage stage'
@@ -3447,6 +3577,66 @@ exceptAll
34473577 -> Q tables1 result params SetOpStage
34483578exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
34493579
3580+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3581+-- CTE (WITH clause) support
3582+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3583+
3584+newtype WithCTEs :: Row (Row Type) -> Row Type -> Type
3585+newtype WithCTEs cteTables cteParams = WithCTEs
3586+ { sql :: String, values :: Array PG.PGValue }
3587+
3588+-- Wrap a complete SELECT query as a named CTE
3589+cte
3590+ :: forall @name tables result params stage cteTables
3591+ . IsSymbol name
3592+ => HasClause "select" stage
3593+ => Row.Cons name result () cteTables
3594+ => Q tables result params stage
3595+ -> WithCTEs cteTables params
3596+cte (Q q) = WithCTEs
3597+ { sql: reflectSymbol (Proxy :: Proxy name) <> " AS (" <> q.sql <> ")"
3598+ , values: q.values
3599+ }
3600+
3601+-- Add another named CTE to an accumulator
3602+andCTE
3603+ :: forall @name tables result params stage cteTables cteParams cteTables' cteParams'
3604+ . IsSymbol name
3605+ => HasClause "select" stage
3606+ => Row.Lacks name cteTables
3607+ => Row.Cons name result cteTables cteTables'
3608+ => Row.Union cteParams params cteParams'
3609+ => Row.Nub cteParams' cteParams'
3610+ => Q tables result params stage
3611+ -> WithCTEs cteTables cteParams
3612+ -> WithCTEs cteTables' cteParams'
3613+andCTE (Q q) (WithCTEs w) = WithCTEs
3614+ { sql: w.sql <> ", " <> reflectSymbol (Proxy :: Proxy name) <> " AS (" <> q.sql <> ")"
3615+ , values: w.values <> q.values
3616+ }
3617+
3618+-- Start main query FROM a named CTE; cteTables becomes available as tables
3619+fromCTE
3620+ :: forall @name result restTables cteTables cteParams
3621+ . IsSymbol name
3622+ => Row.Cons name result restTables cteTables
3623+ => WithCTEs cteTables cteParams
3624+ -> Q cteTables () () ()
3625+fromCTE _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
3626+
3627+-- Finalize: prepend WITH clause and merge params
3628+with_
3629+ :: forall cteTables cteParams mainParams allParams result stage
3630+ . Row.Union cteParams mainParams allParams
3631+ => Row.Nub allParams allParams
3632+ => WithCTEs cteTables cteParams
3633+ -> Q cteTables result mainParams stage
3634+ -> Q cteTables result allParams stage
3635+with_ (WithCTEs w) (Q q) = Q
3636+ { sql: "WITH " <> w.sql <> " " <> q.sql
3637+ , values: w.values <> q.values
3638+ }
3639+
34503640-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
34513641-- Query execution
34523642-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -3462,12 +3652,13 @@ instance
34623652 ( IsSymbol name
34633653 , Row.Cons name typ rest row
34643654 , Row.Lacks name rest
3655+ , FieldToPGValue typ
34653656 , ParamsToArray tail row
34663657 ) =>
34673658 ParamsToArray (RL.Cons name typ tail) row where
34683659 paramsToArray _ rec =
34693660 [ { name: reflectSymbol (Proxy :: Proxy name)
3470− , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
34713662 }
34723663 ]
34733664 <> paramsToArray (Proxy :: Proxy tail) rec
@@ -3581,6 +3772,7 @@ runExecuteTx txn params (Q q) = do
35813772 let allValues = q.values <> values
35823773 PG.txExecute (PG.SQL sql) allValues txn
35833774
3775+
35843776decodeRows :: forall a. ReadForeign a => Array Foreign -> Either (NonEmptyList ForeignError) (Array a)
35853777decodeRows = traverse (readImpl >>> runExcept)
35863778