@@ -2,36 +2,47 @@ module Yoga.Postgres.Schema where
3import Prelude
3import Prelude
5−import Data.Array as Array
5+import Control.Monad.Except (except, runExcept)
6import Data.Array (intercalate, mapWithIndex, foldl)
6import Data.Array (intercalate, mapWithIndex, foldl)
7+import Data.Array as Array
8+import Data.Date (Date, day, month, year)
7−import Data.DateTime (DateTime)
9+import Data.DateTime (DateTime(..))
10+import Data.Either (Either(..), note)
11+import Data.Enum (fromEnum, toEnum)
12+import Data.Int as Int
8import Data.JSDate as JSDate
13import Data.JSDate as JSDate
14+import Data.List.Types (NonEmptyList)
15+import Data.Map as Map
9−import Data.Maybe (Maybe(..))
16+import Data.Maybe (Maybe(..), fromMaybe)
10import Data.Newtype (class Newtype)
17import Data.Newtype (class Newtype)
11import Data.Nullable (toNullable)
18import Data.Nullable (toNullable)
12−import JS.BigInt (BigInt)
19+import Data.Number as Number
13−import Unsafe.Coerce (unsafeCoerce)
14import Data.Reflectable (class Reflectable, reflectType)
20import Data.Reflectable (class Reflectable, reflectType)
21+import Data.String.CodeUnits as SCU
22+import Data.String.Common as Str
23+import Data.String.Pattern (Pattern(..))
15−import Data.String.Regex (regex, replace') as Regex
24+import Data.String.Regex (Regex, regex, replace') as Regex
16−import Data.String.Regex (Regex) as Regex
17import Data.String.Regex.Flags (global) as Regex
25import Data.String.Regex.Flags (global) as Regex
18−import Control.Monad.Except (runExcept)
19−import Data.Either (Either(..))
20−import Data.Map as Map
21import Data.Symbol (class IsSymbol, reflectSymbol)
26import Data.Symbol (class IsSymbol, reflectSymbol)
22−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
23−import Effect.Aff (Aff)
31+import Effect.Aff (Aff, throwError)
32+import Effect.Exception (error) as Exception
24−import Foreign (Foreign, unsafeToForeign)
33+import Foreign (Foreign, ForeignError(..))
34+import JS.BigInt (BigInt)
25import Prim.Boolean (True, False)
35import Prim.Boolean (True, False)
26import Prim.Row (class Cons, class Lacks, class Union, class Nub) as Row
36import Prim.Row (class Cons, class Lacks, class Union, class Nub) as Row
27−import Prim.RowList as RL
28import Prim.RowList (class RowToList)
37import Prim.RowList (class RowToList)
38+import Prim.RowList as RL
29import Prim.Symbol (class Cons, class Append) as Symbol
39import Prim.Symbol (class Cons, class Append) as Symbol
30import Prim.TypeError (class Fail, Beside, Text, Quote)
40import Prim.TypeError (class Fail, Beside, Text, Quote)
31import Record (get) as Record
41import Record (get) as Record
32import Type.Proxy (Proxy(..))
42import Type.Proxy (Proxy(..))
33import Type.RowList (class ListToRow)
43import Type.RowList (class ListToRow)
44+import Unsafe.Coerce (unsafeCoerce)
34−import Yoga.JSON (class ReadForeign, readImpl)
45+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
35import Yoga.Postgres as PG
46import Yoga.Postgres as PG
37-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
48-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -57,6 +68,9 @@ data Unique a
58data Default :: forall k. k -> Type -> Type
69data Default :: forall k. k -> Type -> Type
59data Default val a
70data Default val a
71+data DefaultExpr :: Symbol -> Type -> Type
72+data DefaultExpr expression a
61data References
75data References
@@ -78,6 +92,178 @@ derive instance Newtype Jsonb _
78instance ReadForeign Jsonb where
92instance ReadForeign Jsonb where
79 readImpl = pure <<< Jsonb
93 readImpl = pure <<< Jsonb
95+instance WriteForeign Jsonb where
96+ writeImpl (Jsonb value) = value
98+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99+-- PGUUID: newtype over Data.UUID.UUID for Postgres integration
100+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102+newtype PGUUID = PGUUID UUID
104+derive instance Newtype PGUUID _
105+derive newtype instance Eq PGUUID
106+derive newtype instance Ord PGUUID
107+derive newtype instance Show PGUUID
109+instance ReadForeign PGUUID where
111+ s :: String <- readImpl f
112+ pure (unsafeCoerce s)
114+instance WriteForeign PGUUID where
115+ writeImpl (PGUUID uuid) = writeImpl (UUID.toString uuid)
117+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118+-- PGDate / PGTime: newtypes for DATE and TIME columns
119+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
121+newtype PGDate = PGDate Date
123+derive instance Newtype PGDate _
124+derive instance Eq PGDate
125+derive instance Ord PGDate
126+instance Show PGDate where
127+ show (PGDate d) = "(PGDate " <> show d <> ")"
129+instance ReadForeign PGDate where
131+ except $ note (pure (ForeignError "Invalid date")) (JSDate.toDate jsDate <#> PGDate)
133+ jsDate :: JSDate.JSDate
134+ jsDate = unsafeCoerce f
136+instance WriteForeign PGDate where
137+ writeImpl (PGDate date) =
139+ ( show (fromEnum (year date))
141+ <> pad2 (fromEnum (month date))
143+ <> pad2 (fromEnum (day date))
146+ pad2 n = if n < 10 then "0" <> show n else show n
148+newtype TimestampTZ = TimestampTZ DateTime
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 <> ")"
156+instance ReadForeign TimestampTZ where
158+ except $ note (pure (ForeignError "Invalid timestamp")) (JSDate.toDateTime jsDate <#> TimestampTZ)
160+ jsDate :: JSDate.JSDate
161+ jsDate = unsafeCoerce f
163+newtype PGTime = PGTime Time
165+derive instance Newtype PGTime _
166+derive instance Eq PGTime
167+derive instance Ord PGTime
168+instance Show PGTime where
169+ show (PGTime t) = "(PGTime " <> show t <> ")"
171+instance ReadForeign PGTime where
173+ s :: String <- readImpl f
174+ except $ note (pure (ForeignError ("Invalid time: " <> s))) (parseTime s)
177+ let parts = Str.split (Pattern ":") str
179+ [ hStr, mStr, sStr ] -> do
180+ h <- Int.fromString hStr >>= toEnum
181+ mi <- Int.fromString mStr >>= toEnum
182+ let secParts = Str.split (Pattern ".") sStr
183+ s <- Int.fromString (fromMaybe sStr (Array.head secParts)) >>= toEnum
184+ let fraction = fromMaybe "" (Array.index secParts 1)
185+ let milliseconds = case SCU.length fraction of
187+ 1 -> fraction <> "00"
188+ 2 -> fraction <> "0"
189+ _ -> SCU.take 3 fraction
190+ let ms = fromMaybe bottom (Int.fromString milliseconds >>= toEnum)
191+ pure (PGTime (Time h mi s ms))
194+instance WriteForeign PGTime where
195+ writeImpl (PGTime (Time hour minute second millisecond)) =
197+ ( pad2 (fromEnum hour)
199+ <> pad2 (fromEnum minute)
201+ <> pad2 (fromEnum second)
203+ <> pad3 (fromEnum millisecond)
206+ pad2 n = if n < 10 then "0" <> show n else show n
208+ | n < 10 = "00" <> show n
209+ | n < 100 = "0" <> show n
210+ | otherwise = show n
212+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
213+-- Point: geometric point "(x,y)"
214+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
216+newtype Point = Point { x :: Number, y :: Number }
218+derive instance Newtype Point _
219+derive instance Eq Point
221+instance Show Point where
222+ show (Point { x, y }) = "(Point " <> show x <> " " <> show y <> ")"
224+instance ReadForeign Point where
226+ s :: String <- readImpl f
227+ except $ note (pure (ForeignError ("Invalid point: " <> s))) (parsePoint s)
230+ let inner = SCU.drop 1 (SCU.dropRight 1 str)
231+ let parts = Str.split (Pattern ",") inner
233+ [ xStr, yStr ] -> do
234+ x <- Number.fromString xStr
235+ y <- Number.fromString yStr
236+ pure (Point { x, y })
239+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240+-- TSVector / TSQuery: full-text search types
241+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243+newtype TSVector = TSVector String
245+derive instance Newtype TSVector _
246+derive newtype instance Eq TSVector
247+derive newtype instance Ord TSVector
248+derive newtype instance Show TSVector
250+instance ReadForeign TSVector where
252+ s :: String <- readImpl f
255+newtype TSQuery = TSQuery String
257+derive instance Newtype TSQuery _
258+derive newtype instance Eq TSQuery
259+derive newtype instance Ord TSQuery
260+derive newtype instance Show TSQuery
262+instance ReadForeign TSQuery where
264+ s :: String <- readImpl f
81-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
267-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82-- Nullability: inferred from Maybe
268-- Nullability: inferred from Maybe
83-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
269-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -101,6 +287,7 @@ instance ExtractType a typ => ExtractType (PrimaryKey a) typ
101else instance ExtractType a typ => ExtractType (AutoIncrement a) typ
287else instance ExtractType a typ => ExtractType (AutoIncrement a) typ
102else instance ExtractType a typ => ExtractType (Unique a) typ
288else instance ExtractType a typ => ExtractType (Unique a) typ
103else instance ExtractType a typ => ExtractType (Default val a) typ
289else instance ExtractType a typ => ExtractType (Default val a) typ
290+else instance ExtractType a typ => ExtractType (DefaultExpr expression a) typ
104else instance ExtractType a typ => ExtractType (ForeignKey t r c a) typ
291else instance ExtractType a typ => ExtractType (ForeignKey t r c a) typ
105else instance ExtractType a typ => ExtractType (Nullable a) (Maybe typ)
292else instance ExtractType a typ => ExtractType (Nullable a) (Maybe typ)
106else instance ExtractType a a
293else instance ExtractType a a
@@ -130,6 +317,27 @@ instance PGTypeName DateTime where
130instance PGTypeName BigInt where
317instance PGTypeName BigInt where
131 pgTypeName _ = "BIGINT"
318 pgTypeName _ = "BIGINT"
320+instance PGTypeName PGUUID where
321+ pgTypeName _ = "UUID"
323+instance PGTypeName TimestampTZ where
324+ pgTypeName _ = "TIMESTAMPTZ"
326+instance PGTypeName PGDate where
327+ pgTypeName _ = "DATE"
329+instance PGTypeName PGTime where
330+ pgTypeName _ = "TIME"
332+instance PGTypeName Point where
333+ pgTypeName _ = "POINT"
335+instance PGTypeName TSVector where
336+ pgTypeName _ = "TSVECTOR"
338+instance PGTypeName TSQuery where
339+ pgTypeName _ = "TSQUERY"
133instance PGTypeName Jsonb where
341instance PGTypeName Jsonb where
134 pgTypeName _ = "JSONB"
342 pgTypeName _ = "JSONB"
@@ -152,8 +360,24 @@ instance Reflectable sym String => RenderDefaultValue sym String where
152instance Reflectable val Int => RenderDefaultValue val Int where
360instance Reflectable val Int => RenderDefaultValue val Int where
153 renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
361 renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
363+instance RenderDefaultValue True Boolean where
364+ renderDefaultValue _ _ = "DEFAULT true"
155−instance Reflectable val Boolean => RenderDefaultValue val Boolean where
366+instance RenderDefaultValue False Boolean where
156− renderDefaultValue _ _ = if reflectType (Proxy :: Proxy val) then "DEFAULT true" else "DEFAULT false"
367+ renderDefaultValue _ _ = "DEFAULT false"
369+instance RenderDefaultValue "true" Boolean where
370+ renderDefaultValue _ _ = "DEFAULT true"
372+instance RenderDefaultValue "false" Boolean where
373+ renderDefaultValue _ _ = "DEFAULT false"
375+instance RenderDefaultValue "now()" TimestampTZ where
376+ renderDefaultValue _ _ = "DEFAULT now()"
378+instance RenderDefaultValue "gen_random_uuid()" PGUUID where
379+ renderDefaultValue _ _ = "DEFAULT gen_random_uuid()"
158class RenderConstraint a where
382class RenderConstraint a where
159 renderConstraint :: Proxy a -> String
383 renderConstraint :: Proxy a -> String
@@ -170,6 +394,10 @@ else instance RenderConstraint a => RenderConstraint (Unique a) where
170else instance (RenderDefaultValue val a, RenderConstraint a) => RenderConstraint (Default val a) where
394else instance (RenderDefaultValue val a, RenderConstraint a) => RenderConstraint (Default val a) where
171 renderConstraint _ = joinConstraints (renderDefaultValue (Proxy :: Proxy val) (Proxy :: Proxy a)) (renderConstraint (Proxy :: Proxy a))
395 renderConstraint _ = joinConstraints (renderDefaultValue (Proxy :: Proxy val) (Proxy :: Proxy a)) (renderConstraint (Proxy :: Proxy a))
397+else instance (IsSymbol expression, RenderConstraint a) => RenderConstraint (DefaultExpr expression a) where
398+ renderConstraint _ = joinConstraints
399+ ("DEFAULT " <> reflectSymbol (Proxy :: Proxy expression))
400+ (renderConstraint (Proxy :: Proxy a))
173else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
401else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
174 renderConstraint _ = joinConstraints
402 renderConstraint _ = joinConstraints
175 ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
403 ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
@@ -230,6 +458,11 @@ instance
230 let columns = renderColumnsRL (Proxy :: Proxy rl)
458 let columns = renderColumnsRL (Proxy :: Proxy rl)
231 "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
459 "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
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
233-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
466-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
234-- INSERT SQL generation
467-- INSERT SQL generation
235-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
468-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -241,6 +474,8 @@ instance IsAutoGenerated (AutoIncrement a) where
241 isAutoGenerated _ = true
474 isAutoGenerated _ = true
242else instance IsAutoGenerated (Default v a) where
475else instance IsAutoGenerated (Default v a) where
243 isAutoGenerated _ = true
476 isAutoGenerated _ = true
477+else instance IsAutoGenerated (DefaultExpr expression a) where
478+ isAutoGenerated _ = true
244else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
479else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
245 isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
480 isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
246else instance IsAutoGenerated a => IsAutoGenerated (Unique a) where
481else instance IsAutoGenerated a => IsAutoGenerated (Unique a) where
@@ -284,15 +519,18 @@ instance
284 insertSQLFor = do
519 insertSQLFor = do
285 let tableName = reflectSymbol (Proxy :: Proxy name)
520 let tableName = reflectSymbol (Proxy :: Proxy name)
286 let cols = insertColumnsRL (Proxy :: Proxy rl)
521 let cols = insertColumnsRL (Proxy :: Proxy rl)
522+ if Array.null cols then
523+ "INSERT INTO " <> tableName <> " DEFAULT VALUES RETURNING *"
287− let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
525+ let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
288− "INSERT INTO " <> tableName
526+ "INSERT INTO " <> tableName
290− <> intercalate ", " cols
528+ <> intercalate ", " cols
292− <> " VALUES ("
530+ <> " VALUES ("
293− <> intercalate ", " placeholders
531+ <> intercalate ", " placeholders
295− <> " RETURNING *"
533+ <> " RETURNING *"
297-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
535-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
298-- SELECT SQL generation
536-- SELECT SQL generation
@@ -415,6 +653,31 @@ class SkipSpacesGo head tail result | head tail -> result
415instance SkipSpaces tail result => SkipSpacesGo " " tail result
653instance SkipSpaces tail result => SkipSpacesGo " " tail result
416else instance Symbol.Cons head tail result => SkipSpacesGo head tail result
654else instance Symbol.Cons head tail result => SkipSpacesGo head tail result
656+-- Skip a string literal: consume chars until closing single quote
657+class SkipStringLiteral :: Symbol -> Symbol -> Constraint
658+class SkipStringLiteral sym rest | sym -> rest
660+instance Fail (Text "Unclosed string literal") => SkipStringLiteral "" rest
662+ ( Symbol.Cons h t sym
663+ , SkipStringLiteralGo h t rest
665+ SkipStringLiteral sym rest
667+class SkipStringLiteralGo :: Symbol -> Symbol -> Symbol -> Constraint
668+class SkipStringLiteralGo head tail rest | head tail -> rest
670+-- Closing quote: done
671+instance SkipStringLiteralGo "'" tail tail
672+-- End of string without closing quote
673+else instance Fail (Text "Unclosed string literal") => SkipStringLiteralGo h "" rest
674+-- Any other char: skip and continue
676+ ( Symbol.Cons nextH nextT tail
677+ , SkipStringLiteralGo nextH nextT rest
679+ SkipStringLiteralGo h tail rest
418-- Extract the first word from a Symbol (up to space or end)
681-- Extract the first word from a Symbol (up to space or end)
419class ExtractWord :: Symbol -> Symbol -> Symbol -> Constraint
682class ExtractWord :: Symbol -> Symbol -> Symbol -> Constraint
420class ExtractWord sym word rest | sym -> word rest
683class ExtractWord sym word rest | sym -> word rest
@@ -722,6 +985,15 @@ else instance
723 ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
986 ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
988+-- Bare alias (no AS keyword): "name n" means "name AS n"
990+ ( ResolveColumn colRef tables entry
991+ , ExtractType entry typ
992+ , SkipSpaces afterKeyword rest
993+ , ParseSelectExpectEnd rest tables (RL.Cons keyword typ accRL) outRL
995+ ParseSelectHandleAS keyword afterKeyword colRef tables accRL outRL
725class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
997class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
726class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
998class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
@@ -820,11 +1092,66 @@ instance ResolveAggregateArg "*" tables Star
820else instance ResolveAggregateArg "" tables Star
1092else instance ResolveAggregateArg "" tables Star
821else instance
1093else instance
822 ( SkipSpaces args trimmedFront
1094 ( SkipSpaces args trimmedFront
823− , ExtractWord trimmedFront col _rest
1095+ , ExtractIdent trimmedFront col rest
824− , ResolveAggregateArgCol col tables argType
1096+ , ResolveAggregateArgDispatch col rest tables argType
826 ResolveAggregateArg args tables argType
1098 ResolveAggregateArg args tables argType
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
1104+instance ExtractIdent "" "" ""
1106+ ( Symbol.Cons h t sym
1107+ , ExtractIdentGo h t "" word rest
1109+ ExtractIdent sym word rest
1111+class ExtractIdentGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
1112+class ExtractIdentGo head tail acc word rest | head tail acc -> word rest
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 ""
1119+ ( Symbol.Append acc h acc'
1120+ , Symbol.Cons nextH nextT tail
1121+ , ExtractIdentGo nextH nextT acc' word rest
1123+ ExtractIdentGo h tail acc word rest
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
1130+ ResolveAggregateArgCol col tables argType =>
1131+ ResolveAggregateArgDispatch col "" tables argType
1134+ ( Symbol.Cons h t rest
1135+ , ResolveAggregateArgDispatchByHead h t col tables argType
1137+ ResolveAggregateArgDispatch col rest tables argType
1139+class ResolveAggregateArgDispatchByHead :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1140+class ResolveAggregateArgDispatchByHead head tail col tables argType | head tail col tables -> argType
1142+-- Nested function call: col(...)
1144+ ( ExtractUntilParen tail innerArgs _afterParen
1145+ , ResolveAggregateArg innerArgs tables innerArgType
1146+ , AggregateReturnType col innerArgType argType
1148+ ResolveAggregateArgDispatchByHead "(" tail col tables argType
1150+-- Not a function call: column reference
1152+ ResolveAggregateArgCol col tables argType =>
1153+ ResolveAggregateArgDispatchByHead h t col tables argType
828class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
1155class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
829class ResolveAggregateArgCol col tables argType | col tables -> argType
1156class ResolveAggregateArgCol col tables argType | col tables -> argType
@@ -909,8 +1236,8 @@ else instance
909class AggregateReturnTypeC :: Symbol -> Type -> Type -> Constraint
1236class AggregateReturnTypeC :: Symbol -> Type -> Type -> Constraint
910class AggregateReturnTypeC funcName argType returnType | funcName argType -> returnType
1237class AggregateReturnTypeC funcName argType returnType | funcName argType -> returnType
912−instance AggregateReturnTypeC "COUNT" argType Int
1239+instance AggregateReturnTypeC "COUNT" argType BigInt
913−else instance AggregateReturnTypeC "count" argType Int
1240+else instance AggregateReturnTypeC "count" argType BigInt
914else instance AggregateReturnTypeC "COALESCE" argType argType
1241else instance AggregateReturnTypeC "COALESCE" argType argType
915else instance AggregateReturnTypeC "coalesce" argType argType
1242else instance AggregateReturnTypeC "coalesce" argType argType
916else instance
1243else instance
@@ -1251,7 +1578,7 @@ data NoType
1251class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
1578class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
1252class ParseWhere sym tables params | sym tables -> params
1579class ParseWhere sym tables params | sym tables -> params
1254−instance ParseWhere "" tables ()
1581+instance Fail (Text "Empty WHERE clause") => ParseWhere "" tables params
1255else instance
1582else instance
1256 ( Symbol.Cons h t sym
1583 ( Symbol.Cons h t sym
1257 , ParseWhereGo h t "" NoType tables RL.Nil outRL
1584 , ParseWhereGo h t "" NoType tables RL.Nil outRL
@@ -1277,12 +1604,19 @@ else instance (FlushWhereWord acc currentType tables paramsIn currentType' param
1277else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "!" tail acc currentType tables paramsIn paramsOut
1604else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "!" tail acc currentType tables paramsIn paramsOut
1278else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "(" tail acc currentType tables paramsIn paramsOut
1605else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "(" tail acc currentType tables paramsIn paramsOut
1279else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ")" tail acc currentType tables paramsIn paramsOut
1606else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ")" tail acc currentType tables paramsIn paramsOut
1607+-- String literal: flush word, skip until closing quote
1280−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "'" tail acc currentType tables paramsIn paramsOut
1609+ ( FlushWhereWord acc currentType tables paramsIn currentType' paramsOut'
1610+ , SkipStringLiteral tail rest
1611+ , ParseWhereContinue rest currentType' tables paramsOut' paramsOut
1613+ ParseWhereGo "'" tail acc currentType tables paramsIn paramsOut
1281else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "@" tail acc currentType tables paramsIn paramsOut
1614else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "@" tail acc currentType tables paramsIn paramsOut
1282else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "?" tail acc currentType tables paramsIn paramsOut
1615else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "?" tail acc currentType tables paramsIn paramsOut
1283else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ":" tail acc currentType tables paramsIn paramsOut
1616else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ":" tail acc currentType tables paramsIn paramsOut
1284else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "~" tail acc currentType tables paramsIn paramsOut
1617else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "~" tail acc currentType tables paramsIn paramsOut
1285else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "#" tail acc currentType tables paramsIn paramsOut
1618else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "#" tail acc currentType tables paramsIn paramsOut
1619+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "," tail acc currentType tables paramsIn paramsOut
1287-- End of string: flush final word
1621-- End of string: flush final word
1288else instance
1622else instance
@@ -1386,8 +1720,12 @@ else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut pa
1386else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "u" word currentType tables paramsIn currentTypeOut paramsOut
1720else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "u" word currentType tables paramsIn currentTypeOut paramsOut
1387else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "V" word currentType tables paramsIn currentTypeOut paramsOut
1721else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "V" word currentType tables paramsIn currentTypeOut paramsOut
1388else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "v" word currentType tables paramsIn currentTypeOut paramsOut
1722else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "v" word currentType tables paramsIn currentTypeOut paramsOut
1723+else instance FlushWhereWordP word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "P" word currentType tables paramsIn currentTypeOut paramsOut
1724+else instance FlushWhereWordP word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "p" word currentType tables paramsIn currentTypeOut paramsOut
1725+else instance FlushWhereWordR word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "R" word currentType tables paramsIn currentTypeOut paramsOut
1726+else instance FlushWhereWordR word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "r" word currentType tables paramsIn currentTypeOut paramsOut
1390−-- Catch-all: column reference (letters without keywords like g, h, p, q, r, w, x, y, z)
1728+-- Catch-all: column reference (letters without keywords like g, h, q, w, x, y, z)
1391else instance
1729else instance
1392 ( ResolveColumn word tables entry
1730 ( ResolveColumn word tables entry
1393 , ExtractType entry typ
1731 , ExtractType entry typ
@@ -1400,7 +1738,9 @@ else instance
1400class FlushWhereWordA :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1738class FlushWhereWordA :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1401class FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1739class FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1741+instance FlushWhereWordA "ABS" currentType tables paramsIn currentType paramsIn
1742+else instance FlushWhereWordA "abs" currentType tables paramsIn currentType paramsIn
1403−instance FlushWhereWordA "ANY" currentType tables paramsIn (Array currentType) paramsIn
1743+else instance FlushWhereWordA "ANY" currentType tables paramsIn (Array currentType) paramsIn
1404else instance FlushWhereWordA "any" currentType tables paramsIn (Array currentType) paramsIn
1744else instance FlushWhereWordA "any" currentType tables paramsIn (Array currentType) paramsIn
1405else instance FlushWhereWordA "ALL" currentType tables paramsIn (Array currentType) paramsIn
1745else instance FlushWhereWordA "ALL" currentType tables paramsIn (Array currentType) paramsIn
1406else instance FlushWhereWordA "all" currentType tables paramsIn (Array currentType) paramsIn
1746else instance FlushWhereWordA "all" currentType tables paramsIn (Array currentType) paramsIn
@@ -1436,8 +1776,18 @@ class FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut
1437instance FlushWhereWordC "CAST" currentType tables paramsIn currentType paramsIn
1777instance FlushWhereWordC "CAST" currentType tables paramsIn currentType paramsIn
1438else instance FlushWhereWordC "cast" currentType tables paramsIn currentType paramsIn
1778else instance FlushWhereWordC "cast" currentType tables paramsIn currentType paramsIn
1779+else instance FlushWhereWordC "CEIL" currentType tables paramsIn currentType paramsIn
1780+else instance FlushWhereWordC "ceil" currentType tables paramsIn currentType paramsIn
1781+else instance FlushWhereWordC "COALESCE" currentType tables paramsIn currentType paramsIn
1782+else instance FlushWhereWordC "coalesce" currentType tables paramsIn currentType paramsIn
1783+else instance FlushWhereWordC "CONCAT" currentType tables paramsIn currentType paramsIn
1784+else instance FlushWhereWordC "concat" currentType tables paramsIn currentType paramsIn
1439else instance FlushWhereWordC "COUNT" currentType tables paramsIn Int paramsIn
1785else instance FlushWhereWordC "COUNT" currentType tables paramsIn Int paramsIn
1440else instance FlushWhereWordC "count" currentType tables paramsIn Int paramsIn
1786else instance FlushWhereWordC "count" currentType tables paramsIn Int paramsIn
1787+else instance FlushWhereWordC "CURRENT_DATE" currentType tables paramsIn currentType paramsIn
1788+else instance FlushWhereWordC "current_date" currentType tables paramsIn currentType paramsIn
1789+else instance FlushWhereWordC "CURRENT_TIMESTAMP" currentType tables paramsIn currentType paramsIn
1790+else instance FlushWhereWordC "current_timestamp" currentType tables paramsIn currentType paramsIn
1441else instance
1791else instance
1442 ( ResolveColumn word tables entry
1792 ( ResolveColumn word tables entry
1443 , ExtractType entry typ
1793 , ExtractType entry typ
@@ -1448,7 +1798,8 @@ else instance
1448class FlushWhereWordD :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1798class FlushWhereWordD :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1449class FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1799class FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1801+instance FlushWhereWordD "DATE" currentType tables paramsIn currentType paramsIn
1451−instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1802+else instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1452else instance
1803else instance
1453 ( ResolveColumn word tables entry
1804 ( ResolveColumn word tables entry
1454 , ExtractType entry typ
1805 , ExtractType entry typ
@@ -1473,6 +1824,8 @@ class FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut
1474instance FlushWhereWordF "FALSE" currentType tables paramsIn currentType paramsIn
1825instance FlushWhereWordF "FALSE" currentType tables paramsIn currentType paramsIn
1475else instance FlushWhereWordF "false" currentType tables paramsIn currentType paramsIn
1826else instance FlushWhereWordF "false" currentType tables paramsIn currentType paramsIn
1827+else instance FlushWhereWordF "FLOOR" currentType tables paramsIn currentType paramsIn
1828+else instance FlushWhereWordF "floor" currentType tables paramsIn currentType paramsIn
1476else instance
1829else instance
1477 ( ResolveColumn word tables entry
1830 ( ResolveColumn word tables entry
1478 , ExtractType entry typ
1831 , ExtractType entry typ
@@ -1511,8 +1864,14 @@ else instance
1511class FlushWhereWordL :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1864class FlushWhereWordL :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1512class FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1865class FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1867+instance FlushWhereWordL "LEAST" currentType tables paramsIn currentType paramsIn
1868+else instance FlushWhereWordL "least" currentType tables paramsIn currentType paramsIn
1869+else instance FlushWhereWordL "LENGTH" currentType tables paramsIn Int paramsIn
1870+else instance FlushWhereWordL "length" currentType tables paramsIn Int paramsIn
1514−instance FlushWhereWordL "LIKE" currentType tables paramsIn currentType paramsIn
1871+else instance FlushWhereWordL "LIKE" currentType tables paramsIn currentType paramsIn
1515else instance FlushWhereWordL "like" currentType tables paramsIn currentType paramsIn
1872else instance FlushWhereWordL "like" currentType tables paramsIn currentType paramsIn
1873+else instance FlushWhereWordL "LOWER" currentType tables paramsIn String paramsIn
1874+else instance FlushWhereWordL "lower" currentType tables paramsIn String paramsIn
1516else instance
1875else instance
1517 ( ResolveColumn word tables entry
1876 ( ResolveColumn word tables entry
1518 , ExtractType entry typ
1877 , ExtractType entry typ
@@ -1539,8 +1898,12 @@ class FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut
1540instance FlushWhereWordN "NOT" currentType tables paramsIn currentType paramsIn -- unreachable (caught by FlushWhereWord directly) but included for self-containedness
1899instance FlushWhereWordN "NOT" currentType tables paramsIn currentType paramsIn -- unreachable (caught by FlushWhereWord directly) but included for self-containedness
1541else instance FlushWhereWordN "not" currentType tables paramsIn currentType paramsIn
1900else instance FlushWhereWordN "not" currentType tables paramsIn currentType paramsIn
1901+else instance FlushWhereWordN "NOW" currentType tables paramsIn currentType paramsIn
1902+else instance FlushWhereWordN "now" currentType tables paramsIn currentType paramsIn
1542else instance FlushWhereWordN "NULL" currentType tables paramsIn currentType paramsIn
1903else instance FlushWhereWordN "NULL" currentType tables paramsIn currentType paramsIn
1543else instance FlushWhereWordN "null" currentType tables paramsIn currentType paramsIn
1904else instance FlushWhereWordN "null" currentType tables paramsIn currentType paramsIn
1905+else instance FlushWhereWordN "NULLIF" currentType tables paramsIn currentType paramsIn
1906+else instance FlushWhereWordN "nullif" currentType tables paramsIn currentType paramsIn
1544else instance
1907else instance
1545 ( ResolveColumn word tables entry
1908 ( ResolveColumn word tables entry
1546 , ExtractType entry typ
1909 , ExtractType entry typ
@@ -1551,7 +1914,9 @@ else instance
1551class FlushWhereWordS :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1914class FlushWhereWordS :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1552class FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1915class FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1917+instance FlushWhereWordS "SMALLINT" currentType tables paramsIn currentType paramsIn
1918+else instance FlushWhereWordS "smallint" currentType tables paramsIn currentType paramsIn
1554−instance FlushWhereWordS "SUM" currentType tables paramsIn currentType paramsIn
1919+else instance FlushWhereWordS "SUM" currentType tables paramsIn currentType paramsIn
1555else instance FlushWhereWordS "sum" currentType tables paramsIn currentType paramsIn
1920else instance FlushWhereWordS "sum" currentType tables paramsIn currentType paramsIn
1556else instance FlushWhereWordS "STRING_AGG" currentType tables paramsIn String paramsIn
1921else instance FlushWhereWordS "STRING_AGG" currentType tables paramsIn String paramsIn
1557else instance FlushWhereWordS "string_agg" currentType tables paramsIn String paramsIn
1922else instance FlushWhereWordS "string_agg" currentType tables paramsIn String paramsIn
@@ -1565,11 +1930,23 @@ else instance
1565class FlushWhereWordT :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1930class FlushWhereWordT :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1566class FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1931class FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1933+instance FlushWhereWordT "TRIM" currentType tables paramsIn String paramsIn
1934+else instance FlushWhereWordT "trim" currentType tables paramsIn String paramsIn
1568−instance FlushWhereWordT "TRUE" currentType tables paramsIn currentType paramsIn
1935+else instance FlushWhereWordT "TRUE" currentType tables paramsIn currentType paramsIn
1569else instance FlushWhereWordT "true" currentType tables paramsIn currentType paramsIn
1936else instance FlushWhereWordT "true" currentType tables paramsIn currentType paramsIn
1570else instance FlushWhereWordT "text" currentType tables paramsIn currentType paramsIn
1937else instance FlushWhereWordT "text" currentType tables paramsIn currentType paramsIn
1571else instance FlushWhereWordT "timestamptz" currentType tables paramsIn currentType paramsIn
1938else instance FlushWhereWordT "timestamptz" currentType tables paramsIn currentType paramsIn
1572else instance FlushWhereWordT "timestamp" currentType tables paramsIn currentType paramsIn
1939else instance FlushWhereWordT "timestamp" currentType tables paramsIn currentType paramsIn
1940+else instance FlushWhereWordT "TIME" currentType tables paramsIn currentType paramsIn
1941+else instance FlushWhereWordT "time" currentType tables paramsIn currentType paramsIn
1942+else instance FlushWhereWordT "tsvector" currentType tables paramsIn currentType paramsIn
1943+else instance FlushWhereWordT "TSVECTOR" currentType tables paramsIn currentType paramsIn
1944+else instance FlushWhereWordT "tsquery" currentType tables paramsIn currentType paramsIn
1945+else instance FlushWhereWordT "TSQUERY" currentType tables paramsIn currentType paramsIn
1946+else instance FlushWhereWordT "to_tsvector" currentType tables paramsIn String paramsIn
1947+else instance FlushWhereWordT "TO_TSVECTOR" currentType tables paramsIn String paramsIn
1948+else instance FlushWhereWordT "to_tsquery" currentType tables paramsIn String paramsIn
1949+else instance FlushWhereWordT "TO_TSQUERY" currentType tables paramsIn String paramsIn
1573else instance
1950else instance
1574 ( ResolveColumn word tables entry
1951 ( ResolveColumn word tables entry
1575 , ExtractType entry typ
1952 , ExtractType entry typ
@@ -1580,7 +1957,9 @@ else instance
1580class FlushWhereWordU :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1957class FlushWhereWordU :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1581class FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1958class FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1960+instance FlushWhereWordU "UPPER" currentType tables paramsIn String paramsIn
1961+else instance FlushWhereWordU "upper" currentType tables paramsIn String paramsIn
1583−instance FlushWhereWordU "uuid" currentType tables paramsIn currentType paramsIn
1962+else instance FlushWhereWordU "uuid" currentType tables paramsIn currentType paramsIn
1584else instance
1963else instance
1585 ( ResolveColumn word tables entry
1964 ( ResolveColumn word tables entry
1586 , ExtractType entry typ
1965 , ExtractType entry typ
@@ -1599,6 +1978,32 @@ else instance
1600 FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
1979 FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
1981+class FlushWhereWordP :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1982+class FlushWhereWordP word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1984+instance FlushWhereWordP "point" currentType tables paramsIn currentType paramsIn
1985+else instance FlushWhereWordP "POINT" currentType tables paramsIn currentType paramsIn
1986+else instance FlushWhereWordP "plainto_tsquery" currentType tables paramsIn currentType paramsIn
1987+else instance FlushWhereWordP "PLAINTO_TSQUERY" currentType tables paramsIn currentType paramsIn
1989+ ( ResolveColumn word tables entry
1990+ , ExtractType entry typ
1991+ , UnwrapMaybe typ unwrapped
1993+ FlushWhereWordP word currentType tables paramsIn unwrapped paramsIn
1995+class FlushWhereWordR :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1996+class FlushWhereWordR word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1998+instance FlushWhereWordR "real" currentType tables paramsIn currentType paramsIn
1999+else instance FlushWhereWordR "REAL" currentType tables paramsIn currentType paramsIn
2001+ ( ResolveColumn word tables entry
2002+ , ExtractType entry typ
2003+ , UnwrapMaybe typ unwrapped
2005+ FlushWhereWordR word currentType tables paramsIn unwrapped paramsIn
1602-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2007-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1603-- ValidateColumnList: comma-separated column references
2008-- ValidateColumnList: comma-separated column references
1604-- Used by GROUP BY, DISTINCT ON, ON CONFLICT target
2009-- Used by GROUP BY, DISTINCT ON, ON CONFLICT target
@@ -1671,11 +2076,16 @@ else instance
1671class ValidateOrderByGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
2076class ValidateOrderByGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
1672class ValidateOrderByGo head tail acc tables
2077class ValidateOrderByGo head tail acc tables
1674−-- Comma: flush column, continue
2079+-- Leading/double comma: no column before comma
2081+ Fail (Text "Unexpected comma in ORDER BY clause (missing column name)") =>
2082+ ValidateOrderByGo "," tail "" tables
2084+-- Comma: flush column, continue (non-empty acc)
1676 ( FlushOrderByWord acc tables
2086 ( FlushOrderByWord acc tables
1677 , SkipSpaces tail rest
2087 , SkipSpaces tail rest
1678− , ValidateOrderBy rest tables
2088+ , ValidateOrderByContinueNonEmpty rest tables
1680 ValidateOrderByGo "," tail acc tables
2090 ValidateOrderByGo "," tail acc tables
@@ -1689,7 +2099,7 @@ else instance
1689-- End of string: flush final column
2099-- End of string: flush final column
1690else instance
2100else instance
1691 ( Symbol.Append acc h acc'
2101 ( Symbol.Append acc h acc'
1692− , FlushOrderByWord acc' tables
2102+ , FlushOrderByFinalWord acc' tables
1694 ValidateOrderByGo h "" acc tables
2104 ValidateOrderByGo h "" acc tables
@@ -1701,6 +2111,31 @@ else instance
1702 ValidateOrderByGo h tail acc tables
2112 ValidateOrderByGo h tail acc tables
2114+-- After comma: reject empty (trailing comma)
2115+class ValidateOrderByContinueNonEmpty :: Symbol -> Row (Row Type) -> Constraint
2116+class ValidateOrderByContinueNonEmpty sym tables
2118+instance Fail (Text "Trailing comma in ORDER BY clause") => ValidateOrderByContinueNonEmpty "" tables
2120+ ( Symbol.Cons h t sym
2121+ , ValidateOrderByGo h t "" tables
2123+ ValidateOrderByContinueNonEmpty sym tables
2125+-- End of string: reject bare modifiers (ASC/DESC without column)
2126+class FlushOrderByFinalWord :: Symbol -> Row (Row Type) -> Constraint
2127+class FlushOrderByFinalWord word tables
2129+instance Fail (Text "ORDER BY requires at least one column") => FlushOrderByFinalWord "" tables
2130+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "ASC" tables
2131+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "asc" tables
2132+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "DESC" tables
2133+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "desc" tables
2134+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "NULLS" tables
2135+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "FIRST" tables
2136+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "LAST" tables
2137+else instance ResolveColumn word tables typ => FlushOrderByFinalWord word tables
1704class FlushOrderByWord :: Symbol -> Row (Row Type) -> Constraint
2139class FlushOrderByWord :: Symbol -> Row (Row Type) -> Constraint
1705class FlushOrderByWord word tables
2140class FlushOrderByWord word tables
@@ -2130,6 +2565,14 @@ else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => V
2130else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "(" tail acc tables
2565else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "(" tail acc tables
2131else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ")" tail acc tables
2566else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ")" tail acc tables
2568+-- String literal: skip content between quotes
2570+ ( FlushJoinWord acc tables
2571+ , SkipStringLiteral tail rest
2572+ , ValidateJoinCondition rest tables
2574+ ValidateJoinCondGo "'" tail acc tables
2133-- End of string: flush final word
2576-- End of string: flush final word
2134else instance
2577else instance
2135 ( Symbol.Append acc h acc'
2578 ( Symbol.Append acc h acc'
@@ -2214,13 +2657,85 @@ else instance
2214 , SkipSpaces rest1 rest2
2657 , SkipSpaces rest1 rest2
2215 , ExpectChar rest2 "=" rest3
2658 , ExpectChar rest2 "=" rest3
2216 , SkipSpaces rest3 rest4
2659 , SkipSpaces rest3 rest4
2217− , ExtractWord rest4 excRef rest5
2660+ , SkipExpression rest4 rest5
2218− , ValidateExcludedRef excRef colName
2219− , SkipSpaces rest5 rest6
2220− , ParseAssignmentsContinue rest6 cols
2661+ , ParseAssignmentsContinue rest5 cols
2222 ParseAssignments sym cols
2663 ParseAssignments sym cols
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
2672+instance SkipExpression "" ""
2674+ ( Symbol.Cons h t sym
2675+ , SkipExpressionGo h t rest
2677+ SkipExpression sym rest
2679+class SkipExpressionGo :: Symbol -> Symbol -> Symbol -> Constraint
2680+class SkipExpressionGo head tail rest | head tail -> rest
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
2686+ ( SkipNested tail rest1
2687+ , SkipExpression rest1 rest
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
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
2703+instance SkipExpressionAfterSpace "" ""
2705+ ( Symbol.Cons h t sym
2706+ , SkipExpressionAfterSpaceGo h t sym rest
2708+ SkipExpressionAfterSpace sym rest
2710+class SkipExpressionAfterSpaceGo :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
2711+class SkipExpressionAfterSpaceGo head tail sym rest | head tail sym -> rest
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
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
2724+ ( Symbol.Cons h t sym
2725+ , SkipNestedGo h t rest
2727+ SkipNested sym rest
2729+class SkipNestedGo :: Symbol -> Symbol -> Symbol -> Constraint
2730+class SkipNestedGo head tail rest | head tail -> rest
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
2224class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
2739class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
2225class ParseAssignmentsContinue sym cols
2740class ParseAssignmentsContinue sym cols
@@ -2283,6 +2798,7 @@ class IsAutoGeneratedTC constraints result | constraints -> result
2284instance IsAutoGeneratedTC (AutoIncrement a) True
2799instance IsAutoGeneratedTC (AutoIncrement a) True
2285else instance IsAutoGeneratedTC (Default s a) True
2800else instance IsAutoGeneratedTC (Default s a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
2286else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
2802else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
2287else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
2803else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
2288else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
2804else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
@@ -2338,11 +2854,19 @@ class FieldToPGValue a where
2338 fieldToPGValue :: a -> PG.PGValue
2854 fieldToPGValue :: a -> PG.PGValue
2340instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2856instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2341− fieldToPGValue = toNullable >>> unsafeCoerce
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
2342else instance FieldToPGValue DateTime where
2860else instance FieldToPGValue DateTime where
2343 fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
2861 fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
2344−else instance FieldToPGValue a where
2862+else instance FieldToPGValue TimestampTZ where
2863+ fieldToPGValue (TimestampTZ dt) = JSDate.fromDateTime dt # unsafeCoerce
2864+else instance FieldToPGValue PGDate where
2865+ fieldToPGValue = writeImpl >>> unsafeCoerce
2866+else instance FieldToPGValue PGUUID where
2345 fieldToPGValue = unsafeCoerce
2867 fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
2347class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
2871class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
2348class RecordValuesRL rl row where
2872class RecordValuesRL rl row where
@@ -2410,6 +2934,20 @@ else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
2410else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
2934else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
2411else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
2935else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
2937+class HasMutation :: Row Type -> Constraint
2938+class HasMutation stage
2940+instance (RL.RowToList stage rl, HasMutationRL rl) => HasMutation stage
2942+class HasMutationRL :: RL.RowList Type -> Constraint
2943+class HasMutationRL rl
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
2413toSQL :: forall tables result params stage. Q tables result params stage -> String
2951toSQL :: forall tables result params stage. Q tables result params stage -> String
2414toSQL (Q q) = q.sql
2952toSQL (Q q) = q.sql
@@ -2419,6 +2957,12 @@ from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2419fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
2957fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
2420fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
2958fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
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: [] }
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: [] }
2422selectAll
2966selectAll
2423 :: forall tables name cols result r p stage stage'
2967 :: forall tables name cols result r p stage stage'
2424 . SingleTable tables name cols
2968 . SingleTable tables name cols
@@ -2426,6 +2970,7 @@ selectAll
2426 => StripColumns cols result
2970 => StripColumns cols result
2427 => Row.Lacks "select" stage
2971 => Row.Lacks "select" stage
2428 => Row.Lacks "insert" stage
2972 => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
2429 => Row.Lacks "set" stage
2974 => Row.Lacks "set" stage
2430 => Row.Lacks "delete" stage
2975 => Row.Lacks "delete" stage
2431 => Row.Lacks "where" stage
2976 => Row.Lacks "where" stage
@@ -2443,6 +2988,7 @@ select
2443 => ParseSelect sel tables result
2988 => ParseSelect sel tables result
2444 => Row.Lacks "select" stage
2989 => Row.Lacks "select" stage
2445 => Row.Lacks "insert" stage
2990 => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
2446 => Row.Lacks "set" stage
2992 => Row.Lacks "set" stage
2447 => Row.Lacks "delete" stage
2993 => Row.Lacks "delete" stage
2448 => Row.Lacks "where" stage
2994 => Row.Lacks "where" stage
@@ -2460,6 +3006,7 @@ selectDistinct
2460 => ParseSelect sel tables result
3006 => ParseSelect sel tables result
2461 => Row.Lacks "select" stage
3007 => Row.Lacks "select" stage
2462 => Row.Lacks "insert" stage
3008 => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
2463 => Row.Lacks "set" stage
3010 => Row.Lacks "set" stage
2464 => Row.Lacks "delete" stage
3011 => Row.Lacks "delete" stage
2465 => Row.Lacks "where" stage
3012 => Row.Lacks "where" stage
@@ -2480,6 +3027,7 @@ selectDistinctOn
2480 => ParseSelect sel tables result
3027 => ParseSelect sel tables result
2481 => Row.Lacks "select" stage
3028 => Row.Lacks "select" stage
2482 => Row.Lacks "insert" stage
3029 => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
2483 => Row.Lacks "set" stage
3031 => Row.Lacks "set" stage
2484 => Row.Lacks "delete" stage
3032 => Row.Lacks "delete" stage
2485 => Row.Lacks "where" stage
3033 => Row.Lacks "where" stage
@@ -2570,16 +3118,66 @@ instance
2571instance ParseLimitOffsetParams False name params params
3119instance ParseLimitOffsetParams False name params params
3121+-- Validate that a Symbol is a non-negative integer (all digits, non-empty)
3122+class ValidateNumericLiteral :: Symbol -> Constraint
3123+class ValidateNumericLiteral sym
3126+ ( Symbol.Cons h t sym
3127+ , ValidateAllDigits h t
3129+ ValidateNumericLiteral sym
3131+class ValidateAllDigits :: Symbol -> Symbol -> Constraint
3132+class ValidateAllDigits head tail
3134+instance ValidateAllDigits "0" ""
3135+else instance ValidateAllDigits "1" ""
3136+else instance ValidateAllDigits "2" ""
3137+else instance ValidateAllDigits "3" ""
3138+else instance ValidateAllDigits "4" ""
3139+else instance ValidateAllDigits "5" ""
3140+else instance ValidateAllDigits "6" ""
3141+else instance ValidateAllDigits "7" ""
3142+else instance ValidateAllDigits "8" ""
3143+else instance ValidateAllDigits "9" ""
3144+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "0" tail
3145+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "1" tail
3146+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "2" tail
3147+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "3" tail
3148+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "4" tail
3149+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "5" tail
3150+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "6" tail
3151+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "7" tail
3152+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "8" tail
3153+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "9" tail
3155+ Fail (Beside (Text "LIMIT/OFFSET must be a non-negative integer or $parameter, got: ") (Quote sym)) =>
3156+ ValidateAllDigits head tail
2573class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
3158class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
2574class ParseLimitOffset sym params params' | sym params -> params'
3159class ParseLimitOffset sym params params' | sym params -> params'
2577 ( Symbol.Cons head tail sym
3162 ( Symbol.Cons head tail sym
2578 , IsLimitParam head tail isParam
3163 , IsLimitParam head tail isParam
2579− , ParseLimitOffsetParams isParam tail params params'
3164+ , ParseLimitOffsetBranch isParam sym tail params params'
2581 ParseLimitOffset sym params params'
3166 ParseLimitOffset sym params params'
3168+class ParseLimitOffsetBranch :: Boolean -> Symbol -> Symbol -> Row Type -> Row Type -> Constraint
3169+class ParseLimitOffsetBranch isParam sym paramName params params' | isParam sym paramName params -> params'
3172+ ( Row.Lacks paramName params
3173+ , Row.Cons paramName Int params params'
3175+ ParseLimitOffsetBranch True sym paramName params params'
3178+ ValidateNumericLiteral sym =>
3179+ ParseLimitOffsetBranch False sym paramName params params
2584 :: forall @sym tables result params params' stage stage'
3182 :: forall @sym tables result params params' stage stage'
2585 . ParseLimitOffset sym params params'
3183 . ParseLimitOffset sym params params'
@@ -2634,13 +3232,17 @@ insert rec _ = Q { sql, values }
2634 tableName = reflectSymbol (Proxy :: Proxy name)
3232 tableName = reflectSymbol (Proxy :: Proxy name)
2635 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
3233 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
2636 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
3234 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
3236+ if Array.null colNames then
2637− sql = "INSERT INTO " <> tableName
3237+ "INSERT INTO " <> tableName <> " DEFAULT VALUES"
3239+ "INSERT INTO " <> tableName
2638− <> " ("
3240+ <> " ("
2639− <> intercalate ", " colNames
3241+ <> intercalate ", " colNames
2641− <> " VALUES ("
3243+ <> " VALUES ("
2642− <> intercalate ", " placeholders
3244+ <> intercalate ", " placeholders
2644 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
3246 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
2646-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3248-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -2652,6 +3254,7 @@ returning
2652 . SingleTable tables name cols
3254 . SingleTable tables name cols
2653 => IsSymbol sel
3255 => IsSymbol sel
2654 => ParseSelect sel tables result
3256 => ParseSelect sel tables result
3257+ => HasMutation stage
2655 => Row.Lacks "returning" stage
3258 => Row.Lacks "returning" stage
2656 => Row.Lacks "select" stage
3259 => Row.Lacks "select" stage
2657 => Row.Cons "returning" Unit stage stage'
3260 => Row.Cons "returning" Unit stage stage'
@@ -2663,6 +3266,7 @@ returningAll
2663 :: forall tables name cols result p stage stage'
3266 :: forall tables name cols result p stage stage'
2664 . SingleTable tables name cols
3267 . SingleTable tables name cols
2665 => StripColumns cols result
3268 => StripColumns cols result
3269+ => HasMutation stage
2666 => Row.Lacks "returning" stage
3270 => Row.Lacks "returning" stage
2667 => Row.Lacks "select" stage
3271 => Row.Lacks "select" stage
2668 => Row.Cons "returning" Unit stage stage'
3272 => Row.Cons "returning" Unit stage stage'
@@ -2705,8 +3309,10 @@ set
2705 => ValidateSetColumnsRL setRL cols
3309 => ValidateSetColumnsRL setRL cols
2706 => SetClauseRL setRL
3310 => SetClauseRL setRL
2707 => RecordValuesRL setRL setRow
3311 => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
2708 => Row.Lacks "select" stage
3313 => Row.Lacks "select" stage
2709 => Row.Lacks "insert" stage
3314 => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
2710 => Row.Lacks "set" stage
3316 => Row.Lacks "set" stage
2711 => Row.Lacks "delete" stage
3317 => Row.Lacks "delete" stage
2712 => Row.Cons "set" Unit stage stage'
3318 => Row.Cons "set" Unit stage stage'
@@ -2730,6 +3336,7 @@ delete
2730 => IsSymbol name
3336 => IsSymbol name
2731 => Row.Lacks "select" stage
3337 => Row.Lacks "select" stage
2732 => Row.Lacks "insert" stage
3338 => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
2733 => Row.Lacks "set" stage
3340 => Row.Lacks "set" stage
2734 => Row.Lacks "delete" stage
3341 => Row.Lacks "delete" stage
2735 => Row.Cons "delete" Unit stage stage'
3342 => Row.Cons "delete" Unit stage stage'
@@ -2970,6 +3577,66 @@ exceptAll
2970 -> Q tables1 result params SetOpStage
3577 -> Q tables1 result params SetOpStage
2971exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
3578exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
3580+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3581+-- CTE (WITH clause) support
3582+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3584+newtype WithCTEs :: Row (Row Type) -> Row Type -> Type
3585+newtype WithCTEs cteTables cteParams = WithCTEs
3586+ { sql :: String, values :: Array PG.PGValue }
3588+-- Wrap a complete SELECT query as a named CTE
3590+ :: forall @name tables result params stage cteTables
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 <> ")"
3601+-- Add another named CTE to an accumulator
3603+ :: forall @name tables result params stage cteTables cteParams cteTables' cteParams'
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
3618+-- Start main query FROM a named CTE; cteTables becomes available as tables
3620+ :: forall @name result restTables cteTables cteParams
3622+ => Row.Cons name result restTables cteTables
3623+ => WithCTEs cteTables cteParams
3624+ -> Q cteTables () () ()
3625+fromCTE _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
3627+-- Finalize: prepend WITH clause and merge params
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
2973-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3640-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2974-- Query execution
3641-- Query execution
2975-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3642-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -2985,28 +3652,34 @@ instance
2985 ( IsSymbol name
3652 ( IsSymbol name
2986 , Row.Cons name typ rest row
3653 , Row.Cons name typ rest row
2987 , Row.Lacks name rest
3654 , Row.Lacks name rest
3655+ , FieldToPGValue typ
2988 , ParamsToArray tail row
3656 , ParamsToArray tail row
2990 ParamsToArray (RL.Cons name typ tail) row where
3658 ParamsToArray (RL.Cons name typ tail) row where
2991 paramsToArray _ rec =
3659 paramsToArray _ rec =
2992 [ { name: reflectSymbol (Proxy :: Proxy name)
3660 [ { name: reflectSymbol (Proxy :: Proxy name)
2993− , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
2996 <> paramsToArray (Proxy :: Proxy tail) rec
3664 <> paramsToArray (Proxy :: Proxy tail) rec
2998namedParamRegex :: Regex.Regex
3666namedParamRegex :: Regex.Regex
2999−namedParamRegex = case Regex.regex "\\$[a-zA-Z_][a-zA-Z0-9_]*" Regex.global of
3667+namedParamRegex = case Regex.regex "\\$[a-zA-Z_][a-zA-Z0-9_]*(::[a-zA-Z_]+)?" Regex.global of
3000 Right r -> r
3668 Right r -> r
3001 Left _ -> unsafeCoerce unit
3669 Left _ -> unsafeCoerce unit
3671+stripCast :: String -> String
3672+stripCast s = case SCU.indexOf (Pattern "::") s of
3674+ Just i -> SCU.take i s
3003replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
3676replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
3004replaceNamedParams offset entries sql = do
3677replaceNamedParams offset entries sql = do
3005 let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
3678 let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
3006 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
3679 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
3008 sql' = Regex.replace' namedParamRegex
3681 sql' = Regex.replace' namedParamRegex
3009− ( \match _ -> case Map.lookup match replacements of
3682+ ( \match _ -> case Map.lookup (stripCast match) replacements of
3010 Nothing -> match
3683 Nothing -> match
3011 Just v -> v
3684 Just v -> v
@@ -3027,7 +3700,9 @@ runQuery conn params (Q q) = do
3027 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3700 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3028 let allValues = q.values <> values
3701 let allValues = q.values <> values
3029 result <- PG.query (PG.SQL sql) allValues conn
3702 result <- PG.query (PG.SQL sql) allValues conn
3030− pure (unsafeDecodeRows result.rows)
3703+ case decodeRows result.rows of
3704+ Left errs -> throwError (Exception.error ("Row decode failed: " <> show errs))
3705+ Right rows -> pure rows
3032runQueryOne
3707runQueryOne
3033 :: forall tables result params paramsRL stage
3708 :: forall tables result params paramsRL stage
@@ -3043,7 +3718,11 @@ runQueryOne conn params (Q q) = do
3043 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3718 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3044 let allValues = q.values <> values
3719 let allValues = q.values <> values
3045 result <- PG.queryOne (PG.SQL sql) allValues conn
3720 result <- PG.queryOne (PG.SQL sql) allValues conn
3046− pure (result <#> unsafeDecodeRow)
3721+ case result of
3722+ Nothing -> pure Nothing
3723+ Just row -> case decodeRow row of
3724+ Left errs -> throwError (Exception.error ("Row decode failed: " <> show errs))
3725+ Right a -> pure (Just a)
3048runExecute
3727runExecute
3049 :: forall tables params paramsRL stage
3728 :: forall tables params paramsRL stage
@@ -3075,7 +3754,9 @@ runQueryTx txn params (Q q) = do
3075 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3754 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3076 let allValues = q.values <> values
3755 let allValues = q.values <> values
3077 result <- PG.txQuery (PG.SQL sql) allValues txn
3756 result <- PG.txQuery (PG.SQL sql) allValues txn
3078− pure (unsafeDecodeRows result.rows)
3757+ case decodeRows result.rows of
3758+ Left errs -> throwError (Exception.error ("Row decode failed: " <> show errs))
3759+ Right rows -> pure rows
3080runExecuteTx
3761runExecuteTx
3081 :: forall tables params paramsRL stage
3762 :: forall tables params paramsRL stage
@@ -3091,10 +3772,9 @@ runExecuteTx txn params (Q q) = do
3091 let allValues = q.values <> values
3772 let allValues = q.values <> values
3092 PG.txExecute (PG.SQL sql) allValues txn
3773 PG.txExecute (PG.SQL sql) allValues txn
3094−unsafeDecodeRows :: forall a. ReadForeign a => Array Foreign -> Array a
3095−unsafeDecodeRows = map unsafeDecodeRow
3097−unsafeDecodeRow :: forall a. ReadForeign a => Foreign -> a
3776+decodeRows :: forall a. ReadForeign a => Array Foreign -> Either (NonEmptyList ForeignError) (Array a)
3098−unsafeDecodeRow f = case runExcept (readImpl f) of
3777+decodeRows = traverse (readImpl >>> runExcept)
3099− Left _ -> unsafeCoerce unit
3778+
3100− Right a -> a
3779+decodeRow :: forall a. ReadForeign a => Foreign -> Either (NonEmptyList ForeignError) a
3780+decodeRow = readImpl >>> runExcept