@@ -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
@@ -143,15 +351,33 @@ instance PGTypeName a => PGTypeName (Maybe a) where
143351-- Constraint -> DDL fragment
144352-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
145353
146−class RenderDefault val where
147− renderDefault :: Proxy val -> String
354+class RenderDefaultValue val typ where
355+ renderDefaultValue :: Proxy val -> Proxy typ -> String
356+
357+instance Reflectable sym String => RenderDefaultValue sym String where
358+ renderDefaultValue _ _ = "DEFAULT '" <> reflectType (Proxy :: Proxy sym) <> "'"
359+
360+instance Reflectable val Int => RenderDefaultValue val Int where
361+ renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
362+
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()"
148380
149−instance Reflectable sym String => RenderDefault sym where
150− renderDefault _ = "DEFAULT '" <> reflectType (Proxy :: Proxy sym) <> "'"
151−else instance Reflectable val Int => RenderDefault val where
152− renderDefault _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
153−else instance Reflectable val Boolean => RenderDefault val where
154− renderDefault _ = if reflectType (Proxy :: Proxy val) then "DEFAULT true" else "DEFAULT false"
155381
156382class RenderConstraint a where
157383 renderConstraint :: Proxy a -> String
@@ -165,9 +391,13 @@ else instance RenderConstraint a => RenderConstraint (AutoIncrement a) where
165391else instance RenderConstraint a => RenderConstraint (Unique a) where
166392 renderConstraint _ = joinConstraints "UNIQUE" (renderConstraint (Proxy :: Proxy a))
167393
168−else instance (RenderDefault val, RenderConstraint a) => RenderConstraint (Default val a) where
169− renderConstraint _ = joinConstraints (renderDefault (Proxy :: Proxy val)) (renderConstraint (Proxy :: Proxy a))
394+else instance (RenderDefaultValue val a, RenderConstraint a) => RenderConstraint (Default val a) where
395+ renderConstraint _ = joinConstraints (renderDefaultValue (Proxy :: Proxy val) (Proxy :: Proxy a)) (renderConstraint (Proxy :: Proxy a))
170396
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))
171401else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
172402 renderConstraint _ = joinConstraints
173403 ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
@@ -228,6 +458,11 @@ instance
228458 let columns = renderColumnsRL (Proxy :: Proxy rl)
229459 "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
230460
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+
231466-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
232467-- INSERT SQL generation
233468-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -239,6 +474,8 @@ instance IsAutoGenerated (AutoIncrement a) where
239474 isAutoGenerated _ = true
240475else instance IsAutoGenerated (Default v a) where
241476 isAutoGenerated _ = true
477+else instance IsAutoGenerated (DefaultExpr expression a) where
478+ isAutoGenerated _ = true
242479else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
243480 isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
244481else instance IsAutoGenerated a => IsAutoGenerated (Unique a) where
@@ -282,15 +519,18 @@ instance
282519 insertSQLFor = do
283520 let tableName = reflectSymbol (Proxy :: Proxy name)
284521 let cols = insertColumnsRL (Proxy :: Proxy rl)
285− let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
286− "INSERT INTO " <> tableName
287− <> " ("
288− <> intercalate ", " cols
289− <> ")"
290− <> " VALUES ("
291− <> intercalate ", " placeholders
292− <> ")"
293− <> " 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 *"
294534
295535-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
296536-- SELECT SQL generation
@@ -413,6 +653,31 @@ class SkipSpacesGo head tail result | head tail -> result
413653instance SkipSpaces tail result => SkipSpacesGo " " tail result
414654else instance Symbol.Cons head tail result => SkipSpacesGo head tail result
415655
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+
416681-- Extract the first word from a Symbol (up to space or end)
417682class ExtractWord :: Symbol -> Symbol -> Symbol -> Constraint
418683class ExtractWord sym word rest | sym -> word rest
@@ -720,6 +985,15 @@ else instance
720985 ) =>
721986 ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
722987
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+
723997class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
724998class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
725999
@@ -763,25 +1037,50 @@ instance
7631037
7641038else instance
7651039 ( Symbol.Cons h t tail
766− , ExtractUntilParenGo h t "" args afterParen
1040+ , ExtractUntilParenGo h t "" Z args afterParen
7671041 ) =>
7681042 ExtractUntilParen tail args afterParen
7691043
770−class ExtractUntilParenGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
771−class ExtractUntilParenGo head tail acc args afterParen | head tail acc -> args afterParen
1044+-- Peano depth counter for nested parentheses
1045+data Z
7721046
773−instance ExtractUntilParenGo ")" tail acc acc tail
1047+data S :: Type -> Type
1048+data S n
1049+
1050+class ExtractUntilParenGo :: Symbol -> Symbol -> Symbol -> Type -> Symbol -> Symbol -> Constraint
1051+class ExtractUntilParenGo head tail acc depth args afterParen | head tail acc depth -> args afterParen
1052+
1053+-- ) at depth zero: done
1054+instance ExtractUntilParenGo ")" tail acc Z acc tail
1055+
1056+-- ) at depth > 0: decrement depth, keep going
1057+else instance
1058+ ( Symbol.Append acc ")" acc'
1059+ , Symbol.Cons nextH nextT tail
1060+ , ExtractUntilParenGo nextH nextT acc' n args afterParen
1061+ ) =>
1062+ ExtractUntilParenGo ")" tail acc (S n) args afterParen
7741063
1064+-- ( : increment depth, keep going
1065+else instance
1066+ ( Symbol.Append acc "(" acc'
1067+ , Symbol.Cons nextH nextT tail
1068+ , ExtractUntilParenGo nextH nextT acc' (S depth) args afterParen
1069+ ) =>
1070+ ExtractUntilParenGo "(" tail acc depth args afterParen
1071+
1072+-- Unclosed paren at end of input
7751073else instance
7761074 Fail (Text "Unclosed parenthesis in SELECT clause") =>
777− ExtractUntilParenGo h "" acc args afterParen
1075+ ExtractUntilParenGo h "" acc depth args afterParen
7781076
1077+-- Any other char: accumulate
7791078else instance
7801079 ( Symbol.Append acc h acc'
7811080 , Symbol.Cons nextH nextT tail
782− , ExtractUntilParenGo nextH nextT acc' args afterParen
1081+ , ExtractUntilParenGo nextH nextT acc' depth args afterParen
7831082 ) =>
784− ExtractUntilParenGo h tail acc args afterParen
1083+ ExtractUntilParenGo h tail acc depth args afterParen
7851084
7861085-- Resolve aggregate argument: "*" -> Star, column ref -> unwrapped type
7871086data Star
@@ -793,11 +1092,66 @@ instance ResolveAggregateArg "*" tables Star
7931092else instance ResolveAggregateArg "" tables Star
7941093else instance
7951094 ( SkipSpaces args trimmedFront
796− , ExtractWord trimmedFront col _rest
797− , ResolveAggregateArgCol col tables argType
1095+ , ExtractIdent trimmedFront col rest
1096+ , ResolveAggregateArgDispatch col rest tables argType
7981097 ) =>
7991098 ResolveAggregateArg args tables argType
8001099
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+
8011155class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
8021156class ResolveAggregateArgCol col tables argType | col tables -> argType
8031157
@@ -831,47 +1185,146 @@ else instance
8311185 ResolveAggregateArgColByHead head col tables unwrapped
8321186
8331187-- Map (funcName, argType) -> returnType
1188+-- Dispatches on first character to avoid linear search
8341189class AggregateReturnType :: Symbol -> Type -> Type -> Constraint
8351190class AggregateReturnType funcName argType returnType | funcName argType -> returnType
8361191
837−instance AggregateReturnType "COUNT" argType Int
838−else instance AggregateReturnType "count" argType Int
839−else instance AggregateReturnType "SUM" argType argType
840−else instance AggregateReturnType "sum" argType argType
841−else instance AggregateReturnType "AVG" argType Number
842−else instance AggregateReturnType "avg" argType Number
843−else instance AggregateReturnType "MIN" argType argType
844−else instance AggregateReturnType "min" argType argType
845−else instance AggregateReturnType "MAX" argType argType
846−else instance AggregateReturnType "max" argType argType
847−else instance AggregateReturnType "ARRAY_AGG" argType (Array argType)
848−else instance AggregateReturnType "array_agg" argType (Array argType)
849−else instance AggregateReturnType "STRING_AGG" argType String
850−else instance AggregateReturnType "string_agg" argType String
851−else instance AggregateReturnType "COALESCE" argType argType
852−else instance AggregateReturnType "coalesce" argType argType
853−-- Window functions
854−else instance AggregateReturnType "ROW_NUMBER" argType Int
855−else instance AggregateReturnType "row_number" argType Int
856−else instance AggregateReturnType "RANK" argType Int
857−else instance AggregateReturnType "rank" argType Int
858−else instance AggregateReturnType "DENSE_RANK" argType Int
859−else instance AggregateReturnType "dense_rank" argType Int
860−else instance AggregateReturnType "NTILE" argType Int
861−else instance AggregateReturnType "ntile" argType Int
862−else instance AggregateReturnType "LAG" argType argType
863−else instance AggregateReturnType "lag" argType argType
864−else instance AggregateReturnType "LEAD" argType argType
865−else instance AggregateReturnType "lead" argType argType
866−else instance AggregateReturnType "FIRST_VALUE" argType argType
867−else instance AggregateReturnType "first_value" argType argType
868−else instance AggregateReturnType "LAST_VALUE" argType argType
869−else instance AggregateReturnType "last_value" argType argType
870−else instance AggregateReturnType "NTH_VALUE" argType argType
871−else instance AggregateReturnType "nth_value" argType argType
1192+instance
1193+ ( Symbol.Cons head rest funcName
1194+ , AggregateReturnTypeByHead head funcName argType returnType
1195+ ) =>
1196+ AggregateReturnType funcName argType returnType
1197+
1198+class AggregateReturnTypeByHead :: Symbol -> Symbol -> Type -> Type -> Constraint
1199+class AggregateReturnTypeByHead head funcName argType returnType | head funcName argType -> returnType
1200+
1201+instance AggregateReturnTypeA funcName argType returnType => AggregateReturnTypeByHead "A" funcName argType returnType
1202+else instance AggregateReturnTypeA funcName argType returnType => AggregateReturnTypeByHead "a" funcName argType returnType
1203+else instance AggregateReturnTypeC funcName argType returnType => AggregateReturnTypeByHead "C" funcName argType returnType
1204+else instance AggregateReturnTypeC funcName argType returnType => AggregateReturnTypeByHead "c" funcName argType returnType
1205+else instance AggregateReturnTypeD funcName argType returnType => AggregateReturnTypeByHead "D" funcName argType returnType
1206+else instance AggregateReturnTypeD funcName argType returnType => AggregateReturnTypeByHead "d" funcName argType returnType
1207+else instance AggregateReturnTypeF funcName argType returnType => AggregateReturnTypeByHead "F" funcName argType returnType
1208+else instance AggregateReturnTypeF funcName argType returnType => AggregateReturnTypeByHead "f" funcName argType returnType
1209+else instance AggregateReturnTypeL funcName argType returnType => AggregateReturnTypeByHead "L" funcName argType returnType
1210+else instance AggregateReturnTypeL funcName argType returnType => AggregateReturnTypeByHead "l" funcName argType returnType
1211+else instance AggregateReturnTypeM funcName argType returnType => AggregateReturnTypeByHead "M" funcName argType returnType
1212+else instance AggregateReturnTypeM funcName argType returnType => AggregateReturnTypeByHead "m" funcName argType returnType
1213+else instance AggregateReturnTypeN funcName argType returnType => AggregateReturnTypeByHead "N" funcName argType returnType
1214+else instance AggregateReturnTypeN funcName argType returnType => AggregateReturnTypeByHead "n" funcName argType returnType
1215+else instance AggregateReturnTypeR funcName argType returnType => AggregateReturnTypeByHead "R" funcName argType returnType
1216+else instance AggregateReturnTypeR funcName argType returnType => AggregateReturnTypeByHead "r" funcName argType returnType
1217+else instance AggregateReturnTypeS funcName argType returnType => AggregateReturnTypeByHead "S" funcName argType returnType
1218+else instance AggregateReturnTypeS funcName argType returnType => AggregateReturnTypeByHead "s" funcName argType returnType
8721219else instance
8731220 Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
874− AggregateReturnType funcName argType returnType
1221+ AggregateReturnTypeByHead head funcName argType returnType
1222+
1223+-- A: AVG, ARRAY_AGG
1224+class AggregateReturnTypeA :: Symbol -> Type -> Type -> Constraint
1225+class AggregateReturnTypeA funcName argType returnType | funcName argType -> returnType
1226+
1227+instance AggregateReturnTypeA "AVG" argType Number
1228+else instance AggregateReturnTypeA "avg" argType Number
1229+else instance AggregateReturnTypeA "ARRAY_AGG" argType (Array argType)
1230+else instance AggregateReturnTypeA "array_agg" argType (Array argType)
1231+else instance
1232+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1233+ AggregateReturnTypeA funcName argType returnType
1234+
1235+-- C: COUNT, COALESCE
1236+class AggregateReturnTypeC :: Symbol -> Type -> Type -> Constraint
1237+class AggregateReturnTypeC funcName argType returnType | funcName argType -> returnType
1238+
1239+instance AggregateReturnTypeC "COUNT" argType BigInt
1240+else instance AggregateReturnTypeC "count" argType BigInt
1241+else instance AggregateReturnTypeC "COALESCE" argType argType
1242+else instance AggregateReturnTypeC "coalesce" argType argType
1243+else instance
1244+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1245+ AggregateReturnTypeC funcName argType returnType
1246+
1247+-- D: DENSE_RANK
1248+class AggregateReturnTypeD :: Symbol -> Type -> Type -> Constraint
1249+class AggregateReturnTypeD funcName argType returnType | funcName argType -> returnType
1250+
1251+instance AggregateReturnTypeD "DENSE_RANK" argType Int
1252+else instance AggregateReturnTypeD "dense_rank" argType Int
1253+else instance
1254+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1255+ AggregateReturnTypeD funcName argType returnType
1256+
1257+-- F: FIRST_VALUE
1258+class AggregateReturnTypeF :: Symbol -> Type -> Type -> Constraint
1259+class AggregateReturnTypeF funcName argType returnType | funcName argType -> returnType
1260+
1261+instance AggregateReturnTypeF "FIRST_VALUE" argType argType
1262+else instance AggregateReturnTypeF "first_value" argType argType
1263+else instance
1264+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1265+ AggregateReturnTypeF funcName argType returnType
1266+
1267+-- L: LAG, LEAD, LAST_VALUE
1268+class AggregateReturnTypeL :: Symbol -> Type -> Type -> Constraint
1269+class AggregateReturnTypeL funcName argType returnType | funcName argType -> returnType
1270+
1271+instance AggregateReturnTypeL "LAG" argType argType
1272+else instance AggregateReturnTypeL "lag" argType argType
1273+else instance AggregateReturnTypeL "LEAD" argType argType
1274+else instance AggregateReturnTypeL "lead" argType argType
1275+else instance AggregateReturnTypeL "LAST_VALUE" argType argType
1276+else instance AggregateReturnTypeL "last_value" argType argType
1277+else instance
1278+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1279+ AggregateReturnTypeL funcName argType returnType
1280+
1281+-- M: MIN, MAX
1282+class AggregateReturnTypeM :: Symbol -> Type -> Type -> Constraint
1283+class AggregateReturnTypeM funcName argType returnType | funcName argType -> returnType
1284+
1285+instance AggregateReturnTypeM "MIN" argType argType
1286+else instance AggregateReturnTypeM "min" argType argType
1287+else instance AggregateReturnTypeM "MAX" argType argType
1288+else instance AggregateReturnTypeM "max" argType argType
1289+else instance
1290+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1291+ AggregateReturnTypeM funcName argType returnType
1292+
1293+-- N: NTILE, NTH_VALUE
1294+class AggregateReturnTypeN :: Symbol -> Type -> Type -> Constraint
1295+class AggregateReturnTypeN funcName argType returnType | funcName argType -> returnType
1296+
1297+instance AggregateReturnTypeN "NTILE" argType Int
1298+else instance AggregateReturnTypeN "ntile" argType Int
1299+else instance AggregateReturnTypeN "NTH_VALUE" argType argType
1300+else instance AggregateReturnTypeN "nth_value" argType argType
1301+else instance
1302+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1303+ AggregateReturnTypeN funcName argType returnType
1304+
1305+-- R: ROW_NUMBER, RANK
1306+class AggregateReturnTypeR :: Symbol -> Type -> Type -> Constraint
1307+class AggregateReturnTypeR funcName argType returnType | funcName argType -> returnType
1308+
1309+instance AggregateReturnTypeR "ROW_NUMBER" argType Int
1310+else instance AggregateReturnTypeR "row_number" argType Int
1311+else instance AggregateReturnTypeR "RANK" argType Int
1312+else instance AggregateReturnTypeR "rank" argType Int
1313+else instance
1314+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1315+ AggregateReturnTypeR funcName argType returnType
1316+
1317+-- S: SUM, STRING_AGG
1318+class AggregateReturnTypeS :: Symbol -> Type -> Type -> Constraint
1319+class AggregateReturnTypeS funcName argType returnType | funcName argType -> returnType
1320+
1321+instance AggregateReturnTypeS "SUM" argType argType
1322+else instance AggregateReturnTypeS "sum" argType argType
1323+else instance AggregateReturnTypeS "STRING_AGG" argType String
1324+else instance AggregateReturnTypeS "string_agg" argType String
1325+else instance
1326+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1327+ AggregateReturnTypeS funcName argType returnType
8751328
8761329-- After aggregate ): require AS alias, then continue
8771330class ParseAfterAggregate :: Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
@@ -956,9 +1409,10 @@ else instance
9561409class ParseOverClauseByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
9571410class ParseOverClauseByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
9581411
959−-- Open paren: extract until close paren, skip content, continue to AS
1412+-- Open paren: extract until close paren, validate content, continue to AS
9601413instance
961− ( ExtractUntilParen tail _overContent afterParen
1414+ ( ExtractUntilParen tail overContent afterParen
1415+ , ValidateOverContent overContent tables
9621416 , SkipSpaces afterParen rest
9631417 , ParseAfterAggregate rest tables returnType accRL outRL
9641418 ) =>
@@ -968,6 +1422,153 @@ else instance
9681422 Fail (Text "Expected ( after OVER") =>
9691423 ParseOverClauseByHead h t tables returnType accRL outRL
9701424
1425+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1426+-- ValidateOverContent: validates PARTITION BY / ORDER BY inside OVER(...)
1427+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1428+
1429+class ValidateOverContent :: Symbol -> Row (Row Type) -> Constraint
1430+class ValidateOverContent content tables
1431+
1432+instance ValidateOverContent "" tables
1433+
1434+else instance
1435+ ( SkipSpaces content content'
1436+ , ValidateOverContentTrimmed content' tables
1437+ ) =>
1438+ ValidateOverContent content tables
1439+
1440+class ValidateOverContentTrimmed :: Symbol -> Row (Row Type) -> Constraint
1441+class ValidateOverContentTrimmed content tables
1442+
1443+instance ValidateOverContentTrimmed "" tables
1444+
1445+else instance
1446+ ( ExtractWord content keyword rest
1447+ , ValidateOverKeyword keyword rest tables
1448+ ) =>
1449+ ValidateOverContentTrimmed content tables
1450+
1451+class ValidateOverKeyword :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1452+class ValidateOverKeyword keyword rest tables
1453+
1454+instance
1455+ ( ExtractWord rest byWord afterBy
1456+ , AssertIsBy byWord
1457+ , ValidatePartitionByColumns afterBy tables
1458+ ) =>
1459+ ValidateOverKeyword "PARTITION" rest tables
1460+
1461+else instance
1462+ ( ExtractWord rest byWord afterBy
1463+ , AssertIsBy byWord
1464+ , ValidatePartitionByColumns afterBy tables
1465+ ) =>
1466+ ValidateOverKeyword "partition" rest tables
1467+
1468+else instance
1469+ ( ExtractWord rest byWord afterBy
1470+ , AssertIsBy byWord
1471+ , ValidateOrderBy afterBy tables
1472+ ) =>
1473+ ValidateOverKeyword "ORDER" rest tables
1474+
1475+else instance
1476+ ( ExtractWord rest byWord afterBy
1477+ , AssertIsBy byWord
1478+ , ValidateOrderBy afterBy tables
1479+ ) =>
1480+ ValidateOverKeyword "order" rest tables
1481+
1482+else instance
1483+ Fail (Text "Expected PARTITION BY or ORDER BY inside OVER clause") =>
1484+ ValidateOverKeyword keyword rest tables
1485+
1486+class AssertIsBy :: Symbol -> Constraint
1487+class AssertIsBy sym
1488+
1489+instance AssertIsBy "BY"
1490+else instance AssertIsBy "by"
1491+else instance Fail (Text "Expected BY after PARTITION or ORDER") => AssertIsBy sym
1492+
1493+-- ValidatePartitionByColumns: column list that terminates at ORDER keyword
1494+class ValidatePartitionByColumns :: Symbol -> Row (Row Type) -> Constraint
1495+class ValidatePartitionByColumns sym tables
1496+
1497+instance ValidatePartitionByColumns "" tables
1498+
1499+else instance
1500+ ( SkipSpaces sym sym'
1501+ , ValidatePartitionByColumnsTrimmed sym' tables
1502+ ) =>
1503+ ValidatePartitionByColumns sym tables
1504+
1505+class ValidatePartitionByColumnsTrimmed :: Symbol -> Row (Row Type) -> Constraint
1506+class ValidatePartitionByColumnsTrimmed sym tables
1507+
1508+instance ValidatePartitionByColumnsTrimmed "" tables
1509+
1510+else instance
1511+ ( Symbol.Cons h t sym
1512+ , ValidatePartitionByColumnsGo h t "" tables
1513+ ) =>
1514+ ValidatePartitionByColumnsTrimmed sym tables
1515+
1516+class ValidatePartitionByColumnsGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
1517+class ValidatePartitionByColumnsGo head tail acc tables
1518+
1519+-- Comma: flush column, continue
1520+instance
1521+ ( ResolveColumn acc tables typ
1522+ , SkipSpaces tail rest
1523+ , ValidatePartitionByColumnsTrimmed rest tables
1524+ ) =>
1525+ ValidatePartitionByColumnsGo "," tail acc tables
1526+
1527+-- Space: flush word, check if ORDER keyword or column
1528+else instance
1529+ ( SkipSpaces tail rest
1530+ , FlushPartitionWord acc rest tables
1531+ ) =>
1532+ ValidatePartitionByColumnsGo " " tail acc tables
1533+
1534+-- End of string: flush final column
1535+else instance
1536+ ( Symbol.Append acc h acc'
1537+ , ResolveColumn acc' tables typ
1538+ ) =>
1539+ ValidatePartitionByColumnsGo h "" acc tables
1540+
1541+-- Regular char: accumulate
1542+else instance
1543+ ( Symbol.Append acc h acc'
1544+ , Symbol.Cons nextH nextT tail
1545+ , ValidatePartitionByColumnsGo nextH nextT acc' tables
1546+ ) =>
1547+ ValidatePartitionByColumnsGo h tail acc tables
1548+
1549+class FlushPartitionWord :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1550+class FlushPartitionWord word rest tables
1551+
1552+instance
1553+ ( ExtractWord rest byWord afterBy
1554+ , AssertIsBy byWord
1555+ , ValidateOrderBy afterBy tables
1556+ ) =>
1557+ FlushPartitionWord "ORDER" rest tables
1558+
1559+else instance
1560+ ( ExtractWord rest byWord afterBy
1561+ , AssertIsBy byWord
1562+ , ValidateOrderBy afterBy tables
1563+ ) =>
1564+ FlushPartitionWord "order" rest tables
1565+
1566+else instance
1567+ ( ResolveColumn word tables typ
1568+ , ValidatePartitionByColumnsTrimmed rest tables
1569+ ) =>
1570+ FlushPartitionWord word rest tables
1571+
9711572-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
9721573-- ParseWhere: parse "id = $id AND age > $age" -> params
9731574-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -977,7 +1578,7 @@ data NoType
9771578class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
9781579class ParseWhere sym tables params | sym tables -> params
9791580
980−instance ParseWhere "" tables ()
1581+instance Fail (Text "Empty WHERE clause") => ParseWhere "" tables params
9811582else instance
9821583 ( Symbol.Cons h t sym
9831584 , ParseWhereGo h t "" NoType tables RL.Nil outRL
@@ -994,125 +1595,414 @@ instance
9941595 , SkipSpaces tail rest
9951596 , ParseWhereContinue rest currentType' tables paramsOut' paramsOut
9961597 ) =>
997− ParseWhereGo " " tail acc currentType tables paramsIn paramsOut
1598+ ParseWhereGo " " tail acc currentType tables paramsIn paramsOut
1599+
1600+-- Operators
1601+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "=" tail acc currentType tables paramsIn paramsOut
1602+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ">" tail acc currentType tables paramsIn paramsOut
1603+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "<" tail acc currentType tables paramsIn paramsOut
1604+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "!" tail acc currentType tables paramsIn paramsOut
1605+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "(" tail acc currentType tables paramsIn paramsOut
1606+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
1614+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "@" tail acc currentType tables paramsIn paramsOut
1615+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "?" tail acc currentType tables paramsIn paramsOut
1616+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ":" tail acc currentType tables paramsIn paramsOut
1617+else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "~" tail acc currentType tables paramsIn paramsOut
1618+else 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
1620+
1621+-- End of string: flush final word
1622+else instance
1623+ ( Symbol.Append acc h acc'
1624+ , FlushWhereWord acc' currentType tables paramsIn _ct paramsOut
1625+ ) =>
1626+ ParseWhereGo h "" acc currentType tables paramsIn paramsOut
1627+
1628+-- Regular char (including dot): accumulate
1629+else instance
1630+ ( Symbol.Append acc h acc'
1631+ , Symbol.Cons nextH nextT tail
1632+ , ParseWhereGo nextH nextT acc' currentType tables paramsIn paramsOut
1633+ ) =>
1634+ ParseWhereGo h tail acc currentType tables paramsIn paramsOut
1635+
1636+class ParseWhereContinue :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
1637+class ParseWhereContinue sym currentType tables paramsIn paramsOut | sym currentType tables paramsIn -> paramsOut
1638+
1639+instance ParseWhereContinue "" currentType tables paramsIn paramsIn
1640+else instance
1641+ ( Symbol.Cons h t sym
1642+ , ParseWhereGo h t "" currentType tables paramsIn paramsOut
1643+ ) =>
1644+ ParseWhereContinue sym currentType tables paramsIn paramsOut
1645+
1646+-- Flush a word in WHERE context
1647+-- Dispatches on first character to avoid linear search through all keywords
1648+class FlushWhereWord :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1649+class FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1650+
1651+instance FlushWhereWord "" currentType tables paramsIn currentType paramsIn
1652+-- Most common SQL keywords matched directly for fast resolution
1653+else instance FlushWhereWord "AND" currentType tables paramsIn currentType paramsIn
1654+else instance FlushWhereWord "and" currentType tables paramsIn currentType paramsIn
1655+else instance FlushWhereWord "OR" currentType tables paramsIn currentType paramsIn
1656+else instance FlushWhereWord "or" currentType tables paramsIn currentType paramsIn
1657+else instance FlushWhereWord "NOT" currentType tables paramsIn currentType paramsIn
1658+else instance FlushWhereWord "not" currentType tables paramsIn currentType paramsIn
1659+else instance FlushWhereWord "IS" currentType tables paramsIn currentType paramsIn
1660+else instance FlushWhereWord "is" currentType tables paramsIn currentType paramsIn
1661+else instance FlushWhereWord "NULL" currentType tables paramsIn currentType paramsIn
1662+else instance FlushWhereWord "null" currentType tables paramsIn currentType paramsIn
1663+-- Everything else: dispatch on first character
1664+else instance
1665+ ( Symbol.Cons head rest word
1666+ , FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut
1667+ ) =>
1668+ FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut
1669+
1670+class FlushWhereWordByHead :: Symbol -> Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1671+class FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut | head word currentType tables paramsIn -> currentTypeOut paramsOut
1672+
1673+-- $param: emit with currentType
1674+instance
1675+ ( Symbol.Cons "$" paramName word
1676+ ) =>
1677+ FlushWhereWordByHead "$" word currentType tables paramsIn currentType (RL.Cons paramName currentType paramsIn)
1678+
1679+-- Digit: number literal, pass through
1680+else instance FlushWhereWordByHead "0" word currentType tables paramsIn currentType paramsIn
1681+else instance FlushWhereWordByHead "1" word currentType tables paramsIn currentType paramsIn
1682+else instance FlushWhereWordByHead "2" word currentType tables paramsIn currentType paramsIn
1683+else instance FlushWhereWordByHead "3" word currentType tables paramsIn currentType paramsIn
1684+else instance FlushWhereWordByHead "4" word currentType tables paramsIn currentType paramsIn
1685+else instance FlushWhereWordByHead "5" word currentType tables paramsIn currentType paramsIn
1686+else instance FlushWhereWordByHead "6" word currentType tables paramsIn currentType paramsIn
1687+else instance FlushWhereWordByHead "7" word currentType tables paramsIn currentType paramsIn
1688+else instance FlushWhereWordByHead "8" word currentType tables paramsIn currentType paramsIn
1689+else instance FlushWhereWordByHead "9" word currentType tables paramsIn currentType paramsIn
1690+else instance FlushWhereWordByHead "*" word currentType tables paramsIn currentType paramsIn
1691+
1692+-- Letters with keywords: dispatch to per-letter helpers
1693+else instance FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "A" word currentType tables paramsIn currentTypeOut paramsOut
1694+else instance FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "a" word currentType tables paramsIn currentTypeOut paramsOut
1695+else instance FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "B" word currentType tables paramsIn currentTypeOut paramsOut
1696+else instance FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "b" word currentType tables paramsIn currentTypeOut paramsOut
1697+else instance FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "C" word currentType tables paramsIn currentTypeOut paramsOut
1698+else instance FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "c" word currentType tables paramsIn currentTypeOut paramsOut
1699+else instance FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "D" word currentType tables paramsIn currentTypeOut paramsOut
1700+else instance FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "d" word currentType tables paramsIn currentTypeOut paramsOut
1701+else instance FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "E" word currentType tables paramsIn currentTypeOut paramsOut
1702+else instance FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "e" word currentType tables paramsIn currentTypeOut paramsOut
1703+else instance FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "F" word currentType tables paramsIn currentTypeOut paramsOut
1704+else instance FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "f" word currentType tables paramsIn currentTypeOut paramsOut
1705+else instance FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "I" word currentType tables paramsIn currentTypeOut paramsOut
1706+else instance FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "i" word currentType tables paramsIn currentTypeOut paramsOut
1707+else instance FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "J" word currentType tables paramsIn currentTypeOut paramsOut
1708+else instance FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "j" word currentType tables paramsIn currentTypeOut paramsOut
1709+else instance FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "L" word currentType tables paramsIn currentTypeOut paramsOut
1710+else instance FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "l" word currentType tables paramsIn currentTypeOut paramsOut
1711+else instance FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "M" word currentType tables paramsIn currentTypeOut paramsOut
1712+else instance FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "m" word currentType tables paramsIn currentTypeOut paramsOut
1713+else instance FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "N" word currentType tables paramsIn currentTypeOut paramsOut
1714+else instance FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "n" word currentType tables paramsIn currentTypeOut paramsOut
1715+else instance FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "S" word currentType tables paramsIn currentTypeOut paramsOut
1716+else instance FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "s" word currentType tables paramsIn currentTypeOut paramsOut
1717+else instance FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "T" word currentType tables paramsIn currentTypeOut paramsOut
1718+else instance FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "t" word currentType tables paramsIn currentTypeOut paramsOut
1719+else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "U" word currentType tables paramsIn currentTypeOut paramsOut
1720+else instance FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "u" word currentType tables paramsIn currentTypeOut paramsOut
1721+else instance FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut => FlushWhereWordByHead "V" word currentType tables paramsIn currentTypeOut paramsOut
1722+else 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
1727+
1728+-- Catch-all: column reference (letters without keywords like g, h, q, w, x, y, z)
1729+else instance
1730+ ( ResolveColumn word tables entry
1731+ , ExtractType entry typ
1732+ , UnwrapMaybe typ unwrapped
1733+ ) =>
1734+ FlushWhereWordByHead head word currentType tables paramsIn unwrapped paramsIn
1735+
1736+-- Per-letter keyword helpers
1737+
1738+class FlushWhereWordA :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1739+class FlushWhereWordA word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1740+
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
1744+else instance FlushWhereWordA "any" currentType tables paramsIn (Array currentType) paramsIn
1745+else instance FlushWhereWordA "ALL" currentType tables paramsIn (Array currentType) paramsIn
1746+else instance FlushWhereWordA "all" currentType tables paramsIn (Array currentType) paramsIn
1747+else instance FlushWhereWordA "AS" currentType tables paramsIn currentType paramsIn
1748+else instance FlushWhereWordA "as" currentType tables paramsIn currentType paramsIn
1749+else instance FlushWhereWordA "AVG" currentType tables paramsIn Number paramsIn
1750+else instance FlushWhereWordA "avg" currentType tables paramsIn Number paramsIn
1751+else instance FlushWhereWordA "ARRAY_AGG" currentType tables paramsIn currentType paramsIn
1752+else instance FlushWhereWordA "array_agg" currentType tables paramsIn currentType paramsIn
1753+else instance
1754+ ( ResolveColumn word tables entry
1755+ , ExtractType entry typ
1756+ , UnwrapMaybe typ unwrapped
1757+ ) =>
1758+ FlushWhereWordA word currentType tables paramsIn unwrapped paramsIn
1759+
1760+class FlushWhereWordB :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1761+class FlushWhereWordB word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1762+
1763+instance FlushWhereWordB "BETWEEN" currentType tables paramsIn currentType paramsIn
1764+else instance FlushWhereWordB "between" currentType tables paramsIn currentType paramsIn
1765+else instance FlushWhereWordB "bigint" currentType tables paramsIn currentType paramsIn
1766+else instance FlushWhereWordB "boolean" currentType tables paramsIn currentType paramsIn
1767+else instance
1768+ ( ResolveColumn word tables entry
1769+ , ExtractType entry typ
1770+ , UnwrapMaybe typ unwrapped
1771+ ) =>
1772+ FlushWhereWordB word currentType tables paramsIn unwrapped paramsIn
1773+
1774+class FlushWhereWordC :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1775+class FlushWhereWordC word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1776+
1777+instance FlushWhereWordC "CAST" currentType tables paramsIn currentType paramsIn
1778+else 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
1785+else instance FlushWhereWordC "COUNT" currentType tables paramsIn Int paramsIn
1786+else 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
1791+else instance
1792+ ( ResolveColumn word tables entry
1793+ , ExtractType entry typ
1794+ , UnwrapMaybe typ unwrapped
1795+ ) =>
1796+ FlushWhereWordC word currentType tables paramsIn unwrapped paramsIn
1797+
1798+class FlushWhereWordD :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1799+class FlushWhereWordD word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1800+
1801+instance FlushWhereWordD "DATE" currentType tables paramsIn currentType paramsIn
1802+else instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1803+else instance
1804+ ( ResolveColumn word tables entry
1805+ , ExtractType entry typ
1806+ , UnwrapMaybe typ unwrapped
1807+ ) =>
1808+ FlushWhereWordD word currentType tables paramsIn unwrapped paramsIn
1809+
1810+class FlushWhereWordE :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1811+class FlushWhereWordE word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1812+
1813+instance FlushWhereWordE "EXISTS" currentType tables paramsIn currentType paramsIn
1814+else instance FlushWhereWordE "exists" currentType tables paramsIn currentType paramsIn
1815+else instance
1816+ ( ResolveColumn word tables entry
1817+ , ExtractType entry typ
1818+ , UnwrapMaybe typ unwrapped
1819+ ) =>
1820+ FlushWhereWordE word currentType tables paramsIn unwrapped paramsIn
1821+
1822+class FlushWhereWordF :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1823+class FlushWhereWordF word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1824+
1825+instance FlushWhereWordF "FALSE" currentType tables paramsIn currentType paramsIn
1826+else 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
1829+else instance
1830+ ( ResolveColumn word tables entry
1831+ , ExtractType entry typ
1832+ , UnwrapMaybe typ unwrapped
1833+ ) =>
1834+ FlushWhereWordF word currentType tables paramsIn unwrapped paramsIn
1835+
1836+class FlushWhereWordI :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1837+class FlushWhereWordI word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1838+
1839+instance FlushWhereWordI "IN" currentType tables paramsIn (Array currentType) paramsIn
1840+else instance FlushWhereWordI "in" currentType tables paramsIn (Array currentType) paramsIn
1841+else instance FlushWhereWordI "ILIKE" currentType tables paramsIn currentType paramsIn
1842+else instance FlushWhereWordI "ilike" currentType tables paramsIn currentType paramsIn
1843+else instance FlushWhereWordI "integer" currentType tables paramsIn currentType paramsIn
1844+else instance FlushWhereWordI "int" currentType tables paramsIn currentType paramsIn
1845+else instance
1846+ ( ResolveColumn word tables entry
1847+ , ExtractType entry typ
1848+ , UnwrapMaybe typ unwrapped
1849+ ) =>
1850+ FlushWhereWordI word currentType tables paramsIn unwrapped paramsIn
1851+
1852+class FlushWhereWordJ :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1853+class FlushWhereWordJ word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1854+
1855+instance FlushWhereWordJ "jsonb" currentType tables paramsIn currentType paramsIn
1856+else instance FlushWhereWordJ "json" currentType tables paramsIn currentType paramsIn
1857+else instance
1858+ ( ResolveColumn word tables entry
1859+ , ExtractType entry typ
1860+ , UnwrapMaybe typ unwrapped
1861+ ) =>
1862+ FlushWhereWordJ word currentType tables paramsIn unwrapped paramsIn
1863+
1864+class FlushWhereWordL :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1865+class FlushWhereWordL word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1866+
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
1872+else 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
1875+else instance
1876+ ( ResolveColumn word tables entry
1877+ , ExtractType entry typ
1878+ , UnwrapMaybe typ unwrapped
1879+ ) =>
1880+ FlushWhereWordL word currentType tables paramsIn unwrapped paramsIn
1881+
1882+class FlushWhereWordM :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1883+class FlushWhereWordM word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1884+
1885+instance FlushWhereWordM "MIN" currentType tables paramsIn currentType paramsIn
1886+else instance FlushWhereWordM "min" currentType tables paramsIn currentType paramsIn
1887+else instance FlushWhereWordM "MAX" currentType tables paramsIn currentType paramsIn
1888+else instance FlushWhereWordM "max" currentType tables paramsIn currentType paramsIn
1889+else instance
1890+ ( ResolveColumn word tables entry
1891+ , ExtractType entry typ
1892+ , UnwrapMaybe typ unwrapped
1893+ ) =>
1894+ FlushWhereWordM word currentType tables paramsIn unwrapped paramsIn
9981895
999−-- Operators
1000−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "=" tail acc currentType tables paramsIn paramsOut
1001−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ">" tail acc currentType tables paramsIn paramsOut
1002−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "<" tail acc currentType tables paramsIn paramsOut
1003−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "!" tail acc currentType tables paramsIn paramsOut
1004−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "(" tail acc currentType tables paramsIn paramsOut
1005−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ")" tail acc currentType tables paramsIn paramsOut
1006−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "'" tail acc currentType tables paramsIn paramsOut
1007−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "@" tail acc currentType tables paramsIn paramsOut
1008−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "?" tail acc currentType tables paramsIn paramsOut
1009−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo ":" tail acc currentType tables paramsIn paramsOut
1010−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "~" tail acc currentType tables paramsIn paramsOut
1011−else instance (FlushWhereWord acc currentType tables paramsIn currentType' paramsOut', ParseWhereContinue tail currentType' tables paramsOut' paramsOut) => ParseWhereGo "#" tail acc currentType tables paramsIn paramsOut
1896+class FlushWhereWordN :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1897+class FlushWhereWordN word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
10121898
1013−-- End of string: flush final word
1899+instance FlushWhereWordN "NOT" currentType tables paramsIn currentType paramsIn -- unreachable (caught by FlushWhereWord directly) but included for self-containedness
1900+else 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
1903+else instance FlushWhereWordN "NULL" currentType tables paramsIn currentType paramsIn
1904+else 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
10141907else instance
1015− ( Symbol.Append acc h acc'
1016− , FlushWhereWord acc' currentType tables paramsIn _ct paramsOut
1908+ ( ResolveColumn word tables entry
1909+ , ExtractType entry typ
1910+ , UnwrapMaybe typ unwrapped
10171911 ) =>
1018− ParseWhereGo h "" acc currentType tables paramsIn paramsOut
1912+ FlushWhereWordN word currentType tables paramsIn unwrapped paramsIn
10191913
1020−-- Regular char (including dot): accumulate
1914+class FlushWhereWordS :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1915+class FlushWhereWordS word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1916+
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
1920+else instance FlushWhereWordS "sum" currentType tables paramsIn currentType paramsIn
1921+else instance FlushWhereWordS "STRING_AGG" currentType tables paramsIn String paramsIn
1922+else instance FlushWhereWordS "string_agg" currentType tables paramsIn String paramsIn
10211923else instance
1022− ( Symbol.Append acc h acc'
1023− , Symbol.Cons nextH nextT tail
1024− , ParseWhereGo nextH nextT acc' currentType tables paramsIn paramsOut
1924+ ( ResolveColumn word tables entry
1925+ , ExtractType entry typ
1926+ , UnwrapMaybe typ unwrapped
10251927 ) =>
1026− ParseWhereGo h tail acc currentType tables paramsIn paramsOut
1928+ FlushWhereWordS word currentType tables paramsIn unwrapped paramsIn
1929+
1930+class FlushWhereWordT :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1931+class FlushWhereWordT word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1932+
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
1936+else instance FlushWhereWordT "true" currentType tables paramsIn currentType paramsIn
1937+else instance FlushWhereWordT "text" currentType tables paramsIn currentType paramsIn
1938+else instance FlushWhereWordT "timestamptz" currentType tables paramsIn currentType paramsIn
1939+else 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
1950+else instance
1951+ ( ResolveColumn word tables entry
1952+ , ExtractType entry typ
1953+ , UnwrapMaybe typ unwrapped
1954+ ) =>
1955+ FlushWhereWordT word currentType tables paramsIn unwrapped paramsIn
10271956
1028−class ParseWhereContinue :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
1029−class ParseWhereContinue sym currentType tables paramsIn paramsOut | sym currentType tables paramsIn -> paramsOut
1957+class FlushWhereWordU :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1958+class FlushWhereWordU word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
10301959
1031−instance ParseWhereContinue "" currentType tables paramsIn 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
10321963else instance
1033− ( Symbol.Cons h t sym
1034− , ParseWhereGo h t "" currentType tables paramsIn paramsOut
1964+ ( ResolveColumn word tables entry
1965+ , ExtractType entry typ
1966+ , UnwrapMaybe typ unwrapped
10351967 ) =>
1036− ParseWhereContinue sym currentType tables paramsIn paramsOut
1968+ FlushWhereWordU word currentType tables paramsIn unwrapped paramsIn
10371969
1038−-- Flush a word in WHERE context
1039−class FlushWhereWord :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1040−class FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1970+class FlushWhereWordV :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1971+class FlushWhereWordV word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
10411972
1042−instance FlushWhereWord "" currentType tables paramsIn currentType paramsIn
1043−else instance FlushWhereWord "AND" currentType tables paramsIn currentType paramsIn
1044−else instance FlushWhereWord "OR" currentType tables paramsIn currentType paramsIn
1045−else instance FlushWhereWord "NOT" currentType tables paramsIn currentType paramsIn
1046−else instance FlushWhereWord "IS" currentType tables paramsIn currentType paramsIn
1047−else instance FlushWhereWord "NULL" currentType tables paramsIn currentType paramsIn
1048−else instance FlushWhereWord "LIKE" currentType tables paramsIn currentType paramsIn
1049−else instance FlushWhereWord "ILIKE" currentType tables paramsIn currentType paramsIn
1050−else instance FlushWhereWord "IN" currentType tables paramsIn (Array currentType) paramsIn
1051−else instance FlushWhereWord "TRUE" currentType tables paramsIn currentType paramsIn
1052−else instance FlushWhereWord "FALSE" currentType tables paramsIn currentType paramsIn
1053−else instance FlushWhereWord "BETWEEN" currentType tables paramsIn currentType paramsIn
1054−else instance FlushWhereWord "ANY" currentType tables paramsIn (Array currentType) paramsIn
1055−else instance FlushWhereWord "ALL" currentType tables paramsIn (Array currentType) paramsIn
1056−else instance FlushWhereWord "CAST" currentType tables paramsIn currentType paramsIn
1057−else instance FlushWhereWord "AS" currentType tables paramsIn currentType paramsIn
1058−else instance FlushWhereWord "EXISTS" currentType tables paramsIn currentType paramsIn
1059−-- Aggregate functions (for HAVING support)
1060−else instance FlushWhereWord "COUNT" currentType tables paramsIn Int paramsIn
1061−else instance FlushWhereWord "SUM" currentType tables paramsIn currentType paramsIn
1062−else instance FlushWhereWord "AVG" currentType tables paramsIn Number paramsIn
1063−else instance FlushWhereWord "MIN" currentType tables paramsIn currentType paramsIn
1064−else instance FlushWhereWord "MAX" currentType tables paramsIn currentType paramsIn
1065−else instance FlushWhereWord "ARRAY_AGG" currentType tables paramsIn currentType paramsIn
1066−else instance FlushWhereWord "STRING_AGG" currentType tables paramsIn String paramsIn
1067−else instance FlushWhereWord "*" currentType tables paramsIn currentType paramsIn
1068−-- Postgres type names (for ::type casts)
1069−else instance FlushWhereWord "text" currentType tables paramsIn currentType paramsIn
1070−else instance FlushWhereWord "integer" currentType tables paramsIn currentType paramsIn
1071−else instance FlushWhereWord "bigint" currentType tables paramsIn currentType paramsIn
1072−else instance FlushWhereWord "boolean" currentType tables paramsIn currentType paramsIn
1073−else instance FlushWhereWord "jsonb" currentType tables paramsIn currentType paramsIn
1074−else instance FlushWhereWord "json" currentType tables paramsIn currentType paramsIn
1075−else instance FlushWhereWord "timestamptz" currentType tables paramsIn currentType paramsIn
1076−else instance FlushWhereWord "timestamp" currentType tables paramsIn currentType paramsIn
1077−else instance FlushWhereWord "date" currentType tables paramsIn currentType paramsIn
1078−else instance FlushWhereWord "int" currentType tables paramsIn currentType paramsIn
1079−else instance FlushWhereWord "varchar" currentType tables paramsIn currentType paramsIn
1080−else instance FlushWhereWord "uuid" currentType tables paramsIn currentType paramsIn
1081−-- Non-keyword: check first char
1973+instance FlushWhereWordV "varchar" currentType tables paramsIn currentType paramsIn
10821974else instance
1083− ( Symbol.Cons head rest word
1084− , FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut
1975+ ( ResolveColumn word tables entry
1976+ , ExtractType entry typ
1977+ , UnwrapMaybe typ unwrapped
10851978 ) =>
1086− FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut
1979+ FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
10871980
1088−class FlushWhereWordByHead :: Symbol -> Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1089−class FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut | head word currentType tables paramsIn -> currentTypeOut paramsOut
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
10901983
1091−-- $param: emit with currentType
1092−instance
1093− ( Symbol.Cons "$" paramName word
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
10941992 ) =>
1095− FlushWhereWordByHead "$" word currentType tables paramsIn currentType (RL.Cons paramName currentType paramsIn)
1993+ FlushWhereWordP word currentType tables paramsIn unwrapped paramsIn
10961994
1097−-- Digit: number literal, pass through
1098−else instance FlushWhereWordByHead "0" word currentType tables paramsIn currentType paramsIn
1099−else instance FlushWhereWordByHead "1" word currentType tables paramsIn currentType paramsIn
1100−else instance FlushWhereWordByHead "2" word currentType tables paramsIn currentType paramsIn
1101−else instance FlushWhereWordByHead "3" word currentType tables paramsIn currentType paramsIn
1102−else instance FlushWhereWordByHead "4" word currentType tables paramsIn currentType paramsIn
1103−else instance FlushWhereWordByHead "5" word currentType tables paramsIn currentType paramsIn
1104−else instance FlushWhereWordByHead "6" word currentType tables paramsIn currentType paramsIn
1105−else instance FlushWhereWordByHead "7" word currentType tables paramsIn currentType paramsIn
1106−else instance FlushWhereWordByHead "8" word currentType tables paramsIn currentType paramsIn
1107−else instance FlushWhereWordByHead "9" word currentType tables paramsIn currentType 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
11081997
1109−-- Column reference: resolve and set as currentType
1998+instance FlushWhereWordR "real" currentType tables paramsIn currentType paramsIn
1999+else instance FlushWhereWordR "REAL" currentType tables paramsIn currentType paramsIn
11102000else instance
11112001 ( ResolveColumn word tables entry
11122002 , ExtractType entry typ
11132003 , UnwrapMaybe typ unwrapped
11142004 ) =>
1115− FlushWhereWordByHead head word currentType tables paramsIn unwrapped paramsIn
2005+ FlushWhereWordR word currentType tables paramsIn unwrapped paramsIn
11162006
11172007-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
11182008-- ValidateColumnList: comma-separated column references
@@ -1186,11 +2076,16 @@ else instance
11862076class ValidateOrderByGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
11872077class ValidateOrderByGo head tail acc tables
11882078
1189−-- Comma: flush column, continue
2079+-- Leading/double comma: no column before comma
11902080instance
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
11912086 ( FlushOrderByWord acc tables
11922087 , SkipSpaces tail rest
1193− , ValidateOrderBy rest tables
2088+ , ValidateOrderByContinueNonEmpty rest tables
11942089 ) =>
11952090 ValidateOrderByGo "," tail acc tables
11962091
@@ -1204,7 +2099,7 @@ else instance
12042099-- End of string: flush final column
12052100else instance
12062101 ( Symbol.Append acc h acc'
1207− , FlushOrderByWord acc' tables
2102+ , FlushOrderByFinalWord acc' tables
12082103 ) =>
12092104 ValidateOrderByGo h "" acc tables
12102105
@@ -1216,6 +2111,31 @@ else instance
12162111 ) =>
12172112 ValidateOrderByGo h tail acc tables
12182113
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+
12192139class FlushOrderByWord :: Symbol -> Row (Row Type) -> Constraint
12202140class FlushOrderByWord word tables
12212141
@@ -1288,6 +2208,177 @@ else instance
12882208 ) =>
12892209 ValidateOrderByContinueByHead h t tables
12902210
2211+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2212+-- RowListHas: check if a label exists in a RowList
2213+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2214+
2215+class RowListHas :: Symbol -> RL.RowList Type -> Boolean -> Constraint
2216+class RowListHas label rl has | label rl -> has
2217+
2218+instance RowListHas label RL.Nil False
2219+instance RowListHas label (RL.Cons label typ rest) True
2220+else instance RowListHas label rest has => RowListHas label (RL.Cons other typ rest) has
2221+
2222+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2223+-- CheckDistinctOrderBy: when DISTINCT is in stage, ORDER BY
2224+-- columns must appear in the SELECT result list
2225+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2226+
2227+class CheckDistinctOrderBy :: Row Type -> Symbol -> Row Type -> Constraint
2228+class CheckDistinctOrderBy stage cols result
2229+
2230+instance
2231+ ( RowToList stage stageRL
2232+ , RowListHas "distinct" stageRL isDistinct
2233+ , CheckDistinctOrderByBranch isDistinct cols result
2234+ ) =>
2235+ CheckDistinctOrderBy stage cols result
2236+
2237+class CheckDistinctOrderByBranch :: Boolean -> Symbol -> Row Type -> Constraint
2238+class CheckDistinctOrderByBranch isDistinct cols result
2239+
2240+instance OrderByColumnsInResult cols result => CheckDistinctOrderByBranch True cols result
2241+instance CheckDistinctOrderByBranch False cols result
2242+
2243+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2244+-- OrderByColumnsInResult: validate ORDER BY column refs exist in result row
2245+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2246+
2247+class OrderByColumnsInResult :: Symbol -> Row Type -> Constraint
2248+class OrderByColumnsInResult sym result
2249+
2250+instance OrderByColumnsInResult "" result
2251+else instance
2252+ ( Symbol.Cons h t sym
2253+ , OrderByColumnsInResultGo h t "" result
2254+ ) =>
2255+ OrderByColumnsInResult sym result
2256+
2257+class OrderByColumnsInResultGo :: Symbol -> Symbol -> Symbol -> Row Type -> Constraint
2258+class OrderByColumnsInResultGo head tail acc result
2259+
2260+-- Comma: flush column, continue
2261+instance
2262+ ( FlushOrderByWordInResult acc result
2263+ , SkipSpaces tail rest
2264+ , OrderByColumnsInResult rest result
2265+ ) =>
2266+ OrderByColumnsInResultGo "," tail acc result
2267+
2268+-- Space: flush word, skip modifiers
2269+else instance
2270+ ( SkipSpaces tail rest
2271+ , FlushOrderByThenSkipInResult acc rest result
2272+ ) =>
2273+ OrderByColumnsInResultGo " " tail acc result
2274+
2275+-- End of string: flush final column
2276+else instance
2277+ ( Symbol.Append acc h acc'
2278+ , FlushOrderByWordInResult acc' result
2279+ ) =>
2280+ OrderByColumnsInResultGo h "" acc result
2281+
2282+-- Regular char: accumulate
2283+else instance
2284+ ( Symbol.Append acc h acc'
2285+ , Symbol.Cons nextH nextT tail
2286+ , OrderByColumnsInResultGo nextH nextT acc' result
2287+ ) =>
2288+ OrderByColumnsInResultGo h tail acc result
2289+
2290+class FlushOrderByWordInResult :: Symbol -> Row Type -> Constraint
2291+class FlushOrderByWordInResult word result
2292+
2293+instance FlushOrderByWordInResult "" result
2294+else instance FlushOrderByWordInResult "ASC" result
2295+else instance FlushOrderByWordInResult "asc" result
2296+else instance FlushOrderByWordInResult "DESC" result
2297+else instance FlushOrderByWordInResult "desc" result
2298+else instance FlushOrderByWordInResult "NULLS" result
2299+else instance FlushOrderByWordInResult "FIRST" result
2300+else instance FlushOrderByWordInResult "LAST" result
2301+else instance CheckColumnInResult word result => FlushOrderByWordInResult word result
2302+
2303+class CheckColumnInResult :: Symbol -> Row Type -> Constraint
2304+class CheckColumnInResult col result
2305+
2306+instance
2307+ ( RowToList result rl
2308+ , CheckColumnInResultRL col rl
2309+ ) =>
2310+ CheckColumnInResult col result
2311+
2312+class CheckColumnInResultRL :: Symbol -> RL.RowList Type -> Constraint
2313+class CheckColumnInResultRL col rl
2314+
2315+instance
2316+ Fail (Beside (Text "ORDER BY column ") (Beside (Quote col) (Text " must appear in the SELECT list when using DISTINCT"))) =>
2317+ CheckColumnInResultRL col RL.Nil
2318+
2319+instance CheckColumnInResultRL col (RL.Cons col typ tail)
2320+
2321+else instance CheckColumnInResultRL col tail => CheckColumnInResultRL col (RL.Cons name typ tail)
2322+
2323+class FlushOrderByThenSkipInResult :: Symbol -> Symbol -> Row Type -> Constraint
2324+class FlushOrderByThenSkipInResult colName rest result
2325+
2326+instance FlushOrderByWordInResult colName result => FlushOrderByThenSkipInResult colName "" result
2327+else instance
2328+ ( FlushOrderByWordInResult colName result
2329+ , Symbol.Cons h t rest
2330+ , FlushOrderByThenSkipByHeadInResult h t result
2331+ ) =>
2332+ FlushOrderByThenSkipInResult colName rest result
2333+
2334+class FlushOrderByThenSkipByHeadInResult :: Symbol -> Symbol -> Row Type -> Constraint
2335+class FlushOrderByThenSkipByHeadInResult head tail result
2336+
2337+-- Comma: continue with next column
2338+instance
2339+ ( SkipSpaces tail rest
2340+ , OrderByColumnsInResult rest result
2341+ ) =>
2342+ FlushOrderByThenSkipByHeadInResult "," tail result
2343+
2344+-- Modifier word: consume it, continue
2345+else instance
2346+ ( Symbol.Append h t rest
2347+ , ExtractWord rest word afterWord
2348+ , FlushOrderByWordInResult word result
2349+ , SkipSpaces afterWord rest'
2350+ , OrderByColumnsInResultContinue rest' result
2351+ ) =>
2352+ FlushOrderByThenSkipByHeadInResult h t result
2353+
2354+class OrderByColumnsInResultContinue :: Symbol -> Row Type -> Constraint
2355+class OrderByColumnsInResultContinue sym result
2356+
2357+instance OrderByColumnsInResultContinue "" result
2358+else instance
2359+ ( Symbol.Cons h t sym
2360+ , OrderByColumnsInResultContinueByHead h t result
2361+ ) =>
2362+ OrderByColumnsInResultContinue sym result
2363+
2364+class OrderByColumnsInResultContinueByHead :: Symbol -> Symbol -> Row Type -> Constraint
2365+class OrderByColumnsInResultContinueByHead head tail result
2366+
2367+instance
2368+ ( SkipSpaces tail rest
2369+ , OrderByColumnsInResult rest result
2370+ ) =>
2371+ OrderByColumnsInResultContinueByHead "," tail result
2372+
2373+else instance
2374+ ( Symbol.Append h t rest
2375+ , ExtractWord rest word afterWord
2376+ , FlushOrderByWordInResult word result
2377+ , SkipSpaces afterWord rest'
2378+ , OrderByColumnsInResultContinue rest' result
2379+ ) =>
2380+ OrderByColumnsInResultContinueByHead h t result
2381+
12912382-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
12922383-- ValidateColumns: comma-separated column names (for ON CONFLICT target)
12932384-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1474,6 +2565,14 @@ else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => V
14742565else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "(" tail acc tables
14752566else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ")" tail acc tables
14762567
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+
14772576-- End of string: flush final word
14782577else instance
14792578 ( Symbol.Append acc h acc'
@@ -1558,13 +2657,85 @@ else instance
15582657 , SkipSpaces rest1 rest2
15592658 , ExpectChar rest2 "=" rest3
15602659 , SkipSpaces rest3 rest4
1561− , ExtractWord rest4 excRef rest5
1562− , ValidateExcludedRef excRef colName
1563− , SkipSpaces rest5 rest6
1564− , ParseAssignmentsContinue rest6 cols
2660+ , SkipExpression rest4 rest5
2661+ , ParseAssignmentsContinue rest5 cols
15652662 ) =>
15662663 ParseAssignments sym cols
15672664
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+
15682739class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
15692740class ParseAssignmentsContinue sym cols
15702741
@@ -1627,6 +2798,7 @@ class IsAutoGeneratedTC constraints result | constraints -> result
16272798
16282799instance IsAutoGeneratedTC (AutoIncrement a) True
16292800else instance IsAutoGeneratedTC (Default s a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
16302802else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
16312803else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
16322804else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
@@ -1682,11 +2854,19 @@ class FieldToPGValue a where
16822854 fieldToPGValue :: a -> PG.PGValue
16832855
16842856instance FieldToPGValue a => FieldToPGValue (Maybe a) where
1685− fieldToPGValue = toNullable >>> unsafeCoerce
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
16862860else instance FieldToPGValue DateTime where
16872861 fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
1688−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
16892867 fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
16902870
16912871class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
16922872class RecordValuesRL rl row where
@@ -1740,6 +2920,34 @@ class HasClause label row
17402920
17412921instance Row.Cons label Unit rest row => HasClause label row
17422922
2923+class HasAnyDML :: Row Type -> Constraint
2924+class HasAnyDML stage
2925+
2926+instance (RL.RowToList stage rl, HasAnyDMLRL rl) => HasAnyDML stage
2927+
2928+class HasAnyDMLRL :: RL.RowList Type -> Constraint
2929+class HasAnyDMLRL rl
2930+
2931+instance HasAnyDMLRL (RL.Cons "select" Unit rest)
2932+else instance HasAnyDMLRL (RL.Cons "set" Unit rest)
2933+else instance HasAnyDMLRL (RL.Cons "delete" Unit rest)
2934+else instance HasAnyDMLRL rest => HasAnyDMLRL (RL.Cons label typ rest)
2935+else instance Fail (Text "WHERE requires a preceding SELECT, UPDATE (set), or DELETE") => HasAnyDMLRL RL.Nil
2936+
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+
17432951toSQL :: forall tables result params stage. Q tables result params stage -> String
17442952toSQL (Q q) = q.sql
17452953
@@ -1749,6 +2957,12 @@ from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
17492957fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
17502958fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
17512959
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+
17522966selectAll
17532967 :: forall tables name cols result r p stage stage'
17542968 . SingleTable tables name cols
@@ -1756,6 +2970,7 @@ selectAll
17562970 => StripColumns cols result
17572971 => Row.Lacks "select" stage
17582972 => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
17592974 => Row.Lacks "set" stage
17602975 => Row.Lacks "delete" stage
17612976 => Row.Lacks "where" stage
@@ -1773,6 +2988,7 @@ select
17732988 => ParseSelect sel tables result
17742989 => Row.Lacks "select" stage
17752990 => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
17762992 => Row.Lacks "set" stage
17772993 => Row.Lacks "delete" stage
17782994 => Row.Lacks "where" stage
@@ -1785,11 +3001,12 @@ select
17853001select (Q q) = Q (q { sql = "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
17863002
17873003selectDistinct
1788− :: forall @sel tables result r p stage stage'
3004+ :: forall @sel tables result r p stage stage' stage''
17893005 . IsSymbol sel
17903006 => ParseSelect sel tables result
17913007 => Row.Lacks "select" stage
17923008 => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
17933010 => Row.Lacks "set" stage
17943011 => Row.Lacks "delete" stage
17953012 => Row.Lacks "where" stage
@@ -1797,8 +3014,9 @@ selectDistinct
17973014 => Row.Lacks "limit" stage
17983015 => Row.Lacks "offset" stage
17993016 => Row.Cons "select" Unit stage stage'
3017+ => Row.Cons "distinct" Unit stage' stage''
18003018 => Q tables r p stage
1801− -> Q tables result p stage'
3019+ -> Q tables result p stage''
18023020selectDistinct (Q q) = Q (q { sql = "SELECT DISTINCT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
18033021
18043022selectDistinctOn
@@ -1809,6 +3027,7 @@ selectDistinctOn
18093027 => ParseSelect sel tables result
18103028 => Row.Lacks "select" stage
18113029 => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
18123031 => Row.Lacks "set" stage
18133032 => Row.Lacks "delete" stage
18143033 => Row.Lacks "where" stage
@@ -1824,6 +3043,7 @@ where_
18243043 :: forall @whr tables result params p stage stage'
18253044 . IsSymbol whr
18263045 => ParseWhere whr tables params
3046+ => HasAnyDML stage
18273047 => Row.Lacks "where" stage
18283048 => Row.Lacks "insert" stage
18293049 => Row.Lacks "groupBy" stage
@@ -1840,6 +3060,7 @@ orderBy
18403060 :: forall @cols tables result params stage stage'
18413061 . IsSymbol cols
18423062 => ValidateOrderBy cols tables
3063+ => CheckDistinctOrderBy stage cols result
18433064 => HasClause "select" stage
18443065 => Row.Lacks "orderBy" stage
18453066 => Row.Lacks "limit" stage
@@ -1897,16 +3118,66 @@ instance
18973118
18983119instance ParseLimitOffsetParams False name params params
18993120
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+
19003158class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
19013159class ParseLimitOffset sym params params' | sym params -> params'
19023160
19033161instance
19043162 ( Symbol.Cons head tail sym
19053163 , IsLimitParam head tail isParam
1906− , ParseLimitOffsetParams isParam tail params params'
3164+ , ParseLimitOffsetBranch isParam sym tail params params'
19073165 ) =>
19083166 ParseLimitOffset sym params params'
19093167
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+
19103181limit
19113182 :: forall @sym tables result params params' stage stage'
19123183 . ParseLimitOffset sym params params'
@@ -1961,13 +3232,17 @@ insert rec _ = Q { sql, values }
19613232 tableName = reflectSymbol (Proxy :: Proxy name)
19623233 colNames = columnNamesRL (Proxy :: Proxy userRowRL)
19633234 placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
1964− sql = "INSERT INTO " <> tableName
1965− <> " ("
1966− <> intercalate ", " colNames
1967− <> ")"
1968− <> " VALUES ("
1969− <> intercalate ", " placeholders
1970− <> ")"
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+ <> ")"
19713246 values = recordValuesRL (Proxy :: Proxy userRowRL) rec
19723247
19733248-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -1979,6 +3254,7 @@ returning
19793254 . SingleTable tables name cols
19803255 => IsSymbol sel
19813256 => ParseSelect sel tables result
3257+ => HasMutation stage
19823258 => Row.Lacks "returning" stage
19833259 => Row.Lacks "select" stage
19843260 => Row.Cons "returning" Unit stage stage'
@@ -1990,6 +3266,7 @@ returningAll
19903266 :: forall tables name cols result p stage stage'
19913267 . SingleTable tables name cols
19923268 => StripColumns cols result
3269+ => HasMutation stage
19933270 => Row.Lacks "returning" stage
19943271 => Row.Lacks "select" stage
19953272 => Row.Cons "returning" Unit stage stage'
@@ -2032,8 +3309,10 @@ set
20323309 => ValidateSetColumnsRL setRL cols
20333310 => SetClauseRL setRL
20343311 => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
20353313 => Row.Lacks "select" stage
20363314 => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
20373316 => Row.Lacks "set" stage
20383317 => Row.Lacks "delete" stage
20393318 => Row.Cons "set" Unit stage stage'
@@ -2057,6 +3336,7 @@ delete
20573336 => IsSymbol name
20583337 => Row.Lacks "select" stage
20593338 => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
20603340 => Row.Lacks "set" stage
20613341 => Row.Lacks "delete" stage
20623342 => Row.Cons "delete" Unit stage stage'
@@ -2297,6 +3577,66 @@ exceptAll
22973577 -> Q tables1 result params SetOpStage
22983578exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
22993579
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+
23003640-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
23013641-- Query execution
23023642-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
@@ -2312,28 +3652,34 @@ instance
23123652 ( IsSymbol name
23133653 , Row.Cons name typ rest row
23143654 , Row.Lacks name rest
3655+ , FieldToPGValue typ
23153656 , ParamsToArray tail row
23163657 ) =>
23173658 ParamsToArray (RL.Cons name typ tail) row where
23183659 paramsToArray _ rec =
23193660 [ { name: reflectSymbol (Proxy :: Proxy name)
2320− , value: unsafeToForeign (Record.get (Proxy :: Proxy name) rec)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
23213662 }
23223663 ]
23233664 <> paramsToArray (Proxy :: Proxy tail) rec
23243665
23253666namedParamRegex :: Regex.Regex
2326−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
23273668 Right r -> r
23283669 Left _ -> unsafeCoerce unit
23293670
3671+stripCast :: String -> String
3672+stripCast s = case SCU.indexOf (Pattern "::") s of
3673+ Nothing -> s
3674+ Just i -> SCU.take i s
3675+
23303676replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
23313677replaceNamedParams offset entries sql = do
23323678 let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
23333679 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
23343680 let
23353681 sql' = Regex.replace' namedParamRegex
2336− ( \match _ -> case Map.lookup match replacements of
3682+ ( \match _ -> case Map.lookup (stripCast match) replacements of
23373683 Nothing -> match
23383684 Just v -> v
23393685 )
@@ -2354,7 +3700,9 @@ runQuery conn params (Q q) = do
23543700 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
23553701 let allValues = q.values <> values
23563702 result <- PG.query (PG.SQL sql) allValues conn
2357− 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
23583706
23593707runQueryOne
23603708 :: forall tables result params paramsRL stage
@@ -2370,7 +3718,11 @@ runQueryOne conn params (Q q) = do
23703718 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
23713719 let allValues = q.values <> values
23723720 result <- PG.queryOne (PG.SQL sql) allValues conn
2373− 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)
23743726
23753727runExecute
23763728 :: forall tables params paramsRL stage
@@ -2402,7 +3754,9 @@ runQueryTx txn params (Q q) = do
24023754 let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
24033755 let allValues = q.values <> values
24043756 result <- PG.txQuery (PG.SQL sql) allValues txn
2405− 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
24063760
24073761runExecuteTx
24083762 :: forall tables params paramsRL stage
@@ -2418,10 +3772,9 @@ runExecuteTx txn params (Q q) = do
24183772 let allValues = q.values <> values
24193773 PG.txExecute (PG.SQL sql) allValues txn
24203774
2421−unsafeDecodeRows :: forall a. ReadForeign a => Array Foreign -> Array a
2422−unsafeDecodeRows = map unsafeDecodeRow
24233775
2424−unsafeDecodeRow :: forall a. ReadForeign a => Foreign -> a
2425−unsafeDecodeRow f = case runExcept (readImpl f) of
2426− Left _ -> unsafeCoerce unit
2427− 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