@@ -0,0 +1,3780 @@
1+module Yoga.Postgres.Schema where
5+import Control.Monad.Except (except, runExcept)
6+import Data.Array (intercalate, mapWithIndex, foldl)
7+import Data.Array as Array
8+import Data.Date (Date, day, month, year)
9+import Data.DateTime (DateTime(..))
10+import Data.Either (Either(..), note)
11+import Data.Enum (fromEnum, toEnum)
12+import Data.Int as Int
13+import Data.JSDate as JSDate
14+import Data.List.Types (NonEmptyList)
15+import Data.Map as Map
16+import Data.Maybe (Maybe(..), fromMaybe)
17+import Data.Newtype (class Newtype)
18+import Data.Nullable (toNullable)
19+import Data.Number as Number
20+import Data.Reflectable (class Reflectable, reflectType)
21+import Data.String.CodeUnits as SCU
22+import Data.String.Common as Str
23+import Data.String.Pattern (Pattern(..))
24+import Data.String.Regex (Regex, regex, replace') as Regex
25+import Data.String.Regex.Flags (global) as Regex
26+import Data.Symbol (class IsSymbol, reflectSymbol)
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)
35+import Prim.Boolean (True, False)
36+import Prim.Row (class Cons, class Lacks, class Union, class Nub) as Row
37+import Prim.RowList (class RowToList)
38+import Prim.RowList as RL
39+import Prim.Symbol (class Cons, class Append) as Symbol
40+import Prim.TypeError (class Fail, Beside, Text, Quote)
41+import Record (get) as Record
42+import Type.Proxy (Proxy(..))
43+import Type.RowList (class ListToRow)
44+import Unsafe.Coerce (unsafeCoerce)
45+import Yoga.JSON (class ReadForeign, class WriteForeign, readImpl, writeImpl)
46+import Yoga.Postgres as PG
48+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
50+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
52+data Table :: Symbol -> Row Type -> Type
53+data Table name columns = Table
55+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
56+-- Constraint wrappers (used with # operator from Type.Function)
57+-- Int # PrimaryKey # AutoIncrement = AutoIncrement (PrimaryKey Int)
58+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
60+data PrimaryKey :: Type -> Type
63+data AutoIncrement :: Type -> Type
66+data Unique :: Type -> Type
69+data Default :: forall k. k -> Type -> Type
71+data DefaultExpr :: Symbol -> Type -> Type
72+data DefaultExpr expression a
77+data ForeignKey :: Symbol -> Type -> Symbol -> Type -> Type
78+data ForeignKey table references col a
80+-- Internal: used by LEFT JOIN to mark columns as nullable
81+data Nullable :: Type -> Type
84+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
86+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
88+newtype Jsonb = Jsonb Foreign
90+derive instance Newtype Jsonb _
92+instance ReadForeign Jsonb where
93+ readImpl = pure <<< Jsonb
95+instance WriteForeign Jsonb where
96+ writeImpl (Jsonb value) = value
98+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
99+-- PGUUID: newtype over Data.UUID.UUID for Postgres integration
100+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
102+newtype PGUUID = PGUUID UUID
104+derive instance Newtype PGUUID _
105+derive newtype instance Eq PGUUID
106+derive newtype instance Ord PGUUID
107+derive newtype instance Show PGUUID
109+instance ReadForeign PGUUID where
111+ s :: String <- readImpl f
112+ pure (unsafeCoerce s)
114+instance WriteForeign PGUUID where
115+ writeImpl (PGUUID uuid) = writeImpl (UUID.toString uuid)
117+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
118+-- PGDate / PGTime: newtypes for DATE and TIME columns
119+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
121+newtype PGDate = PGDate Date
123+derive instance Newtype PGDate _
124+derive instance Eq PGDate
125+derive instance Ord PGDate
126+instance Show PGDate where
127+ show (PGDate d) = "(PGDate " <> show d <> ")"
129+instance ReadForeign PGDate where
131+ except $ note (pure (ForeignError "Invalid date")) (JSDate.toDate jsDate <#> PGDate)
133+ jsDate :: JSDate.JSDate
134+ jsDate = unsafeCoerce f
136+instance WriteForeign PGDate where
137+ writeImpl (PGDate date) =
139+ ( show (fromEnum (year date))
141+ <> pad2 (fromEnum (month date))
143+ <> pad2 (fromEnum (day date))
146+ pad2 n = if n < 10 then "0" <> show n else show n
148+newtype TimestampTZ = TimestampTZ DateTime
150+derive instance Newtype TimestampTZ _
151+derive instance Eq TimestampTZ
152+derive instance Ord TimestampTZ
153+instance Show TimestampTZ where
154+ show (TimestampTZ dt) = "(TimestampTZ " <> show dt <> ")"
156+instance ReadForeign TimestampTZ where
158+ except $ note (pure (ForeignError "Invalid timestamp")) (JSDate.toDateTime jsDate <#> TimestampTZ)
160+ jsDate :: JSDate.JSDate
161+ jsDate = unsafeCoerce f
163+newtype PGTime = PGTime Time
165+derive instance Newtype PGTime _
166+derive instance Eq PGTime
167+derive instance Ord PGTime
168+instance Show PGTime where
169+ show (PGTime t) = "(PGTime " <> show t <> ")"
171+instance ReadForeign PGTime where
173+ s :: String <- readImpl f
174+ except $ note (pure (ForeignError ("Invalid time: " <> s))) (parseTime s)
177+ let parts = Str.split (Pattern ":") str
179+ [ hStr, mStr, sStr ] -> do
180+ h <- Int.fromString hStr >>= toEnum
181+ mi <- Int.fromString mStr >>= toEnum
182+ let secParts = Str.split (Pattern ".") sStr
183+ s <- Int.fromString (fromMaybe sStr (Array.head secParts)) >>= toEnum
184+ let fraction = fromMaybe "" (Array.index secParts 1)
185+ let milliseconds = case SCU.length fraction of
187+ 1 -> fraction <> "00"
188+ 2 -> fraction <> "0"
189+ _ -> SCU.take 3 fraction
190+ let ms = fromMaybe bottom (Int.fromString milliseconds >>= toEnum)
191+ pure (PGTime (Time h mi s ms))
194+instance WriteForeign PGTime where
195+ writeImpl (PGTime (Time hour minute second millisecond)) =
197+ ( pad2 (fromEnum hour)
199+ <> pad2 (fromEnum minute)
201+ <> pad2 (fromEnum second)
203+ <> pad3 (fromEnum millisecond)
206+ pad2 n = if n < 10 then "0" <> show n else show n
208+ | n < 10 = "00" <> show n
209+ | n < 100 = "0" <> show n
210+ | otherwise = show n
212+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
213+-- Point: geometric point "(x,y)"
214+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
216+newtype Point = Point { x :: Number, y :: Number }
218+derive instance Newtype Point _
219+derive instance Eq Point
221+instance Show Point where
222+ show (Point { x, y }) = "(Point " <> show x <> " " <> show y <> ")"
224+instance ReadForeign Point where
226+ s :: String <- readImpl f
227+ except $ note (pure (ForeignError ("Invalid point: " <> s))) (parsePoint s)
230+ let inner = SCU.drop 1 (SCU.dropRight 1 str)
231+ let parts = Str.split (Pattern ",") inner
233+ [ xStr, yStr ] -> do
234+ x <- Number.fromString xStr
235+ y <- Number.fromString yStr
236+ pure (Point { x, y })
239+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
240+-- TSVector / TSQuery: full-text search types
241+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
243+newtype TSVector = TSVector String
245+derive instance Newtype TSVector _
246+derive newtype instance Eq TSVector
247+derive newtype instance Ord TSVector
248+derive newtype instance Show TSVector
250+instance ReadForeign TSVector where
252+ s :: String <- readImpl f
255+newtype TSQuery = TSQuery String
257+derive instance Newtype TSQuery _
258+derive newtype instance Eq TSQuery
259+derive newtype instance Ord TSQuery
260+derive newtype instance Show TSQuery
262+instance ReadForeign TSQuery where
264+ s :: String <- readImpl f
267+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
268+-- Nullability: inferred from Maybe
269+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
271+class IsNullable a where
272+ isNullable :: Proxy a -> Boolean
274+instance IsNullable (Maybe a) where
276+else instance IsNullable a where
277+ isNullable _ = false
279+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
280+-- ExtractType: recursively unwrap constraint wrappers to get base type
281+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
283+class ExtractType :: Type -> Type -> Constraint
284+class ExtractType wrapped typ | wrapped -> typ
286+instance ExtractType a typ => ExtractType (PrimaryKey a) typ
287+else instance ExtractType a typ => ExtractType (AutoIncrement a) typ
288+else instance ExtractType a typ => ExtractType (Unique a) typ
289+else instance ExtractType a typ => ExtractType (Default val a) typ
290+else instance ExtractType a typ => ExtractType (DefaultExpr expression a) typ
291+else instance ExtractType a typ => ExtractType (ForeignKey t r c a) typ
292+else instance ExtractType a typ => ExtractType (Nullable a) (Maybe typ)
293+else instance ExtractType a a
295+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
296+-- PureScript type -> Postgres type name
297+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
299+class PGTypeName a where
300+ pgTypeName :: Proxy a -> String
302+instance PGTypeName Int where
303+ pgTypeName _ = "INTEGER"
305+instance PGTypeName String where
306+ pgTypeName _ = "TEXT"
308+instance PGTypeName Boolean where
309+ pgTypeName _ = "BOOLEAN"
311+instance PGTypeName Number where
312+ pgTypeName _ = "DOUBLE PRECISION"
314+instance PGTypeName DateTime where
315+ pgTypeName _ = "TIMESTAMPTZ"
317+instance PGTypeName BigInt where
318+ pgTypeName _ = "BIGINT"
320+instance PGTypeName PGUUID where
321+ pgTypeName _ = "UUID"
323+instance PGTypeName TimestampTZ where
324+ pgTypeName _ = "TIMESTAMPTZ"
326+instance PGTypeName PGDate where
327+ pgTypeName _ = "DATE"
329+instance PGTypeName PGTime where
330+ pgTypeName _ = "TIME"
332+instance PGTypeName Point where
333+ pgTypeName _ = "POINT"
335+instance PGTypeName TSVector where
336+ pgTypeName _ = "TSVECTOR"
338+instance PGTypeName TSQuery where
339+ pgTypeName _ = "TSQUERY"
341+instance PGTypeName Jsonb where
342+ pgTypeName _ = "JSONB"
344+instance PGTypeName a => PGTypeName (Array a) where
345+ pgTypeName _ = pgTypeName (Proxy :: Proxy a) <> "[]"
347+instance PGTypeName a => PGTypeName (Maybe a) where
348+ pgTypeName _ = pgTypeName (Proxy :: Proxy a)
350+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
351+-- Constraint -> DDL fragment
352+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
354+class RenderDefaultValue val typ where
355+ renderDefaultValue :: Proxy val -> Proxy typ -> String
357+instance Reflectable sym String => RenderDefaultValue sym String where
358+ renderDefaultValue _ _ = "DEFAULT '" <> reflectType (Proxy :: Proxy sym) <> "'"
360+instance Reflectable val Int => RenderDefaultValue val Int where
361+ renderDefaultValue _ _ = "DEFAULT " <> show (reflectType (Proxy :: Proxy val))
363+instance RenderDefaultValue True Boolean where
364+ renderDefaultValue _ _ = "DEFAULT true"
366+instance RenderDefaultValue False Boolean where
367+ renderDefaultValue _ _ = "DEFAULT false"
369+instance RenderDefaultValue "true" Boolean where
370+ renderDefaultValue _ _ = "DEFAULT true"
372+instance RenderDefaultValue "false" Boolean where
373+ renderDefaultValue _ _ = "DEFAULT false"
375+instance RenderDefaultValue "now()" TimestampTZ where
376+ renderDefaultValue _ _ = "DEFAULT now()"
378+instance RenderDefaultValue "gen_random_uuid()" PGUUID where
379+ renderDefaultValue _ _ = "DEFAULT gen_random_uuid()"
382+class RenderConstraint a where
383+ renderConstraint :: Proxy a -> String
385+instance RenderConstraint a => RenderConstraint (PrimaryKey a) where
386+ renderConstraint _ = joinConstraints "PRIMARY KEY" (renderConstraint (Proxy :: Proxy a))
388+else instance RenderConstraint a => RenderConstraint (AutoIncrement a) where
389+ renderConstraint _ = joinConstraints "GENERATED ALWAYS AS IDENTITY" (renderConstraint (Proxy :: Proxy a))
391+else instance RenderConstraint a => RenderConstraint (Unique a) where
392+ renderConstraint _ = joinConstraints "UNIQUE" (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))
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))
401+else instance (IsSymbol table, IsSymbol col, RenderConstraint a) => RenderConstraint (ForeignKey table References col a) where
402+ renderConstraint _ = joinConstraints
403+ ("REFERENCES " <> reflectSymbol (Proxy :: Proxy table) <> "(" <> reflectSymbol (Proxy :: Proxy col) <> ")")
404+ (renderConstraint (Proxy :: Proxy a))
406+else instance RenderConstraint a => RenderConstraint (Nullable a) where
407+ renderConstraint _ = renderConstraint (Proxy :: Proxy a)
409+else instance RenderConstraint a where
410+ renderConstraint _ = ""
412+joinConstraints :: String -> String -> String
413+joinConstraints a b = case a, b of
416+ s1, s2 -> s1 <> " " <> s2
418+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
420+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
422+class CreateTableDDL a where
423+ createTableDDL :: String
425+class RenderColumnsRL :: RL.RowList Type -> Constraint
426+class RenderColumnsRL rl where
427+ renderColumnsRL :: Proxy rl -> Array String
429+instance RenderColumnsRL RL.Nil where
430+ renderColumnsRL _ = []
434+ , ExtractType entry typ
437+ , RenderConstraint entry
438+ , RenderColumnsRL tail
440+ RenderColumnsRL (RL.Cons name entry tail) where
441+ renderColumnsRL _ = do
442+ let colName = reflectSymbol (Proxy :: Proxy name)
443+ let colType = pgTypeName (Proxy :: Proxy typ)
444+ let nullable = isNullable (Proxy :: Proxy typ)
445+ let notNull = if nullable then "" else " NOT NULL"
446+ let constraints = renderConstraint (Proxy :: Proxy entry)
447+ let constraintsSuffix = if constraints == "" then "" else " " <> constraints
448+ [ colName <> " " <> colType <> notNull <> constraintsSuffix ] <> renderColumnsRL (Proxy :: Proxy tail)
453+ , RenderColumnsRL rl
455+ CreateTableDDL (Table name cols) where
457+ let tableName = reflectSymbol (Proxy :: Proxy name)
458+ let columns = renderColumnsRL (Proxy :: Proxy rl)
459+ "CREATE TABLE " <> tableName <> " (" <> intercalate ", " columns <> ")"
461+createTableDDLIfNotExists :: forall @a. CreateTableDDL a => String
462+createTableDDLIfNotExists = do
463+ let ddl = createTableDDL @a
464+ "CREATE TABLE IF NOT EXISTS" <> SCU.drop 12 ddl
466+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
467+-- INSERT SQL generation
468+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
470+class IsAutoGenerated a where
471+ isAutoGenerated :: Proxy a -> Boolean
473+instance IsAutoGenerated (AutoIncrement a) where
474+ isAutoGenerated _ = true
475+else instance IsAutoGenerated (Default v a) where
476+ isAutoGenerated _ = true
477+else instance IsAutoGenerated (DefaultExpr expression a) where
478+ isAutoGenerated _ = true
479+else instance IsAutoGenerated a => IsAutoGenerated (PrimaryKey a) where
480+ isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
481+else instance IsAutoGenerated a => IsAutoGenerated (Unique a) where
482+ isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
483+else instance IsAutoGenerated a => IsAutoGenerated (ForeignKey t r c a) where
484+ isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
485+else instance IsAutoGenerated a => IsAutoGenerated (Nullable a) where
486+ isAutoGenerated _ = isAutoGenerated (Proxy :: Proxy a)
487+else instance IsAutoGenerated a where
488+ isAutoGenerated _ = false
490+class InsertColumnsRL :: RL.RowList Type -> Constraint
491+class InsertColumnsRL rl where
492+ insertColumnsRL :: Proxy rl -> Array String
494+instance InsertColumnsRL RL.Nil where
495+ insertColumnsRL _ = []
499+ , IsAutoGenerated entry
500+ , InsertColumnsRL tail
502+ InsertColumnsRL (RL.Cons name entry tail) where
505+ rest = insertColumnsRL (Proxy :: Proxy tail)
507+ if isAutoGenerated (Proxy :: Proxy entry) then rest
508+ else [ reflectSymbol (Proxy :: Proxy name) ] <> rest
510+class InsertSQLFor a where
511+ insertSQLFor :: String
516+ , InsertColumnsRL rl
518+ InsertSQLFor (Table name cols) where
520+ let tableName = reflectSymbol (Proxy :: Proxy name)
521+ let cols = insertColumnsRL (Proxy :: Proxy rl)
522+ if Array.null cols then
523+ "INSERT INTO " <> tableName <> " DEFAULT VALUES RETURNING *"
525+ let placeholders = cols # mapWithIndex \i _ -> "$" <> show (i + 1)
526+ "INSERT INTO " <> tableName
528+ <> intercalate ", " cols
531+ <> intercalate ", " placeholders
535+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
536+-- SELECT SQL generation
537+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
539+class SelectAllSQLFor a where
540+ selectAllSQLFor :: String
545+ SelectAllSQLFor (Table name cols) where
546+ selectAllSQLFor = "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name)
548+class WhereClauseRL :: RL.RowList Type -> Constraint
549+class WhereClauseRL rl where
550+ whereClauseRL :: Proxy rl -> Int -> Array String
552+instance WhereClauseRL RL.Nil where
553+ whereClauseRL _ _ = []
555+instance (IsSymbol name, WhereClauseRL tail) => WhereClauseRL (RL.Cons name typ tail) where
556+ whereClauseRL _ idx =
557+ [ reflectSymbol (Proxy :: Proxy name) <> " = $" <> show idx ]
558+ <> whereClauseRL (Proxy :: Proxy tail) (idx + 1)
560+class SelectWhereSQLFor a whereRow where
561+ selectWhereSQLFor :: String
565+ , RowToList whereRow whereRL
566+ , WhereClauseRL whereRL
568+ SelectWhereSQLFor (Table name cols) whereRow where
569+ selectWhereSQLFor = do
570+ let tableName = reflectSymbol (Proxy :: Proxy name)
571+ let conditions = whereClauseRL (Proxy :: Proxy whereRL) 1
572+ "SELECT * FROM " <> tableName <> " WHERE " <> intercalate " AND " conditions
574+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
575+-- UPDATE SQL generation
576+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
578+class ColumnCountRL :: RL.RowList Type -> Constraint
579+class ColumnCountRL rl where
580+ columnCountRL :: Proxy rl -> Int
582+instance ColumnCountRL RL.Nil where
585+instance ColumnCountRL tail => ColumnCountRL (RL.Cons name typ tail) where
586+ columnCountRL _ = 1 + columnCountRL (Proxy :: Proxy tail)
588+class UpdateSQLFor table setRow whereRow where
589+ updateSQLFor :: String
593+ , RowToList setRow setRL
594+ , RowToList whereRow whereRL
595+ , WhereClauseRL setRL
596+ , WhereClauseRL whereRL
597+ , ColumnCountRL setRL
599+ UpdateSQLFor (Table name cols) setRow whereRow where
601+ let tableName = reflectSymbol (Proxy :: Proxy name)
602+ let setClauses = whereClauseRL (Proxy :: Proxy setRL) 1
603+ let setCount = columnCountRL (Proxy :: Proxy setRL)
604+ let whereClauses = whereClauseRL (Proxy :: Proxy whereRL) (setCount + 1)
605+ "UPDATE " <> tableName
607+ <> intercalate ", " setClauses
609+ <> intercalate " AND " whereClauses
611+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
612+-- DELETE SQL generation
613+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
615+class DeleteSQLFor table whereRow where
616+ deleteSQLFor :: String
620+ , RowToList whereRow whereRL
621+ , WhereClauseRL whereRL
623+ DeleteSQLFor (Table name cols) whereRow where
625+ let tableName = reflectSymbol (Proxy :: Proxy name)
626+ let conditions = whereClauseRL (Proxy :: Proxy whereRL) 1
627+ "DELETE FROM " <> tableName <> " WHERE " <> intercalate " AND " conditions
629+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
630+-- Utility type classes
631+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
633+class UnwrapMaybe :: Type -> Type -> Constraint
634+class UnwrapMaybe a b | a -> b
636+instance UnwrapMaybe (Maybe a) a
637+else instance UnwrapMaybe a a
639+-- Skip leading spaces
640+class SkipSpaces :: Symbol -> Symbol -> Constraint
641+class SkipSpaces sym result | sym -> result
643+instance SkipSpaces "" ""
645+ ( Symbol.Cons head tail sym
646+ , SkipSpacesGo head tail result
648+ SkipSpaces sym result
650+class SkipSpacesGo :: Symbol -> Symbol -> Symbol -> Constraint
651+class SkipSpacesGo head tail result | head tail -> result
653+instance SkipSpaces tail result => SkipSpacesGo " " tail result
654+else instance Symbol.Cons head tail result => SkipSpacesGo head tail result
656+-- Skip a string literal: consume chars until closing single quote
657+class SkipStringLiteral :: Symbol -> Symbol -> Constraint
658+class SkipStringLiteral sym rest | sym -> rest
660+instance Fail (Text "Unclosed string literal") => SkipStringLiteral "" rest
662+ ( Symbol.Cons h t sym
663+ , SkipStringLiteralGo h t rest
665+ SkipStringLiteral sym rest
667+class SkipStringLiteralGo :: Symbol -> Symbol -> Symbol -> Constraint
668+class SkipStringLiteralGo head tail rest | head tail -> rest
670+-- Closing quote: done
671+instance SkipStringLiteralGo "'" tail tail
672+-- End of string without closing quote
673+else instance Fail (Text "Unclosed string literal") => SkipStringLiteralGo h "" rest
674+-- Any other char: skip and continue
676+ ( Symbol.Cons nextH nextT tail
677+ , SkipStringLiteralGo nextH nextT rest
679+ SkipStringLiteralGo h tail rest
681+-- Extract the first word from a Symbol (up to space or end)
682+class ExtractWord :: Symbol -> Symbol -> Symbol -> Constraint
683+class ExtractWord sym word rest | sym -> word rest
685+instance ExtractWord "" "" ""
687+ ( Symbol.Cons h t sym
688+ , ExtractWordGo h t "" word rest
690+ ExtractWord sym word rest
692+class ExtractWordGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
693+class ExtractWordGo head tail acc word rest | head tail acc -> word rest
696+instance (SkipSpaces tail rest) => ExtractWordGo " " tail acc acc rest
697+-- Stop on comma (keep comma in rest)
698+else instance Symbol.Cons "," tail rest => ExtractWordGo "," tail acc acc rest
700+else instance Symbol.Append acc h word => ExtractWordGo h "" acc word ""
701+-- Regular char: accumulate
703+ ( Symbol.Append acc h acc'
704+ , Symbol.Cons nextH nextT tail
705+ , ExtractWordGo nextH nextT acc' word rest
707+ ExtractWordGo h tail acc word rest
709+-- StripColumns: (name :: String # Unique, ...) -> (name :: String, ...)
710+class StripColumnsRL :: RL.RowList Type -> RL.RowList Type -> Constraint
711+class StripColumnsRL rl out | rl -> out
713+instance StripColumnsRL RL.Nil RL.Nil
714+instance (ExtractType entry typ, StripColumnsRL tail out') => StripColumnsRL (RL.Cons name entry tail) (RL.Cons name typ out')
716+class StripColumns :: Row Type -> Row Type -> Constraint
717+class StripColumns cols result | cols -> result
719+instance (RowToList cols rl, StripColumnsRL rl outRL, ListToRow outRL result) => StripColumns cols result
721+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
722+-- SingleTable: extract name and cols from a single-entry tables row
723+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
725+class SingleTable :: Row (Row Type) -> Symbol -> Row Type -> Constraint
726+class SingleTable tables name cols | tables -> name cols
729+ ( RowToList tables (RL.Cons name cols RL.Nil)
732+ SingleTable tables name cols
734+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
735+-- SplitOnDot: "users.id" -> ("users", "id"); "name" -> unqualified
736+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
738+class SplitOnDot :: Symbol -> Boolean -> Symbol -> Symbol -> Constraint
739+class SplitOnDot sym hasDot table col | sym -> hasDot table col
742+ ( Symbol.Cons h t sym
743+ , SplitOnDotGo h t "" hasDot table col
745+ SplitOnDot sym hasDot table col
747+class SplitOnDotGo :: Symbol -> Symbol -> Symbol -> Boolean -> Symbol -> Symbol -> Constraint
748+class SplitOnDotGo head tail acc hasDot table col | head tail acc -> hasDot table col
750+-- Found dot: acc is table, rest is column
751+instance SplitOnDotGo "." tail acc True acc tail
753+-- End of string without dot: unqualified
755+ ( Symbol.Append acc h col
757+ SplitOnDotGo h "" acc False "" col
759+-- Regular char: accumulate
761+ ( Symbol.Append acc h acc'
762+ , Symbol.Cons nextH nextT tail
763+ , SplitOnDotGo nextH nextT acc' hasDot table col
765+ SplitOnDotGo h tail acc hasDot table col
767+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
768+-- ResolveColumn: look up a column (qualified or unqualified) in tables
769+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
771+class ResolveColumn :: Symbol -> Row (Row Type) -> Type -> Constraint
772+class ResolveColumn word tables typ | word tables -> typ
775+ ( SplitOnDot word hasDot table col
776+ , ResolveColumnBranch hasDot table col tables typ
778+ ResolveColumn word tables typ
780+class ResolveColumnBranch :: Boolean -> Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
781+class ResolveColumnBranch hasDot table col tables typ | hasDot table col tables -> typ
783+-- Qualified: has dot
785+ ( Row.Cons table tableCols restTables tables
786+ , Row.Cons col typ restCols tableCols
788+ ResolveColumnBranch True table col tables typ
790+-- Unqualified: no dot
792+ ( RowToList tables tablesRL
793+ , FindUnqualifiedColumn col tablesRL typ
795+ ResolveColumnBranch False table col tables typ
797+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
798+-- FindUnqualifiedColumn: search all tables for a column name
799+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
801+class FindUnqualifiedColumn :: Symbol -> RL.RowList (Row Type) -> Type -> Constraint
802+class FindUnqualifiedColumn col tablesRL typ | col tablesRL -> typ
805+ ( Fail (Beside (Text "Column ") (Beside (Quote col) (Text " not found in any table")))
807+ FindUnqualifiedColumn col RL.Nil typ
810+ ( RowToList tableCols colsRL
811+ , HasColumnRL col colsRL found
812+ , FindUnqualifiedColumnDecide found col tableName tableCols tailTables typ
814+ FindUnqualifiedColumn col (RL.Cons tableName tableCols tailTables) typ
816+class HasColumnRL :: Symbol -> RL.RowList Type -> Boolean -> Constraint
817+class HasColumnRL col rl found | col rl -> found
819+instance HasColumnRL col RL.Nil False
820+instance HasColumnRL col (RL.Cons col typ tail) True
821+else instance HasColumnRL col tail found => HasColumnRL col (RL.Cons name typ tail) found
823+class FindUnqualifiedColumnDecide :: Boolean -> Symbol -> Symbol -> Row Type -> RL.RowList (Row Type) -> Type -> Constraint
824+class FindUnqualifiedColumnDecide found col tableName tableCols restTables typ | found col tableName tableCols restTables -> typ
826+-- Found in this table: verify it's not in remaining tables
828+ ( Row.Cons col typ restCols tableCols
829+ , AssertNotInRemainingTables col restTables
831+ FindUnqualifiedColumnDecide True col tableName tableCols restTables typ
833+-- Not found in this table: keep searching
835+ FindUnqualifiedColumn col restTables typ =>
836+ FindUnqualifiedColumnDecide False col tableName tableCols restTables typ
838+class AssertNotInRemainingTables :: Symbol -> RL.RowList (Row Type) -> Constraint
839+class AssertNotInRemainingTables col tablesRL
841+instance AssertNotInRemainingTables col RL.Nil
843+ ( RowToList tableCols colsRL
844+ , HasColumnRL col colsRL found
845+ , AssertNotAmbiguous found col tableName
846+ , AssertNotInRemainingTables col tail
848+ AssertNotInRemainingTables col (RL.Cons tableName tableCols tail)
850+class AssertNotAmbiguous :: Boolean -> Symbol -> Symbol -> Constraint
851+class AssertNotAmbiguous found col tableName
853+instance AssertNotAmbiguous False col tableName
855+ ( Fail (Beside (Text "Column ") (Beside (Quote col) (Text " is ambiguous - qualify with table name")))
857+ AssertNotAmbiguous True col tableName
859+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
860+-- ParseSelect: parse "users.name, posts.title AS t" against tables
861+-- Result labels: column name (after dot) or explicit AS alias
862+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
864+class ParseSelect :: Symbol -> Row (Row Type) -> Row Type -> Constraint
865+class ParseSelect sym tables result | sym tables -> result
867+instance ParseSelect "" tables ()
869+ ( Symbol.Cons h t sym
870+ , ParseSelectGo h t "" tables RL.Nil outRL
871+ , ListToRow outRL result
873+ ParseSelect sym tables result
875+class ParseSelectGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
876+class ParseSelectGo head tail acc tables accRL outRL | head tail acc tables accRL -> outRL
878+-- Leading/double comma: no column name before comma
880+ Fail (Text "Unexpected comma in SELECT clause (missing column name)") =>
881+ ParseSelectGo "," tail "" tables accRL outRL
883+-- Comma: emit column, continue
885+ ( ResolveColumn acc tables entry
886+ , ExtractType entry typ
887+ , SplitOnDot acc _hasDot _table colName
888+ , SkipSpaces tail rest
889+ , ParseSelectContinue rest tables (RL.Cons colName typ accRL) outRL
891+ ParseSelectGo "," tail acc tables accRL outRL
893+-- Space: column reference done, check for AS or comma
895+ ( SkipSpaces tail rest
896+ , ParseSelectAfterCol acc rest tables accRL outRL
898+ ParseSelectGo " " tail acc tables accRL outRL
900+-- Open paren: aggregate function call
902+ ( ExtractUntilParen tail args afterParen
903+ , ResolveAggregateArg args tables argType
904+ , AggregateReturnType acc argType returnType
905+ , SkipSpaces afterParen rest
906+ , ParseAfterAggregate rest tables returnType accRL outRL
908+ ParseSelectGo "(" tail acc tables accRL outRL
910+-- End of string: emit final column
912+ ( Symbol.Append acc h acc'
913+ , ResolveColumn acc' tables entry
914+ , ExtractType entry typ
915+ , SplitOnDot acc' _hasDot _table colName
917+ ParseSelectGo h "" acc tables accRL (RL.Cons colName typ accRL)
919+-- Regular char (including dot): accumulate
921+ ( Symbol.Append acc h acc'
922+ , Symbol.Cons nextH nextT tail
923+ , ParseSelectGo nextH nextT acc' tables accRL outRL
925+ ParseSelectGo h tail acc tables accRL outRL
927+-- After column name + space: AS alias, comma, or end
928+class ParseSelectAfterCol :: Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
929+class ParseSelectAfterCol colRef rest tables accRL outRL | colRef rest tables accRL -> outRL
931+-- End: emit column with default label (column name after dot)
933+ ( ResolveColumn colRef tables entry
934+ , ExtractType entry typ
935+ , SplitOnDot colRef _hasDot _table colName
937+ ParseSelectAfterCol colRef "" tables accRL (RL.Cons colName typ accRL)
939+-- Non-empty: branch on first char
941+ ( Symbol.Cons h t rest
942+ , ParseSelectAfterColByHead h t colRef tables accRL outRL
944+ ParseSelectAfterCol colRef rest tables accRL outRL
946+class ParseSelectAfterColByHead :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
947+class ParseSelectAfterColByHead head tail colRef tables accRL outRL | head tail colRef tables accRL -> outRL
949+-- Comma: emit column with default label, continue
951+ ( ResolveColumn colRef tables entry
952+ , ExtractType entry typ
953+ , SplitOnDot colRef _hasDot _table colName
954+ , SkipSpaces tail rest
955+ , ParseSelectContinue rest tables (RL.Cons colName typ accRL) outRL
957+ ParseSelectAfterColByHead "," tail colRef tables accRL outRL
959+-- Otherwise (AS ...): extract word
961+ ( Symbol.Append h t rest
962+ , ExtractWord rest keyword afterKeyword
963+ , ParseSelectHandleAS keyword afterKeyword colRef tables accRL outRL
965+ ParseSelectAfterColByHead h t colRef tables accRL outRL
967+class ParseSelectHandleAS :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
968+class ParseSelectHandleAS keyword afterKeyword colRef tables accRL outRL | keyword afterKeyword colRef tables accRL -> outRL
971+ ( ExtractWord afterKeyword alias afterAlias
972+ , ResolveColumn colRef tables entry
973+ , ExtractType entry typ
974+ , SkipSpaces afterAlias rest
975+ , ParseSelectExpectEnd rest tables (RL.Cons alias typ accRL) outRL
977+ ParseSelectHandleAS "AS" afterKeyword colRef tables accRL outRL
980+ ( ExtractWord afterKeyword alias afterAlias
981+ , ResolveColumn colRef tables entry
982+ , ExtractType entry typ
983+ , SkipSpaces afterAlias rest
984+ , ParseSelectExpectEnd rest tables (RL.Cons alias typ accRL) outRL
986+ ParseSelectHandleAS "as" afterKeyword colRef tables accRL outRL
988+-- Bare alias (no AS keyword): "name n" means "name AS n"
990+ ( ResolveColumn colRef tables entry
991+ , ExtractType entry typ
992+ , SkipSpaces afterKeyword rest
993+ , ParseSelectExpectEnd rest tables (RL.Cons keyword typ accRL) outRL
995+ ParseSelectHandleAS keyword afterKeyword colRef tables accRL outRL
997+class ParseSelectExpectEnd :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
998+class ParseSelectExpectEnd sym tables accRL outRL | sym tables accRL -> outRL
1000+instance ParseSelectExpectEnd "" tables accRL accRL
1002+ ( Symbol.Cons h t sym
1003+ , ParseSelectExpectEndByHead h t tables accRL outRL
1005+ ParseSelectExpectEnd sym tables accRL outRL
1007+class ParseSelectExpectEndByHead :: Symbol -> Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
1008+class ParseSelectExpectEndByHead head tail tables accRL outRL | head tail tables accRL -> outRL
1011+ ( SkipSpaces tail rest
1012+ , ParseSelectContinue rest tables accRL outRL
1014+ ParseSelectExpectEndByHead "," tail tables accRL outRL
1016+class ParseSelectContinue :: Symbol -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
1017+class ParseSelectContinue sym tables accRL outRL | sym tables accRL -> outRL
1019+instance Fail (Text "Trailing comma in SELECT clause") => ParseSelectContinue "" tables accRL outRL
1021+ ( Symbol.Cons h t sym
1022+ , ParseSelectGo h t "" tables accRL outRL
1024+ ParseSelectContinue sym tables accRL outRL
1026+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1027+-- Aggregate functions in SELECT: COUNT(*), SUM(col), etc.
1028+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1030+-- Extract characters until closing paren
1031+class ExtractUntilParen :: Symbol -> Symbol -> Symbol -> Constraint
1032+class ExtractUntilParen tail args afterParen | tail -> args afterParen
1035+ Fail (Text "Unclosed parenthesis in SELECT clause") =>
1036+ ExtractUntilParen "" args afterParen
1039+ ( Symbol.Cons h t tail
1040+ , ExtractUntilParenGo h t "" Z args afterParen
1042+ ExtractUntilParen tail args afterParen
1044+-- Peano depth counter for nested parentheses
1047+data S :: Type -> Type
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
1053+-- ) at depth zero: done
1054+instance ExtractUntilParenGo ")" tail acc Z acc tail
1056+-- ) at depth > 0: decrement depth, keep going
1058+ ( Symbol.Append acc ")" acc'
1059+ , Symbol.Cons nextH nextT tail
1060+ , ExtractUntilParenGo nextH nextT acc' n args afterParen
1062+ ExtractUntilParenGo ")" tail acc (S n) args afterParen
1064+-- ( : increment depth, keep going
1066+ ( Symbol.Append acc "(" acc'
1067+ , Symbol.Cons nextH nextT tail
1068+ , ExtractUntilParenGo nextH nextT acc' (S depth) args afterParen
1070+ ExtractUntilParenGo "(" tail acc depth args afterParen
1072+-- Unclosed paren at end of input
1074+ Fail (Text "Unclosed parenthesis in SELECT clause") =>
1075+ ExtractUntilParenGo h "" acc depth args afterParen
1077+-- Any other char: accumulate
1079+ ( Symbol.Append acc h acc'
1080+ , Symbol.Cons nextH nextT tail
1081+ , ExtractUntilParenGo nextH nextT acc' depth args afterParen
1083+ ExtractUntilParenGo h tail acc depth args afterParen
1085+-- Resolve aggregate argument: "*" -> Star, column ref -> unwrapped type
1088+class ResolveAggregateArg :: Symbol -> Row (Row Type) -> Type -> Constraint
1089+class ResolveAggregateArg args tables argType | args tables -> argType
1091+instance ResolveAggregateArg "*" tables Star
1092+else instance ResolveAggregateArg "" tables Star
1094+ ( SkipSpaces args trimmedFront
1095+ , ExtractIdent trimmedFront col rest
1096+ , ResolveAggregateArgDispatch col rest tables argType
1098+ ResolveAggregateArg args tables argType
1100+-- ExtractIdent: like ExtractWord but also stops at "(" so we can detect nested function calls
1101+class ExtractIdent :: Symbol -> Symbol -> Symbol -> Constraint
1102+class ExtractIdent sym word rest | sym -> word rest
1104+instance ExtractIdent "" "" ""
1106+ ( Symbol.Cons h t sym
1107+ , ExtractIdentGo h t "" word rest
1109+ ExtractIdent sym word rest
1111+class ExtractIdentGo :: Symbol -> Symbol -> Symbol -> Symbol -> Symbol -> Constraint
1112+class ExtractIdentGo head tail acc word rest | head tail acc -> word rest
1114+instance (SkipSpaces tail rest) => ExtractIdentGo " " tail acc acc rest
1115+else instance Symbol.Cons "," tail rest => ExtractIdentGo "," tail acc acc rest
1116+else instance Symbol.Append "(" tail rest => ExtractIdentGo "(" tail acc acc rest
1117+else instance Symbol.Append acc h word => ExtractIdentGo h "" acc word ""
1119+ ( Symbol.Append acc h acc'
1120+ , Symbol.Cons nextH nextT tail
1121+ , ExtractIdentGo nextH nextT acc' word rest
1123+ ExtractIdentGo h tail acc word rest
1125+-- After extracting the identifier, check if followed by "(" (nested function call) or not (column ref)
1126+class ResolveAggregateArgDispatch :: Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1127+class ResolveAggregateArgDispatch col rest tables argType | col rest tables -> argType
1130+ ResolveAggregateArgCol col tables argType =>
1131+ ResolveAggregateArgDispatch col "" tables argType
1134+ ( Symbol.Cons h t rest
1135+ , ResolveAggregateArgDispatchByHead h t col tables argType
1137+ ResolveAggregateArgDispatch col rest tables argType
1139+class ResolveAggregateArgDispatchByHead :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1140+class ResolveAggregateArgDispatchByHead head tail col tables argType | head tail col tables -> argType
1142+-- Nested function call: col(...)
1144+ ( ExtractUntilParen tail innerArgs _afterParen
1145+ , ResolveAggregateArg innerArgs tables innerArgType
1146+ , AggregateReturnType col innerArgType argType
1148+ ResolveAggregateArgDispatchByHead "(" tail col tables argType
1150+-- Not a function call: column reference
1152+ ResolveAggregateArgCol col tables argType =>
1153+ ResolveAggregateArgDispatchByHead h t col tables argType
1155+class ResolveAggregateArgCol :: Symbol -> Row (Row Type) -> Type -> Constraint
1156+class ResolveAggregateArgCol col tables argType | col tables -> argType
1158+instance ResolveAggregateArgCol "*" tables Star
1160+ ( Symbol.Cons h _t col
1161+ , ResolveAggregateArgColByHead h col tables argType
1163+ ResolveAggregateArgCol col tables argType
1165+class ResolveAggregateArgColByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> Constraint
1166+class ResolveAggregateArgColByHead head col tables argType | head col tables -> argType
1168+-- Numeric literals: treat as Star (return type determined by function)
1169+instance ResolveAggregateArgColByHead "0" col tables Star
1170+else instance ResolveAggregateArgColByHead "1" col tables Star
1171+else instance ResolveAggregateArgColByHead "2" col tables Star
1172+else instance ResolveAggregateArgColByHead "3" col tables Star
1173+else instance ResolveAggregateArgColByHead "4" col tables Star
1174+else instance ResolveAggregateArgColByHead "5" col tables Star
1175+else instance ResolveAggregateArgColByHead "6" col tables Star
1176+else instance ResolveAggregateArgColByHead "7" col tables Star
1177+else instance ResolveAggregateArgColByHead "8" col tables Star
1178+else instance ResolveAggregateArgColByHead "9" col tables Star
1181+ ( ResolveColumn col tables entry
1182+ , ExtractType entry typ
1183+ , UnwrapMaybe typ unwrapped
1185+ ResolveAggregateArgColByHead head col tables unwrapped
1187+-- Map (funcName, argType) -> returnType
1188+-- Dispatches on first character to avoid linear search
1189+class AggregateReturnType :: Symbol -> Type -> Type -> Constraint
1190+class AggregateReturnType funcName argType returnType | funcName argType -> returnType
1193+ ( Symbol.Cons head rest funcName
1194+ , AggregateReturnTypeByHead head funcName argType returnType
1196+ AggregateReturnType funcName argType returnType
1198+class AggregateReturnTypeByHead :: Symbol -> Symbol -> Type -> Type -> Constraint
1199+class AggregateReturnTypeByHead head funcName argType returnType | head funcName argType -> returnType
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
1220+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1221+ AggregateReturnTypeByHead head funcName argType returnType
1223+-- A: AVG, ARRAY_AGG
1224+class AggregateReturnTypeA :: Symbol -> Type -> Type -> Constraint
1225+class AggregateReturnTypeA funcName argType returnType | funcName argType -> returnType
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)
1232+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1233+ AggregateReturnTypeA funcName argType returnType
1235+-- C: COUNT, COALESCE
1236+class AggregateReturnTypeC :: Symbol -> Type -> Type -> Constraint
1237+class AggregateReturnTypeC funcName argType returnType | funcName argType -> returnType
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
1244+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1245+ AggregateReturnTypeC funcName argType returnType
1248+class AggregateReturnTypeD :: Symbol -> Type -> Type -> Constraint
1249+class AggregateReturnTypeD funcName argType returnType | funcName argType -> returnType
1251+instance AggregateReturnTypeD "DENSE_RANK" argType Int
1252+else instance AggregateReturnTypeD "dense_rank" argType Int
1254+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1255+ AggregateReturnTypeD funcName argType returnType
1258+class AggregateReturnTypeF :: Symbol -> Type -> Type -> Constraint
1259+class AggregateReturnTypeF funcName argType returnType | funcName argType -> returnType
1261+instance AggregateReturnTypeF "FIRST_VALUE" argType argType
1262+else instance AggregateReturnTypeF "first_value" argType argType
1264+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1265+ AggregateReturnTypeF funcName argType returnType
1267+-- L: LAG, LEAD, LAST_VALUE
1268+class AggregateReturnTypeL :: Symbol -> Type -> Type -> Constraint
1269+class AggregateReturnTypeL funcName argType returnType | funcName argType -> returnType
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
1278+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1279+ AggregateReturnTypeL funcName argType returnType
1282+class AggregateReturnTypeM :: Symbol -> Type -> Type -> Constraint
1283+class AggregateReturnTypeM funcName argType returnType | funcName argType -> returnType
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
1290+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1291+ AggregateReturnTypeM funcName argType returnType
1293+-- N: NTILE, NTH_VALUE
1294+class AggregateReturnTypeN :: Symbol -> Type -> Type -> Constraint
1295+class AggregateReturnTypeN funcName argType returnType | funcName argType -> returnType
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
1302+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1303+ AggregateReturnTypeN funcName argType returnType
1305+-- R: ROW_NUMBER, RANK
1306+class AggregateReturnTypeR :: Symbol -> Type -> Type -> Constraint
1307+class AggregateReturnTypeR funcName argType returnType | funcName argType -> returnType
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
1314+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1315+ AggregateReturnTypeR funcName argType returnType
1317+-- S: SUM, STRING_AGG
1318+class AggregateReturnTypeS :: Symbol -> Type -> Type -> Constraint
1319+class AggregateReturnTypeS funcName argType returnType | funcName argType -> returnType
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
1326+ Fail (Beside (Text "Unknown function: ") (Quote funcName)) =>
1327+ AggregateReturnTypeS funcName argType returnType
1329+-- After aggregate ): require AS alias, then continue
1330+class ParseAfterAggregate :: Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
1331+class ParseAfterAggregate rest tables returnType accRL outRL | rest tables returnType accRL -> outRL
1333+-- End of string without alias
1335+ Fail (Text "Aggregate function requires AS alias (e.g. COUNT(*) AS cnt)") =>
1336+ ParseAfterAggregate "" tables returnType accRL outRL
1339+ ( Symbol.Cons h t rest
1340+ , ParseAfterAggregateByHead h t tables returnType accRL outRL
1342+ ParseAfterAggregate rest tables returnType accRL outRL
1344+class ParseAfterAggregateByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
1345+class ParseAfterAggregateByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
1347+-- Comma without alias
1349+ Fail (Text "Aggregate function requires AS alias (e.g. COUNT(*) AS cnt)") =>
1350+ ParseAfterAggregateByHead "," tail tables returnType accRL outRL
1352+-- Otherwise: extract keyword and dispatch (OVER or AS)
1354+ ( Symbol.Append h t rest
1355+ , ExtractWord rest keyword afterKeyword
1356+ , ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL
1358+ ParseAfterAggregateByHead h t tables returnType accRL outRL
1360+class ParseAfterAggregateKeyword :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
1361+class ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL | keyword afterKeyword tables returnType accRL -> outRL
1363+-- OVER: parse the over clause, then continue to AS
1365+ ( SkipSpaces afterKeyword rest
1366+ , ParseOverClause rest tables returnType accRL outRL
1368+ ParseAfterAggregateKeyword "OVER" afterKeyword tables returnType accRL outRL
1371+ ( SkipSpaces afterKeyword rest
1372+ , ParseOverClause rest tables returnType accRL outRL
1374+ ParseAfterAggregateKeyword "over" afterKeyword tables returnType accRL outRL
1376+-- AS: extract alias
1378+ ( ExtractWord afterKeyword alias afterAlias
1379+ , SkipSpaces afterAlias rest
1380+ , ParseSelectExpectEnd rest tables (RL.Cons alias returnType accRL) outRL
1382+ ParseAfterAggregateKeyword "AS" afterKeyword tables returnType accRL outRL
1385+ ( ExtractWord afterKeyword alias afterAlias
1386+ , SkipSpaces afterAlias rest
1387+ , ParseSelectExpectEnd rest tables (RL.Cons alias returnType accRL) outRL
1389+ ParseAfterAggregateKeyword "as" afterKeyword tables returnType accRL outRL
1392+ Fail (Text "Aggregate function requires AS alias (e.g. COUNT(*) AS cnt)") =>
1393+ ParseAfterAggregateKeyword keyword afterKeyword tables returnType accRL outRL
1395+-- Parse OVER (...) clause: expect (, skip until ), then continue to AS alias
1396+class ParseOverClause :: Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
1397+class ParseOverClause rest tables returnType accRL outRL | rest tables returnType accRL -> outRL
1400+ Fail (Text "Expected ( after OVER") =>
1401+ ParseOverClause "" tables returnType accRL outRL
1404+ ( Symbol.Cons h t rest
1405+ , ParseOverClauseByHead h t tables returnType accRL outRL
1407+ ParseOverClause rest tables returnType accRL outRL
1409+class ParseOverClauseByHead :: Symbol -> Symbol -> Row (Row Type) -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
1410+class ParseOverClauseByHead head tail tables returnType accRL outRL | head tail tables returnType accRL -> outRL
1412+-- Open paren: extract until close paren, validate content, continue to AS
1414+ ( ExtractUntilParen tail overContent afterParen
1415+ , ValidateOverContent overContent tables
1416+ , SkipSpaces afterParen rest
1417+ , ParseAfterAggregate rest tables returnType accRL outRL
1419+ ParseOverClauseByHead "(" tail tables returnType accRL outRL
1422+ Fail (Text "Expected ( after OVER") =>
1423+ ParseOverClauseByHead h t tables returnType accRL outRL
1425+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1426+-- ValidateOverContent: validates PARTITION BY / ORDER BY inside OVER(...)
1427+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1429+class ValidateOverContent :: Symbol -> Row (Row Type) -> Constraint
1430+class ValidateOverContent content tables
1432+instance ValidateOverContent "" tables
1435+ ( SkipSpaces content content'
1436+ , ValidateOverContentTrimmed content' tables
1438+ ValidateOverContent content tables
1440+class ValidateOverContentTrimmed :: Symbol -> Row (Row Type) -> Constraint
1441+class ValidateOverContentTrimmed content tables
1443+instance ValidateOverContentTrimmed "" tables
1446+ ( ExtractWord content keyword rest
1447+ , ValidateOverKeyword keyword rest tables
1449+ ValidateOverContentTrimmed content tables
1451+class ValidateOverKeyword :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1452+class ValidateOverKeyword keyword rest tables
1455+ ( ExtractWord rest byWord afterBy
1456+ , AssertIsBy byWord
1457+ , ValidatePartitionByColumns afterBy tables
1459+ ValidateOverKeyword "PARTITION" rest tables
1462+ ( ExtractWord rest byWord afterBy
1463+ , AssertIsBy byWord
1464+ , ValidatePartitionByColumns afterBy tables
1466+ ValidateOverKeyword "partition" rest tables
1469+ ( ExtractWord rest byWord afterBy
1470+ , AssertIsBy byWord
1471+ , ValidateOrderBy afterBy tables
1473+ ValidateOverKeyword "ORDER" rest tables
1476+ ( ExtractWord rest byWord afterBy
1477+ , AssertIsBy byWord
1478+ , ValidateOrderBy afterBy tables
1480+ ValidateOverKeyword "order" rest tables
1483+ Fail (Text "Expected PARTITION BY or ORDER BY inside OVER clause") =>
1484+ ValidateOverKeyword keyword rest tables
1486+class AssertIsBy :: Symbol -> Constraint
1487+class AssertIsBy sym
1489+instance AssertIsBy "BY"
1490+else instance AssertIsBy "by"
1491+else instance Fail (Text "Expected BY after PARTITION or ORDER") => AssertIsBy sym
1493+-- ValidatePartitionByColumns: column list that terminates at ORDER keyword
1494+class ValidatePartitionByColumns :: Symbol -> Row (Row Type) -> Constraint
1495+class ValidatePartitionByColumns sym tables
1497+instance ValidatePartitionByColumns "" tables
1500+ ( SkipSpaces sym sym'
1501+ , ValidatePartitionByColumnsTrimmed sym' tables
1503+ ValidatePartitionByColumns sym tables
1505+class ValidatePartitionByColumnsTrimmed :: Symbol -> Row (Row Type) -> Constraint
1506+class ValidatePartitionByColumnsTrimmed sym tables
1508+instance ValidatePartitionByColumnsTrimmed "" tables
1511+ ( Symbol.Cons h t sym
1512+ , ValidatePartitionByColumnsGo h t "" tables
1514+ ValidatePartitionByColumnsTrimmed sym tables
1516+class ValidatePartitionByColumnsGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
1517+class ValidatePartitionByColumnsGo head tail acc tables
1519+-- Comma: flush column, continue
1521+ ( ResolveColumn acc tables typ
1522+ , SkipSpaces tail rest
1523+ , ValidatePartitionByColumnsTrimmed rest tables
1525+ ValidatePartitionByColumnsGo "," tail acc tables
1527+-- Space: flush word, check if ORDER keyword or column
1529+ ( SkipSpaces tail rest
1530+ , FlushPartitionWord acc rest tables
1532+ ValidatePartitionByColumnsGo " " tail acc tables
1534+-- End of string: flush final column
1536+ ( Symbol.Append acc h acc'
1537+ , ResolveColumn acc' tables typ
1539+ ValidatePartitionByColumnsGo h "" acc tables
1541+-- Regular char: accumulate
1543+ ( Symbol.Append acc h acc'
1544+ , Symbol.Cons nextH nextT tail
1545+ , ValidatePartitionByColumnsGo nextH nextT acc' tables
1547+ ValidatePartitionByColumnsGo h tail acc tables
1549+class FlushPartitionWord :: Symbol -> Symbol -> Row (Row Type) -> Constraint
1550+class FlushPartitionWord word rest tables
1553+ ( ExtractWord rest byWord afterBy
1554+ , AssertIsBy byWord
1555+ , ValidateOrderBy afterBy tables
1557+ FlushPartitionWord "ORDER" rest tables
1560+ ( ExtractWord rest byWord afterBy
1561+ , AssertIsBy byWord
1562+ , ValidateOrderBy afterBy tables
1564+ FlushPartitionWord "order" rest tables
1567+ ( ResolveColumn word tables typ
1568+ , ValidatePartitionByColumnsTrimmed rest tables
1570+ FlushPartitionWord word rest tables
1572+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1573+-- ParseWhere: parse "id = $id AND age > $age" -> params
1574+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1578+class ParseWhere :: Symbol -> Row (Row Type) -> Row Type -> Constraint
1579+class ParseWhere sym tables params | sym tables -> params
1581+instance Fail (Text "Empty WHERE clause") => ParseWhere "" tables params
1583+ ( Symbol.Cons h t sym
1584+ , ParseWhereGo h t "" NoType tables RL.Nil outRL
1585+ , ListToRow outRL params
1587+ ParseWhere sym tables params
1589+class ParseWhereGo :: Symbol -> Symbol -> Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> RL.RowList Type -> Constraint
1590+class ParseWhereGo head tail acc currentType tables paramsIn paramsOut | head tail acc currentType tables paramsIn -> paramsOut
1592+-- Space: flush word, continue
1594+ ( FlushWhereWord acc currentType tables paramsIn currentType' paramsOut'
1595+ , SkipSpaces tail rest
1596+ , ParseWhereContinue rest currentType' tables paramsOut' paramsOut
1598+ ParseWhereGo " " tail acc currentType tables paramsIn paramsOut
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
1609+ ( FlushWhereWord acc currentType tables paramsIn currentType' paramsOut'
1610+ , SkipStringLiteral tail rest
1611+ , ParseWhereContinue rest currentType' tables paramsOut' paramsOut
1613+ ParseWhereGo "'" tail acc currentType tables paramsIn paramsOut
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
1621+-- End of string: flush final word
1623+ ( Symbol.Append acc h acc'
1624+ , FlushWhereWord acc' currentType tables paramsIn _ct paramsOut
1626+ ParseWhereGo h "" acc currentType tables paramsIn paramsOut
1628+-- Regular char (including dot): accumulate
1630+ ( Symbol.Append acc h acc'
1631+ , Symbol.Cons nextH nextT tail
1632+ , ParseWhereGo nextH nextT acc' currentType tables paramsIn paramsOut
1634+ ParseWhereGo h tail acc currentType tables paramsIn paramsOut
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
1639+instance ParseWhereContinue "" currentType tables paramsIn paramsIn
1641+ ( Symbol.Cons h t sym
1642+ , ParseWhereGo h t "" currentType tables paramsIn paramsOut
1644+ ParseWhereContinue sym currentType tables paramsIn paramsOut
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
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
1665+ ( Symbol.Cons head rest word
1666+ , FlushWhereWordByHead head word currentType tables paramsIn currentTypeOut paramsOut
1668+ FlushWhereWord word currentType tables paramsIn currentTypeOut paramsOut
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
1673+-- $param: emit with currentType
1675+ ( Symbol.Cons "$" paramName word
1677+ FlushWhereWordByHead "$" word currentType tables paramsIn currentType (RL.Cons paramName currentType paramsIn)
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
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
1728+-- Catch-all: column reference (letters without keywords like g, h, q, w, x, y, z)
1730+ ( ResolveColumn word tables entry
1731+ , ExtractType entry typ
1732+ , UnwrapMaybe typ unwrapped
1734+ FlushWhereWordByHead head word currentType tables paramsIn unwrapped paramsIn
1736+-- Per-letter keyword helpers
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
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
1754+ ( ResolveColumn word tables entry
1755+ , ExtractType entry typ
1756+ , UnwrapMaybe typ unwrapped
1758+ FlushWhereWordA word currentType tables paramsIn unwrapped paramsIn
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
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
1768+ ( ResolveColumn word tables entry
1769+ , ExtractType entry typ
1770+ , UnwrapMaybe typ unwrapped
1772+ FlushWhereWordB word currentType tables paramsIn unwrapped paramsIn
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
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
1792+ ( ResolveColumn word tables entry
1793+ , ExtractType entry typ
1794+ , UnwrapMaybe typ unwrapped
1796+ FlushWhereWordC word currentType tables paramsIn unwrapped paramsIn
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
1801+instance FlushWhereWordD "DATE" currentType tables paramsIn currentType paramsIn
1802+else instance FlushWhereWordD "date" currentType tables paramsIn currentType paramsIn
1804+ ( ResolveColumn word tables entry
1805+ , ExtractType entry typ
1806+ , UnwrapMaybe typ unwrapped
1808+ FlushWhereWordD word currentType tables paramsIn unwrapped paramsIn
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
1813+instance FlushWhereWordE "EXISTS" currentType tables paramsIn currentType paramsIn
1814+else instance FlushWhereWordE "exists" currentType tables paramsIn currentType paramsIn
1816+ ( ResolveColumn word tables entry
1817+ , ExtractType entry typ
1818+ , UnwrapMaybe typ unwrapped
1820+ FlushWhereWordE word currentType tables paramsIn unwrapped paramsIn
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
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
1830+ ( ResolveColumn word tables entry
1831+ , ExtractType entry typ
1832+ , UnwrapMaybe typ unwrapped
1834+ FlushWhereWordF word currentType tables paramsIn unwrapped paramsIn
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
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
1846+ ( ResolveColumn word tables entry
1847+ , ExtractType entry typ
1848+ , UnwrapMaybe typ unwrapped
1850+ FlushWhereWordI word currentType tables paramsIn unwrapped paramsIn
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
1855+instance FlushWhereWordJ "jsonb" currentType tables paramsIn currentType paramsIn
1856+else instance FlushWhereWordJ "json" currentType tables paramsIn currentType paramsIn
1858+ ( ResolveColumn word tables entry
1859+ , ExtractType entry typ
1860+ , UnwrapMaybe typ unwrapped
1862+ FlushWhereWordJ word currentType tables paramsIn unwrapped paramsIn
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
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
1876+ ( ResolveColumn word tables entry
1877+ , ExtractType entry typ
1878+ , UnwrapMaybe typ unwrapped
1880+ FlushWhereWordL word currentType tables paramsIn unwrapped paramsIn
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
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
1890+ ( ResolveColumn word tables entry
1891+ , ExtractType entry typ
1892+ , UnwrapMaybe typ unwrapped
1894+ FlushWhereWordM word currentType tables paramsIn unwrapped paramsIn
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
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
1908+ ( ResolveColumn word tables entry
1909+ , ExtractType entry typ
1910+ , UnwrapMaybe typ unwrapped
1912+ FlushWhereWordN word currentType tables paramsIn unwrapped paramsIn
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
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
1924+ ( ResolveColumn word tables entry
1925+ , ExtractType entry typ
1926+ , UnwrapMaybe typ unwrapped
1928+ FlushWhereWordS word currentType tables paramsIn unwrapped paramsIn
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
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
1951+ ( ResolveColumn word tables entry
1952+ , ExtractType entry typ
1953+ , UnwrapMaybe typ unwrapped
1955+ FlushWhereWordT word currentType tables paramsIn unwrapped paramsIn
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
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
1964+ ( ResolveColumn word tables entry
1965+ , ExtractType entry typ
1966+ , UnwrapMaybe typ unwrapped
1968+ FlushWhereWordU word currentType tables paramsIn unwrapped paramsIn
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
1973+instance FlushWhereWordV "varchar" currentType tables paramsIn currentType paramsIn
1975+ ( ResolveColumn word tables entry
1976+ , ExtractType entry typ
1977+ , UnwrapMaybe typ unwrapped
1979+ FlushWhereWordV word currentType tables paramsIn unwrapped paramsIn
1981+class FlushWhereWordP :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1982+class FlushWhereWordP word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1984+instance FlushWhereWordP "point" currentType tables paramsIn currentType paramsIn
1985+else instance FlushWhereWordP "POINT" currentType tables paramsIn currentType paramsIn
1986+else instance FlushWhereWordP "plainto_tsquery" currentType tables paramsIn currentType paramsIn
1987+else instance FlushWhereWordP "PLAINTO_TSQUERY" currentType tables paramsIn currentType paramsIn
1989+ ( ResolveColumn word tables entry
1990+ , ExtractType entry typ
1991+ , UnwrapMaybe typ unwrapped
1993+ FlushWhereWordP word currentType tables paramsIn unwrapped paramsIn
1995+class FlushWhereWordR :: Symbol -> Type -> Row (Row Type) -> RL.RowList Type -> Type -> RL.RowList Type -> Constraint
1996+class FlushWhereWordR word currentType tables paramsIn currentTypeOut paramsOut | word currentType tables paramsIn -> currentTypeOut paramsOut
1998+instance FlushWhereWordR "real" currentType tables paramsIn currentType paramsIn
1999+else instance FlushWhereWordR "REAL" currentType tables paramsIn currentType paramsIn
2001+ ( ResolveColumn word tables entry
2002+ , ExtractType entry typ
2003+ , UnwrapMaybe typ unwrapped
2005+ FlushWhereWordR word currentType tables paramsIn unwrapped paramsIn
2007+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2008+-- ValidateColumnList: comma-separated column references
2009+-- Used by GROUP BY, DISTINCT ON, ON CONFLICT target
2010+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2012+class ValidateColumnList :: Symbol -> Row (Row Type) -> Constraint
2013+class ValidateColumnList sym tables
2015+instance Fail (Text "Empty column list") => ValidateColumnList "" tables
2017+ ( Symbol.Cons h t sym
2018+ , ValidateColumnListGo h t "" tables
2020+ ValidateColumnList sym tables
2022+class ValidateColumnListContinue :: Symbol -> Row (Row Type) -> Constraint
2023+class ValidateColumnListContinue sym tables
2025+instance ValidateColumnListContinue "" tables
2027+ ( Symbol.Cons h t sym
2028+ , ValidateColumnListGo h t "" tables
2030+ ValidateColumnListContinue sym tables
2032+class ValidateColumnListGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
2033+class ValidateColumnListGo head tail acc tables
2036+ ( ResolveColumn acc tables typ
2037+ , SkipSpaces tail rest
2038+ , ValidateColumnListContinue rest tables
2040+ ValidateColumnListGo "," tail acc tables
2043+ ( SkipSpaces tail rest
2044+ , ResolveColumn acc tables typ
2045+ , ValidateColumnListContinue rest tables
2047+ ValidateColumnListGo " " tail acc tables
2050+ ( Symbol.Append acc h acc'
2051+ , ResolveColumn acc' tables typ
2053+ ValidateColumnListGo h "" acc tables
2056+ ( Symbol.Append acc h acc'
2057+ , Symbol.Cons nextH nextT tail
2058+ , ValidateColumnListGo nextH nextT acc' tables
2060+ ValidateColumnListGo h tail acc tables
2062+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2063+-- ValidateOrderBy: validate ORDER BY with qualified column support
2064+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2066+class ValidateOrderBy :: Symbol -> Row (Row Type) -> Constraint
2067+class ValidateOrderBy sym tables
2069+instance ValidateOrderBy "" tables
2071+ ( Symbol.Cons h t sym
2072+ , ValidateOrderByGo h t "" tables
2074+ ValidateOrderBy sym tables
2076+class ValidateOrderByGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
2077+class ValidateOrderByGo head tail acc tables
2079+-- Leading/double comma: no column before comma
2081+ Fail (Text "Unexpected comma in ORDER BY clause (missing column name)") =>
2082+ ValidateOrderByGo "," tail "" tables
2084+-- Comma: flush column, continue (non-empty acc)
2086+ ( FlushOrderByWord acc tables
2087+ , SkipSpaces tail rest
2088+ , ValidateOrderByContinueNonEmpty rest tables
2090+ ValidateOrderByGo "," tail acc tables
2092+-- Space: flush column, skip modifiers
2094+ ( SkipSpaces tail rest
2095+ , FlushOrderByThenSkip acc rest tables
2097+ ValidateOrderByGo " " tail acc tables
2099+-- End of string: flush final column
2101+ ( Symbol.Append acc h acc'
2102+ , FlushOrderByFinalWord acc' tables
2104+ ValidateOrderByGo h "" acc tables
2106+-- Regular char (including dot): accumulate
2108+ ( Symbol.Append acc h acc'
2109+ , Symbol.Cons nextH nextT tail
2110+ , ValidateOrderByGo nextH nextT acc' tables
2112+ ValidateOrderByGo h tail acc tables
2114+-- After comma: reject empty (trailing comma)
2115+class ValidateOrderByContinueNonEmpty :: Symbol -> Row (Row Type) -> Constraint
2116+class ValidateOrderByContinueNonEmpty sym tables
2118+instance Fail (Text "Trailing comma in ORDER BY clause") => ValidateOrderByContinueNonEmpty "" tables
2120+ ( Symbol.Cons h t sym
2121+ , ValidateOrderByGo h t "" tables
2123+ ValidateOrderByContinueNonEmpty sym tables
2125+-- End of string: reject bare modifiers (ASC/DESC without column)
2126+class FlushOrderByFinalWord :: Symbol -> Row (Row Type) -> Constraint
2127+class FlushOrderByFinalWord word tables
2129+instance Fail (Text "ORDER BY requires at least one column") => FlushOrderByFinalWord "" tables
2130+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "ASC" tables
2131+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "asc" tables
2132+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "DESC" tables
2133+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "desc" tables
2134+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "NULLS" tables
2135+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "FIRST" tables
2136+else instance Fail (Text "ORDER BY requires a column name before ASC/DESC") => FlushOrderByFinalWord "LAST" tables
2137+else instance ResolveColumn word tables typ => FlushOrderByFinalWord word tables
2139+class FlushOrderByWord :: Symbol -> Row (Row Type) -> Constraint
2140+class FlushOrderByWord word tables
2142+instance FlushOrderByWord "" tables
2143+else instance FlushOrderByWord "ASC" tables
2144+else instance FlushOrderByWord "asc" tables
2145+else instance FlushOrderByWord "DESC" tables
2146+else instance FlushOrderByWord "desc" tables
2147+else instance FlushOrderByWord "NULLS" tables
2148+else instance FlushOrderByWord "FIRST" tables
2149+else instance FlushOrderByWord "LAST" tables
2150+else instance ResolveColumn word tables typ => FlushOrderByWord word tables
2152+class FlushOrderByThenSkip :: Symbol -> Symbol -> Row (Row Type) -> Constraint
2153+class FlushOrderByThenSkip colName rest tables
2155+instance FlushOrderByWord colName tables => FlushOrderByThenSkip colName "" tables
2157+ ( FlushOrderByWord colName tables
2158+ , Symbol.Cons h t rest
2159+ , FlushOrderByThenSkipByHead h t tables
2161+ FlushOrderByThenSkip colName rest tables
2163+class FlushOrderByThenSkipByHead :: Symbol -> Symbol -> Row (Row Type) -> Constraint
2164+class FlushOrderByThenSkipByHead head tail tables
2166+-- Comma: continue with next column
2168+ ( SkipSpaces tail rest
2169+ , ValidateOrderBy rest tables
2171+ FlushOrderByThenSkipByHead "," tail tables
2173+-- Modifier word: consume it, continue
2175+ ( Symbol.Append h t rest
2176+ , ExtractWord rest word afterWord
2177+ , FlushOrderByWord word tables
2178+ , SkipSpaces afterWord rest'
2179+ , ValidateOrderByContinue rest' tables
2181+ FlushOrderByThenSkipByHead h t tables
2183+class ValidateOrderByContinue :: Symbol -> Row (Row Type) -> Constraint
2184+class ValidateOrderByContinue sym tables
2186+instance ValidateOrderByContinue "" tables
2188+ ( Symbol.Cons h t sym
2189+ , ValidateOrderByContinueByHead h t tables
2191+ ValidateOrderByContinue sym tables
2193+class ValidateOrderByContinueByHead :: Symbol -> Symbol -> Row (Row Type) -> Constraint
2194+class ValidateOrderByContinueByHead head tail tables
2197+ ( SkipSpaces tail rest
2198+ , ValidateOrderBy rest tables
2200+ ValidateOrderByContinueByHead "," tail tables
2203+ ( Symbol.Append h t rest
2204+ , ExtractWord rest word afterWord
2205+ , FlushOrderByWord word tables
2206+ , SkipSpaces afterWord rest'
2207+ , ValidateOrderByContinue rest' tables
2209+ ValidateOrderByContinueByHead h t tables
2211+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2212+-- RowListHas: check if a label exists in a RowList
2213+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2215+class RowListHas :: Symbol -> RL.RowList Type -> Boolean -> Constraint
2216+class RowListHas label rl has | label rl -> has
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
2222+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2223+-- CheckDistinctOrderBy: when DISTINCT is in stage, ORDER BY
2224+-- columns must appear in the SELECT result list
2225+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2227+class CheckDistinctOrderBy :: Row Type -> Symbol -> Row Type -> Constraint
2228+class CheckDistinctOrderBy stage cols result
2231+ ( RowToList stage stageRL
2232+ , RowListHas "distinct" stageRL isDistinct
2233+ , CheckDistinctOrderByBranch isDistinct cols result
2235+ CheckDistinctOrderBy stage cols result
2237+class CheckDistinctOrderByBranch :: Boolean -> Symbol -> Row Type -> Constraint
2238+class CheckDistinctOrderByBranch isDistinct cols result
2240+instance OrderByColumnsInResult cols result => CheckDistinctOrderByBranch True cols result
2241+instance CheckDistinctOrderByBranch False cols result
2243+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2244+-- OrderByColumnsInResult: validate ORDER BY column refs exist in result row
2245+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2247+class OrderByColumnsInResult :: Symbol -> Row Type -> Constraint
2248+class OrderByColumnsInResult sym result
2250+instance OrderByColumnsInResult "" result
2252+ ( Symbol.Cons h t sym
2253+ , OrderByColumnsInResultGo h t "" result
2255+ OrderByColumnsInResult sym result
2257+class OrderByColumnsInResultGo :: Symbol -> Symbol -> Symbol -> Row Type -> Constraint
2258+class OrderByColumnsInResultGo head tail acc result
2260+-- Comma: flush column, continue
2262+ ( FlushOrderByWordInResult acc result
2263+ , SkipSpaces tail rest
2264+ , OrderByColumnsInResult rest result
2266+ OrderByColumnsInResultGo "," tail acc result
2268+-- Space: flush word, skip modifiers
2270+ ( SkipSpaces tail rest
2271+ , FlushOrderByThenSkipInResult acc rest result
2273+ OrderByColumnsInResultGo " " tail acc result
2275+-- End of string: flush final column
2277+ ( Symbol.Append acc h acc'
2278+ , FlushOrderByWordInResult acc' result
2280+ OrderByColumnsInResultGo h "" acc result
2282+-- Regular char: accumulate
2284+ ( Symbol.Append acc h acc'
2285+ , Symbol.Cons nextH nextT tail
2286+ , OrderByColumnsInResultGo nextH nextT acc' result
2288+ OrderByColumnsInResultGo h tail acc result
2290+class FlushOrderByWordInResult :: Symbol -> Row Type -> Constraint
2291+class FlushOrderByWordInResult word result
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
2303+class CheckColumnInResult :: Symbol -> Row Type -> Constraint
2304+class CheckColumnInResult col result
2307+ ( RowToList result rl
2308+ , CheckColumnInResultRL col rl
2310+ CheckColumnInResult col result
2312+class CheckColumnInResultRL :: Symbol -> RL.RowList Type -> Constraint
2313+class CheckColumnInResultRL col rl
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
2319+instance CheckColumnInResultRL col (RL.Cons col typ tail)
2321+else instance CheckColumnInResultRL col tail => CheckColumnInResultRL col (RL.Cons name typ tail)
2323+class FlushOrderByThenSkipInResult :: Symbol -> Symbol -> Row Type -> Constraint
2324+class FlushOrderByThenSkipInResult colName rest result
2326+instance FlushOrderByWordInResult colName result => FlushOrderByThenSkipInResult colName "" result
2328+ ( FlushOrderByWordInResult colName result
2329+ , Symbol.Cons h t rest
2330+ , FlushOrderByThenSkipByHeadInResult h t result
2332+ FlushOrderByThenSkipInResult colName rest result
2334+class FlushOrderByThenSkipByHeadInResult :: Symbol -> Symbol -> Row Type -> Constraint
2335+class FlushOrderByThenSkipByHeadInResult head tail result
2337+-- Comma: continue with next column
2339+ ( SkipSpaces tail rest
2340+ , OrderByColumnsInResult rest result
2342+ FlushOrderByThenSkipByHeadInResult "," tail result
2344+-- Modifier word: consume it, continue
2346+ ( Symbol.Append h t rest
2347+ , ExtractWord rest word afterWord
2348+ , FlushOrderByWordInResult word result
2349+ , SkipSpaces afterWord rest'
2350+ , OrderByColumnsInResultContinue rest' result
2352+ FlushOrderByThenSkipByHeadInResult h t result
2354+class OrderByColumnsInResultContinue :: Symbol -> Row Type -> Constraint
2355+class OrderByColumnsInResultContinue sym result
2357+instance OrderByColumnsInResultContinue "" result
2359+ ( Symbol.Cons h t sym
2360+ , OrderByColumnsInResultContinueByHead h t result
2362+ OrderByColumnsInResultContinue sym result
2364+class OrderByColumnsInResultContinueByHead :: Symbol -> Symbol -> Row Type -> Constraint
2365+class OrderByColumnsInResultContinueByHead head tail result
2368+ ( SkipSpaces tail rest
2369+ , OrderByColumnsInResult rest result
2371+ OrderByColumnsInResultContinueByHead "," tail result
2374+ ( Symbol.Append h t rest
2375+ , ExtractWord rest word afterWord
2376+ , FlushOrderByWordInResult word result
2377+ , SkipSpaces afterWord rest'
2378+ , OrderByColumnsInResultContinue rest' result
2380+ OrderByColumnsInResultContinueByHead h t result
2382+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2383+-- ValidateColumns: comma-separated column names (for ON CONFLICT target)
2384+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2386+class ValidateColumns :: Symbol -> Row Type -> Constraint
2387+class ValidateColumns sym cols
2389+instance ValidateColumns "" cols
2391+ ( Symbol.Cons h t sym
2392+ , ValidateColumnsGo h t "" cols
2394+ ValidateColumns sym cols
2396+class ValidateColumnsGo :: Symbol -> Symbol -> Symbol -> Row Type -> Constraint
2397+class ValidateColumnsGo head tail acc cols
2399+-- Comma: flush name, continue
2401+ ( FlushColumnWord acc cols
2402+ , SkipSpaces tail rest
2403+ , ValidateColumns rest cols
2405+ ValidateColumnsGo "," tail acc cols
2407+-- Space: flush name, skip to comma or end
2409+ ( SkipSpaces tail rest
2410+ , ValidateAfterName acc rest cols
2412+ ValidateColumnsGo " " tail acc cols
2414+-- End of string: flush final name
2416+ ( Symbol.Append acc h acc'
2417+ , FlushColumnWord acc' cols
2419+ ValidateColumnsGo h "" acc cols
2421+-- Regular char: accumulate
2423+ ( Symbol.Append acc h acc'
2424+ , Symbol.Cons nextH nextT tail
2425+ , ValidateColumnsGo nextH nextT acc' cols
2427+ ValidateColumnsGo h tail acc cols
2429+class FlushColumnWord :: Symbol -> Row Type -> Constraint
2430+class FlushColumnWord word cols
2432+instance FlushColumnWord "" cols
2433+else instance FlushColumnWord "$" cols
2434+else instance FlushColumnWord "AND" cols
2435+else instance FlushColumnWord "OR" cols
2436+else instance FlushColumnWord "NOT" cols
2437+else instance FlushColumnWord "IS" cols
2438+else instance FlushColumnWord "NULL" cols
2439+else instance FlushColumnWord "LIKE" cols
2440+else instance FlushColumnWord "ILIKE" cols
2441+else instance FlushColumnWord "IN" cols
2442+else instance FlushColumnWord "TRUE" cols
2443+else instance FlushColumnWord "FALSE" cols
2444+else instance FlushColumnWord "BETWEEN" cols
2445+else instance FlushColumnWord "ANY" cols
2446+else instance FlushColumnWord "ALL" cols
2448+ ( Symbol.Cons head rest word
2449+ , FlushColumnWordByHead head word cols
2451+ FlushColumnWord word cols
2453+class FlushColumnWordByHead :: Symbol -> Symbol -> Row Type -> Constraint
2454+class FlushColumnWordByHead head word cols
2456+instance FlushColumnWordByHead "$" word cols
2457+else instance FlushColumnWordByHead "0" word cols
2458+else instance FlushColumnWordByHead "1" word cols
2459+else instance FlushColumnWordByHead "2" word cols
2460+else instance FlushColumnWordByHead "3" word cols
2461+else instance FlushColumnWordByHead "4" word cols
2462+else instance FlushColumnWordByHead "5" word cols
2463+else instance FlushColumnWordByHead "6" word cols
2464+else instance FlushColumnWordByHead "7" word cols
2465+else instance FlushColumnWordByHead "8" word cols
2466+else instance FlushColumnWordByHead "9" word cols
2467+else instance Row.Cons word typ rest cols => FlushColumnWordByHead head word cols
2469+ Fail (Beside (Beside (Text "Column ") (Quote word)) (Text " does not exist in the table")) =>
2470+ FlushColumnWordByHead head word cols
2472+class ValidateAfterName :: Symbol -> Symbol -> Row Type -> Constraint
2473+class ValidateAfterName acc rest cols
2475+instance FlushColumnWord acc cols => ValidateAfterName acc "" cols
2477+ ( FlushColumnWord acc cols
2478+ , Symbol.Cons h t rest
2479+ , ValidateAfterNameByHead h t cols
2481+ ValidateAfterName acc rest cols
2483+class ValidateAfterNameByHead :: Symbol -> Symbol -> Row Type -> Constraint
2484+class ValidateAfterNameByHead head tail cols
2487+ ( SkipSpaces tail rest
2488+ , ValidateColumns rest cols
2490+ ValidateAfterNameByHead "," tail cols
2493+ ( Symbol.Append h t rest
2494+ , ExtractWord rest word afterWord
2495+ , HandleAfterColumnWord word afterWord cols
2497+ ValidateAfterNameByHead h t cols
2499+class HandleAfterColumnWord :: Symbol -> Symbol -> Row Type -> Constraint
2500+class HandleAfterColumnWord word rest cols
2502+instance SkipAlias rest cols => HandleAfterColumnWord "AS" rest cols
2503+else instance SkipAlias rest cols => HandleAfterColumnWord "as" rest cols
2505+class SkipAlias :: Symbol -> Row Type -> Constraint
2506+class SkipAlias sym cols
2508+instance SkipAlias "" cols
2510+ ( ExtractWord sym _alias afterAlias
2511+ , SkipSpaces afterAlias rest
2512+ , ExpectCommaOrEnd rest cols
2516+class ExpectCommaOrEnd :: Symbol -> Row Type -> Constraint
2517+class ExpectCommaOrEnd sym cols
2519+instance ExpectCommaOrEnd "" cols
2521+ ( Symbol.Cons h t sym
2522+ , ExpectCommaOrEndByHead h t cols
2524+ ExpectCommaOrEnd sym cols
2526+class ExpectCommaOrEndByHead :: Symbol -> Symbol -> Row Type -> Constraint
2527+class ExpectCommaOrEndByHead head tail cols
2530+ ( SkipSpaces tail rest
2531+ , ValidateColumns rest cols
2533+ ExpectCommaOrEndByHead "," tail cols
2535+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2536+-- ValidateJoinCondition: validate ON clause column references
2537+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2539+class ValidateJoinCondition :: Symbol -> Row (Row Type) -> Constraint
2540+class ValidateJoinCondition sym tables
2542+instance ValidateJoinCondition "" tables
2544+ ( Symbol.Cons h t sym
2545+ , ValidateJoinCondGo h t "" tables
2547+ ValidateJoinCondition sym tables
2549+class ValidateJoinCondGo :: Symbol -> Symbol -> Symbol -> Row (Row Type) -> Constraint
2550+class ValidateJoinCondGo head tail acc tables
2552+-- Space: flush word, continue
2554+ ( FlushJoinWord acc tables
2555+ , SkipSpaces tail rest
2556+ , ValidateJoinCondition rest tables
2558+ ValidateJoinCondGo " " tail acc tables
2561+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "=" tail acc tables
2562+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ">" tail acc tables
2563+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "<" tail acc tables
2564+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "!" tail acc tables
2565+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo "(" tail acc tables
2566+else instance (FlushJoinWord acc tables, ValidateJoinCondition tail tables) => ValidateJoinCondGo ")" tail acc tables
2568+-- String literal: skip content between quotes
2570+ ( FlushJoinWord acc tables
2571+ , SkipStringLiteral tail rest
2572+ , ValidateJoinCondition rest tables
2574+ ValidateJoinCondGo "'" tail acc tables
2576+-- End of string: flush final word
2578+ ( Symbol.Append acc h acc'
2579+ , FlushJoinWord acc' tables
2581+ ValidateJoinCondGo h "" acc tables
2583+-- Regular char (including dot): accumulate
2585+ ( Symbol.Append acc h acc'
2586+ , Symbol.Cons nextH nextT tail
2587+ , ValidateJoinCondGo nextH nextT acc' tables
2589+ ValidateJoinCondGo h tail acc tables
2591+class FlushJoinWord :: Symbol -> Row (Row Type) -> Constraint
2592+class FlushJoinWord word tables
2594+instance FlushJoinWord "" tables
2595+else instance FlushJoinWord "AND" tables
2596+else instance FlushJoinWord "OR" tables
2597+else instance FlushJoinWord "NOT" tables
2598+else instance FlushJoinWord "IS" tables
2599+else instance FlushJoinWord "NULL" tables
2600+else instance FlushJoinWord "TRUE" tables
2601+else instance FlushJoinWord "FALSE" tables
2603+ ( Symbol.Cons head rest word
2604+ , FlushJoinWordByHead head word tables
2606+ FlushJoinWord word tables
2608+class FlushJoinWordByHead :: Symbol -> Symbol -> Row (Row Type) -> Constraint
2609+class FlushJoinWordByHead head word tables
2611+instance FlushJoinWordByHead "$" word tables
2612+else instance FlushJoinWordByHead "0" word tables
2613+else instance FlushJoinWordByHead "1" word tables
2614+else instance FlushJoinWordByHead "2" word tables
2615+else instance FlushJoinWordByHead "3" word tables
2616+else instance FlushJoinWordByHead "4" word tables
2617+else instance FlushJoinWordByHead "5" word tables
2618+else instance FlushJoinWordByHead "6" word tables
2619+else instance FlushJoinWordByHead "7" word tables
2620+else instance FlushJoinWordByHead "8" word tables
2621+else instance FlushJoinWordByHead "9" word tables
2622+else instance ResolveColumn word tables typ => FlushJoinWordByHead head word tables
2624+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2626+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2628+class ParseConflictAction :: Symbol -> Row Type -> Constraint
2629+class ParseConflictAction sym cols
2632+ ( ExtractWord sym w1 rest1
2633+ , ExpectKeyword w1 "DO"
2634+ , ExtractWord rest1 w2 rest2
2635+ , ExpectKeyword w2 "UPDATE"
2636+ , ExtractWord rest2 w3 rest3
2637+ , ExpectKeyword w3 "SET"
2638+ , ParseAssignments rest3 cols
2640+ ParseConflictAction sym cols
2642+class ExpectKeyword :: Symbol -> Symbol -> Constraint
2643+class ExpectKeyword actual expected
2645+instance ExpectKeyword a a
2647+ Fail (Beside (Beside (Text "Expected keyword ") (Quote expected)) (Beside (Text " but got ") (Quote actual))) =>
2648+ ExpectKeyword actual expected
2650+class ParseAssignments :: Symbol -> Row Type -> Constraint
2651+class ParseAssignments sym cols
2653+instance ParseAssignments "" cols
2655+ ( ExtractWord sym colName rest1
2656+ , Row.Cons colName colType colRest cols
2657+ , SkipSpaces rest1 rest2
2658+ , ExpectChar rest2 "=" rest3
2659+ , SkipSpaces rest3 rest4
2660+ , SkipExpression rest4 rest5
2661+ , ParseAssignmentsContinue rest5 cols
2663+ ParseAssignments sym cols
2665+-- | Skip a SQL expression on the RHS of an assignment.
2666+-- | Handles balanced parentheses so that e.g. now() and
2667+-- | COALESCE(EXCLUDED.value, '') are consumed as a single expression.
2668+-- | Stops at an unparenthesised comma or end-of-string.
2669+class SkipExpression :: Symbol -> Symbol -> Constraint
2670+class SkipExpression sym rest | sym -> rest
2672+instance SkipExpression "" ""
2674+ ( Symbol.Cons h t sym
2675+ , SkipExpressionGo h t rest
2677+ SkipExpression sym rest
2679+class SkipExpressionGo :: Symbol -> Symbol -> Symbol -> Constraint
2680+class SkipExpressionGo head tail rest | head tail -> rest
2682+-- Comma at depth 0 → stop, keep comma in rest
2683+instance Symbol.Cons "," tail rest => SkipExpressionGo "," tail rest
2684+-- Open paren → enter nested expression
2686+ ( SkipNested tail rest1
2687+ , SkipExpression rest1 rest
2689+ SkipExpressionGo "(" tail rest
2690+-- Space → skip and continue (expressions can have spaces, e.g. "EXCLUDED.col")
2691+else instance (SkipSpaces tail rest1, SkipExpressionAfterSpace rest1 rest) => SkipExpressionGo " " tail rest
2692+-- End of string after this char
2693+else instance SkipExpressionGo h "" ""
2694+-- Regular char → continue
2695+else instance (Symbol.Cons h2 t2 tail, SkipExpressionGo h2 t2 rest) => SkipExpressionGo h tail rest
2697+-- | After a space in an expression, check if we've hit a comma or a new assignment (word = ...).
2698+-- | If the next word is a column name followed by '=', this is a new assignment, so stop.
2699+-- | Otherwise, continue the expression.
2700+class SkipExpressionAfterSpace :: Symbol -> Symbol -> Constraint
2701+class SkipExpressionAfterSpace sym rest | sym -> rest
2703+instance SkipExpressionAfterSpace "" ""
2705+ ( Symbol.Cons h t sym
2706+ , SkipExpressionAfterSpaceGo h t sym rest
2708+ SkipExpressionAfterSpace sym rest
2710+class SkipExpressionAfterSpaceGo :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
2711+class SkipExpressionAfterSpaceGo head tail sym rest | head tail sym -> rest
2714+instance Symbol.Cons "," tail rest => SkipExpressionAfterSpaceGo "," tail sym rest
2715+-- Otherwise continue the expression
2716+else instance (SkipExpressionGo h tail rest) => SkipExpressionAfterSpaceGo h tail sym rest
2718+-- | Skip a parenthesised group (after the opening paren has been consumed).
2719+-- | Handles nesting.
2720+class SkipNested :: Symbol -> Symbol -> Constraint
2721+class SkipNested sym rest | sym -> rest
2724+ ( Symbol.Cons h t sym
2725+ , SkipNestedGo h t rest
2727+ SkipNested sym rest
2729+class SkipNestedGo :: Symbol -> Symbol -> Symbol -> Constraint
2730+class SkipNestedGo head tail rest | head tail -> rest
2732+-- Close paren → done
2733+instance SkipNestedGo ")" tail tail
2734+-- Open paren → recurse
2735+else instance (SkipNested tail rest1, SkipNested rest1 rest) => SkipNestedGo "(" tail rest
2736+-- Any other char → continue
2737+else instance (SkipNested tail rest) => SkipNestedGo h tail rest
2739+class ParseAssignmentsContinue :: Symbol -> Row Type -> Constraint
2740+class ParseAssignmentsContinue sym cols
2742+instance ParseAssignmentsContinue "" cols
2744+ ( Symbol.Cons h t sym
2745+ , ParseAssignmentsContinueByHead h t cols
2747+ ParseAssignmentsContinue sym cols
2749+class ParseAssignmentsContinueByHead :: Symbol -> Symbol -> Row Type -> Constraint
2750+class ParseAssignmentsContinueByHead head tail cols
2753+ ( SkipSpaces tail rest
2754+ , ParseAssignments rest cols
2756+ ParseAssignmentsContinueByHead "," tail cols
2758+class ExpectChar :: Symbol -> Symbol -> Symbol -> Constraint
2759+class ExpectChar sym char rest | sym char -> rest
2762+ ( Symbol.Cons h t sym
2763+ , ExpectCharMatch h t char rest
2765+ ExpectChar sym char rest
2767+class ExpectCharMatch :: Symbol -> Symbol -> Symbol -> Symbol -> Constraint
2768+class ExpectCharMatch head tail expected rest | head tail expected -> rest
2770+instance ExpectCharMatch c tail c tail
2772+ Fail (Beside (Beside (Text "Expected '") (Quote expected)) (Beside (Text "' but got '") (Quote head))) =>
2773+ ExpectCharMatch head tail expected rest
2775+class ValidateExcludedRef :: Symbol -> Symbol -> Constraint
2776+class ValidateExcludedRef ref colName
2779+ ( Symbol.Append "EXCLUDED." colName expected
2780+ , MatchSymbol ref expected
2782+ ValidateExcludedRef ref colName
2784+class MatchSymbol :: Symbol -> Symbol -> Constraint
2785+class MatchSymbol a b
2787+instance MatchSymbol a a
2789+ Fail (Beside (Beside (Text "Expected ") (Quote expected)) (Beside (Text " but got ") (Quote actual))) =>
2790+ MatchSymbol actual expected
2792+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2793+-- Type-level IsAutoGenerated (Boolean kind)
2794+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2796+class IsAutoGeneratedTC :: Type -> Boolean -> Constraint
2797+class IsAutoGeneratedTC constraints result | constraints -> result
2799+instance IsAutoGeneratedTC (AutoIncrement a) True
2800+else instance IsAutoGeneratedTC (Default s a) True
2801+else instance IsAutoGeneratedTC (DefaultExpr expression a) True
2802+else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (PrimaryKey a) result
2803+else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Unique a) result
2804+else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (ForeignKey t r c a) result
2805+else instance IsAutoGeneratedTC a result => IsAutoGeneratedTC (Nullable a) result
2806+else instance IsAutoGeneratedTC a False
2808+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2809+-- InsertableColumnsRL: filter out auto-generated columns
2810+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2812+class InsertableColumnsRL :: RL.RowList Type -> RL.RowList Type -> Constraint
2813+class InsertableColumnsRL tableRL outRL | tableRL -> outRL
2815+instance InsertableColumnsRL RL.Nil RL.Nil
2817+ ( IsAutoGeneratedTC entry isAuto
2818+ , ExtractType entry typ
2819+ , InsertableColumnDecide isAuto name typ tail outRL
2821+ InsertableColumnsRL (RL.Cons name entry tail) outRL
2823+class InsertableColumnDecide :: Boolean -> Symbol -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
2824+class InsertableColumnDecide isAuto name typ tail outRL | isAuto name typ tail -> outRL
2826+instance InsertableColumnsRL tail outRL => InsertableColumnDecide True name typ tail outRL
2827+instance InsertableColumnsRL tail outRL => InsertableColumnDecide False name typ tail (RL.Cons name typ outRL)
2829+-- Split insertable columns into required (non-Maybe) and optional (Maybe)
2830+class RequiredColumnsRL :: RL.RowList Type -> RL.RowList Type -> Constraint
2831+class RequiredColumnsRL rl out | rl -> out
2833+instance RequiredColumnsRL RL.Nil RL.Nil
2834+instance RequiredColumnsRL tail out => RequiredColumnsRL (RL.Cons name (Maybe a) tail) out
2835+else instance RequiredColumnsRL tail out => RequiredColumnsRL (RL.Cons name typ tail) (RL.Cons name typ out)
2837+-- Generate column names from a RowList
2838+class ColumnNamesRL :: RL.RowList Type -> Constraint
2839+class ColumnNamesRL rl where
2840+ columnNamesRL :: Proxy rl -> Array String
2842+instance ColumnNamesRL RL.Nil where
2843+ columnNamesRL _ = []
2845+instance (IsSymbol name, ColumnNamesRL tail) => ColumnNamesRL (RL.Cons name typ tail) where
2846+ columnNamesRL _ = [ reflectSymbol (Proxy :: Proxy name) ] <> columnNamesRL (Proxy :: Proxy tail)
2848+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2849+-- RecordValuesRL: extract record values in RowList order
2850+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2852+class FieldToPGValue :: Type -> Constraint
2853+class FieldToPGValue a where
2854+ fieldToPGValue :: a -> PG.PGValue
2856+instance FieldToPGValue a => FieldToPGValue (Maybe a) where
2857+ fieldToPGValue = case _ of
2858+ Nothing -> unsafeCoerce (toNullable Nothing)
2859+ Just value -> fieldToPGValue value
2860+else instance FieldToPGValue DateTime where
2861+ fieldToPGValue = JSDate.fromDateTime >>> unsafeCoerce
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
2867+ fieldToPGValue = unsafeCoerce
2868+else instance WriteForeign a => FieldToPGValue a where
2869+ fieldToPGValue = writeImpl >>> unsafeCoerce
2871+class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
2872+class RecordValuesRL rl row where
2873+ recordValuesRL :: Proxy rl -> { | row } -> Array PG.PGValue
2875+instance RecordValuesRL RL.Nil row where
2876+ recordValuesRL _ _ = []
2880+ , Row.Cons name typ rest row
2881+ , FieldToPGValue typ
2882+ , RecordValuesRL tail row
2884+ RecordValuesRL (RL.Cons name typ tail) row where
2885+ recordValuesRL _ rec =
2886+ [ fieldToPGValue (Record.get (Proxy :: Proxy name) rec) ]
2887+ <> recordValuesRL (Proxy :: Proxy tail) rec
2889+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2890+-- MakeNullableRL: wrap column types in Maybe for LEFT JOIN
2891+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2893+class MakeNullableRL :: RL.RowList Type -> RL.RowList Type -> Constraint
2894+class MakeNullableRL rl out | rl -> out
2896+instance MakeNullableRL RL.Nil RL.Nil
2898+ ( ExtractType entry typ
2899+ , MakeNullableDecide typ name entry tail out
2901+ MakeNullableRL (RL.Cons name entry tail) out
2903+class MakeNullableDecide :: Type -> Symbol -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
2904+class MakeNullableDecide typ name entry tail out | typ name entry tail -> out
2906+-- Already nullable: keep as-is
2907+instance MakeNullableRL tail out' => MakeNullableDecide (Maybe a) name entry tail (RL.Cons name entry out')
2908+-- Not nullable: wrap in Nullable
2909+else instance MakeNullableRL tail out' => MakeNullableDecide typ name entry tail (RL.Cons name (Nullable entry) out')
2911+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2912+-- Query builder: unified Q type
2913+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
2915+newtype Q :: Row (Row Type) -> Row Type -> Row Type -> Row Type -> Type
2916+newtype Q tables result params stage = Q { sql :: String, values :: Array PG.PGValue }
2918+class HasClause :: Symbol -> Row Type -> Constraint
2919+class HasClause label row
2921+instance Row.Cons label Unit rest row => HasClause label row
2923+class HasAnyDML :: Row Type -> Constraint
2924+class HasAnyDML stage
2926+instance (RL.RowToList stage rl, HasAnyDMLRL rl) => HasAnyDML stage
2928+class HasAnyDMLRL :: RL.RowList Type -> Constraint
2929+class HasAnyDMLRL rl
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
2937+class HasMutation :: Row Type -> Constraint
2938+class HasMutation stage
2940+instance (RL.RowToList stage rl, HasMutationRL rl) => HasMutation stage
2942+class HasMutationRL :: RL.RowList Type -> Constraint
2943+class HasMutationRL rl
2945+instance HasMutationRL (RL.Cons "insert" Unit rest)
2946+else instance HasMutationRL (RL.Cons "set" Unit rest)
2947+else instance HasMutationRL (RL.Cons "delete" Unit rest)
2948+else instance HasMutationRL rest => HasMutationRL (RL.Cons label typ rest)
2949+else instance Fail (Text "RETURNING requires a preceding INSERT, UPDATE (set), or DELETE") => HasMutationRL RL.Nil
2951+toSQL :: forall tables result params stage. Q tables result params stage -> String
2954+from :: forall name cols tables. IsSymbol name => Row.Cons name cols () tables => Proxy (Table name cols) -> Q tables () () ()
2955+from _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2957+fromAs :: forall @alias name cols tables. IsSymbol name => IsSymbol alias => Row.Cons alias cols () tables => Proxy (Table name cols) -> Q tables () () ()
2958+fromAs _ = Q { sql: reflectSymbol (Proxy :: Proxy name) <> " " <> reflectSymbol (Proxy :: Proxy alias), values: [] }
2960+into :: forall name cols tables. IsSymbol name => Row.Cons name cols () tables => Proxy (Table name cols) -> Q tables () () (into :: Unit)
2961+into _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2963+update :: forall name cols tables. IsSymbol name => Row.Cons name cols () tables => Proxy (Table name cols) -> Q tables () () (update :: Unit)
2964+update _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
2967+ :: forall tables name cols result r p stage stage'
2968+ . SingleTable tables name cols
2970+ => StripColumns cols result
2971+ => Row.Lacks "select" stage
2972+ => Row.Lacks "insert" stage
2973+ => Row.Lacks "into" stage
2974+ => Row.Lacks "set" stage
2975+ => Row.Lacks "delete" stage
2976+ => Row.Lacks "where" stage
2977+ => Row.Lacks "orderBy" stage
2978+ => Row.Lacks "limit" stage
2979+ => Row.Lacks "offset" stage
2980+ => Row.Cons "select" Unit stage stage'
2981+ => Q tables r p stage
2982+ -> Q tables result p stage'
2983+selectAll (Q q) = Q (q { sql = "SELECT * FROM " <> q.sql })
2986+ :: forall @sel tables result r p stage stage'
2988+ => ParseSelect sel tables result
2989+ => Row.Lacks "select" stage
2990+ => Row.Lacks "insert" stage
2991+ => Row.Lacks "into" stage
2992+ => Row.Lacks "set" stage
2993+ => Row.Lacks "delete" stage
2994+ => Row.Lacks "where" stage
2995+ => Row.Lacks "orderBy" stage
2996+ => Row.Lacks "limit" stage
2997+ => Row.Lacks "offset" stage
2998+ => Row.Cons "select" Unit stage stage'
2999+ => Q tables r p stage
3000+ -> Q tables result p stage'
3001+select (Q q) = Q (q { sql = "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
3004+ :: forall @sel tables result r p stage stage' stage''
3006+ => ParseSelect sel tables result
3007+ => Row.Lacks "select" stage
3008+ => Row.Lacks "insert" stage
3009+ => Row.Lacks "into" stage
3010+ => Row.Lacks "set" stage
3011+ => Row.Lacks "delete" stage
3012+ => Row.Lacks "where" stage
3013+ => Row.Lacks "orderBy" stage
3014+ => Row.Lacks "limit" stage
3015+ => Row.Lacks "offset" stage
3016+ => Row.Cons "select" Unit stage stage'
3017+ => Row.Cons "distinct" Unit stage' stage''
3018+ => Q tables r p stage
3019+ -> Q tables result p stage''
3020+selectDistinct (Q q) = Q (q { sql = "SELECT DISTINCT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
3023+ :: forall @on @sel tables result r p stage stage'
3026+ => ValidateColumnList on tables
3027+ => ParseSelect sel tables result
3028+ => Row.Lacks "select" stage
3029+ => Row.Lacks "insert" stage
3030+ => Row.Lacks "into" stage
3031+ => Row.Lacks "set" stage
3032+ => Row.Lacks "delete" stage
3033+ => Row.Lacks "where" stage
3034+ => Row.Lacks "orderBy" stage
3035+ => Row.Lacks "limit" stage
3036+ => Row.Lacks "offset" stage
3037+ => Row.Cons "select" Unit stage stage'
3038+ => Q tables r p stage
3039+ -> Q tables result p stage'
3040+selectDistinctOn (Q q) = Q (q { sql = "SELECT DISTINCT ON (" <> reflectSymbol (Proxy :: Proxy on) <> ") " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> q.sql })
3043+ :: forall @whr tables result params p stage stage'
3045+ => ParseWhere whr tables params
3047+ => Row.Lacks "where" stage
3048+ => Row.Lacks "insert" stage
3049+ => Row.Lacks "groupBy" stage
3050+ => Row.Lacks "having" stage
3051+ => Row.Lacks "orderBy" stage
3052+ => Row.Lacks "limit" stage
3053+ => Row.Lacks "offset" stage
3054+ => Row.Cons "where" Unit stage stage'
3055+ => Q tables result p stage
3056+ -> Q tables result params stage'
3057+where_ (Q q) = Q (q { sql = q.sql <> " WHERE " <> reflectSymbol (Proxy :: Proxy whr) })
3060+ :: forall @cols tables result params stage stage'
3062+ => ValidateOrderBy cols tables
3063+ => CheckDistinctOrderBy stage cols result
3064+ => HasClause "select" stage
3065+ => Row.Lacks "orderBy" stage
3066+ => Row.Lacks "limit" stage
3067+ => Row.Lacks "offset" stage
3068+ => Row.Cons "orderBy" Unit stage stage'
3069+ => Q tables result params stage
3070+ -> Q tables result params stage'
3071+orderBy (Q q) = Q (q { sql = q.sql <> " ORDER BY " <> reflectSymbol (Proxy :: Proxy cols) })
3074+ :: forall @cols tables result params stage stage'
3076+ => ValidateColumnList cols tables
3077+ => HasClause "select" stage
3078+ => Row.Lacks "groupBy" stage
3079+ => Row.Lacks "having" stage
3080+ => Row.Lacks "orderBy" stage
3081+ => Row.Lacks "limit" stage
3082+ => Row.Lacks "offset" stage
3083+ => Row.Cons "groupBy" Unit stage stage'
3084+ => Q tables result params stage
3085+ -> Q tables result params stage'
3086+groupBy (Q q) = Q (q { sql = q.sql <> " GROUP BY " <> reflectSymbol (Proxy :: Proxy cols) })
3089+ :: forall @cond tables result params havingParams allParams stage stage'
3091+ => ParseWhere cond tables havingParams
3092+ => Row.Union params havingParams allParams
3093+ => Row.Nub allParams allParams
3094+ => HasClause "groupBy" stage
3095+ => Row.Lacks "having" stage
3096+ => Row.Lacks "orderBy" stage
3097+ => Row.Lacks "limit" stage
3098+ => Row.Lacks "offset" stage
3099+ => Row.Cons "having" Unit stage stage'
3100+ => Q tables result params stage
3101+ -> Q tables result allParams stage'
3102+having (Q q) = Q (q { sql = q.sql <> " HAVING " <> reflectSymbol (Proxy :: Proxy cond) })
3104+class IsLimitParam :: Symbol -> Symbol -> Boolean -> Constraint
3105+class IsLimitParam head tail isParam | head tail -> isParam
3107+instance IsLimitParam "$" tail True
3108+else instance IsLimitParam head tail False
3110+class ParseLimitOffsetParams :: Boolean -> Symbol -> Row Type -> Row Type -> Constraint
3111+class ParseLimitOffsetParams isParam name params params' | isParam name params -> params'
3114+ ( Row.Lacks name params
3115+ , Row.Cons name Int params params'
3117+ ParseLimitOffsetParams True name params params'
3119+instance ParseLimitOffsetParams False name params params
3121+-- Validate that a Symbol is a non-negative integer (all digits, non-empty)
3122+class ValidateNumericLiteral :: Symbol -> Constraint
3123+class ValidateNumericLiteral sym
3126+ ( Symbol.Cons h t sym
3127+ , ValidateAllDigits h t
3129+ ValidateNumericLiteral sym
3131+class ValidateAllDigits :: Symbol -> Symbol -> Constraint
3132+class ValidateAllDigits head tail
3134+instance ValidateAllDigits "0" ""
3135+else instance ValidateAllDigits "1" ""
3136+else instance ValidateAllDigits "2" ""
3137+else instance ValidateAllDigits "3" ""
3138+else instance ValidateAllDigits "4" ""
3139+else instance ValidateAllDigits "5" ""
3140+else instance ValidateAllDigits "6" ""
3141+else instance ValidateAllDigits "7" ""
3142+else instance ValidateAllDigits "8" ""
3143+else instance ValidateAllDigits "9" ""
3144+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "0" tail
3145+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "1" tail
3146+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "2" tail
3147+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "3" tail
3148+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "4" tail
3149+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "5" tail
3150+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "6" tail
3151+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "7" tail
3152+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "8" tail
3153+else instance (Symbol.Cons h2 t2 tail, ValidateAllDigits h2 t2) => ValidateAllDigits "9" tail
3155+ Fail (Beside (Text "LIMIT/OFFSET must be a non-negative integer or $parameter, got: ") (Quote sym)) =>
3156+ ValidateAllDigits head tail
3158+class ParseLimitOffset :: Symbol -> Row Type -> Row Type -> Constraint
3159+class ParseLimitOffset sym params params' | sym params -> params'
3162+ ( Symbol.Cons head tail sym
3163+ , IsLimitParam head tail isParam
3164+ , ParseLimitOffsetBranch isParam sym tail params params'
3166+ ParseLimitOffset sym params params'
3168+class ParseLimitOffsetBranch :: Boolean -> Symbol -> Symbol -> Row Type -> Row Type -> Constraint
3169+class ParseLimitOffsetBranch isParam sym paramName params params' | isParam sym paramName params -> params'
3172+ ( Row.Lacks paramName params
3173+ , Row.Cons paramName Int params params'
3175+ ParseLimitOffsetBranch True sym paramName params params'
3178+ ValidateNumericLiteral sym =>
3179+ ParseLimitOffsetBranch False sym paramName params params
3182+ :: forall @sym tables result params params' stage stage'
3183+ . ParseLimitOffset sym params params'
3185+ => HasClause "select" stage
3186+ => Row.Lacks "limit" stage
3187+ => Row.Cons "limit" Unit stage stage'
3188+ => Q tables result params stage
3189+ -> Q tables result params' stage'
3190+limit (Q q) = Q (q { sql = q.sql <> " LIMIT " <> reflectSymbol (Proxy :: Proxy sym) })
3193+ :: forall @sym tables result params params' stage stage'
3194+ . ParseLimitOffset sym params params'
3196+ => HasClause "select" stage
3197+ => Row.Lacks "offset" stage
3198+ => Row.Cons "offset" Unit stage stage'
3199+ => Q tables result params stage
3200+ -> Q tables result params' stage'
3201+offset (Q q) = Q (q { sql = q.sql <> " OFFSET " <> reflectSymbol (Proxy :: Proxy sym) })
3203+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3205+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3208+ :: forall tables name cols colsRL insertableRL insertable requiredRL required
3209+ optionalProvided missing userRow userRowRL stage stage'
3210+ . SingleTable tables name cols
3211+ => RowToList cols colsRL
3212+ => InsertableColumnsRL colsRL insertableRL
3213+ => ListToRow insertableRL insertable
3214+ => RequiredColumnsRL insertableRL requiredRL
3215+ => ListToRow requiredRL required
3216+ => Row.Union required optionalProvided userRow
3217+ => Row.Union userRow missing insertable
3218+ => RowToList userRow userRowRL
3219+ => ColumnNamesRL userRowRL
3220+ => RecordValuesRL userRowRL userRow
3222+ => Row.Lacks "select" stage
3223+ => Row.Lacks "insert" stage
3224+ => Row.Lacks "set" stage
3225+ => Row.Lacks "delete" stage
3226+ => Row.Cons "insert" Unit stage stage'
3228+ -> Q tables () () stage
3229+ -> Q tables () () stage'
3230+insert rec _ = Q { sql, values }
3232+ tableName = reflectSymbol (Proxy :: Proxy name)
3233+ colNames = columnNamesRL (Proxy :: Proxy userRowRL)
3234+ placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
3236+ if Array.null colNames then
3237+ "INSERT INTO " <> tableName <> " DEFAULT VALUES"
3239+ "INSERT INTO " <> tableName
3241+ <> intercalate ", " colNames
3244+ <> intercalate ", " placeholders
3246+ values = recordValuesRL (Proxy :: Proxy userRowRL) rec
3248+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3250+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3253+ :: forall @sel tables name cols result p stage stage'
3254+ . SingleTable tables name cols
3256+ => ParseSelect sel tables result
3257+ => HasMutation stage
3258+ => Row.Lacks "returning" stage
3259+ => Row.Lacks "select" stage
3260+ => Row.Cons "returning" Unit stage stage'
3261+ => Q tables () p stage
3262+ -> Q tables result p stage'
3263+returning (Q q) = Q (q { sql = q.sql <> " RETURNING " <> reflectSymbol (Proxy :: Proxy sel) })
3266+ :: forall tables name cols result p stage stage'
3267+ . SingleTable tables name cols
3268+ => StripColumns cols result
3269+ => HasMutation stage
3270+ => Row.Lacks "returning" stage
3271+ => Row.Lacks "select" stage
3272+ => Row.Cons "returning" Unit stage stage'
3273+ => Q tables () p stage
3274+ -> Q tables result p stage'
3275+returningAll (Q q) = Q (q { sql = q.sql <> " RETURNING *" })
3277+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3278+-- UPDATE builder (SET)
3279+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3281+class ValidateSetColumnsRL :: RL.RowList Type -> Row Type -> Constraint
3282+class ValidateSetColumnsRL rl cols
3284+instance ValidateSetColumnsRL RL.Nil cols
3286+ ( Row.Cons name entry rest cols
3287+ , ExtractType entry typ
3288+ , ValidateSetColumnsRL tail cols
3290+ ValidateSetColumnsRL (RL.Cons name typ tail) cols
3292+class SetClauseRL :: RL.RowList Type -> Constraint
3293+class SetClauseRL rl where
3294+ setClauseRL :: Proxy rl -> Int -> Array String
3296+instance SetClauseRL RL.Nil where
3297+ setClauseRL _ _ = []
3299+instance (IsSymbol name, SetClauseRL tail) => SetClauseRL (RL.Cons name typ tail) where
3300+ setClauseRL _ idx =
3301+ [ reflectSymbol (Proxy :: Proxy name) <> " = $" <> show idx ]
3302+ <> setClauseRL (Proxy :: Proxy tail) (idx + 1)
3305+ :: forall tables name cols setRow setRL stage stage'
3306+ . SingleTable tables name cols
3307+ => RowToList setRow setRL
3309+ => ValidateSetColumnsRL setRL cols
3310+ => SetClauseRL setRL
3311+ => RecordValuesRL setRL setRow
3312+ => HasClause "update" stage
3313+ => Row.Lacks "select" stage
3314+ => Row.Lacks "insert" stage
3315+ => Row.Lacks "into" stage
3316+ => Row.Lacks "set" stage
3317+ => Row.Lacks "delete" stage
3318+ => Row.Cons "set" Unit stage stage'
3320+ -> Q tables () () stage
3321+ -> Q tables () () stage'
3322+set rec _ = Q { sql, values }
3324+ tableName = reflectSymbol (Proxy :: Proxy name)
3325+ setClauses = setClauseRL (Proxy :: Proxy setRL) 1
3326+ sql = "UPDATE " <> tableName <> " SET " <> intercalate ", " setClauses
3327+ values = recordValuesRL (Proxy :: Proxy setRL) rec
3329+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3331+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3334+ :: forall tables name cols r p stage stage'
3335+ . SingleTable tables name cols
3337+ => Row.Lacks "select" stage
3338+ => Row.Lacks "insert" stage
3339+ => Row.Lacks "into" stage
3340+ => Row.Lacks "set" stage
3341+ => Row.Lacks "delete" stage
3342+ => Row.Cons "delete" Unit stage stage'
3343+ => Q tables r p stage
3344+ -> Q tables () () stage'
3345+delete _ = Q { sql: "DELETE FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
3347+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3349+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3352+ :: forall @target @action tables name cols result params stage stage'
3353+ . SingleTable tables name cols
3356+ => ValidateColumns target cols
3357+ => ParseConflictAction action cols
3358+ => HasClause "insert" stage
3359+ => Row.Lacks "conflict" stage
3360+ => Row.Cons "conflict" Unit stage stage'
3361+ => Q tables result params stage
3362+ -> Q tables result params stage'
3363+onConflict (Q q) = Q
3366+ <> " ON CONFLICT ("
3367+ <> reflectSymbol (Proxy :: Proxy target)
3369+ <> reflectSymbol (Proxy :: Proxy action)
3374+ :: forall @target tables name cols result params stage stage'
3375+ . SingleTable tables name cols
3377+ => ValidateColumns target cols
3378+ => HasClause "insert" stage
3379+ => Row.Lacks "conflict" stage
3380+ => Row.Cons "conflict" Unit stage stage'
3381+ => Q tables result params stage
3382+ -> Q tables result params stage'
3383+onConflictDoNothing (Q q) = Q
3386+ <> " ON CONFLICT ("
3387+ <> reflectSymbol (Proxy :: Proxy target)
3392+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3394+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3397+ :: forall @cond name cols tables tables' r p stage
3400+ => Row.Lacks name tables
3401+ => Row.Cons name cols tables tables'
3402+ => ValidateJoinCondition cond tables'
3403+ => Row.Lacks "select" stage
3404+ => Row.Lacks "insert" stage
3405+ => Row.Lacks "set" stage
3406+ => Row.Lacks "delete" stage
3407+ => Proxy (Table name cols)
3408+ -> Q tables r p stage
3409+ -> Q tables' () () (join :: Unit)
3410+innerJoin _ (Q q) = Q
3413+ <> reflectSymbol (Proxy :: Proxy name)
3415+ <> reflectSymbol (Proxy :: Proxy cond)
3420+ :: forall @cond name cols colsRL nullableColsRL nullableCols tables tables' r p stage
3423+ => RowToList cols colsRL
3424+ => MakeNullableRL colsRL nullableColsRL
3425+ => ListToRow nullableColsRL nullableCols
3426+ => Row.Lacks name tables
3427+ => Row.Cons name nullableCols tables tables'
3428+ => ValidateJoinCondition cond tables'
3429+ => Row.Lacks "select" stage
3430+ => Row.Lacks "insert" stage
3431+ => Row.Lacks "set" stage
3432+ => Row.Lacks "delete" stage
3433+ => Proxy (Table name cols)
3434+ -> Q tables r p stage
3435+ -> Q tables' () () (join :: Unit)
3436+leftJoin _ (Q q) = Q
3439+ <> reflectSymbol (Proxy :: Proxy name)
3441+ <> reflectSymbol (Proxy :: Proxy cond)
3446+ :: forall @alias @cond name cols tables tables' r p stage
3450+ => Row.Lacks alias tables
3451+ => Row.Cons alias cols tables tables'
3452+ => ValidateJoinCondition cond tables'
3453+ => Row.Lacks "select" stage
3454+ => Row.Lacks "insert" stage
3455+ => Row.Lacks "set" stage
3456+ => Row.Lacks "delete" stage
3457+ => Proxy (Table name cols)
3458+ -> Q tables r p stage
3459+ -> Q tables' () () (join :: Unit)
3460+innerJoinAs _ (Q q) = Q
3463+ <> reflectSymbol (Proxy :: Proxy name)
3465+ <> reflectSymbol (Proxy :: Proxy alias)
3467+ <> reflectSymbol (Proxy :: Proxy cond)
3472+ :: forall @alias @cond name cols colsRL nullableColsRL nullableCols tables tables' r p stage
3476+ => RowToList cols colsRL
3477+ => MakeNullableRL colsRL nullableColsRL
3478+ => ListToRow nullableColsRL nullableCols
3479+ => Row.Lacks alias tables
3480+ => Row.Cons alias nullableCols tables tables'
3481+ => ValidateJoinCondition cond tables'
3482+ => Row.Lacks "select" stage
3483+ => Row.Lacks "insert" stage
3484+ => Row.Lacks "set" stage
3485+ => Row.Lacks "delete" stage
3486+ => Proxy (Table name cols)
3487+ -> Q tables r p stage
3488+ -> Q tables' () () (join :: Unit)
3489+leftJoinAs _ (Q q) = Q
3492+ <> reflectSymbol (Proxy :: Proxy name)
3494+ <> reflectSymbol (Proxy :: Proxy alias)
3496+ <> reflectSymbol (Proxy :: Proxy cond)
3500+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3501+-- Set operations: UNION, INTERSECT, EXCEPT
3502+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3515+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3516+ . HasClause "select" stage1
3517+ => HasClause "select" stage2
3518+ => Row.Union params1 params2 params
3519+ => Row.Nub params params
3520+ => Q tables1 result params1 stage1
3521+ -> Q tables2 result params2 stage2
3522+ -> Q tables1 result params SetOpStage
3523+union (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") UNION (" <> q2.sql <> ")", values: q1.values <> q2.values }
3526+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3527+ . HasClause "select" stage1
3528+ => HasClause "select" stage2
3529+ => Row.Union params1 params2 params
3530+ => Row.Nub params params
3531+ => Q tables1 result params1 stage1
3532+ -> Q tables2 result params2 stage2
3533+ -> Q tables1 result params SetOpStage
3534+unionAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") UNION ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
3537+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3538+ . HasClause "select" stage1
3539+ => HasClause "select" stage2
3540+ => Row.Union params1 params2 params
3541+ => Row.Nub params params
3542+ => Q tables1 result params1 stage1
3543+ -> Q tables2 result params2 stage2
3544+ -> Q tables1 result params SetOpStage
3545+intersect (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") INTERSECT (" <> q2.sql <> ")", values: q1.values <> q2.values }
3548+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3549+ . HasClause "select" stage1
3550+ => HasClause "select" stage2
3551+ => Row.Union params1 params2 params
3552+ => Row.Nub params params
3553+ => Q tables1 result params1 stage1
3554+ -> Q tables2 result params2 stage2
3555+ -> Q tables1 result params SetOpStage
3556+intersectAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") INTERSECT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
3559+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3560+ . HasClause "select" stage1
3561+ => HasClause "select" stage2
3562+ => Row.Union params1 params2 params
3563+ => Row.Nub params params
3564+ => Q tables1 result params1 stage1
3565+ -> Q tables2 result params2 stage2
3566+ -> Q tables1 result params SetOpStage
3567+except_ (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT (" <> q2.sql <> ")", values: q1.values <> q2.values }
3570+ :: forall tables1 tables2 result params1 params2 params stage1 stage2
3571+ . HasClause "select" stage1
3572+ => HasClause "select" stage2
3573+ => Row.Union params1 params2 params
3574+ => Row.Nub params params
3575+ => Q tables1 result params1 stage1
3576+ -> Q tables2 result params2 stage2
3577+ -> Q tables1 result params SetOpStage
3578+exceptAll (Q q1) (Q q2) = Q { sql: "(" <> q1.sql <> ") EXCEPT ALL (" <> q2.sql <> ")", values: q1.values <> q2.values }
3580+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3581+-- CTE (WITH clause) support
3582+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3584+newtype WithCTEs :: Row (Row Type) -> Row Type -> Type
3585+newtype WithCTEs cteTables cteParams = WithCTEs
3586+ { sql :: String, values :: Array PG.PGValue }
3588+-- Wrap a complete SELECT query as a named CTE
3590+ :: forall @name tables result params stage cteTables
3592+ => HasClause "select" stage
3593+ => Row.Cons name result () cteTables
3594+ => Q tables result params stage
3595+ -> WithCTEs cteTables params
3596+cte (Q q) = WithCTEs
3597+ { sql: reflectSymbol (Proxy :: Proxy name) <> " AS (" <> q.sql <> ")"
3601+-- Add another named CTE to an accumulator
3603+ :: forall @name tables result params stage cteTables cteParams cteTables' cteParams'
3605+ => HasClause "select" stage
3606+ => Row.Lacks name cteTables
3607+ => Row.Cons name result cteTables cteTables'
3608+ => Row.Union cteParams params cteParams'
3609+ => Row.Nub cteParams' cteParams'
3610+ => Q tables result params stage
3611+ -> WithCTEs cteTables cteParams
3612+ -> WithCTEs cteTables' cteParams'
3613+andCTE (Q q) (WithCTEs w) = WithCTEs
3614+ { sql: w.sql <> ", " <> reflectSymbol (Proxy :: Proxy name) <> " AS (" <> q.sql <> ")"
3615+ , values: w.values <> q.values
3618+-- Start main query FROM a named CTE; cteTables becomes available as tables
3620+ :: forall @name result restTables cteTables cteParams
3622+ => Row.Cons name result restTables cteTables
3623+ => WithCTEs cteTables cteParams
3624+ -> Q cteTables () () ()
3625+fromCTE _ = Q { sql: reflectSymbol (Proxy :: Proxy name), values: [] }
3627+-- Finalize: prepend WITH clause and merge params
3629+ :: forall cteTables cteParams mainParams allParams result stage
3630+ . Row.Union cteParams mainParams allParams
3631+ => Row.Nub allParams allParams
3632+ => WithCTEs cteTables cteParams
3633+ -> Q cteTables result mainParams stage
3634+ -> Q cteTables result allParams stage
3635+with_ (WithCTEs w) (Q q) = Q
3636+ { sql: "WITH " <> w.sql <> " " <> q.sql
3637+ , values: w.values <> q.values
3640+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3642+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
3644+class ParamsToArray :: RL.RowList Type -> Row Type -> Constraint
3645+class ParamsToArray rl row where
3646+ paramsToArray :: Proxy rl -> { | row } -> Array { name :: String, value :: Foreign }
3648+instance ParamsToArray RL.Nil row where
3649+ paramsToArray _ _ = []
3653+ , Row.Cons name typ rest row
3654+ , Row.Lacks name rest
3655+ , FieldToPGValue typ
3656+ , ParamsToArray tail row
3658+ ParamsToArray (RL.Cons name typ tail) row where
3659+ paramsToArray _ rec =
3660+ [ { name: reflectSymbol (Proxy :: Proxy name)
3661+ , value: unsafeCoerce (fieldToPGValue (Record.get (Proxy :: Proxy name) rec))
3664+ <> paramsToArray (Proxy :: Proxy tail) rec
3666+namedParamRegex :: Regex.Regex
3667+namedParamRegex = case Regex.regex "\\$[a-zA-Z_][a-zA-Z0-9_]*(::[a-zA-Z_]+)?" Regex.global of
3669+ Left _ -> unsafeCoerce unit
3671+stripCast :: String -> String
3672+stripCast s = case SCU.indexOf (Pattern "::") s of
3674+ Just i -> SCU.take i s
3676+replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
3677+replaceNamedParams offset entries sql = do
3678+ let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
3679+ let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
3681+ sql' = Regex.replace' namedParamRegex
3682+ ( \match _ -> case Map.lookup (stripCast match) replacements of
3687+ { sql: sql', values: map (\e -> unsafeCoerce e.value) indexed }
3690+ :: forall tables result params paramsRL stage
3691+ . RowToList params paramsRL
3692+ => ParamsToArray paramsRL params
3693+ => ReadForeign { | result }
3696+ -> Q tables result params stage
3697+ -> Aff (Array { | result })
3698+runQuery conn params (Q q) = do
3699+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
3700+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3701+ let allValues = q.values <> values
3702+ result <- PG.query (PG.SQL sql) allValues conn
3703+ case decodeRows result.rows of
3704+ Left errs -> throwError (Exception.error ("Row decode failed: " <> show errs))
3705+ Right rows -> pure rows
3708+ :: forall tables result params paramsRL stage
3709+ . RowToList params paramsRL
3710+ => ParamsToArray paramsRL params
3711+ => ReadForeign { | result }
3714+ -> Q tables result params stage
3715+ -> Aff (Maybe { | result })
3716+runQueryOne conn params (Q q) = do
3717+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
3718+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3719+ let allValues = q.values <> values
3720+ result <- PG.queryOne (PG.SQL sql) allValues conn
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)
3728+ :: forall tables params paramsRL stage
3729+ . RowToList params paramsRL
3730+ => ParamsToArray paramsRL params
3733+ -> Q tables () params stage
3735+runExecute conn params (Q q) = do
3736+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
3737+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3738+ let allValues = q.values <> values
3739+ PG.execute (PG.SQL sql) allValues conn
3741+-- Transaction variants
3744+ :: forall tables result params paramsRL stage
3745+ . RowToList params paramsRL
3746+ => ParamsToArray paramsRL params
3747+ => ReadForeign { | result }
3750+ -> Q tables result params stage
3751+ -> Aff (Array { | result })
3752+runQueryTx txn params (Q q) = do
3753+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
3754+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3755+ let allValues = q.values <> values
3756+ result <- PG.txQuery (PG.SQL sql) allValues txn
3757+ case decodeRows result.rows of
3758+ Left errs -> throwError (Exception.error ("Row decode failed: " <> show errs))
3759+ Right rows -> pure rows
3762+ :: forall tables params paramsRL stage
3763+ . RowToList params paramsRL
3764+ => ParamsToArray paramsRL params
3767+ -> Q tables () params stage
3769+runExecuteTx txn params (Q q) = do
3770+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
3771+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
3772+ let allValues = q.values <> values
3773+ PG.txExecute (PG.SQL sql) allValues txn
3776+decodeRows :: forall a. ReadForeign a => Array Foreign -> Either (NonEmptyList ForeignError) (Array a)
3777+decodeRows = traverse (readImpl >>> runExcept)
3779+decodeRow :: forall a. ReadForeign a => Foreign -> Either (NonEmptyList ForeignError) a
3780+decodeRow = readImpl >>> runExcept