@@ -2,36 +2,47 @@ module Yoga.Postgres.Schema where
22
33import Prelude
44
5−import Data.Array as Array
5+import Control.Monad.Except (except, runExcept)
66import Data.Array (intercalate, mapWithIndex, foldl)
7−import Data.DateTime (DateTime)
7+import Data.Array as Array
8+import Data.Date (Date, day, month, year)
9+import Data.DateTime (DateTime(..))
10+import Data.Either (Either(..), note)
11+import Data.Enum (fromEnum, toEnum)
12+import Data.Int as Int
813import Data.JSDate as JSDate
9−import Data.Maybe (Maybe(..))
14+import Data.List.Types (NonEmptyList)
15+import Data.Map as Map
16+import Data.Maybe (Maybe(..), fromMaybe)
1017import Data.Newtype (class Newtype)
1118import Data.Nullable (toNullable)
12−import JS.BigInt (BigInt)
13−import Unsafe.Coerce (unsafeCoerce)
19+import Data.Number as Number
1420import Data.Reflectable (class Reflectable, reflectType)
15−import Data.String.Regex (regex, replace') as Regex
16−import Data.String.Regex (Regex) as Regex
21+import Data.String.CodeUnits as SCU
22+import Data.String.Common as Str
23+import Data.String.Pattern (Pattern(..))
24+import Data.String.Regex (Regex, regex, replace') as Regex
1725import Data.String.Regex.Flags (global) as Regex
18−import Control.Monad.Except (runExcept)
19−import Data.Either (Either(..))
20−import Data.Map as Map
2126import Data.Symbol (class IsSymbol, reflectSymbol)
22−import Type.Function (type (#))
23−import Effect.Aff (Aff)
24−import Foreign (Foreign, unsafeToForeign)
27+import Data.Time (Time(..))
28+import Data.Traversable (traverse)
29+import Data.UUID (UUID)
30+import Data.UUID as UUID
31+import Effect.Aff (Aff, throwError)
32+import Effect.Exception (error) as Exception
33+import Foreign (Foreign, ForeignError(..))
34+import JS.BigInt (BigInt)
2535import Prim.Boolean (True, False)
2636import Prim.Row (class Cons, class Lacks, class Union, class Nub) as Row
27−import Prim.RowList as RL
2837import Prim.RowList (class RowToList)
38+import Prim.RowList as RL
2939import Prim.Symbol (class Cons, class Append) as Symbol
3040import Prim.TypeError (class Fail, Beside, Text, Quote)
3141import Record (get) as Record
3242import Type.Proxy (Proxy(..))
3343import Type.RowList (class ListToRow)
34−import Yoga.JSON (class ReadForeign, readImpl)
44+import Unsafe.Coerce (unsafeCoerce)
45+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
3546import Yoga.Postgres as PG
3647
3748-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -57,6 +68,9 @@ data Unique a
5768
5869data Default :: forall k. k -> Type -> Type
5970data Default val a
71+data DefaultExpr :: Symbol -> Type -> Type
72+data DefaultExpr expression a
73+
6074
6175data References
6276
@@ -78,6 +92,178 @@ derive instance Newtype Jsonb _
7892instance ReadForeign Jsonb where
7993 readImpl = pure <<< Jsonb
8094
95+instance WriteForeign Jsonb where
96+ writeImpl (Jsonb value) = value
97+
98+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99+-- PGUUID: newtype over Data.UUID.UUID for Postgres integration
100+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
101+
102+newtype PGUUID = PGUUID UUID
103+
104+derive instance Newtype PGUUID _
105+derive newtype instance Eq PGUUID
106+derive newtype instance Ord PGUUID
107+derive newtype instance Show PGUUID
108+
109+instance ReadForeign PGUUID where
110+ readImpl f = do
111+ s :: String <- readImpl f
112+ pure (unsafeCoerce s)
113+
114+instance WriteForeign PGUUID where
115+ writeImpl (PGUUID uuid) = writeImpl (UUID.toString uuid)
116+
117+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118+-- PGDate / PGTime: newtypes for DATE and TIME columns
119+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
120+
121+newtype PGDate = PGDate Date
122+
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 <> ")"
128+
129+instance ReadForeign PGDate where
130+ readImpl f =
131+ except $ note (pure (ForeignError "Invalid date")) (JSDate.toDate jsDate <#> PGDate)
132+ where
133+ jsDate :: JSDate.JSDate
134+ jsDate = unsafeCoerce f
135+
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+
163+newtype PGTime = PGTime Time
164+
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 <> ")"
170+
171+instance ReadForeign PGTime where
172+ readImpl f = do
173+ s :: String <- readImpl f
174+ except $ note (pure (ForeignError ("Invalid time: " <> s))) (parseTime s)
175+ where
176+ parseTime str = do
177+ let parts = Str.split (Pattern ":") str
178+ case parts of
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
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)
191+ pure (PGTime (Time h mi s ms))
192+ _ -> Nothing
193+
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+
212+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
213+-- Point: geometric point "(x,y)"
214+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
215+
216+newtype Point = Point { x :: Number, y :: Number }
217+
218+derive instance Newtype Point _
219+derive instance Eq Point
220+
221+instance Show Point where
222+ show (Point { x, y }) = "(Point " <> show x <> " " <> show y <> ")"
223+
224+instance ReadForeign Point where
225+ readImpl f = do
226+ s :: String <- readImpl f
227+ except $ note (pure (ForeignError ("Invalid point: " <> s))) (parsePoint s)
228+ where
229+ parsePoint str = do
230+ let inner = SCU.drop 1 (SCU.dropRight 1 str)
231+ let parts = Str.split (Pattern ",") inner
232+ case parts of
233+ [ xStr, yStr ] -> do
234+ x <- Number.fromString xStr
235+ y <- Number.fromString yStr
236+ pure (Point { x, y })
237+ _ -> Nothing
238+
239+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240+-- TSVector / TSQuery: full-text search types
241+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
242+
243+newtype TSVector = TSVector String
244+
245+derive instance Newtype TSVector _
246+derive newtype instance Eq TSVector
247+derive newtype instance Ord TSVector
248+derive newtype instance Show TSVector
249+
250+instance ReadForeign TSVector where
251+ readImpl f = do
252+ s :: String <- readImpl f
253+ pure (TSVector s)
254+
255+newtype TSQuery = TSQuery String
256+
257+derive instance Newtype TSQuery _
258+derive newtype instance Eq TSQuery
259+derive newtype instance Ord TSQuery
260+derive newtype instance Show TSQuery
261+
262+instance ReadForeign TSQuery where
263+ readImpl f = do
264+ s :: String <- readImpl f
265+ pure (TSQuery s)
266+
81267-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
82268-- Nullability: inferred from Maybe
83269-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -101,6 +287,7 @@ instance ExtractType a typ => ExtractType (PrimaryKey a) typ
101287else instance ExtractType a typ => ExtractType (AutoIncrement a) typ
102288else instance ExtractType a typ => ExtractType (Unique a) typ
103289else instance ExtractType a typ => ExtractType (Default val a) typ
290+else instance ExtractType a typ => ExtractType (DefaultExpr expression a) typ
104291else instance ExtractType a typ => ExtractType (ForeignKey t r c a) typ
105292else instance ExtractType a typ => ExtractType (Nullable a) (Maybe typ)
106293else instance ExtractType a a
@@ -130,6 +317,27 @@ instance PGTypeName DateTime where
130317instance PGTypeName BigInt where
131318 pgTypeName _ = "BIGINT"
132319
320+instance PGTypeName PGUUID where
321+ pgTypeName _ = "UUID"
322+
323+instance PGTypeName TimestampTZ where
324+ pgTypeName _ = "TIMESTAMPTZ"
325+
326+instance PGTypeName PGDate where
327+ pgTypeName _ = "DATE"
328+
329+instance PGTypeName PGTime where
330+ pgTypeName _ = "TIME"
331+
332+instance PGTypeName Point where
333+ pgTypeName _ = "POINT"
334+
335+instance PGTypeName TSVector where
336+ pgTypeName _ = "TSVECTOR"
337+
338+instance PGTypeName TSQuery where
339+ pgTypeName _ = "TSQUERY"
340+
133341instance PGTypeName Jsonb where
134342 pgTypeName _ = "JSONB"
135343
@@ -152,8 +360,24 @@ instance Reflectable sym String => RenderDefaultValue sym String where
152360instance Reflectable val Int => RenderDefaultValue val Int where
153361 renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
154362
155−instance Reflectable val Boolean => RenderDefaultValue val Boolean where
156− 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+
157381
158382class RenderConstraint a where
159383 renderConstraint :: Proxy a -> String
@@ -170,6 +394,10 @@ else instance RenderConstraint a => RenderConstraint (Unique a) where
170394else instance (RenderDefaultValue val a, RenderConstraint a) => RenderConstraint (Default val a) where
171395 renderConstraint _ = joinConstraints (renderDefaultValue (Proxy :: Proxy val) (Proxy :: Proxy a)) (renderConstraint (Proxy :: Proxy a))
172396
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))
173401else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
174402 renderConstraint _ = joinConstraints
175403 ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
@@ -230,6 +458,11 @@ instance
230458 let columns = renderColumnsRL (Proxy :: Proxy rl)
231459 "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
232460
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+
233466-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
234467-- INSERT SQL generation
235468-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -241,6 +474,8 @@ instance IsAutoGenerated (AutoIncrement a) where
241474 isAutoGenerated _ = true
242475else instance IsAutoGenerated (Default v a) where
243476 isAutoGenerated _ = true
477+else instance IsAutoGenerated (DefaultExpr expression a) where
478+ isAutoGenerated _ = true
244479else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
245480 isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
246481else instance IsAutoGenerated a => IsAutoGenerated (Unique a) where
@@ -284,15 +519,18 @@ instance
284519 insertSQLFor = do
285520 let tableName = reflectSymbol (Proxy :: Proxy name)
286521 let cols = insertColumnsRL (Proxy :: Proxy rl)
287− let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
288− "INSERT INTO " <> tableName
289− <> " ("
290− <> intercalate ", " cols
291− <> ")"
292− <> " VALUES ("
293− <> intercalate ", " placeholders
294− <> ")"
295− <> " 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 *"
296534
297535-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
298536-- SELECT SQL generation
@@ -415,6 +653,31 @@ class SkipSpacesGo head tail result | head tail -> result
415653instance SkipSpaces tail result => SkipSpacesGo " " tail result
416654else instance Symbol.Cons head tail result => SkipSpacesGo head tail result
417655
656+-- Skip a string literal: consume chars until closing single quote
657+class SkipStringLiteral :: Symbol -> Symbol -> Constraint
658+class SkipStringLiteral sym rest | sym -> rest
659+
660+instance Fail (Text "Unclosed string literal") => SkipStringLiteral "" rest
661+else instance
662+ ( Symbol.Cons h t sym
663+ , SkipStringLiteralGo h t rest
664+ ) =>
665+ SkipStringLiteral sym rest
666+
667+class SkipStringLiteralGo :: Symbol -> Symbol -> Symbol -> Constraint
668+class SkipStringLiteralGo head tail rest | head tail -> rest
669+
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
675+else instance
676+ ( Symbol.Cons nextH nextT tail
677+ , SkipStringLiteralGo nextH nextT rest
678+ ) =>
679+ SkipStringLiteralGo h tail rest
680+
418681-- Extract the first word from a Symbol (up to space or end)
419682class ExtractWord :: Symbol -> Symbol -> Symbol -> Constraint
420683class ExtractWord sym word rest | sym -> word rest
@@ -722,6 +985,15 @@ else instance
722985 ) =>
723986 ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
724987
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+
725997class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
726998class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
727999
@@ -820,11 +1092,66 @@ instance ResolveAggregateArg "*" tables Star
8201092else instance ResolveAggregateArg "" tables Star
8211093else instance
8221094 ( SkipSpaces args trimmedFront
823− , ExtractWord trimmedFront col _rest
824− , ResolveAggregateArgCol col tables argType
1095+ , ExtractIdent trimmedFront col rest
1096+ , ResolveAggregateArgDispatch col rest tables argType
8251097 ) =>
8261098 ResolveAggregateArg args tables argType
8271099
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+
8281155class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
8291156class ResolveAggregateArgCol col tables argType | col tables -> argType
8301157
@@ -909,8 +1236,8 @@ else instance
9091236class AggregateReturnTypeC :: Symbol -> Type -> Type -> Constraint
9101237class AggregateReturnTypeC funcName argType returnType | funcName argType -> returnType
9111238
912−instance AggregateReturnTypeC "COUNT" argType Int
913−else instance AggregateReturnTypeC "count" argType Int
1239+instance AggregateReturnTypeC "COUNT" argType BigInt
1240+else instance AggregateReturnTypeC "count" argType BigInt
9141241else instance AggregateReturnTypeC "COALESCE" argType argType
9151242else instance AggregateReturnTypeC "coalesce" argType argType
9161243else instance
@@ -1251,7 +1578,7 @@ data NoType
12511578class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
12521579class ParseWhere sym tables params | sym tables -> params
12531580
1254−instance ParseWhere "" tables ()
1581+instance Fail (Text "Empty WHERE clause") => ParseWhere "" tables params
12551582else instance
12561583 ( Symbol.Cons h t sym
12571584 , ParseWhereGo h t "" NoType tables RL.Nil outRL
@@ -1277,12 +1604,19 @@ else instance (FlushWhereWord acc currentType tables paramsIn currentType' param
12771604else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "!" tail acc currentType tables paramsIn paramsOut
12781605else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "(" tail acc currentType tables paramsIn paramsOut
12791606else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ")" tail acc currentType tables paramsIn paramsOut
1280−else 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
1608+else instance
1609+ ( FlushWhereWord acc currentType tables paramsIn currentType' paramsOut'
1610+ , SkipStringLiteral tail rest
1611+ , ParseWhereContinue rest currentType' tables paramsOut' paramsOut
1612+ ) =>
1613+ ParseWhereGo "'" tail acc currentType tables paramsIn paramsOut
12811614else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "@" tail acc currentType tables paramsIn paramsOut
12821615else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "?" tail acc currentType tables paramsIn paramsOut
12831616else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ":" tail acc currentType tables paramsIn paramsOut
12841617else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "~" tail acc currentType tables paramsIn paramsOut
12851618else 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
12861620
12871621-- End of string: flush final word
12881622else instance
@@ -1386,8 +1720,12 @@ else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut pa
13861720else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "u" word currentType tables paramsIn currentTypeOut paramsOut
13871721else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "V" word currentType tables paramsIn currentTypeOut paramsOut
13881722else 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
13891727
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)
13911729else instance
13921730 ( ResolveColumn word tables entry
13931731 , ExtractType entry typ
@@ -1400,7 +1738,9 @@ else instance
14001738class FlushWhereWordA :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
14011739class FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
14021740
1403−instance FlushWhereWordA "ANY" currentType tables paramsIn (Array currentType) paramsIn
1741+instance FlushWhereWordA "ABS" currentType tables paramsIn currentType paramsIn
1742+else instance FlushWhereWordA "abs" currentType tables paramsIn currentType paramsIn
1743+else instance FlushWhereWordA "ANY" currentType tables paramsIn (Array currentType) paramsIn
14041744else instance FlushWhereWordA "any" currentType tables paramsIn (Array currentType) paramsIn
14051745else instance FlushWhereWordA "ALL" currentType tables paramsIn (Array currentType) paramsIn
14061746else instance FlushWhereWordA "all" currentType tables paramsIn (Array currentType) paramsIn
@@ -1436,8 +1776,18 @@ class FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut
14361776
14371777instance FlushWhereWordC "CAST" currentType tables paramsIn currentType paramsIn
14381778else 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
14391785else instance FlushWhereWordC "COUNT" currentType tables paramsIn Int paramsIn
14401786else 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
14411791else instance
14421792 ( ResolveColumn word tables entry
14431793 , ExtractType entry typ
@@ -1448,7 +1798,8 @@ else instance
14481798class FlushWhereWordD :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
14491799class FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
14501800
1451−instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1801+instance FlushWhereWordD "DATE" currentType tables paramsIn currentType paramsIn
1802+else instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
14521803else instance
14531804 ( ResolveColumn word tables entry
14541805 , ExtractType entry typ
@@ -1473,6 +1824,8 @@ class FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut
14731824
14741825instance FlushWhereWordF "FALSE" currentType tables paramsIn currentType paramsIn
14751826else 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
14761829else instance
14771830 ( ResolveColumn word tables entry
14781831 , ExtractType entry typ
@@ -1511,8 +1864,14 @@ else instance
15111864class FlushWhereWordL :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
15121865class FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
15131866
1514−instance FlushWhereWordL "LIKE" currentType tables paramsIn currentType paramsIn
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
1871+else instance FlushWhereWordL "LIKE" currentType tables paramsIn currentType paramsIn
15151872else 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
15161875else instance
15171876 ( ResolveColumn word tables entry
15181877 , ExtractType entry typ
@@ -1539,8 +1898,12 @@ class FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut
15391898
15401899instance FlushWhereWordN "NOT" currentType tables paramsIn currentType paramsIn -- unreachable (caught by FlushWhereWord directly) but included for self-containedness
15411900else 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
15421903else instance FlushWhereWordN "NULL" currentType tables paramsIn currentType paramsIn
15431904else 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
15441907else instance
15451908 ( ResolveColumn word tables entry
15461909 , ExtractType entry typ
@@ -1551,7 +1914,9 @@ else instance
15511914class FlushWhereWordS :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
15521915class FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
15531916
1554−instance FlushWhereWordS "SUM" currentType tables paramsIn currentType paramsIn
1917+instance FlushWhereWordS "SMALLINT" currentType tables paramsIn currentType paramsIn
1918+else instance FlushWhereWordS "smallint" currentType tables paramsIn currentType paramsIn
1919+else instance FlushWhereWordS "SUM" currentType tables paramsIn currentType paramsIn
15551920else instance FlushWhereWordS "sum" currentType tables paramsIn currentType paramsIn
15561921else instance FlushWhereWordS "STRING_AGG" currentType tables paramsIn String paramsIn
15571922else instance FlushWhereWordS "string_agg" currentType tables paramsIn String paramsIn
@@ -1565,11 +1930,23 @@ else instance
15651930class FlushWhereWordT :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
15661931class FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
15671932
1568−instance FlushWhereWordT "TRUE" currentType tables paramsIn currentType paramsIn
1933+instance FlushWhereWordT "TRIM" currentType tables paramsIn String paramsIn
1934+else instance FlushWhereWordT "trim" currentType tables paramsIn String paramsIn
1935+else instance FlushWhereWordT "TRUE" currentType tables paramsIn currentType paramsIn
15691936else instance FlushWhereWordT "true" currentType tables paramsIn currentType paramsIn
15701937else instance FlushWhereWordT "text" currentType tables paramsIn currentType paramsIn
15711938else instance FlushWhereWordT "timestamptz" currentType tables paramsIn currentType paramsIn
15721939else 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
15731950else instance
15741951 ( ResolveColumn word tables entry
15751952 , ExtractType entry typ
@@ -1580,7 +1957,9 @@ else instance
15801957class FlushWhereWordU :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
15811958class FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
15821959
1583−instance FlushWhereWordU "uuid" currentType tables paramsIn currentType paramsIn
1960+instance FlushWhereWordU "UPPER" currentType tables paramsIn String paramsIn
1961+else instance FlushWhereWordU "upper" currentType tables paramsIn String paramsIn
1962+else instance FlushWhereWordU "uuid" currentType tables paramsIn currentType paramsIn
15841963else instance
15851964 ( ResolveColumn word tables entry
15861965 , ExtractType entry typ
@@ -1599,6 +1978,32 @@ else instance
15991978 ) =>
16001979 FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
16011980
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
1983+
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
1988+else instance
1989+ ( ResolveColumn word tables entry
1990+ , ExtractType entry typ
1991+ , UnwrapMaybe typ unwrapped
1992+ ) =>
1993+ FlushWhereWordP word currentType tables paramsIn unwrapped paramsIn
1994+
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
1997+
1998+instance FlushWhereWordR "real" currentType tables paramsIn currentType paramsIn
1999+else instance FlushWhereWordR "REAL" currentType tables paramsIn currentType paramsIn
2000+else instance
2001+ ( ResolveColumn word tables entry
2002+ , ExtractType entry typ
2003+ , UnwrapMaybe typ unwrapped
2004+ ) =>
2005+ FlushWhereWordR word currentType tables paramsIn unwrapped paramsIn
2006+
16022007-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
16032008-- ValidateColumnList: comma-separated column references
16042009-- Used by GROUP BY, DISTINCT ON, ON CONFLICT target
@@ -1671,11 +2076,16 @@ else instance
16712076class ValidateOrderByGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
16722077class ValidateOrderByGo head tail acc tables
16732078
1674−-- Comma: flush column, continue
2079+-- Leading/double comma: no column before comma
16752080instance
2081+ Fail (Text "Unexpected comma in ORDER BY clause (missing column name)") =>
2082+ ValidateOrderByGo "," tail "" tables
2083+
2084+-- Comma: flush column, continue (non-empty acc)
2085+else instance
16762086 ( FlushOrderByWord acc tables
16772087 , SkipSpaces tail rest
1678− , ValidateOrderBy rest tables
2088+ , ValidateOrderByContinueNonEmpty rest tables
16792089 ) =>
16802090 ValidateOrderByGo "," tail acc tables
16812091
@@ -1689,7 +2099,7 @@ else instance
16892099-- End of string: flush final column
16902100else instance
16912101 ( Symbol.Append acc h acc'
1692− , FlushOrderByWord acc' tables
2102+ , FlushOrderByFinalWord acc' tables
16932103 ) =>
16942104 ValidateOrderByGo h "" acc tables
16952105
@@ -1701,6 +2111,31 @@ else instance
17012111 ) =>
17022112 ValidateOrderByGo h tail acc tables
17032113
2114+-- After comma: reject empty (trailing comma)
2115+class ValidateOrderByContinueNonEmpty :: Symbol -> Row (Row Type) -> Constraint
2116+class ValidateOrderByContinueNonEmpty sym tables
2117+
2118+instance Fail (Text "Trailing comma in ORDER BY clause") => ValidateOrderByContinueNonEmpty "" tables
2119+else instance
2120+ ( Symbol.Cons h t sym
2121+ , ValidateOrderByGo h t "" tables
2122+ ) =>
2123+ ValidateOrderByContinueNonEmpty sym tables
2124+
2125+-- End of string: reject bare modifiers (ASC/DESC without column)
2126+class FlushOrderByFinalWord :: Symbol -> Row (Row Type) -> Constraint
2127+class FlushOrderByFinalWord word tables
2128+
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
2138+
17042139class FlushOrderByWord :: Symbol -> Row (Row Type) -> Constraint
17052140class FlushOrderByWord word tables
17062141
@@ -2130,6 +2565,14 @@ else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => V
21302565else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "(" tail acc tables
21312566else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ")" tail acc tables
21322567
2568+-- String literal: skip content between quotes
2569+else instance
2570+ ( FlushJoinWord acc tables
2571+ , SkipStringLiteral tail rest
2572+ , ValidateJoinCondition rest tables
2573+ ) =>
2574+ ValidateJoinCondGo "'" tail acc tables
2575+
21332576-- End of string: flush final word
21342577else instance
21352578 ( Symbol.Append acc h acc'
@@ -2214,13 +2657,85 @@ else instance
22142657 , SkipSpaces rest1 rest2
22152658 , ExpectChar rest2 "=" rest3
22162659 , SkipSpaces rest3 rest4
2217− , ExtractWord rest4 excRef rest5
2218− , ValidateExcludedRef excRef colName
2219− , SkipSpaces rest5 rest6
2220− , ParseAssignmentsContinue rest6 cols
2660+ , SkipExpression rest4 rest5
2661+ , ParseAssignmentsContinue rest5 cols
22212662 ) =>
22222663 ParseAssignments sym cols
22232664
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+
22242739class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
22252740class ParseAssignmentsContinue sym cols
22262741
@@ -2283,6 +2798,7 @@ class IsAutoGeneratedTC constraints result | constraints -> result
22832798
22842799instance IsAutoGeneratedTC (AutoIncrement a) True
22852800else instance IsAutoGeneratedTC (Default s a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
22862802else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
22872803else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
22882804else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
@@ -2338,11 +2854,19 @@ class FieldToPGValue a where
23382854 fieldToPGValue :: a -> PG.PGValue
23392855
23402856instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2341− fieldToPGValue = toNullable >>> unsafeCoerce
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
23422860else instance FieldToPGValue DateTime where
23432861 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
23452867 fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
23462870
23472871class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
23482872class RecordValuesRL rl row where
@@ -2410,6 +2934,20 @@ else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
24102934else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
24112935else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
24122936
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+
24132951toSQL :: forall tables result params stage. Q tables result params stage -> String
24142952toSQL (Q q) = q.sql
24152953
@@ -2419,6 +2957,12 @@ from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
24192957fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
24202958fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
24212959
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+
24222966selectAll
24232967 :: forall tables name cols result r p stage stage'
24242968 . SingleTable tables name cols
@@ -2426,6 +2970,7 @@ selectAll
24262970 => StripColumns cols result
24272971 => Row.Lacks "select" stage
24282972 => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
24292974 => Row.Lacks "set" stage
24302975 => Row.Lacks "delete" stage
24312976 => Row.Lacks "where" stage
@@ -2443,6 +2988,7 @@ select
24432988 => ParseSelect sel tables result
24442989 => Row.Lacks "select" stage
24452990 => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
24462992 => Row.Lacks "set" stage
24472993 => Row.Lacks "delete" stage
24482994 => Row.Lacks "where" stage
@@ -2460,6 +3006,7 @@ selectDistinct
24603006 => ParseSelect sel tables result
24613007 => Row.Lacks "select" stage
24623008 => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
24633010 => Row.Lacks "set" stage
24643011 => Row.Lacks "delete" stage
24653012 => Row.Lacks "where" stage
@@ -2480,6 +3027,7 @@ selectDistinctOn
24803027 => ParseSelect sel tables result
24813028 => Row.Lacks "select" stage
24823029 => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
24833031 => Row.Lacks "set" stage
24843032 => Row.Lacks "delete" stage
24853033 => Row.Lacks "where" stage
@@ -2570,16 +3118,66 @@ instance
25703118
25713119instance ParseLimitOffsetParams False name params params
25723120
3121+-- Validate that a Symbol is a non-negative integer (all digits, non-empty)
3122+class ValidateNumericLiteral :: Symbol -> Constraint
3123+class ValidateNumericLiteral sym
3124+
3125+instance
3126+ ( Symbol.Cons h t sym
3127+ , ValidateAllDigits h t
3128+ ) =>
3129+ ValidateNumericLiteral sym
3130+
3131+class ValidateAllDigits :: Symbol -> Symbol -> Constraint
3132+class ValidateAllDigits head tail
3133+
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
3154+else instance
3155+ Fail (Beside (Text "LIMIT/OFFSET must be a non-negative integer or $parameter, got: ") (Quote sym)) =>
3156+ ValidateAllDigits head tail
3157+
25733158class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
25743159class ParseLimitOffset sym params params' | sym params -> params'
25753160
25763161instance
25773162 ( Symbol.Cons head tail sym
25783163 , IsLimitParam head tail isParam
2579− , ParseLimitOffsetParams isParam tail params params'
3164+ , ParseLimitOffsetBranch isParam sym tail params params'
25803165 ) =>
25813166 ParseLimitOffset sym params params'
25823167
3168+class ParseLimitOffsetBranch :: Boolean -> Symbol -> Symbol -> Row Type -> Row Type -> Constraint
3169+class ParseLimitOffsetBranch isParam sym paramName params params' | isParam sym paramName params -> params'
3170+
3171+instance
3172+ ( Row.Lacks paramName params
3173+ , Row.Cons paramName Int params params'
3174+ ) =>
3175+ ParseLimitOffsetBranch True sym paramName params params'
3176+
3177+instance
3178+ ValidateNumericLiteral sym =>
3179+ ParseLimitOffsetBranch False sym paramName params params
3180+
25833181limit
25843182 :: forall @sym tables result params params' stage stage'
25853183 . ParseLimitOffset sym params params'
@@ -2634,13 +3232,17 @@ insert rec _ = Q { sql, values }
26343232 tableName = reflectSymbol (Proxy :: Proxy name)
26353233 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
26363234 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
2637− sql = "INSERT INTO " <> tableName
2638− <> " ("
2639− <> intercalate ", " colNames
2640− <> ")"
2641− <> " VALUES ("
2642− <> intercalate ", " placeholders
2643− <> ")"
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+ <> ")"
26443246 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
26453247
26463248-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -2652,6 +3254,7 @@ returning
26523254 . SingleTable tables name cols
26533255 => IsSymbol sel
26543256 => ParseSelect sel tables result
3257+ => HasMutation stage
26553258 => Row.Lacks "returning" stage
26563259 => Row.Lacks "select" stage
26573260 => Row.Cons "returning" Unit stage stage'
@@ -2663,6 +3266,7 @@ returningAll
26633266 :: forall tables name cols result p stage stage'
26643267 . SingleTable tables name cols
26653268 => StripColumns cols result
3269+ => HasMutation stage
26663270 => Row.Lacks "returning" stage
26673271 => Row.Lacks "select" stage
26683272 => Row.Cons "returning" Unit stage stage'
@@ -2705,8 +3309,10 @@ set
27053309 => ValidateSetColumnsRL setRL cols
27063310 => SetClauseRL setRL
27073311 => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
27083313 => Row.Lacks "select" stage
27093314 => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
27103316 => Row.Lacks "set" stage
27113317 => Row.Lacks "delete" stage
27123318 => Row.Cons "set" Unit stage stage'
@@ -2730,6 +3336,7 @@ delete
27303336 => IsSymbol name
27313337 => Row.Lacks "select" stage
27323338 => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
27333340 => Row.Lacks "set" stage
27343341 => Row.Lacks "delete" stage
27353342 => Row.Cons "delete" Unit stage stage'
@@ -2970,6 +3577,66 @@ exceptAll
29703577 -> Q tables1 result params SetOpStage
29713578exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
29723579
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+
29733640-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
29743641-- Query execution
29753642-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -2985,28 +3652,34 @@ instance
29853652 ( IsSymbol name
29863653 , Row.Cons name typ rest row
29873654 , Row.Lacks name rest
3655+ , FieldToPGValue typ
29883656 , ParamsToArray tail row
29893657 ) =>
29903658 ParamsToArray (RL.Cons name typ tail) row where
29913659 paramsToArray _ rec =
29923660 [ { name: reflectSymbol (Proxy :: Proxy name)
2993− , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
29943662 }
29953663 ]
29963664 <> paramsToArray (Proxy :: Proxy tail) rec
29973665
29983666namedParamRegex :: 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
30003668 Right r -> r
30013669 Left _ -> unsafeCoerce unit
30023670
3671+stripCast :: String -> String
3672+stripCast s = case SCU.indexOf (Pattern "::") s of
3673+ Nothing -> s
3674+ Just i -> SCU.take i s
3675+
30033676replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
30043677replaceNamedParams offset entries sql = do
30053678 let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
30063679 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
30073680 let
30083681 sql' = Regex.replace' namedParamRegex
3009− ( \match _ -> case Map.lookup match replacements of
3682+ ( \match _ -> case Map.lookup (stripCast match) replacements of
30103683 Nothing -> match
30113684 Just v -> v
30123685 )
@@ -3027,7 +3700,9 @@ runQuery conn params (Q q) = do
30273700 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
30283701 let allValues = q.values <> values
30293702 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
30313706
30323707runQueryOne
30333708 :: forall tables result params paramsRL stage
@@ -3043,7 +3718,11 @@ runQueryOne conn params (Q q) = do
30433718 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
30443719 let allValues = q.values <> values
30453720 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)
30473726
30483727runExecute
30493728 :: forall tables params paramsRL stage
@@ -3075,7 +3754,9 @@ runQueryTx txn params (Q q) = do
30753754 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
30763755 let allValues = q.values <> values
30773756 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
30793760
30803761runExecuteTx
30813762 :: forall tables params paramsRL stage
@@ -3091,10 +3772,9 @@ runExecuteTx txn params (Q q) = do
30913772 let allValues = q.values <> values
30923773 PG.txExecute (PG.SQL sql) allValues txn
30933774
3094−unsafeDecodeRows :: forall a. ReadForeign a => Array Foreign -> Array a
3095−unsafeDecodeRows = map unsafeDecodeRow
30963775
3097−unsafeDecodeRow :: forall a. ReadForeign a => Foreign -> a
3098−unsafeDecodeRow f = case runExcept (readImpl f) of
3099− Left _ -> unsafeCoerce unit
3100− Right a -> a
3776+decodeRows :: forall a. ReadForeign a => Array Foreign -> Either (NonEmptyList ForeignError) (Array a)
3777+decodeRows = traverse (readImpl >>> runExcept)
3778+
3779+decodeRow :: forall a. ReadForeign a => Foreign -> Either (NonEmptyList ForeignError) a
3780+decodeRow = readImpl >>> runExcept