@@ -2,8 +2,10 @@ module Yoga.Postgres.Schema where
3import Prelude
3import Prelude
5+import Data.Array as Array
5import Data.Array (intercalate, mapWithIndex, foldl)
6import Data.Array (intercalate, mapWithIndex, foldl)
6import Data.Maybe (Maybe(..))
7import Data.Maybe (Maybe(..))
8+import Data.Nullable (toNullable)
7import Unsafe.Coerce (unsafeCoerce)
9import Unsafe.Coerce (unsafeCoerce)
8import Data.Reflectable (class Reflectable, reflectType)
10import Data.Reflectable (class Reflectable, reflectType)
9import Data.String.Regex (regex, replace') as Regex
11import Data.String.Regex (regex, replace') as Regex
@@ -16,12 +18,14 @@ import Data.Symbol (class IsSymbol, reflectSymbol)
16import Data.Tuple.Nested (type (/\))
18import Data.Tuple.Nested (type (/\))
17import Effect.Aff (Aff)
19import Effect.Aff (Aff)
18import Foreign (Foreign, unsafeToForeign)
20import Foreign (Foreign, unsafeToForeign)
21+import Prim.Boolean (True, False)
19import Prim.Row (class Cons, class Lacks) as Row
22import Prim.Row (class Cons, class Lacks) as Row
20import Prim.RowList as RL
23import Prim.RowList as RL
21import Prim.RowList (class RowToList)
24import Prim.RowList (class RowToList)
22import Prim.Symbol (class Cons, class Append) as Symbol
25import Prim.Symbol (class Cons, class Append) as Symbol
23import Prim.TypeError (class Fail, Beside, Text, Quote)
26import Prim.TypeError (class Fail, Beside, Text, Quote)
24import Record (get) as Record
27import Record (get) as Record
28+import Type.Data.Boolean (class Or)
25import Type.Proxy (Proxy(..))
29import Type.Proxy (Proxy(..))
26import Type.RowList (class ListToRow)
30import Type.RowList (class ListToRow)
27import Yoga.JSON (class ReadForeign, readImpl)
31import Yoga.JSON (class ReadForeign, readImpl)
@@ -887,10 +891,10 @@ else instance
887-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
891-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
889newtype Q :: Symbol -> Row Type -> Row Type -> Row Type -> Type
893newtype Q :: Symbol -> Row Type -> Row Type -> Row Type -> Type
890−newtype Q name cols result params = Q String
894+newtype Q name cols result params = Q { sql :: String, values :: Array PG.PGValue }
892from :: forall name cols. Proxy (Table name cols) -> Q name cols () ()
896from :: forall name cols. Proxy (Table name cols) -> Q name cols () ()
893−from _ = Q ""
897+from _ = Q { sql: "", values: [] }
896 :: forall name cols result r p
900 :: forall name cols result r p
@@ -898,7 +902,7 @@ selectAll
898 => StripColumns cols result
902 => StripColumns cols result
899 => Q name cols r p
903 => Q name cols r p
900 -> Q name cols result p
904 -> Q name cols result p
901−selectAll _ = Q ("SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name))
905+selectAll _ = Q { sql: "SELECT * FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
904 :: forall @sel name cols result r p
908 :: forall @sel name cols result r p
@@ -907,7 +911,7 @@ select
907 => ParseSelect sel cols result
911 => ParseSelect sel cols result
908 => Q name cols r p
912 => Q name cols r p
909 -> Q name cols result p
913 -> Q name cols result p
910−select _ = Q ("SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> reflectSymbol (Proxy :: Proxy name))
914+select _ = Q { sql: "SELECT " <> reflectSymbol (Proxy :: Proxy sel) <> " FROM " <> reflectSymbol (Proxy :: Proxy name), values: [] }
913 :: forall @whr name cols result params p
917 :: forall @whr name cols result params p
@@ -915,10 +919,195 @@ where_
915 => ParseWhere whr cols params
919 => ParseWhere whr cols params
916 => Q name cols result p
920 => Q name cols result p
917 -> Q name cols result params
921 -> Q name cols result params
918−where_ (Q base) = Q (base <> " WHERE " <> reflectSymbol (Proxy :: Proxy whr))
922+where_ (Q q) = Q (q { sql = q.sql <> " WHERE " <> reflectSymbol (Proxy :: Proxy whr) })
920toSQL :: forall name cols result params. Q name cols result params -> String
924toSQL :: forall name cols result params. Q name cols result params -> String
921−toSQL (Q s) = s
925+toSQL (Q q) = q.sql
927+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
928+-- Type-level IsAutoGenerated (Boolean kind)
929+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
931+class IsAutoGeneratedTC :: Type -> Boolean -> Constraint
932+class IsAutoGeneratedTC constraints result | constraints -> result
934+instance IsAutoGeneratedTC AutoIncrement True
935+else instance IsAutoGeneratedTC (Default a) True
936+else instance IsAutoGeneratedTC (DefaultInt a) True
937+else instance IsAutoGeneratedTC (DefaultBool a) True
938+else instance (IsAutoGeneratedTC a ra, IsAutoGeneratedTC b rb, Or ra rb result) => IsAutoGeneratedTC (a /\ b) result
939+else instance IsAutoGeneratedTC a False
941+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
942+-- InsertableColumnsRL: filter out auto-generated columns
943+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
945+class InsertableColumnsRL :: RL.RowList Type -> RL.RowList Type -> Constraint
946+class InsertableColumnsRL tableRL outRL | tableRL -> outRL
948+instance InsertableColumnsRL RL.Nil RL.Nil
950+ ( IsAutoGeneratedTC constraints isAuto
951+ , InsertableColumnDecide isAuto name typ tail outRL
953+ InsertableColumnsRL (RL.Cons name (Column typ constraints) tail) outRL
955+class InsertableColumnDecide :: Boolean -> Symbol -> Type -> RL.RowList Type -> RL.RowList Type -> Constraint
956+class InsertableColumnDecide isAuto name typ tail outRL | isAuto name typ tail -> outRL
958+instance InsertableColumnsRL tail outRL => InsertableColumnDecide True name typ tail outRL
959+instance InsertableColumnsRL tail outRL => InsertableColumnDecide False name typ tail (RL.Cons name typ outRL)
961+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
962+-- RecordValuesRL: extract record values in RowList order
963+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
965+class FieldToPGValue :: Type -> Constraint
966+class FieldToPGValue a where
967+ fieldToPGValue :: a -> PG.PGValue
969+instance FieldToPGValue a => FieldToPGValue (Maybe a) where
970+ fieldToPGValue = toNullable >>> unsafeCoerce
971+else instance FieldToPGValue a where
972+ fieldToPGValue = unsafeCoerce
974+class RecordValuesRL :: RL.RowList Type -> Row Type -> Constraint
975+class RecordValuesRL rl row where
976+ recordValuesRL :: Proxy rl -> { | row } -> Array PG.PGValue
978+instance RecordValuesRL RL.Nil row where
979+ recordValuesRL _ _ = []
983+ , Row.Cons name typ rest row
984+ , FieldToPGValue typ
985+ , RecordValuesRL tail row
987+ RecordValuesRL (RL.Cons name typ tail) row where
988+ recordValuesRL _ rec =
989+ [ fieldToPGValue (Record.get (Proxy :: Proxy name) rec) ]
990+ <> recordValuesRL (Proxy :: Proxy tail) rec
992+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
994+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
997+ :: forall name cols colsRL insertableRL insertRow
998+ . RowToList cols colsRL
999+ => InsertableColumnsRL colsRL insertableRL
1000+ => ListToRow insertableRL insertRow
1002+ => InsertColumnsRL colsRL
1003+ => RecordValuesRL insertableRL insertRow
1005+ -> Q name cols () ()
1006+ -> Q name cols () ()
1007+insert rec _ = Q { sql, values }
1009+ tableName = reflectSymbol (Proxy :: Proxy name)
1010+ colNames = insertColumnsRL (Proxy :: Proxy colsRL)
1011+ placeholders = colNames # mapWithIndex \i _ -> "$" <> show (i + 1)
1012+ sql = "INSERT INTO " <> tableName
1014+ <> intercalate ", " colNames
1017+ <> intercalate ", " placeholders
1019+ values = recordValuesRL (Proxy :: Proxy insertableRL) rec
1021+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1023+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1026+ :: forall @sel name cols result p
1028+ => ParseSelect sel cols result
1029+ => Q name cols () p
1030+ -> Q name cols result p
1031+returning (Q q) = Q (q { sql = q.sql <> " RETURNING " <> reflectSymbol (Proxy :: Proxy sel) })
1033+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1034+-- UPDATE builder (SET)
1035+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1037+class ValidateSetColumnsRL :: RL.RowList Type -> Row Type -> Constraint
1038+class ValidateSetColumnsRL rl cols
1040+instance ValidateSetColumnsRL RL.Nil cols
1042+ ( Row.Cons name (Column typ constraints) rest cols
1043+ , ValidateSetColumnsRL tail cols
1045+ ValidateSetColumnsRL (RL.Cons name typ tail) cols
1047+class SetClauseRL :: RL.RowList Type -> Constraint
1048+class SetClauseRL rl where
1049+ setClauseRL :: Proxy rl -> Int -> Array String
1051+instance SetClauseRL RL.Nil where
1052+ setClauseRL _ _ = []
1054+instance (IsSymbol name, SetClauseRL tail) => SetClauseRL (RL.Cons name typ tail) where
1055+ setClauseRL _ idx =
1056+ [ reflectSymbol (Proxy :: Proxy name) <> " = $" <> show idx ]
1057+ <> setClauseRL (Proxy :: Proxy tail) (idx + 1)
1060+ :: forall name cols setRow setRL
1061+ . RowToList setRow setRL
1063+ => ValidateSetColumnsRL setRL cols
1064+ => SetClauseRL setRL
1065+ => RecordValuesRL setRL setRow
1067+ -> Q name cols () ()
1068+ -> Q name cols () ()
1069+set rec _ = Q { sql, values }
1071+ tableName = reflectSymbol (Proxy :: Proxy name)
1072+ setClauses = setClauseRL (Proxy :: Proxy setRL) 1
1073+ sql = "UPDATE " <> tableName <> " SET " <> intercalate ", " setClauses
1074+ values = recordValuesRL (Proxy :: Proxy setRL) rec
1076+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1078+-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1081+ :: forall @target @action name cols result params
1084+ => ValidateColumns target cols
1085+ => Q name cols result params
1086+ -> Q name cols result params
1087+onConflict (Q q) = Q
1090+ <> " ON CONFLICT ("
1091+ <> reflectSymbol (Proxy :: Proxy target)
1093+ <> reflectSymbol (Proxy :: Proxy action)
1098+ :: forall @target name cols result params
1100+ => ValidateColumns target cols
1101+ => Q name cols result params
1102+ -> Q name cols result params
1103+onConflictDoNothing (Q q) = Q
1106+ <> " ON CONFLICT ("
1107+ <> reflectSymbol (Proxy :: Proxy target)
923-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1112-- ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
924-- Query execution
1113-- Query execution
@@ -950,9 +1139,9 @@ namedParamRegex = case Regex.regex "\\$[a-zA-Z_][a-zA-Z0-9_]*" Regex.global of
950 Right r -> r
1139 Right r -> r
951 Left _ -> unsafeCoerce unit
1140 Left _ -> unsafeCoerce unit
953−replaceNamedParams :: Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
1142+replaceNamedParams :: Int -> Array { name :: String, value :: Foreign } -> String -> { sql :: String, values :: Array PG.PGValue }
954−replaceNamedParams entries sql = do
1143+replaceNamedParams offset entries sql = do
955− let indexed = entries # mapWithIndex \i e -> { idx: i + 1, name: e.name, value: e.value }
1144+ let indexed = entries # mapWithIndex \i e -> { idx: offset + i + 1, name: e.name, value: e.value }
956 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
1145 let replacements = indexed # foldl (\m e -> Map.insert ("$" <> e.name) ("$" <> show e.idx) m) Map.empty
958 sql' = Regex.replace' namedParamRegex
1147 sql' = Regex.replace' namedParamRegex
@@ -972,10 +1161,11 @@ runQuery
972 -> { | params }
1161 -> { | params }
973 -> Q name cols result params
1162 -> Q name cols result params
974 -> Aff (Array { | result })
1163 -> Aff (Array { | result })
975−runQuery conn params (Q sql) = do
1164+runQuery conn params (Q q) = do
976 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
1165 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
977− let { sql: sql', values } = replaceNamedParams entries sql
1166+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
1167+ let allValues = q.values <> values
978− result <- PG.query (PG.SQL sql') values conn
1168+ result <- PG.query (PG.SQL sql) allValues conn
979 pure (unsafeDecodeRows result.rows)
1169 pure (unsafeDecodeRows result.rows)
981runQueryOne
1171runQueryOne
@@ -987,12 +1177,27 @@ runQueryOne
987 -> { | params }
1177 -> { | params }
988 -> Q name cols result params
1178 -> Q name cols result params
989 -> Aff (Maybe { | result })
1179 -> Aff (Maybe { | result })
990−runQueryOne conn params (Q sql) = do
1180+runQueryOne conn params (Q q) = do
991 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
1181 let entries = paramsToArray (Proxy :: Proxy paramsRL) params
992− let { sql: sql', values } = replaceNamedParams entries sql
1182+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
1183+ let allValues = q.values <> values
993− result <- PG.queryOne (PG.SQL sql') values conn
1184+ result <- PG.queryOne (PG.SQL sql) allValues conn
994 pure (result <#> unsafeDecodeRow)
1185 pure (result <#> unsafeDecodeRow)
1188+ :: forall name cols params paramsRL
1189+ . RowToList params paramsRL
1190+ => ParamsToArray paramsRL params
1193+ -> Q name cols () params
1195+runExecute conn params (Q q) = do
1196+ let entries = paramsToArray (Proxy :: Proxy paramsRL) params
1197+ let { sql, values } = replaceNamedParams (Array.length q.values) entries q.sql
1198+ let allValues = q.values <> values
1199+ PG.execute (PG.SQL sql) allValues conn
996unsafeDecodeRows :: forall a. ReadForeign a => Array Foreign -> Array a
1201unsafeDecodeRows :: forall a. ReadForeign a => Array Foreign -> Array a
997unsafeDecodeRows = map unsafeDecodeRow
1202unsafeDecodeRows = map unsafeDecodeRow