@@ -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
@@ -1001,11 +1092,66 @@ instance ResolveAggregateArg "*" tables Star
10011092else instance ResolveAggregateArg "" tables Star
10021093else instance
10031094 ( SkipSpaces args trimmedFront
1004− , ExtractWord trimmedFront col _rest
1005− , ResolveAggregateArgCol col tables argType
1095+ , ExtractIdent trimmedFront col rest
1096+ , ResolveAggregateArgDispatch col rest tables argType
10061097 ) =>
10071098 ResolveAggregateArg args tables argType
10081099
1100+-- ExtractIdent: like ExtractWord but also stops at "(" so we can detect nested function calls
1101+class ExtractIdent :: Symbol -> Symbol -> Symbol -> Constraint
1102+class ExtractIdent sym word rest | sym -> word rest
1103+
1104+instance ExtractIdent "" "" ""
1105+else instance
1106+ ( Symbol.Cons h t sym
1107+ , ExtractIdentGo h t "" word rest
1108+ ) =>
1109+ ExtractIdent sym word rest
1110+
1111+class ExtractIdentGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
1112+class ExtractIdentGo head tail acc word rest | head tail acc -> word rest
1113+
1114+instance (SkipSpaces tail rest) => ExtractIdentGo " " tail acc acc rest
1115+else instance Symbol.Cons "," tail rest => ExtractIdentGo "," tail acc acc rest
1116+else instance Symbol.Append "(" tail rest => ExtractIdentGo "(" tail acc acc rest
1117+else instance Symbol.Append acc h word => ExtractIdentGo h "" acc word ""
1118+else instance
1119+ ( Symbol.Append acc h acc'
1120+ , Symbol.Cons nextH nextT tail
1121+ , ExtractIdentGo nextH nextT acc' word rest
1122+ ) =>
1123+ ExtractIdentGo h tail acc word rest
1124+
1125+-- After extracting the identifier, check if followed by "(" (nested function call) or not (column ref)
1126+class ResolveAggregateArgDispatch :: Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1127+class ResolveAggregateArgDispatch col rest tables argType | col rest tables -> argType
1128+
1129+instance
1130+ ResolveAggregateArgCol col tables argType =>
1131+ ResolveAggregateArgDispatch col "" tables argType
1132+
1133+else instance
1134+ ( Symbol.Cons h t rest
1135+ , ResolveAggregateArgDispatchByHead h t col tables argType
1136+ ) =>
1137+ ResolveAggregateArgDispatch col rest tables argType
1138+
1139+class ResolveAggregateArgDispatchByHead :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1140+class ResolveAggregateArgDispatchByHead head tail col tables argType | head tail col tables -> argType
1141+
1142+-- Nested function call: col(...)
1143+instance
1144+ ( ExtractUntilParen tail innerArgs _afterParen
1145+ , ResolveAggregateArg innerArgs tables innerArgType
1146+ , AggregateReturnType col innerArgType argType
1147+ ) =>
1148+ ResolveAggregateArgDispatchByHead "(" tail col tables argType
1149+
1150+-- Not a function call: column reference
1151+else instance
1152+ ResolveAggregateArgCol col tables argType =>
1153+ ResolveAggregateArgDispatchByHead h t col tables argType
1154+
10091155class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
10101156class ResolveAggregateArgCol col tables argType | col tables -> argType
10111157
@@ -2511,13 +2657,85 @@ else instance
25112657 , SkipSpaces rest1 rest2
25122658 , ExpectChar rest2 "=" rest3
25132659 , SkipSpaces rest3 rest4
2514− , ExtractWord rest4 excRef rest5
2515− , ValidateExcludedRef excRef colName
2516− , SkipSpaces rest5 rest6
2517− , ParseAssignmentsContinue rest6 cols
2660+ , SkipExpression rest4 rest5
2661+ , ParseAssignmentsContinue rest5 cols
25182662 ) =>
25192663 ParseAssignments sym cols
25202664
2665+-- | Skip a SQL expression on the RHS of an assignment.
2666+-- | Handles balanced parentheses so that e.g. now() and
2667+-- | COALESCE(EXCLUDED.value, '') are consumed as a single expression.
2668+-- | Stops at an unparenthesised comma or end-of-string.
2669+class SkipExpression :: Symbol -> Symbol -> Constraint
2670+class SkipExpression sym rest | sym -> rest
2671+
2672+instance SkipExpression "" ""
2673+else instance
2674+ ( Symbol.Cons h t sym
2675+ , SkipExpressionGo h t rest
2676+ ) =>
2677+ SkipExpression sym rest
2678+
2679+class SkipExpressionGo :: Symbol -> Symbol -> Symbol -> Constraint
2680+class SkipExpressionGo head tail rest | head tail -> rest
2681+
2682+-- Comma at depth 0 → stop, keep comma in rest
2683+instance Symbol.Cons "," tail rest => SkipExpressionGo "," tail rest
2684+-- Open paren → enter nested expression
2685+else instance
2686+ ( SkipNested tail rest1
2687+ , SkipExpression rest1 rest
2688+ ) =>
2689+ SkipExpressionGo "(" tail rest
2690+-- Space → skip and continue (expressions can have spaces, e.g. "EXCLUDED.col")
2691+else instance (SkipSpaces tail rest1, SkipExpressionAfterSpace rest1 rest) => SkipExpressionGo " " tail rest
2692+-- End of string after this char
2693+else instance SkipExpressionGo h "" ""
2694+-- Regular char → continue
2695+else instance (Symbol.Cons h2 t2 tail, SkipExpressionGo h2 t2 rest) => SkipExpressionGo h tail rest
2696+
2697+-- | After a space in an expression, check if we've hit a comma or a new assignment (word = ...).
2698+-- | If the next word is a column name followed by '=', this is a new assignment, so stop.
2699+-- | Otherwise, continue the expression.
2700+class SkipExpressionAfterSpace :: Symbol -> Symbol -> Constraint
2701+class SkipExpressionAfterSpace sym rest | sym -> rest
2702+
2703+instance SkipExpressionAfterSpace "" ""
2704+else instance
2705+ ( Symbol.Cons h t sym
2706+ , SkipExpressionAfterSpaceGo h t sym rest
2707+ ) =>
2708+ SkipExpressionAfterSpace sym rest
2709+
2710+class SkipExpressionAfterSpaceGo :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
2711+class SkipExpressionAfterSpaceGo head tail sym rest | head tail sym -> rest
2712+
2713+-- Comma → stop
2714+instance Symbol.Cons "," tail rest => SkipExpressionAfterSpaceGo "," tail sym rest
2715+-- Otherwise continue the expression
2716+else instance (SkipExpressionGo h tail rest) => SkipExpressionAfterSpaceGo h tail sym rest
2717+
2718+-- | Skip a parenthesised group (after the opening paren has been consumed).
2719+-- | Handles nesting.
2720+class SkipNested :: Symbol -> Symbol -> Constraint
2721+class SkipNested sym rest | sym -> rest
2722+
2723+instance
2724+ ( Symbol.Cons h t sym
2725+ , SkipNestedGo h t rest
2726+ ) =>
2727+ SkipNested sym rest
2728+
2729+class SkipNestedGo :: Symbol -> Symbol -> Symbol -> Constraint
2730+class SkipNestedGo head tail rest | head tail -> rest
2731+
2732+-- Close paren → done
2733+instance SkipNestedGo ")" tail tail
2734+-- Open paren → recurse
2735+else instance (SkipNested tail rest1, SkipNested rest1 rest) => SkipNestedGo "(" tail rest
2736+-- Any other char → continue
2737+else instance (SkipNested tail rest) => SkipNestedGo h tail rest
2738+
25212739class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
25222740class ParseAssignmentsContinue sym cols
25232741
@@ -2580,7 +2798,7 @@ class IsAutoGeneratedTC constraints result | constraints -> result
25802798
25812799instance IsAutoGeneratedTC (AutoIncrement a) True
25822800else instance IsAutoGeneratedTC (Default s a) True
2583−else instance IsAutoGeneratedTC (DefaultExpr expr a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
25842802else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
25852803else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
25862804else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
@@ -2636,13 +2854,19 @@ class FieldToPGValue a where
26362854 fieldToPGValue :: a -> PG.PGValue
26372855
26382856instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2639− fieldToPGValue = toNullable >>> unsafeCoerce
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
26402860else instance FieldToPGValue DateTime where
26412861 fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
2862+else instance FieldToPGValue TimestampTZ where
2863+ fieldToPGValue (TimestampTZ dt) = JSDate.fromDateTime dt # unsafeCoerce
26422864else instance FieldToPGValue PGDate where
2643− fieldToPGValue (PGDate d) = JSDate.fromDateTime (DateTime d bottom) # unsafeCoerce
2644−else instance FieldToPGValue a where
2865+ fieldToPGValue = writeImpl >>> unsafeCoerce
2866+else instance FieldToPGValue PGUUID where
26452867 fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
26462870
26472871class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
26482872class RecordValuesRL rl row where
@@ -2710,6 +2934,20 @@ else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
27102934else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
27112935else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
27122936
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+
27132951toSQL :: forall tables result params stage. Q tables result params stage -> String
27142952toSQL (Q q) = q.sql
27152953
@@ -2719,6 +2957,12 @@ from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
27192957fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
27202958fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
27212959
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+
27222966selectAll
27232967 :: forall tables name cols result r p stage stage'
27242968 . SingleTable tables name cols
@@ -2726,6 +2970,7 @@ selectAll
27262970 => StripColumns cols result
27272971 => Row.Lacks "select" stage
27282972 => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
27292974 => Row.Lacks "set" stage
27302975 => Row.Lacks "delete" stage
27312976 => Row.Lacks "where" stage
@@ -2743,6 +2988,7 @@ select
27432988 => ParseSelect sel tables result
27442989 => Row.Lacks "select" stage
27452990 => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
27462992 => Row.Lacks "set" stage
27472993 => Row.Lacks "delete" stage
27482994 => Row.Lacks "where" stage
@@ -2760,6 +3006,7 @@ selectDistinct
27603006 => ParseSelect sel tables result
27613007 => Row.Lacks "select" stage
27623008 => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
27633010 => Row.Lacks "set" stage
27643011 => Row.Lacks "delete" stage
27653012 => Row.Lacks "where" stage
@@ -2780,6 +3027,7 @@ selectDistinctOn
27803027 => ParseSelect sel tables result
27813028 => Row.Lacks "select" stage
27823029 => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
27833031 => Row.Lacks "set" stage
27843032 => Row.Lacks "delete" stage
27853033 => Row.Lacks "where" stage
@@ -2984,13 +3232,17 @@ insert rec _ = Q { sql, values }
29843232 tableName = reflectSymbol (Proxy :: Proxy name)
29853233 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
29863234 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
2987− sql = "INSERT INTO " <> tableName
2988− <> " ("
2989− <> intercalate ", " colNames
2990− <> ")"
2991− <> " VALUES ("
2992− <> intercalate ", " placeholders
2993− <> ")"
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+ <> ")"
29943246 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
29953247
29963248-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -3002,6 +3254,7 @@ returning
30023254 . SingleTable tables name cols
30033255 => IsSymbol sel
30043256 => ParseSelect sel tables result
3257+ => HasMutation stage
30053258 => Row.Lacks "returning" stage
30063259 => Row.Lacks "select" stage
30073260 => Row.Cons "returning" Unit stage stage'
@@ -3013,6 +3266,7 @@ returningAll
30133266 :: forall tables name cols result p stage stage'
30143267 . SingleTable tables name cols
30153268 => StripColumns cols result
3269+ => HasMutation stage
30163270 => Row.Lacks "returning" stage
30173271 => Row.Lacks "select" stage
30183272 => Row.Cons "returning" Unit stage stage'
@@ -3055,8 +3309,10 @@ set
30553309 => ValidateSetColumnsRL setRL cols
30563310 => SetClauseRL setRL
30573311 => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
30583313 => Row.Lacks "select" stage
30593314 => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
30603316 => Row.Lacks "set" stage
30613317 => Row.Lacks "delete" stage
30623318 => Row.Cons "set" Unit stage stage'
@@ -3080,6 +3336,7 @@ delete
30803336 => IsSymbol name
30813337 => Row.Lacks "select" stage
30823338 => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
30833340 => Row.Lacks "set" stage
30843341 => Row.Lacks "delete" stage
30853342 => Row.Cons "delete" Unit stage stage'
@@ -3320,6 +3577,66 @@ exceptAll
33203577 -> Q tables1 result params SetOpStage
33213578exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
33223579
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+
33233640-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
33243641-- Query execution
33253642-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -3335,12 +3652,13 @@ instance
33353652 ( IsSymbol name
33363653 , Row.Cons name typ rest row
33373654 , Row.Lacks name rest
3655+ , FieldToPGValue typ
33383656 , ParamsToArray tail row
33393657 ) =>
33403658 ParamsToArray (RL.Cons name typ tail) row where
33413659 paramsToArray _ rec =
33423660 [ { name: reflectSymbol (Proxy :: Proxy name)
3343− , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
33443662 }
33453663 ]
33463664 <> paramsToArray (Proxy :: Proxy tail) rec
@@ -3454,6 +3772,7 @@ runExecuteTx txn params (Q q) = do
34543772 let allValues = q.values <> values
34553773 PG.txExecute (PG.SQL sql) allValues txn
34563774
3775+
34573776decodeRows :: forall a. ReadForeign a => Array Foreign -> Either (NonEmptyList ForeignError) (Array a)
34583777decodeRows = traverse (readImpl >>> runExcept)
34593778