packages feed

pg-schema 0.7.1.2 → 0.8.0.0

raw patch · 20 files changed

+1420/−382 lines, 20 filesdep ~basePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: base

API changes (from Hackage documentation)

- PgSchema.DML: type TreeSch (ann :: Ann) = CSchema AnnSch ann
+ PgSchema.DML: FromHere :: PathKind
+ PgSchema.DML: PathElem :: s -> s -> PathKind -> PathElem' s
+ PgSchema.DML: ToHere :: PathKind
+ PgSchema.DML: [peDbName] :: PathElem' s -> s
+ PgSchema.DML: [peKind] :: PathElem' s -> PathKind
+ PgSchema.DML: [peName] :: PathElem' s -> s
+ PgSchema.DML: data PathElem' s
+ PgSchema.DML: data PathKind
+ PgSchema.DML: type HasSchema (ann :: Ann) = CSchema AnnSch ann
+ PgSchema.DML: type PathElem = PathElem' Text
+ PgSchema.DML: type PathElemK = PathElem' Symbol
+ PgSchema.DML: type UpdateByKeyNonReturning (ann :: Ann) r = (PlainIn ann r, HasSchema ann, CheckHasKey ann ColsDbNames Cols ann r)
+ PgSchema.DML: type UpdateByKeyReturning (ann :: Ann) r r' = (UpdateByKeyNonReturning ann r, PlainOut ann r', RequireBareRow r')
+ PgSchema.DML: type UpdateTreeNonReturning (ann :: Ann) r = (HasSchema ann, TreeIn ann r, AllHasKeyTree ann r '[] :: [Symbol])
+ PgSchema.DML: type UpdateTreeReturning (ann :: Ann) r r' = (UpdateTreeNonReturning ann r, TreeOut ann r', ReturningMatchesUpdate ann r r')
+ PgSchema.DML: type UpsertByKeyNonReturning (ann :: Ann) r = (PlainIn ann r, HasSchema ann, CheckAllMandatoryAndHasKey ann ColsDbNames Cols ann r)
+ PgSchema.DML: type UpsertByKeyReturning (ann :: Ann) r r' = (UpsertByKeyNonReturning ann r, PlainOut ann r', RequireBareRow r')
+ PgSchema.DML: updateByKey :: forall (ann :: Ann) -> forall r r'. UpdateByKeyReturning ann r r' => Connection -> [r] -> IO ([Maybe r'], Text)
+ PgSchema.DML: updateByKeyText :: forall (ann :: Ann) -> forall r r' s. (CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, HasSchema ann) => s
+ PgSchema.DML: updateByKeyText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, CRecInfo ann r, HasSchema ann) => s
+ PgSchema.DML: updateByKey_ :: forall (ann :: Ann) -> forall r. UpdateByKeyNonReturning ann r => Connection -> [r] -> IO (Int64, Text)
+ PgSchema.DML: updateJSON :: forall (ann :: Ann) -> forall r r'. UpdateTreeReturning ann r r' => Connection -> [r] -> IO ([r'], Text)
+ PgSchema.DML: updateJSONText :: forall (ann :: Ann) -> forall r r'. (HasSchema ann, CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, Ord s) => s
+ PgSchema.DML: updateJSONText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s
+ PgSchema.DML: updateJSON_ :: forall (ann :: Ann) -> forall r. UpdateTreeNonReturning ann r => Connection -> [r] -> IO Text
+ PgSchema.DML: upsertByKey :: forall (ann :: Ann) -> forall r r'. UpsertByKeyReturning ann r r' => Connection -> [r] -> IO ([r'], Text)
+ PgSchema.DML: upsertByKeyText :: forall (ann :: Ann) -> forall r r' s. (UpsertByKeyReturning ann r r', IsString s, Monoid s) => s
+ PgSchema.DML: upsertByKeyText_ :: forall (ann :: Ann) -> forall r s. (UpsertByKeyNonReturning ann r, IsString s, Monoid s) => s
+ PgSchema.DML: upsertByKey_ :: forall (ann :: Ann) -> forall r. UpsertByKeyNonReturning ann r => Connection -> [r] -> IO (Int64, Text)
- PgSchema.DML: [CondWithPath] :: forall (path :: [(Symbol, PathKind)]) (ren :: Renamer) sch (t :: NameNSK). ToStar (MapRenPath ren path) => Cond ren sch (TabOnDPath sch t (MapRenPath ren path)) -> CondWithPath ren sch t
+ PgSchema.DML: [CondWithPath] :: forall (path :: [PathElemK]) (ren :: Renamer) sch (t :: NameNSK). PathCtx ren sch t path => [PathElem] -> Cond ren sch (TabOnDPathRen ren sch t path) -> CondWithPath ren sch t
- PgSchema.DML: [DistWithPath] :: forall {k} (path :: [(Symbol, PathKind)]) (ren :: Renamer) (sch :: k) (t :: NameNSK). ToStar (MapRenPath ren path) => Dist ren sch (TabOnDPath sch t (MapRenPath ren path)) -> DistWithPath ren sch t
+ PgSchema.DML: [DistWithPath] :: forall {k1} (path :: [PathElemK]) (ren :: Renamer) (sch :: k1) (t :: NameNSK). PathCtx ren sch t path => [PathElem] -> Dist ren sch (TabOnDPathRen ren sch t path) -> DistWithPath ren sch t
- PgSchema.DML: [LimOffWithPath] :: forall {k} (path :: [(Symbol, PathKind)]) (ren :: Renamer) (sch :: k) (t :: NameNSK). (TabDPath sch t (MapRenPath ren path), ToStar (MapRenPath ren path), Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ 'RelMany) => LO -> LimOffWithPath ren sch t
+ PgSchema.DML: [LimOffWithPath] :: forall {k} {k1} (path :: [PathElemK]) (ren :: k) (sch :: k1) (t :: NameNSK). (PathCtx ren sch t path, PathEndsMany sch t path) => [PathElem] -> LO -> LimOffWithPath ren sch t
- PgSchema.DML: [OrdWithPath] :: forall {k} (path :: [(Symbol, PathKind)]) (ren :: Renamer) (sch :: k) (t :: NameNSK). ToStar (MapRenPath ren path) => [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> OrdWithPath ren sch t
+ PgSchema.DML: [OrdWithPath] :: forall {k1} (path :: [PathElemK]) (ren :: Renamer) (sch :: k1) (t :: NameNSK). PathCtx ren sch t path => [PathElem] -> [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> OrdWithPath ren sch t
- PgSchema.DML: data DistWithPath (ren :: Renamer) (sch :: k) (t :: NameNSK)
+ PgSchema.DML: data DistWithPath (ren :: Renamer) (sch :: k1) (t :: NameNSK)
- PgSchema.DML: data LimOffWithPath (ren :: Renamer) (sch :: k) (t :: NameNSK)
+ PgSchema.DML: data LimOffWithPath (ren :: k) (sch :: k1) (t :: NameNSK)
- PgSchema.DML: data OrdWithPath (ren :: Renamer) (sch :: k) (t :: NameNSK)
+ PgSchema.DML: data OrdWithPath (ren :: Renamer) (sch :: k1) (t :: NameNSK)
- PgSchema.DML: insertJSONText :: forall (ann :: Ann) -> forall r r'. (TreeSch ann, CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, Ord s) => s
+ PgSchema.DML: insertJSONText :: forall (ann :: Ann) -> forall r r'. (HasSchema ann, CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, Ord s) => s
- PgSchema.DML: insertJSONText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, Ord s, TreeSch ann, CRecInfo ann r) => s
+ PgSchema.DML: insertJSONText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s
- PgSchema.DML: qDistinct :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) (t' :: NameNSK). TabOnDPath2 sch t path ~ '(t', 'RelMany) => MonadQP ren sch t path
+ PgSchema.DML: qDistinct :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). PathEndsMany sch t path => MonadQP ren sch t path
- PgSchema.DML: qDistinctOn :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]). [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> MonadQP ren sch t path
+ PgSchema.DML: qDistinctOn :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> MonadQP ren sch t path
- PgSchema.DML: qLimit :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]). Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ 'RelMany => Natural -> MonadQP ren sch t path
+ PgSchema.DML: qLimit :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). PathEndsMany sch t path => Natural -> MonadQP ren sch t path
- PgSchema.DML: qOffset :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]). Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ 'RelMany => Natural -> MonadQP ren sch t path
+ PgSchema.DML: qOffset :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). PathEndsMany sch t path => Natural -> MonadQP ren sch t path
- PgSchema.DML: qOrderBy :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]). [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> MonadQP ren sch t path
+ PgSchema.DML: qOrderBy :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> MonadQP ren sch t path
- PgSchema.DML: qPath :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) (path' :: [(Symbol, PathKind)]) (path'' :: [(Symbol, PathKind)]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Nothing :: Maybe PathKind) p ren sch t path path' path'' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
+ PgSchema.DML: qPath :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]) (path' :: [PathElemK]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Nothing :: Maybe PathKind) p ren sch t path path' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
- PgSchema.DML: qPathFromHere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) (path' :: [(Symbol, PathKind)]) (path'' :: [(Symbol, PathKind)]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Just 'FromHere) p ren sch t path path' path'' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
+ PgSchema.DML: qPathFromHere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]) (path' :: [PathElemK]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Just 'FromHere) p ren sch t path path' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
- PgSchema.DML: qPathToHere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) (path' :: [(Symbol, PathKind)]) (path'' :: [(Symbol, PathKind)]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Just 'ToHere) p ren sch t path path' path'' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
+ PgSchema.DML: qPathToHere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]) (path' :: [PathElemK]) (tabPath :: NameNSK) (p' :: Symbol) (k :: PathKind). forall (p :: Symbol) -> PathCheck ('Just 'ToHere) p ren sch t path path' tabPath p' k => MonadQP ren sch t path' -> MonadQP ren sch t path
- PgSchema.DML: qWhere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]). Cond ren sch (TabOnDPath sch t (MapRenPath ren path)) -> MonadQP ren sch t path
+ PgSchema.DML: qWhere :: forall (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]). Cond ren sch (TabOnDPathRen ren sch t path) -> MonadQP ren sch t path
- PgSchema.DML: type InsertReturning (ann :: Ann) r r' = (InsertNonReturning ann r, PlainOut ann r')
+ PgSchema.DML: type InsertReturning (ann :: Ann) r r' = (InsertNonReturning ann r, PlainOut ann r', RequireBareRow r')
- PgSchema.DML: type InsertTreeNonReturning (ann :: Ann) r = (TreeSch ann, TreeIn ann r, AllMandatoryTree ann r '[] :: [Symbol])
+ PgSchema.DML: type InsertTreeNonReturning (ann :: Ann) r = (HasSchema ann, TreeIn ann r, AllMandatoryTree ann r '[] :: [Symbol])
- PgSchema.DML: type InsertTreeReturning (ann :: Ann) r r' = (InsertTreeNonReturning ann r, TreeOut ann r', ReturningIsSubtree ann r r')
+ PgSchema.DML: type InsertTreeReturning (ann :: Ann) r r' = (InsertTreeNonReturning ann r, TreeOut ann r', ReturningMatchesInsert ann r r')
- PgSchema.DML: type MonadQP (ren :: Renamer) sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) = (TabDPath sch t MapRenPath ren path, ToStar MapRenPath ren path) => RWS Proxy path () QueryParam ren sch t ()
+ PgSchema.DML: type MonadQP (ren :: Renamer) sch (t :: NameNSK) (path :: [PathElemK]) = PathCtx ren sch t path => RWS Proxy path () QueryParam ren sch t ()
- PgSchema.DML: type UpsertTreeNonReturning (ann :: Ann) r = (TreeSch ann, TreeIn ann r, AllMandatoryOrHasKeyTree ann r '[] :: [Symbol])
+ PgSchema.DML: type UpsertTreeNonReturning (ann :: Ann) r = (HasSchema ann, TreeIn ann r, AllMandatoryOrHasKeyTree ann r '[] :: [Symbol])
- PgSchema.DML: type UpsertTreeReturning (ann :: Ann) r r' = (UpsertTreeNonReturning ann r, TreeOut ann r', ReturningIsSubtree ann r r')
+ PgSchema.DML: type UpsertTreeReturning (ann :: Ann) r r' = (UpsertTreeNonReturning ann r, TreeOut ann r', ReturningMatchesUpsert ann r r')
- PgSchema.DML: upsertJSONText :: forall (ann :: Ann) -> forall r r'. (TreeSch ann, CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, Ord s) => s
+ PgSchema.DML: upsertJSONText :: forall (ann :: Ann) -> forall r r'. (HasSchema ann, CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, Ord s) => s
- PgSchema.DML: upsertJSONText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, Ord s, TreeSch ann, CRecInfo ann r) => s
+ PgSchema.DML: upsertJSONText_ :: forall (ann :: Ann) -> forall r s. (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s

Files

ChangeLog.md view
@@ -1,5 +1,24 @@ # Changelog for pg-schema +## 0.8.0.0++- Fix SELECT path matching after renamer: promoted `PathElem'` (`peName`,+  `peDbName`, `peKind`) replaces `(Text, PathKind)`; runtime `pathStepMatches`+  supports point paths and broadcast by shared db fk name; independent filters+  on aliased sibling fields (e.g. `mid1RootFk` vs `mid1_root_fk2`).+- Internal SELECT paths: `[PathElemK]` in `PathCtx` / `TabOnDPathRen`; removed+  `EffPath` on renamer-only pairs; `TabOnDPath2` walks `peDbName`.+- Flat DML: `upsertByKey` / `upsertByKey_` and `updateByKey` / `updateByKey_`+  (`upsertByKey` requires all mandatory columns and a full key — always+  `INSERT … ON CONFLICT …`; `updateByKey` is key-only, never inserts, bare+  returning type `r'`, result `IO ([Maybe r'], Text)`).+- Tree DML: `updateJSON` / `updateJSON_` (update-only); `InsertMode` for the+  JSON pipeline.+- Tree returning: Maybe or Bare for `upsertJSON`.+- New constraints: `AllHasKeyTree`, `CheckHasKey`, `ReturningMatches{Insert,Upsert,Update}`,+  `FromJSON` / `ToJSON` for `Maybe` rows and `null` in returning arrays.+- Rename `TreeSch` to `HasSchema`+ ## 0.7.1.2 - Fix bug with aggregates (Min/Max/Sum/Avg) 
pg-schema.cabal view
@@ -1,6 +1,6 @@ cabal-version:  3.12 name:           pg-schema-version:        0.7.1.2+version:        0.8.0.0 category:       Database author:         Dmitry Olshansky maintainer:     olshanskydr@gmail.com@@ -8,6 +8,10 @@ license:        BSD-3-Clause license-file:   LICENSE build-type:     Simple+tested-with:+  GHC ==9.10.3+  , GHC ==9.12.4+  , GHC ==9.14.1 synopsis:       Type-level definition of database schema and safe DML for PostgreSQL. description:     == Schema definition@@ -130,6 +134,8 @@     PgSchema.DML.Insert     PgSchema.DML.InsertJSON     PgSchema.DML.Insert.Types+    PgSchema.DML.KeyedWrite+    PgSchema.DML.UpsertByKey     PgSchema.DML.Select     PgSchema.DML.Select.Types     PgSchema.DML.Update@@ -252,7 +258,7 @@     Paths_pg_schema     PackageInfo_pg_schema   build-depends:-      base >= 4.21.0 && < 4.22+      base >= 4.20 && < 5     , bytestring >= 0.12.2 && < 0.13     , directory >= 1.3.9 && < 1.4     , pg-schema@@ -270,6 +276,7 @@     Tests.BaseConverts     Tests.Conditions     Tests.Hierarchy+    Tests.KeyedDML     Tests.InsertJSONTransaction     Tests.UpsertUniqueKey     Tests.Path
src/PgSchema/Ann.hs view
@@ -7,6 +7,8 @@ import qualified Data.Aeson.Key as Key import Data.Coerce import Data.Singletons.TH (genDefunSymbols)+import Data.Type.Bool+import Data.Type.Equality import Data.Typeable import Data.Text qualified as T import Data.Kind@@ -84,10 +86,15 @@   MapRen f '[] = '[]   MapRen f (x ': xs) = ApplyRenamer f x ': MapRen f xs -type family MapRenPath (f :: Renamer) (xs :: [(Symbol, PathKind)]) :: [(Symbol, PathKind)] where-  MapRenPath f '[] = '[]-  MapRenPath f ('(x, k) ': xs) = '(ApplyRenamer f x, k) ': MapRenPath f xs+-- | End table after walking @path@ from @t@.+type TabOnDPathRen ren sch t path = TabAtPath sch t path +-- | 'ToStar' for demotion and forced 'TabOnDPath2' walk (invalid edges error here).+type PathCtx ren sch t path =+  ( ToStar path+  , TabAtPath sch t path ~ TabAtPath sch t path+  )+ -------------------------------------------------------------------------------- -- Case dispatch --------------------------------------------------------------------------------@@ -95,6 +102,7 @@ data ColsCase = NonGenericCase | GenericCase  type family ColsCaseOf (r :: Type) :: ColsCase where+  ColsCaseOf (Maybe r)             = ColsCaseOf r   ColsCaseOf (a :. b)              = 'NonGenericCase   ColsCaseOf ((_ :: Symbol) := _) = 'NonGenericCase   ColsCaseOf r = 'GenericCase@@ -123,6 +131,7 @@ --------------------------------------------------------------------------------  type family ColsNonGeneric (ann :: Ann) r :: [ColInfo NameNSK] where+  ColsNonGeneric ann (Maybe r) = ColsNonGeneric ann r   ColsNonGeneric ann (a :. b) = Normalize (Cols ann a SP.++ Cols ann b)   ColsNonGeneric ann (fld := t) = Col ann fld t @@ -211,11 +220,24 @@     toJSON (PgTag r) = Object (KM.fromList (toPairs @ann @colsCase @cols r))     toEncoding (PgTag r) = pairs (foldMap (uncurry (.=)) $ toPairs @ann @colsCase @cols r) +type family RowNotMaybe (r :: Type) :: Constraint where+  RowNotMaybe (Maybe _) = TypeError+    ( Text "Use FromJSON (PgTag ann (Maybe r)) for optional top-level rows." )+  RowNotMaybe _ = ()+ instance-  (cols ~ Cols ann r, colsCase ~ ColsCaseOf r, FromJSONCols ann colsCase cols r)+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r, FromJSONCols ann colsCase cols r+  , RowNotMaybe r )   => FromJSON (PgTag ann r) where    parseJSON v = PgTag <$> parseJSONCols @ann @colsCase @cols v +instance {-# OVERLAPPING #-}+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r+  , FromJSONCols ann colsCase cols r )+  => FromJSON (PgTag ann (Maybe r)) where+   parseJSON Null = pure $ PgTag Nothing+   parseJSON v = PgTag . Just <$> parseJSONCols @ann @colsCase @cols v+ -- >>> type AnnRel = 'Ann RenamerId PgCatalog (PGC "pg_constraint") -- >>> rel = PgRelation{ constraint__namespace = PgTag "a", conname = "b", constraint__class = PgClassShort (PgTag "c") "d", constraint__fclass = PgClassShort (PgTag "e") "f", conkey = pgArr' [1,2], confkey = pgArr' [] } -- >>> rec = "conname" =: ("x" :: T.Text) :. "conname" =: ("z" :: T.Text) :. rel :. "conname" =: ("y" :: T.Text)@@ -255,6 +277,20 @@       where         keyTxt = demote @(NameSymNat sn) +instance+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r+  , FromJSONCols ann colsCase cols r )+  => FromJSONCols ann colsCase cols (Maybe r) where+    parseJSONCols Null = pure Nothing+    parseJSONCols v = Just <$> parseJSONCols @ann @colsCase @cols v++instance+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r+  , ToJSONCols ann colsCase cols r )+  => ToJSONCols ann colsCase cols (Maybe r) where+    toPairs Nothing = []+    toPairs (Just r) = toPairs @ann @colsCase @cols r+ -- Note: we use "split ~" instead of " '(colsA, colsB) ~ " to avoid ambiguity. instance   ( ca ~ ColsCaseOf a, cb ~ ColsCaseOf b@@ -357,10 +393,16 @@     toRow (PgTag r) = toRowCols @ann @colsCase @cols r  instance-  (cols ~ Cols ann r, colsCase ~ ColsCaseOf r, FromRowCols ann colsCase cols r)+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r, FromRowCols ann colsCase cols r+  , RowNotMaybe r )   => FromRow (PgTag ann r) where     fromRow = PgTag <$> fromRowCols @ann @colsCase @cols +instance {-# OVERLAPPING #-}+  ( cols ~ Cols ann r, colsCase ~ ColsCaseOf r, FromRowCols ann colsCase cols r)+  => FromRow (PgTag ann (Maybe r)) where+    fromRow = PgTag . Just <$> fromRowCols @ann @colsCase @cols+ -- >>> type AnnRel = 'Ann RenamerId PgCatalog 1 (PGC "pg_constraint") -- >>> (r1 :: [PgTag AnnRel ( ("conkey" := Int16))]) <- query_ conn "select 1::int2" -- >>> r1@@ -690,8 +732,18 @@   HasAnyFullIdentity' '[] rs ks ann = ()   HasAnyFullIdentity' (_ ': _) rs ks ann = HasAnyFullIdentity rs ks ann -genDefunSymbols [ ''CheckAllMandatory, ''CheckAllMandatoryOrHasKey]+-- | Every node must include a full primary or eligible unique key.+type family CheckHasKey (ann :: Ann) (rs :: [Symbol]) :: Constraint where+  CheckHasKey ('Ann ren sch d tab) rs =+    HasAnyFullIdentity rs (IdentityCandidates sch tab) ('Ann ren sch d tab) +-- | All mandatory fields and a full primary or eligible unique key (flat upsert).+type CheckAllMandatoryAndHasKey ann rs =+  (CheckAllMandatory ann rs, CheckHasKey ann rs)++genDefunSymbols+  [ ''CheckAllMandatory, ''CheckAllMandatoryOrHasKey, ''CheckHasKey ]+ -------------------------------------------------------------------------------- -- Recursive AllMandatory / PK for tree (JSON insert / upsert) --------------------------------------------------------------------------------@@ -719,6 +771,12 @@   AllMandatoryOrHasKeyTree ann r rFlds =     WalkLevelAnn CheckAllMandatoryOrHasKeySym0 ann (TRecordInfo ann r) rFlds +type family AllHasKeyTree (ann :: Ann) (r :: Type) (rFlds :: [Symbol])+  :: Constraint where+  AllHasKeyTree ann [r] rFlds = AllHasKeyTree ann r rFlds+  AllHasKeyTree ann r rFlds =+    WalkLevelAnn CheckHasKeySym0 ann (TRecordInfo ann r) rFlds+ -------------------------------------------------------------------------------- -- Returning tree must be subtree of input tree (with path) --------------------------------------------------------------------------------@@ -750,7 +808,188 @@       , CheckSubtreeAt path fisIn xs )   CheckSubtreeAt path fisIn (_ ': xs) = CheckSubtreeAt path fisIn xs --- | Check that returning tree is a subtree of input tree+-- | Returning shape must be reachable from the input tree: each relation branch+-- in @rOut@ must exist in @rIn@. Plain columns in @rOut@ are not compared to @rIn@+-- (see 'CheckSubtreeAt'). Used by tree @*JSON@ returning rules, not flat DML. type family ReturningIsSubtree (ann :: Ann) (rIn :: Type) (rOut :: Type) :: Constraint where   ReturningIsSubtree ann rIn rOut =     CheckSubtreeAt '[] (TRecordInfo ann rIn) (TRecordInfo ann rOut)++--------------------------------------------------------------------------------+-- Returning row optionality (Maybe) vs input mandatory coverage+--------------------------------------------------------------------------------++type family AnnChild (ann :: Ann) (childTab :: NameNSK) :: Ann where+  AnnChild ('Ann ren sch _ _) childTab = 'Ann ren sch 0 childTab++type family InnerRow (r :: Type) :: Type where+  InnerRow (Maybe r) = r+  InnerRow r = TypeError+    ( Text "Update returning row must be wrapped in Maybe."+    :$$: Text "Got: " :<>: ShowType r )++type family RequireMaybeRow (r :: Type) :: Constraint where+  RequireMaybeRow (Maybe _) = ()+  RequireMaybeRow r = TypeError+    ( Text "Update returning row must be Maybe ..."+    :$$: Text "Got: " :<>: ShowType r )++-- | Flat @RETURNING@ row must not be wrapped in @Maybe@ (use @IO [Maybe r']@ on+-- 'updateByKey' for per-row absence). Nullable columns may still be @Maybe@ fields.+type family RequireBareRow (r :: Type) :: Constraint where+  RequireBareRow r = RequireBareRow' r++type family RequireBareRow' (r :: Type) :: Constraint where+  RequireBareRow' (Maybe x) = TypeError+    ( Text "Flat returning row must not be wrapped in Maybe."+    :$$: Text "Use IO [Maybe r'] on updateByKey when a row may be absent."+    :$$: Text "Got: " :<>: ShowType (Maybe x) )+  RequireBareRow' _ = ()++type family ListElem (t :: Type) :: Type where+  ListElem [e] = e+  ListElem t = TypeError+    ( Text "Expected a Haskell list type for a child branch."+    :$$: Text "Got: " :<>: ShowType t )++type family SymNatName (sn :: SymNat) :: Symbol where+  SymNatName '(s, _) = s++type family ColTypeByJsonName (name :: Symbol) (cols :: [ColInfo NameNSK]) :: Type where+  ColTypeByJsonName name '[] = TypeError+    ( Text "Returning field not found in output type: " :<>: ShowType name )+  ColTypeByJsonName name ('ColInfo sn t _ _ ': xs) =+    If (SymNatName sn == name) t (ColTypeByJsonName name xs)++type family CoveredRs (ann :: Ann) (fis :: [FieldInfo Symbol]) :: [Symbol] where+  CoveredRs ann '[] = '[]+  CoveredRs ann ('FieldInfo _ db ('RFPlain _) ': xs) = db ': CoveredRs ann xs+  CoveredRs ('Ann ren sch d tab)+    ('FieldInfo _ _ ('RFToHere ('RecordInfo _ _) _) ': xs) =+    CoveredRs ('Ann ren sch d tab) xs+  CoveredRs ann (_ ': xs) = CoveredRs ann xs++type family AssertNoMaybe (path :: [Symbol]) (tab :: NameNSK) (fld :: Symbol) (t :: Type)+  :: Constraint where+  AssertNoMaybe path tab fld (Maybe _) = TypeError+    ( Text "Returning must not use Maybe here (insert or upsert insert-capable node)."+    :$$: Text "At path: " :<>: ShowType path+    :$$: Text "Table: " :<>: ShowType tab+    :$$: Text "Field: " :<>: ShowType fld )+  AssertNoMaybe path tab fld _ = ()++type family AssertMustBeMaybe (path :: [Symbol]) (tab :: NameNSK) (fld :: Symbol)+  (t :: Type) :: Constraint where+  AssertMustBeMaybe path tab fld (Maybe _) = ()+  AssertMustBeMaybe path tab fld t = TypeError+    ( Text "Returning must use Maybe here (update or key-only upsert)."+    :$$: Text "At path: " :<>: ShowType path+    :$$: Text "Table: " :<>: ShowType tab+    :$$: Text "Field: " :<>: ShowType fld+    :$$: Text "Got: " :<>: ShowType t )++type family CheckUpsertListElem (path :: [Symbol]) (tab :: NameNSK) (fld :: Symbol)+  (restMandatory :: [Symbol]) (elemTy :: Type) :: Constraint where+  CheckUpsertListElem path tab fld '[] elemTy =+    AssertNoMaybe path tab fld (ListElem elemTy)+  CheckUpsertListElem path tab fld (_ ': _) elemTy =+    AssertMustBeMaybe path tab fld (ListElem elemTy)++type family SkipOutField (db :: Symbol) (fis :: [FieldInfo Symbol])+  :: [FieldInfo Symbol] where+  SkipOutField db '[] = '[]+  SkipOutField db ('FieldInfo _ db _ ': xs) = xs+  SkipOutField db (_ ': xs) = SkipOutField db xs++type family CheckReturningUpsertAt (path :: [Symbol]) (ann :: Ann) (rIn :: Type)+  (rOut :: Type) (fisIn :: [FieldInfo Symbol]) (fisOut :: [FieldInfo Symbol])+  :: Constraint where+  CheckReturningUpsertAt path ann rIn rOut '[] '[] = ()+  CheckReturningUpsertAt path ('Ann ren sch d tab) rIn rOut fisIn+    ('FieldInfo jsonName db ('RFToHere ('RecordInfo childTab childFIsOut) _) ': outs) =+    ( CheckUpsertListElem path tab jsonName+        (RestMandatory sch tab (CoveredRs ('Ann ren sch d tab) fisIn))+        (ColTypeByJsonName jsonName (Cols ('Ann ren sch d tab) rOut))+    , CheckReturningUpsertAt (Snoc path db) (AnnChild ('Ann ren sch d tab) childTab) rIn rOut+        (FindChildAt path db fisIn) childFIsOut+    , CheckReturningUpsertAt path ('Ann ren sch d tab) rIn rOut fisIn (SkipOutField db outs) )+  CheckReturningUpsertAt path ann rIn rOut+    (_ ': ins) ('FieldInfo _ db _ ': outs) =+    CheckReturningUpsertAt path ann rIn rOut ins (SkipOutField db outs)+  CheckReturningUpsertAt path ann rIn rOut fisIn fisOut =+    CheckSubtreeAt path fisIn fisOut++type family CheckReturningUpdateAt (path :: [Symbol]) (ann :: Ann) (rIn :: Type)+  (rOut :: Type) (fisIn :: [FieldInfo Symbol]) (fisOut :: [FieldInfo Symbol])+  :: Constraint where+  CheckReturningUpdateAt path ann rIn rOut '[] '[] = ()+  CheckReturningUpdateAt path ('Ann ren sch d tab) rIn rOut fisIn+    ('FieldInfo jsonName db ('RFToHere ('RecordInfo childTab childFIsOut) _) ': outs) =+    ( AssertMustBeMaybe path tab jsonName+        (ListElem (ColTypeByJsonName jsonName (Cols ('Ann ren sch d tab) rOut)))+    , CheckReturningUpdateAt (Snoc path db) (AnnChild ('Ann ren sch d tab) childTab) rIn rOut+        (FindChildAt path db fisIn) childFIsOut+    , CheckReturningUpdateAt path ('Ann ren sch d tab) rIn rOut fisIn (SkipOutField db outs) )+  CheckReturningUpdateAt path ann rIn rOut+    (_ ': ins) ('FieldInfo _ db _ ': outs) =+    CheckReturningUpdateAt path ann rIn rOut ins (SkipOutField db outs)+  CheckReturningUpdateAt path ann rIn rOut fisIn fisOut =+    CheckSubtreeAt path fisIn fisOut++type family CheckReturningInsertAt (path :: [Symbol]) (ann :: Ann) (rIn :: Type)+  (rOut :: Type) (fisIn :: [FieldInfo Symbol]) (fisOut :: [FieldInfo Symbol])+  :: Constraint where+  CheckReturningInsertAt path ann rIn rOut '[] '[] = ()+  CheckReturningInsertAt path ('Ann ren sch d tab) rIn rOut fisIn+    ('FieldInfo jsonName db ('RFToHere ('RecordInfo childTab childFIsOut) _) ': outs) =+    ( AssertNoMaybe path tab jsonName+        (ColTypeByJsonName jsonName (Cols ('Ann ren sch d tab) rOut))+    , CheckReturningInsertAt (Snoc path db) (AnnChild ('Ann ren sch d tab) childTab) rIn rOut+        (FindChildAt path db fisIn) childFIsOut+    , CheckReturningInsertAt path ('Ann ren sch d tab) rIn rOut fisIn (SkipOutField db outs) )+  CheckReturningInsertAt path ann rIn rOut+    (_ ': ins) ('FieldInfo _ db _ ': outs) =+    CheckReturningInsertAt path ann rIn rOut ins (SkipOutField db outs)+  CheckReturningInsertAt path ann rIn rOut fisIn fisOut =+    CheckSubtreeAt path fisIn fisOut++-- | Checks that tree @RETURNING@ type @rOut@ matches input @rIn@ after insert:+--+-- * Each relation branch in @rOut@ exists in @rIn@ at the same path; plain columns+--   in @rOut@ need not appear in @rIn@ ('CheckSubtreeAt').+-- * For every child list in @rOut@: the list element type must not be @Maybe@+--   ('AssertNoMaybe').+type family ReturningMatchesInsert (ann :: Ann) (rIn :: Type) (rOut :: Type)+  :: Constraint where+  ReturningMatchesInsert ann rIn rOut =+    ( ReturningIsSubtree ann rIn rOut+    , CheckReturningInsertAt '[] ann rIn rOut+        (TRecordInfo ann rIn) (TRecordInfo ann rOut) )++-- | Checks that tree @RETURNING@ type @rOut@ matches input @rIn@ after upsert:+--+-- * Same relation-subtree rules as 'ReturningMatchesInsert'.+-- * For each child list in @rOut@: if @rIn@ still lacks mandatory plain columns at+--   that node, the element type must be @Maybe@; otherwise it must be bare+--   ('CheckUpsertListElem').+type family ReturningMatchesUpsert (ann :: Ann) (rIn :: Type) (rOut :: Type)+  :: Constraint where+  ReturningMatchesUpsert ann rIn rOut =+    ( ReturningIsSubtree ann rIn rOut+    , CheckReturningUpsertAt '[] ann rIn rOut+        (TRecordInfo ann rIn) (TRecordInfo ann rOut) )++-- | Checks that tree @RETURNING@ type @rOut@ matches input @rIn@ after update:+--+-- * @rOut@ must be @Maybe row@ ('RequireMaybeRow'); checks apply to 'InnerRow rOut'.+-- * Each relation branch in that row exists in @rIn@ at the same path; plain columns+--   in the returning row need not appear in @rIn@.+-- * For every child list there: the element type must be @Maybe@+--   ('AssertMustBeMaybe').+type family ReturningMatchesUpdate (ann :: Ann) (rIn :: Type) (rOut :: Type)+  :: Constraint where+  ReturningMatchesUpdate ann rIn rOut =+    ( RequireMaybeRow rOut+    , ReturningIsSubtree ann rIn (InnerRow rOut)+    , CheckReturningUpdateAt '[] ann rIn (InnerRow rOut)+        (TRecordInfo ann rIn) (TRecordInfo ann (InnerRow rOut)) )
src/PgSchema/DML.hs view
@@ -46,8 +46,13 @@ -- children in the same way. 'insertJSON' / 'insertJSON_' require every mandatory -- column and emit plain @INSERT@ only (duplicates fail in the database). -- 'upsertJSON' / 'upsertJSON_' relax mandatory requirements and may @UPDATE@ or--- @INSERT … ON CONFLICT …@ (see their Haddock). Plain 'insertSch' / 'insertSch_' follow the--- same safety story for flat (non-tree) rows.+-- @INSERT … ON CONFLICT …@ (see their Haddock).+-- 'updateJSON' / 'updateJSON_' update existing rows only (never @INSERT@).+-- Flat 'insertSch', 'upsertByKey', and 'updateByKey' use 'PlainOut' for @RETURNING@+-- (any plain columns of the table; 'RequireBareRow' forbids @Maybe@ on the whole+-- row). Tree @*JSON@ uses 'ReturningMatches{Insert,Upsert,Update}' instead.+-- 'upsertByKey' requires mandatory columns and a full key; 'updateByKey' is+-- key-only and returns @IO ([Maybe r'], Text)@. -- -- 'selectSch' decodes each row into a Haskell type @r@ whose fields describe the -- root table columns and nested data for relations (multiple list-shaped children@@ -68,18 +73,13 @@ -- module PgSchema.DML   (-  -- * DML-  -- ** Select-  -- *** Execute SQL-    selectSch, selectText, Selectable-  -- *** Monad to set Query Params-  , MonadQP, qpEmpty+  -- * Select+    selectSch, selectText+  -- ** Query Params+  , QueryParamAnn, QueryParam(..), qpEmpty   , qRoot, qPath, qPathFromHere, qPathToHere, qWhere, qOrderBy   , qDistinct, qDistinctOn, qLimit, qOffset-  -- **** Internals-  , QueryParam(..), QueryParamAnn, CondWithPath(..), OrdWithPath(..)-  , LimOffWithPath(..), DistWithPath(..)-  -- *** Conditions+  -- ** Conditions   -- | Example: @"name" =? "John"@   , (<?),(>?),(<=?),(>=?),(=?)   -- | Null-safe equality on nullable columns (@IS NOT DISTINCT FROM@).@@ -88,28 +88,80 @@   -- | Example: @"name" ~~? "%joh%"@   ,(~=?),(~~?)   , (|||), (&&&), pnot, pnull, pin, pinArr-  , pparent, pchild, TabParam(..), defTabParam-  , pUnsafeCond, UnsafeCol(..)-  , Cond(..), CondAnn, Cmp(..), BoolOp(..)+  , pparent, pchild+  , pUnsafeCond+  -- ** Order By and Limit/Offset+  , ordf, ascf, descf, defLO, lo1+  -- ** Internals+  , Selectable, CRecInfo(..)+  , MonadQP, CondWithPath(..), OrdWithPath(..)+  , Cond(..), CondAnn, Cmp(..), BoolOp(..), TabParam(..), defTabParam   , CondMonad, SomeToField(..), showCmp, tabPref, qual   , CDBField, CDBValue, CDBFieldNullable, CRelDef-  -- *** Order By and others-  , ordf, ascf, descf, defLO, lo1+  , LimOffWithPath(..), DistWithPath(..)   , OrdDirection(..), OrdFld(..), Dist(..), LO(..)-  -- ** Plain Insert-  , insertSch, insertSch_, insertText, insertText_, AllPlain-  , InsertNonReturning, InsertReturning-  -- ** Update+  -- * Plain DML+  -- | You can insert, update or upsert data.+  --+  -- - For insert you need all mandatory fields.+  -- - For key-based update you need a full primary or unique key.+  -- - For upsert you need all mandatory fields _and_ some key.+  --+  -- You can get in return any subset of the columns of the table which was affected by the operation.+  -- All rows returned in the same order as the input tree.+  -- For update you get `Maybe t`. It is `Nothing` if the row was not found by the key.+  --+  -- All these constraints are checked at compile time.+  --+  -- There is also operations to update or delete data by a condition.+  --+  , insertSch, insertSch_, insertText, insertText_+  , upsertByKey, upsertByKey_, upsertByKeyText, upsertByKeyText_+  , updateByKey, updateByKey_, updateByKeyText, updateByKeyText_   , updateByCond, updateByCond_, updateText, updateText_-  , UpdateReturning, UpdateNonReturning, CRecInfo(..)-  -- ** Tree-base Insert/Upsert+  , deleteByCond, deleteText+  -- ** Constraints+  , AllPlain, InsertNonReturning, InsertReturning+  , UpsertByKeyNonReturning, UpsertByKeyReturning+  , UpdateByKeyNonReturning, UpdateByKeyReturning+  , UpdateReturning, UpdateNonReturning+  -- * Tree-based DML+  -- | You can insert, update or upsert data right into the tree structure+  -- (to the several related tables at once).+  --+  -- - For insert you need all mandatory fields at every node.+  -- - For update you need a full primary or unique key at every node.+  -- - For upsert you need all mandatory fields _or_ some key at every node.+  --+  --     At each node:+  --+  --     - the operation works like an insert if there is no key+  --     - the operation works like an update if there is not all mandatory fields+  --     - the operation works like an upsert if there is all mandatory fields and a key exists+  --+  -- You don't need foreign keys for children in your data (and often even keys for parents).+  -- They will be determined automatically.+  --+  -- You can get in return any subset of the columns of the tables which were affected by the operation.+  -- All rows returned in the same order as the input tree.+  -- For update or some nodes of upsert (where not all mandatory fields are present),+  -- the returning type is `Maybe t`. It is 'Nothing' if the row was not found by the key.+  --+  -- All these constraints are checked at compile time.+  --+  -- For all these operations you should use types that can be converted to JSON (and from JSON for returning).+  -- You don't need instances but all fields should be serializable into JSON.+  -- I.e. you can't use 'Bytestring' for fields here.   , insertJSON, insertJSON_, upsertJSON, upsertJSON_-  , insertJSONText, insertJSONText_, upsertJSONText, upsertJSONText_-  , TreeIn, TreeOut, AllMandatoryTree, AllMandatoryOrHasKeyTree, TreeSch+  , updateJSON, updateJSON_, insertJSONText, insertJSONText_+  , upsertJSONText, upsertJSONText_, updateJSONText, updateJSONText_+  -- ** Constraints+  , TreeIn, TreeOut, AllMandatoryTree, AllMandatoryOrHasKeyTree, AllHasKeyTree+  , HasSchema   , InsertTreeNonReturning, InsertTreeReturning-  , UpsertTreeNonReturning, UpsertTreeReturning, TRecordInfo-  -- ** Delete-  , deleteByCond, deleteText+  , UpsertTreeNonReturning, UpsertTreeReturning+  , UpdateTreeNonReturning, UpdateTreeReturning, TRecordInfo+  , ReturningMatchesInsert, ReturningMatchesUpsert, ReturningMatchesUpdate   -- * Types   , Ann(..), ToStar   -- ** Renamers@@ -121,7 +173,9 @@   -- ** Enum   , PGEnum   -- ** Aggregates-  , Aggr'(..), Aggr(..), AggrFun(..)+  , Aggr(..), Aggr'(..), AggrFun(..)+  -- ** Unsafe+  , UnsafeCol(..)   -- ** Arrays   , PgArr(..), pgArr', unPgArr'   -- ** Conversion checks@@ -129,6 +183,7 @@   -- ** Other types   , PgChar(..), PgOid(..)   , NameNS'(..), type (->>), NameNSK, (->>)+  , PathElem'(..), PathElem, PathElemK, PathKind(..)   , TRelDef, RelDef'(..), RdFrom, RdTo, FdType, FdNullable, CTabDef(..)   ) where @@ -139,6 +194,7 @@ import PgSchema.DML.Insert as I import PgSchema.DML.Insert.Types as I import PgSchema.DML.InsertJSON as I+import PgSchema.DML.UpsertByKey as UBK import PgSchema.DML.Update as U import PgSchema.DML.Delete as D import PgSchema.Schema as S
src/PgSchema/DML/Insert.hs view
@@ -14,9 +14,10 @@   -- | Insert records into a table.--- You can request any subset of columns from the inserted row via the result type. ----- All mandatory fields having no defaults should be present.+-- Returning type @r'@ may name any plain columns of the table ('PlainOut');+-- they need not appear in the input row (e.g. @RETURNING id@ without @id@ in @r@).+-- All mandatory fields having no defaults should be present in @r@. -- insertSch   :: forall ann -> forall r r'. InsertReturning ann r r'
src/PgSchema/DML/Insert/Types.hs view
@@ -13,8 +13,10 @@ -- Plain Insert / Upsert / Update -------------------------------------------------------------------------------- +-- | Check that (input) record corresponds to annotation, can be converted to Row and contains only plain fields. type PlainIn ann r = (CRecInfo ann r, AllPlain ann r, ToRow (PgTag ann r)) +-- | Check that (output) record corresponds to annotation, can be converted from Row and contains only plain fields. type PlainOut ann r' = (CRecInfo ann r', AllPlain ann r', FromRow (PgTag ann r'))  -- | Plain insert without @RETURNING@.@@ -22,10 +24,10 @@ type InsertNonReturning ann r =   (PlainIn ann r, CheckAllMandatory ann (ColsDbNames (Cols ann r))) --- | Plain insert with @RETURNING@.--- Check that all inserted and returned fields belong to the root table--- and all mandatory fields are present.-type InsertReturning ann r r' = (InsertNonReturning ann r, PlainOut ann r')+-- | Plain insert with @RETURNING@: @r'@ is any bare plain column subset of the+-- table ('PlainOut'); tree 'ReturningMatchesInsert' is for 'insertJSON' only.+type InsertReturning ann r r' =+  ( InsertNonReturning ann r, PlainOut ann r', RequireBareRow r' )  -- | Plain update with @RETURNING@. -- Check that all updated and returned fields belong to the root table.@@ -35,10 +37,34 @@ -- Check that all updated fields belong to the root table. type UpdateNonReturning ann r = PlainIn ann r -type TreeSch ann = CSchema (AnnSch ann)+-- | Upsert one table row by primary / unique key (@ToRow@, no JSON).+--+-- Requires all mandatory fields and a full primary or eligible unique key so+-- the operation is always @INSERT … ON CONFLICT …@. Use 'InsertNonReturning' or+-- 'UpdateByKeyNonReturning' for insert-only or update-only flat writes.+type UpsertByKeyNonReturning ann r =+  ( PlainIn ann r, HasSchema ann+  , CheckAllMandatoryAndHasKey ann (ColsDbNames (Cols ann r)) ) +type UpsertByKeyReturning ann r r' =+  ( UpsertByKeyNonReturning ann r, PlainOut ann r', RequireBareRow r' )++-- | Update one table row by primary / unique key (@ToRow@, no JSON).+type UpdateByKeyNonReturning ann r =+  ( PlainIn ann r, HasSchema ann+  , CheckHasKey ann (ColsDbNames (Cols ann r)) )++-- | Flat update by key: bare @r'@; the API returns @IO ([Maybe r'], Text)@.+type UpdateByKeyReturning ann r r' =+  ( UpdateByKeyNonReturning ann r, PlainOut ann r', RequireBareRow r' )++-- | Check that annotation contains a schema.+type HasSchema ann = CSchema (AnnSch ann)++-- | Check that (input) record corresponds to annotation and can be converted to JSON. type TreeIn ann r = (CRecInfo ann r, ToJSON (PgTag ann r)) +-- | Check that (output) record corresponds to annotation and can be converted from JSON. type TreeOut ann r' = (CRecInfo ann r', FromJSON (PgTag ann r'),   Typeable ann, Typeable r') @@ -47,7 +73,7 @@ -- Check that all mandatory fields are present in all tables in tree. -- Reference fields in the child tables are not checked - they are inserted automatically. type InsertTreeNonReturning ann r =-  (TreeSch ann, TreeIn ann r, AllMandatoryTree ann r '[])+  (HasSchema ann, TreeIn ann r, AllMandatoryTree ann r '[])  -- | Insert tree with @RETURNING@. --@@ -56,7 +82,7 @@ -- -- It also checks that we get returnings only from the tables we inserted into. type InsertTreeReturning ann r r' =-  ( InsertTreeNonReturning ann r, TreeOut ann r', ReturningIsSubtree ann r r' )+  ( InsertTreeNonReturning ann r, TreeOut ann r', ReturningMatchesInsert ann r r' )  -- | Upsert tree without @RETURNING@. --@@ -64,7 +90,7 @@ -- present at each tree node. -- Reference fields in the child tables are not checked - they are inserted automatically. type UpsertTreeNonReturning ann r =-  (TreeSch ann, TreeIn ann r, AllMandatoryOrHasKeyTree ann r '[])+  (HasSchema ann, TreeIn ann r, AllMandatoryOrHasKeyTree ann r '[])  -- | Upsert tree with @RETURNING@. --@@ -74,4 +100,13 @@ -- -- It also checks that we get returnings only from the tables we upserted into. type UpsertTreeReturning ann r r' =-  ( UpsertTreeNonReturning ann r, TreeOut ann r', ReturningIsSubtree ann r r' )+  ( UpsertTreeNonReturning ann r, TreeOut ann r', ReturningMatchesUpsert ann r r' )++-- | Update tree without @RETURNING@ (key required at every node).+type UpdateTreeNonReturning ann r =+  (HasSchema ann, TreeIn ann r, AllHasKeyTree ann r '[])++-- | Update tree with @RETURNING@ ('Maybe' on every list row in @r'@).+type UpdateTreeReturning ann r r' =+  ( UpdateTreeNonReturning ann r, TreeOut ann r'+  , ReturningMatchesUpdate ann r r' )
src/PgSchema/DML/InsertJSON.hs view
@@ -1,7 +1,10 @@ {-# LANGUAGE OverloadedRecordDot #-} module PgSchema.DML.InsertJSON-  ( insertJSON, insertJSON_, upsertJSON, upsertJSON_-  , insertJSONText, insertJSONText_, upsertJSONText, upsertJSONText_ ) where+  ( InsertMode(..)+  , insertJSON, insertJSON_, upsertJSON, upsertJSON_+  , updateJSON, updateJSON_+  , insertJSONText, insertJSONText_, upsertJSONText, upsertJSONText_+  , updateJSONText, updateJSONText_ ) where  import Control.Monad import Control.Monad.RWS@@ -18,6 +21,7 @@ import Data.Traversable import PgSchema.Ann import PgSchema.DML.Insert.Types+import PgSchema.DML.KeyedWrite (mandatoryDbNames, pickKeyNames) import Database.PostgreSQL.LibPQ qualified as PQ import Database.PostgreSQL.Simple import Database.PostgreSQL.Simple.Internal (withConnection)@@ -28,77 +32,65 @@ import Prelude as P  +data InsertMode = Insert | Upsert | Update deriving (Eq, Show)++ -- | Insert records into a table and its children using JSON data internally. -- -- Uses the same SQL pipeline as 'upsertJSON', but each row is a plain @INSERT@ -- (no @UPDATE@, no @INSERT … ON CONFLICT …@). Any duplicate primary or unique key--- raises a PostgreSQL error.------ Requires all mandatory columns at every node (see 'InsertTreeReturning').-+-- raises a PostgreSQL error. Requires all mandatory columns at every node+-- ('InsertTreeReturning'; 'ReturningMatchesInsert' on @r'@). Flat 'insertSch'+-- uses 'PlainOut' only. insertJSON   :: forall ann -> forall r r'. InsertTreeReturning ann r r'   => Connection -> [r] -> IO ([r'], Text)-insertJSON ann @r @r' conn rs = insertJSONImpl ann @r @r' conn rs True+insertJSON ann @r @r' conn rs = insertJSONImpl ann @r @r' conn rs Insert  -- | Like 'insertJSON', but does not return rows. -- insertJSON_   :: forall ann -> forall r. InsertTreeNonReturning ann r   => Connection -> [r] -> IO Text-insertJSON_ ann @r conn rs = insertJSONImpl_ ann @r conn rs True+insertJSON_ ann @r conn rs = insertJSONImpl_ ann @r conn rs Insert  -- | Upsert a forest of rows into the root table and its /child/ tables in one -- round-trip, using JSON inside PostgreSQL (same pipeline as 'insertJSON'). ----- __Input shape (@r@):__ a record tree that may contain the root table’s columns--- and nested /child/ branches (one-to-many from the root downward). There are no--- nested /parent/ branches: parent keys are implied by the tree you send, not by--- embedding parent rows inside children.------ __Output shape (@r'@):__ a record tree whose graph of nested tables is a--- /subgraph/ of the input: the same tables can appear, but you choose which--- columns (and which levels) appear in the result—whatever is available through--- the generated @RETURNING@/result projection. Field sets may differ from @r@;--- relation structure cannot grow beyond what you sent in.------ __What to supply at each node:__ at every level, each row must either include--- all mandatory columns (for columns that are mandatory in the schema /sense of--- this API/) or, alternatively, enough columns to identify an existing row via--- the primary key or a unique constraint (including constraints with nullable--- columns when the table has no @NOT NULL@ unique constraint besides PK).--- If a nullable key column is @null@ in the payload, conflict/update semantics--- follow PostgreSQL (see haddock on 'upsertJSON').--- Foreign-key columns that are filled in by the parent level (for example--- after an auto-generated id on insert) do /not/ need to be present on the child--- payload.------ __Insert vs update vs upsert per row:__ the engine picks one of @INSERT@,--- @UPDATE@, or @UPSERT@ from the keys and mandatory fields you provide:------ * all mandatory fields present and no full identity key  →  @INSERT@--- * identity (PK or first eligible unique whose columns are all in the node), not all mandatory  →  @UPDATE@--- * identity present /and/ all mandatory fields  →  @UPSERT@ (@INSERT … ON CONFLICT …@)------ 'insertJSON' shares this execution path but is insert-only at runtime and--- requires /every/ mandatory field at compile time. 'upsertJSON' relaxes both.+-- See Haddock on previous versions for input/output shape and per-row @INSERT@ /+-- @UPDATE@ / @UPSERT@ selection. Returning list elements use bare rows where the+-- input node has all mandatory fields, and @Maybe@ otherwise ('ReturningMatchesUpsert'). upsertJSON   :: forall ann -> forall r r'. UpsertTreeReturning ann r r'   => Connection -> [r] -> IO ([r'], Text)-upsertJSON ann @r @r' conn rs = insertJSONImpl ann @r @r' conn rs False+upsertJSON ann @r @r' conn rs = insertJSONImpl ann @r @r' conn rs Upsert  -- | Like 'upsertJSON', but does not return rows. -- upsertJSON_   :: forall ann -> forall r. UpsertTreeNonReturning ann r   => Connection -> [r] -> IO Text-upsertJSON_ ann @r conn rs = insertJSONImpl_ ann @r conn rs False+upsertJSON_ ann @r conn rs = insertJSONImpl_ ann @r conn rs Upsert +-- | Update an existing forest (never @INSERT@). Returning type @r'@ must satisfy+-- 'ReturningMatchesUpdate' (@Maybe@ on each child list element). Flat+-- 'updateByKey' instead uses bare @r'@ and returns @IO ([Maybe r'], Text)@.+updateJSON+  :: forall ann -> forall r r'. UpdateTreeReturning ann r r'+  => Connection -> [r] -> IO ([r'], Text)+updateJSON ann @r @r' conn rs = insertJSONImpl ann @r @r' conn rs Update++-- | Like 'updateJSON', but does not return rows.+--+updateJSON_+  :: forall ann -> forall r. UpdateTreeNonReturning ann r+  => Connection -> [r] -> IO Text+updateJSON_ ann @r conn rs = insertJSONImpl_ ann @r conn rs Update+ insertJSONImpl-  :: forall ann -> forall r r'. (TreeSch ann, TreeIn ann r, TreeOut ann r')-  => Connection -> [r] -> Bool -> IO ([r'], Text)-  -- ^ @Bool@ is @isInsertOnly@.-insertJSONImpl ann @r @r' conn rs isInsertOnly = withTransactionIfNot conn do+  :: forall ann -> forall r r'. (HasSchema ann, TreeIn ann r, TreeOut ann r')+  => Connection -> [r] -> InsertMode -> IO ([r'], Text)+insertJSONImpl ann @r @r' conn rs mode = withTransactionIfNot conn do   let sql' = T.unpack sql in trace' sql' $ void $ execute_ conn $ fromString sql'   [Only res] <- let q = "select pg_temp.__ins(?)" in     traceShow' q@@ -107,7 +99,7 @@   void $ execute_ conn "drop function pg_temp.__ins"   pure (unPgTag @ann @r' <$> res, sql)   where-    sql = insertJSONText' isInsertOnly (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+    sql = insertJSONText' mode (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))       (getRecordInfo @ann @r) (getRecordInfo @ann @r').fields  withTransactionIfNot :: Connection -> IO a -> IO a@@ -124,50 +116,61 @@         \unknown transaction status"  insertJSONImpl_-  :: forall ann -> forall r. (TreeSch ann, TreeIn ann r)-  => Connection -> [r] -> Bool -> IO Text-insertJSONImpl_ ann @r conn rs isInsertOnly = withTransactionIfNot conn do+  :: forall ann -> forall r. (HasSchema ann, TreeIn ann r)+  => Connection -> [r] -> InsertMode -> IO Text+insertJSONImpl_ ann @r conn rs mode = withTransactionIfNot conn do   void $ trace' (T.unpack sql) $ execute_ conn $ fromString $ T.unpack sql   void $ execute conn "call pg_temp.__ins(?)" $ Only $ PgTag @ann @r <$> rs   sql <$ execute_ conn "drop procedure pg_temp.__ins"   where-    sql = insertJSONText' isInsertOnly (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+    sql = insertJSONText' mode (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))       (getRecordInfo @ann @r) []  insertJSONText_ :: forall ann -> forall r s.-  (IsString s, Monoid s, Ord s, TreeSch ann, CRecInfo ann r) => s+  (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s insertJSONText_ ann @r =-  insertJSONText' True (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+  insertJSONText' Insert (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))     (getRecordInfo @ann @r) []  insertJSONText :: forall ann -> forall r r'.-  ( TreeSch ann, CRecInfo ann r, CRecInfo ann r'+  ( HasSchema ann, CRecInfo ann r, CRecInfo ann r'   , IsString s, Monoid s, Ord s ) => s insertJSONText ann @r @r' =-  insertJSONText' True (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+  insertJSONText' Insert (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))     (getRecordInfo @ann @r) (getRecordInfo @ann @r').fields  upsertJSONText_ :: forall ann -> forall r s.-  (IsString s, Monoid s, Ord s, TreeSch ann, CRecInfo ann r) => s+  (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s upsertJSONText_ ann @r =-  insertJSONText' False (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+  insertJSONText' Upsert (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))     (getRecordInfo @ann @r) []  upsertJSONText :: forall ann -> forall r r'.-  ( TreeSch ann, CRecInfo ann r, CRecInfo ann r'+  ( HasSchema ann, CRecInfo ann r, CRecInfo ann r'   , IsString s, Monoid s, Ord s ) => s upsertJSONText ann @r @r' =-  insertJSONText' False (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+  insertJSONText' Upsert (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))     (getRecordInfo @ann @r) (getRecordInfo @ann @r').fields --- | @isInsertOnly@: plain @INSERT@ only ('insertJSON'); 'False' enables upsert--- key resolution ('upsertJSON').+updateJSONText_ :: forall ann -> forall r s.+  (IsString s, Monoid s, Ord s, HasSchema ann, CRecInfo ann r) => s+updateJSONText_ ann @r =+  insertJSONText' Update (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+    (getRecordInfo @ann @r) []++updateJSONText :: forall ann -> forall r r'.+  ( HasSchema ann, CRecInfo ann r, CRecInfo ann r'+  , IsString s, Monoid s, Ord s ) => s+updateJSONText ann @r @r' =+  insertJSONText' Update (typDefMap @(AnnSch ann)) (tabInfoMap @(AnnSch ann))+    (getRecordInfo @ann @r) (getRecordInfo @ann @r').fields+ insertJSONText'   :: forall s. (IsString s, Monoid s, Ord s)-  => Bool+  => InsertMode   -> M.Map NameNS TypDef -> M.Map NameNS TabInfo -> RecordInfo Text   -> [FieldInfo Text] -> s-insertJSONText' isInsertOnly mapTypes mapTabs ir qfs = unlines'+insertJSONText' mode mapTypes mapTabs ir qfs = unlines'   [ maybe     "create or replace procedure pg_temp.__ins(data_0 jsonb) as $$"     (const "create or replace function pg_temp.__ins(data_0 jsonb) returns jsonb as $$")@@ -181,13 +184,10 @@   , "$$ language plpgsql;" ]   where     (mbRes, (decl, body)) =-      evalRWS (insertJSONTextM isInsertOnly mapTypes mapTabs ir qfs [] [])+      evalRWS (insertJSONTextM mode mapTypes mapTabs ir qfs [] [] [])         ("  ",0) 0  type MonadInsert s = RWS (s, Int) ([s],[s]) Int--- R: (leading spaces to format code, number of table in tree)--- W: (lines of declarations, lines of function body)--- S: maximum number of table in tree "in use"  data OP = INS | UPD | UPS deriving (Eq, Show) @@ -197,40 +197,37 @@   , info :: v }   deriving (Eq, Show) --- | Runtime conflict/update targets: PK, then @NOT NULL@ unique constraints,--- then nullable unique constraints only when the table has no @NOT NULL@ UK.--- Compile-time checks use all keys from 'IdentityCandidates'.-identityCandidatesFromTab :: TabInfo -> [[Text]]-identityCandidatesFromTab ti =-  P.filter (not . P.null) [ti.tiDef.tdKey] <> notNullUks <> nullUks-  where-    isNullable = (== Just True) . fmap (.fdNullable) . (`M.lookup` ti.tiFlds)-    (nullUks, notNullUks) = L.partition (P.any isNullable) ti.tiDef.tdUniq--mandatoryDbNames :: TabInfo -> [Text]-mandatoryDbNames ti =-  M.keys $ M.filter (\fd -> not $ fd.fdNullable || fd.fdHasDefault) ti.tiFlds- insertJSONTextM   :: forall s. (IsString s, Monoid s, Ord s)-  => Bool+  => InsertMode   -> M.Map NameNS TypDef -> M.Map NameNS TabInfo -> RecordInfo Text-  -> [FieldInfo Text] -> [s] -> [s] -> MonadInsert s (Maybe s)-insertJSONTextM isInsertOnly mapTypes mapTabs ri qfs fromFields toVars = do+  -> [FieldInfo Text] -> [s] -> [s] -> [Text] -> MonadInsert s (Maybe s)+insertJSONTextM mode mapTypes mapTabs ri qfs fromFields toVars parentDbKeys = do   (spaces, n) <- ask   let     sn = show' n     dataN = "data_" <> sn     rowN  = "row_" <> sn+    foundN = "found_" <> sn     mbArrN = ("arr_" <> sn) <$ guard (not $ P.null qfs)     decs = (if n == 0 then P.id else (dataN <> " jsonb;" :))-      [rowN <> " record;"]+      (foundN <> " boolean;" : [rowN <> " record;"])       <> foldMap (pure . (<> " jsonb[];")) mbArrN <> qretDecls     qretDecls = qretPairs <&> \(fld, typ) -> fld <> sn <> " " <> typ <> "; "     initArray = foldMap (pure . (<> ":= '{}';")) mbArrN     startLoop =       ["for " <> rowN <> " in select * from jsonb_array_elements("       <> dataN <> ")", "loop"]+    keyOnlyInput =+      let+        mbTab = mapTabs M.!? ri.tabName+        mflds = foldMap (fmap fromText . mandatoryDbNames) mbTab+        srcFldsLoc = parentDbKeys <> ((.dbName) <$> iplains)+      in not $ P.null $ mflds L.\\ srcFldsLoc+    keyNamesTxt = case mapTabs M.!? ri.tabName of+      Just ti ->+        fromMaybe [] $ pickKeyNames ti (parentDbKeys <> ((.dbName) <$> iplains))+      Nothing -> []     (ins, op) = resolve       where         jsonFld ip = case mapTypes M.!? ip.info.fdType of@@ -258,12 +255,7 @@         ins0 =           [ "  insert into " <> qualTabName <> "(" <> intercalate' ", " srcFlds <> ")"           , "    values (" <> intercalate' ", " srcVars <> ")"]-        mbTab = mapTabs M.!? ri.tabName-        mflds = foldMap (fmap fromText . mandatoryDbNames) mbTab-        keyNames-          | isInsertOnly = []-          | otherwise = F.fold $ L.find (P.null . (L.\\ srcFlds))-              $ fmap fromText <$> foldMap identityCandidatesFromTab mbTab+        keyNames = fromText <$> keyNamesTxt         (plainsKey, plainsOthers) =           L.partition ((`L.elem` keyNames) . fst) plains         sWhere = "where " <> intercalate' " and "@@ -284,20 +276,25 @@               <> ["  " <> intercalate' " " xs]             Nothing -> ins0 <> addSemiColon ([ "    on conflict do nothing"] <> rets)               <> ["  if not found then"-                , "    select " <> intercalate' ", " qretFlds <> " into " <> intercalate' ", " qretVars+                , "    select " <> intercalate' ", " qretFlds <> " into "+                  <> intercalate' ", " qretVars                 , "      from " <> qualTabName                 , "      " <> sWhere <> ";"                 , "  end if;"]           | P.null conflictCols = addSemiColon ins0           | otherwise = addSemiColon $ ins0             <> [ "    on conflict (" <> intercalate' ", " conflictCols <> ")"-              , "      do update set " <> intercalate' ", " (plainsOthers <&>-                  \(name, _) -> name <> " = " <> "EXCLUDED." <> name) ]+              , "      do update set " <> intercalate' ", "+                  (plainsOthers <&> \(name, _) -> name <> " = EXCLUDED." <> name) ]             <> rets         resolve-          | not isInsertOnly, not $ P.null $ mflds L.\\ srcFlds =+          | mode == Update =             (addSemiColon (upd0 <> rets), UPD)-          | not isInsertOnly, not $ P.null keyNames = (ups0 keyNames, UPS)+          | mode == Insert =+            (addSemiColon (ins0 <> rets), INS)+          | keyOnlyInput =+            (addSemiColon (upd0 <> rets), UPD)+          | not $ P.null keyNamesTxt = (ups0 keyNames, UPS)           | otherwise = (addSemiColon (ins0 <> rets), INS)     endLoop = "end loop;"     processChildren = do@@ -310,27 +307,55 @@         let           qfs' = foldMap ((.fields) . snd)             $ L.find (\(qc, _) -> qc.jsonName == child.jsonName) qchildren-        mbArr <- local (second $ const n') $ insertJSONTextM isInsertOnly+        let parentKeys = nub $ P.map (.fromName) child.info+        mbArr <- local (second $ const n') $ insertJSONTextM mode           mapTypes mapTabs           childRi qfs' (fromText . (.fromName) <$> child.info)           ((<> sn) . fromText . (.toName) <$> child.info)+          parentKeys         pure (fromText child.jsonName, mbArr)+      pure arrs+    appendReturning arrN arrs =       let-        appendArray = foldMap (\arrN -> pure $ arrN <> ":= array_append("-          <> arrN <> ", jsonb_build_object(" <> jsonFlds <> "));") mbArrN-          where-            jsonFlds = intercalate' ", "-              $ (qplains <&> \qp -> "'" <> fromText qp.jsonName-              <> "', " <> fromText qp.dbName <> sn)-              <> (arrs <&> \(jsonN, arr) -> "'" <> jsonN <> "', to_jsonb(" <> arr <> ")")-      tell (mempty, fmap (spaces' <>) appendArray)-  tell (decs, fmap (spaces <>) $ initArray <> startLoop <> ins)-  case op of+        jsonFlds = intercalate' ", "+          $ (qplains <&> \qp -> "'" <> fromText qp.jsonName+            <> "', " <> fromText qp.dbName <> sn)+          <> (arrs <&> \(jsonN, arr) -> "'" <> jsonN <> "', to_jsonb(" <> arr <> ")")+        obj = "jsonb_build_object(" <> jsonFlds <> ")"+      in case (mode, op) of+        (Update, UPD) ->+          [ spaces <> "  if " <> foundN <> " then"+          , spaces <> "    " <> arrN <> " := array_append(" <> arrN <> ", " <> obj <> ");"+          , spaces <> "  else"+          , spaces <> "    " <> arrN <> " := array_append("+            <> arrN <> ", 'null'::jsonb);"+          , spaces <> "  end if;" ]+        (Upsert, UPD)+          | keyOnlyInput ->+            [ spaces <> "  if " <> foundN <> " then"+            , spaces <> "    " <> arrN <> " := array_append(" <> arrN <> ", " <> obj <> ");"+            , spaces <> "  else"+            , spaces <> "    " <> arrN <> " := array_append("+              <> arrN <> ", 'null'::jsonb);"+            , spaces <> "  end if;" ]+          | otherwise ->+            [ spaces <> "  if " <> foundN <> " then"+            , spaces <> "    " <> arrN <> " := array_append(" <> arrN <> ", " <> obj <> ");"+            , spaces <> "  end if;" ]+        _ ->+          [ spaces <> "  " <> arrN <> " := array_append(" <> arrN <> ", " <> obj <> ");" ]+  tell (decs, fmap (spaces <>) $ initArray <> startLoop <> ins+    <> [spaces <> "  " <> foundN <> " := found;"])+  arrs <- case op of     UPD -> do       tell (mempty, pure $ spaces <> "  if found then")-      local (first ("    " <>)) processChildren+      ch <- local (first ("    " <>)) processChildren       tell (mempty, pure $ spaces <> "  end if;")+      pure ch     _ -> local (first ("  " <>)) processChildren+  case mbArrN of+    Nothing -> pure ()+    Just arrN -> tell (mempty, appendReturning arrN arrs)   tell (mempty, pure $ spaces <> endLoop)   pure mbArrN   where
+ src/PgSchema/DML/KeyedWrite.hs view
@@ -0,0 +1,56 @@+{-# LANGUAGE OverloadedRecordDot #-}+module PgSchema.DML.KeyedWrite+  ( mandatoryDbNames, identityCandidatesFromTab, pickKeyNames+  , keyWhereClause, upsertOnConflict+  , keyedUpdateSetAndKeys ) where++import Data.List as L+import Data.Map as M+import Data.Maybe+import Data.Text as T+import PgSchema.Schema+import Prelude as P+++-- | Runtime conflict/update targets (same order as 'InsertJSON').+identityCandidatesFromTab :: TabInfo -> [[Text]]+identityCandidatesFromTab ti =+  P.filter (not . P.null) [ti.tiDef.tdKey] <> notNullUks <> nullUks+  where+    isNullable = (== Just True) . fmap (.fdNullable) . (`M.lookup` ti.tiFlds)+    (nullUks, notNullUks) = L.partition (P.any isNullable) ti.tiDef.tdUniq++mandatoryDbNames :: TabInfo -> [Text]+mandatoryDbNames ti =+  M.keys $ M.filter (\fd -> not $ fd.fdNullable || fd.fdHasDefault) ti.tiFlds++pickKeyNames :: TabInfo -> [Text] -> Maybe [Text]+pickKeyNames ti srcFlds =+  L.find (P.null . (L.\\ srcFlds)) $ identityCandidatesFromTab ti++keyWhereClause :: [(Text, Text)] -> Text+keyWhereClause pairs =+  " where " <> T.intercalate " and "+    (L.zipWith keyValDistinct (fst <$> pairs) (snd <$> pairs))+  where+    keyValDistinct name val = name <> " IS NOT DISTINCT FROM " <> val++-- | Partition plain columns for @UPDATE@ (SET non-keys, then WHERE keys).+keyedUpdateSetAndKeys+  :: TabInfo -> [(Text, Text)] -> ([(Text, Text)], [(Text, Text)])+keyedUpdateSetAndKeys ti plains =+  let+    srcFlds = fst <$> plains+    keyNames = fromMaybe [] $ pickKeyNames ti srcFlds+  in L.partition ((`L.elem` keyNames) . fst) plains++upsertOnConflict :: [(Text, Text)] -> [Text] -> Text+upsertOnConflict plainsOthers conflictCols =+  case (L.null plainsOthers, conflictCols) of+    (True, _) -> " on conflict do nothing"+    (_, []) -> ""+    (_, cols) ->+      " on conflict (" <> T.intercalate ", " cols <> ")"+        <> " do update set "+        <> T.intercalate ", "+          ( L.map (\(name, _) -> name <> " = EXCLUDED." <> name) plainsOthers )
src/PgSchema/DML/Select.hs view
@@ -27,13 +27,12 @@ import GHC.TypeLits import PgSchema.Types import PgSchema.Utils.Internal-import PgSchema.Utils.TF import Prelude as P   data QueryRead ren sch t = QueryRead   { qrCurrTabNum  :: Int-  , qrPath        :: [(Text, PathKind)]+  , qrPath        :: [PathElem]   , qrParam       :: QueryParam ren sch t }  data ParentInfo = ParentInfo@@ -42,7 +41,7 @@   , piToNum       :: Int   , piParentTab   :: NameNS   , piRefs        :: [Ref' Text]-  , piPath        :: [(Text, PathKind)] }+  , piPath        :: [PathElem] }   deriving Show  data QueryState = QueryState@@ -52,6 +51,11 @@  type MonadQuery ren sch t m = (MonadRWS (QueryRead ren sch t) [SomeToField] QueryState m) +-- | Checks that @r@ describes a decodable @SELECT@ row for @ann@:+--+-- * 'CRecInfo': every field of @r@ maps to a plain column or relation edge of the+--   root table in the schema (field names, kinds, and nesting match 'Cols').+-- * 'FromRow': each mapped field can be read from a PostgreSQL result column. type Selectable ann r = (CRecInfo ann r, FromRow (PgTag ann r))  -- | Run a single @SELECT@ for root table @tab@ (see annotation @ann@ with schema@@ -119,27 +123,27 @@   let     basePath = L.reverse qrPath     (unTextI -> condText, condPars) = F.fold $ L.reverse-      $ mapMaybe (\(CondWithPath @path cond) -> let p = demote @(MapRenPath ren path) in+      $ mapMaybe (\(CondWithPath p cond) ->         if-          | not (basePath `L.isPrefixOf` p) -> Nothing-          | p == basePath -> Just $ first TextI $ pgCond qrCurrTabNum cond-          | otherwise -> L.find ((p ==) . (basePath <>) . (.piPath)) parents+          | not (pathIsPrefixOf basePath p) -> Nothing+          | pathsEqual p basePath -> Just $ first TextI $ pgCond qrCurrTabNum cond+          | otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents             <&> \pari -> first (TextI @" and ") $ pgCond pari.piToNum cond         ) qrParam.qpConds     (unTextI -> ordText, ordPars) = F.fold $ L.reverse-      $ mapMaybe (\(OrdWithPath @path ord) -> let p = demote @(MapRenPath ren path) in+      $ mapMaybe (\(OrdWithPath p ord) ->         if-          | not (basePath `L.isPrefixOf` p) -> Nothing-          | p == basePath -> Just $ pgOrd qrCurrTabNum ord-          | otherwise -> L.find ((p ==) . (basePath <>) . (.piPath)) parents+          | not (pathIsPrefixOf basePath p) -> Nothing+          | pathsEqual p basePath -> Just $ pgOrd qrCurrTabNum ord+          | otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents             <&> \pari -> pgOrd pari.piToNum ord         ) qrParam.qpOrds     (distTexts, distPars) = F.fold $ L.reverse-      $ mapMaybe (\(DistWithPath @path dist) -> let p = demote @(MapRenPath ren path) in+      $ mapMaybe (\(DistWithPath p dist) ->         if-          | not (basePath `L.isPrefixOf` p) -> Nothing-          | p == basePath -> Just $ pgDist qrCurrTabNum dist-          | otherwise -> L.find ((p ==) . (basePath <>) . (.piPath)) parents+          | not (pathIsPrefixOf basePath p) -> Nothing+          | pathsEqual p basePath -> Just $ pgDist qrCurrTabNum dist+          | otherwise -> L.find (parentPathMatches p basePath . (.piPath)) parents             <&> \pari -> pgDist pari.piToNum dist         ) qrParam.qpDistinct     sel@@ -219,10 +223,12 @@         , piToNum     = n2         , piParentTab = ri.tabName         , piRefs      = refs-        , piPath      = (fi.fieldDbName, FromHere) : qrPath } : qsParents }+        , piPath      = mkPathStep fi.fieldName fi.fieldDbName FromHere : qrPath }+        : qsParents }     n2 <- gets qsLastTabNum     (flds, pars) <- listen $ local-      (\qr -> qr{ qrCurrTabNum = n2, qrPath = (fi.fieldDbName, FromHere) : qrPath })+      (\qr -> qr{ qrCurrTabNum = n2+        , qrPath = mkPathStep fi.fieldName fi.fieldDbName FromHere : qrPath })       $ traverse fieldM ri.fields     val <- if L.any (fdNullable . fromDef) refs       then do@@ -236,7 +242,8 @@     QueryState {qsLastTabNum = (+1) -> tabNum, qsParents} <- get     modify (const $ QueryState tabNum [])     selText <- local-      (\qr -> qr { qrCurrTabNum = tabNum, qrPath = (fi.fieldDbName, ToHere) : qrPath })+      (\qr -> qr { qrCurrTabNum = tabNum+        , qrPath = mkPathStep fi.fieldName fi.fieldDbName ToHere : qrPath })       $ selectM (refCond tabNum qrCurrTabNum refs) ri     modify (\qs -> qs { qsParents = qsParents })     let@@ -261,18 +268,18 @@         fldt n = (("t" <> show' n <> ".") <>)  withLOWithPath-  :: forall ren sch t r. (LO -> r) -> [(Text, PathKind)] -> LimOffWithPath ren sch t -> Maybe r-withLOWithPath f p (LimOffWithPath @p lo) =-  guard (p == demote @(MapRenPath ren p)) >> pure (f lo)+  :: forall ren sch t r. (LO -> r) -> [PathElem] -> LimOffWithPath ren sch t -> Maybe r+withLOWithPath f p (LimOffWithPath p' lo) =+  guard (pathsEqual p p') >> pure (f lo)  withLOsWithPath-  :: forall ren sch t r. (LO -> r) -> [(Text, PathKind)] -> [LimOffWithPath ren sch t] -> Maybe r+  :: forall ren sch t r. (LO -> r) -> [PathElem] -> [LimOffWithPath ren sch t] -> Maybe r withLOsWithPath f p = join . L.find isJust . L.map (withLOWithPath f p) -lowp :: forall ren sch t. forall (path::[(Symbol, PathKind)]) ->-  (ToStar (MapRenPath ren path), TabDPath sch t (MapRenPath ren path)-  , Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ RelMany) => LO -> LimOffWithPath ren sch t-lowp p = LimOffWithPath @p+lowp :: forall ren sch t. forall (path :: [PathElemK]) ->+  ( PathCtx ren sch t path, PathEndsMany sch t path ) =>+  LO -> LimOffWithPath ren sch t+lowp p = LimOffWithPath @p (demote @p)  rootLO :: forall ren sch t. LO -> LimOffWithPath ren sch t rootLO = lowp []@@ -282,7 +289,7 @@   maybe "" ((" limit " <>) . show') ml    <> maybe "" ((" offset " <>) . show') mo -loByPath :: forall ren sch t. [(Text, PathKind)] -> [LimOffWithPath ren sch t] -> Text+loByPath :: forall ren sch t. [PathElem] -> [LimOffWithPath ren sch t] -> Text loByPath p = fromMaybe mempty . withLOsWithPath convLO p  runCond :: Int -> CondMonad a -> (a,[SomeToField])@@ -371,59 +378,62 @@ pgDist n dist = evalRWS (convDist dist) ("o", pure n) 0  withCondWithPath :: forall ren sch t r. (forall t'. Cond ren sch t' -> r) ->-  [(Text, PathKind)] -> CondWithPath ren sch t -> Maybe r-withCondWithPath f p (CondWithPath @p' cond) = f cond <$ guard (p == demote @(MapRenPath ren p'))+  [PathElem] -> CondWithPath ren sch t -> Maybe r+withCondWithPath f p (CondWithPath p' cond) =+  f cond <$ guard (pathsEqual p p')  withCondsWithPath :: forall ren sch t r. (forall t'. Cond ren sch t' -> r) ->-  [(Text, PathKind)] -> [CondWithPath ren sch t] -> Maybe r+  [PathElem] -> [CondWithPath ren sch t] -> Maybe r withCondsWithPath f p = join . L.find isJust . L.map (withCondWithPath f p) -cwp :: forall path -> forall ren sch t t1.-  (t1 ~ TabOnDPath sch t (MapRenPath ren path), ToStar (MapRenPath ren path))-  => Cond ren sch t1 -> CondWithPath ren sch t-cwp p = CondWithPath @p+cwp :: forall path -> forall ren sch t.+  PathCtx ren sch t path =>+  Cond ren sch (TabOnDPathRen ren sch t path) -> CondWithPath ren sch t+cwp p = CondWithPath @p (demote @p)  rootCond :: Cond ren sch t -> CondWithPath ren sch t rootCond = cwp [] -condByPath :: Int -> [(Text, PathKind)] -> [CondWithPath ren sch t] -> (Text, [SomeToField])+condByPath :: Int -> [PathElem] -> [CondWithPath ren sch t] -> (Text, [SomeToField]) condByPath num p = F.fold . withCondsWithPath (pgCond num) p -ordByPath :: Int -> [(Text, PathKind)] -> [OrdWithPath ren sch t] -> (TextI ",", [SomeToField])+ordByPath :: Int -> [PathElem] -> [OrdWithPath ren sch t] -> (TextI ",", [SomeToField]) ordByPath num p = F.fold . withOrdsWithPath (pgOrd num) p -distByPath :: Int -> [(Text, PathKind)] -> [DistWithPath ren sch t] -> (DistTexts, [SomeToField])+distByPath :: Int -> [PathElem] -> [DistWithPath ren sch t] -> (DistTexts, [SomeToField]) distByPath num p = F.fold . withDistsWithPath (pgDist num) p  withOrdWithPath :: forall ren sch t r. (forall t'. [OrdFld ren sch t'] -> r) ->-  [(Text, PathKind)] -> OrdWithPath ren sch t -> Maybe r-withOrdWithPath f p (OrdWithPath @p ord) = f ord <$ guard (p == demote @(MapRenPath ren p))+  [PathElem] -> OrdWithPath ren sch t -> Maybe r+withOrdWithPath f p (OrdWithPath p' ord) =+  f ord <$ guard (pathsEqual p p')  withDistWithPath :: forall ren sch t r. (forall t'. Dist ren sch t' -> r) ->-  [(Text, PathKind)] -> DistWithPath ren sch t -> Maybe r-withDistWithPath f p (DistWithPath @p dist) = f dist <$ guard (p == demote @(MapRenPath ren p))+  [PathElem] -> DistWithPath ren sch t -> Maybe r+withDistWithPath f p (DistWithPath p' dist) =+  f dist <$ guard (pathsEqual p p')  -- withOrdsWithPath :: forall ren sch t r . (forall t'. [OrdFld ren sch t'] -> r) ->-  [(Text, PathKind)] -> [OrdWithPath ren sch t] -> Maybe r+  [PathElem] -> [OrdWithPath ren sch t] -> Maybe r withOrdsWithPath f p = join . L.find isJust . L.map (withOrdWithPath f p)  withDistsWithPath :: forall ren sch t r . (forall t'. Dist ren sch t' -> r) ->-  [(Text, PathKind)] -> [DistWithPath ren sch t] -> Maybe r+  [PathElem] -> [DistWithPath ren sch t] -> Maybe r withDistsWithPath f p = join . L.find isJust . L.map (withDistWithPath f p)  owp :: forall path -> forall sch t t'.-  (ToStar (MapRenPath ren path), TabOnDPath sch t (MapRenPath ren path) ~ t') =>+  ( PathCtx ren sch t path, TabOnDPathRen ren sch t path ~ t' ) =>   [OrdFld ren sch t'] -> OrdWithPath ren sch t-owp p = OrdWithPath @p+owp p = OrdWithPath @p (demote @p)  rootOrd :: forall ren sch t. [OrdFld ren sch t] -> OrdWithPath ren sch t rootOrd = owp [] -dwp :: forall path -> forall ren sch t t'.-  (ToStar (MapRenPath ren path), TabOnDPath2 sch t (MapRenPath ren path) ~ '(t', 'RelMany)) =>-  Dist ren sch t' -> DistWithPath ren sch t-dwp p = DistWithPath @p+dwp :: forall path -> forall ren sch t.+  ( PathCtx ren sch t path, PathEndsMany sch t path ) =>+  Dist ren sch (TabOnDPathRen ren sch t path) -> DistWithPath ren sch t+dwp p = DistWithPath @p (demote @p)  rootDist :: forall ren sch t. Dist ren sch t -> DistWithPath ren sch t rootDist = dwp []
src/PgSchema/DML/Select/Types.hs view
@@ -29,7 +29,6 @@ import PgSchema.Ann import PgSchema.Schema import PgSchema.Types-import PgSchema.Utils.Internal import PgSchema.Utils.TF  @@ -53,7 +52,7 @@ qpEmpty = QueryParam [] [] [] []  type MonadQP ren sch t path-  = (TabDPath sch t (MapRenPath ren path), ToStar (MapRenPath ren path))+  = PathCtx ren sch t path   => RWS (Proxy path) () (QueryParam ren sch t) ()  -- | Execute 'MonadQP' and get 'QueryParam'.@@ -67,8 +66,8 @@ -- The 'Symbol' must name the foreign-key constraint (edge to the parent or from the child) -- for the step away from the current table. ---qPath :: forall ren sch t path path' path'' tabPath p' k. forall (p :: Symbol)-  -> PathCheck 'Nothing p ren sch t path path' path'' tabPath p' k+qPath :: forall ren sch t path path' tabPath p' k. forall (p :: Symbol)+  -> PathCheck 'Nothing p ren sch t path path' tabPath p' k   => MonadQP ren sch t path' -> MonadQP ren sch t path qPath _p m = put . fst . execRWS m Proxy =<< get @@ -77,8 +76,8 @@ -- The 'Symbol' must name the foreign-key constraint (edge to the parent) -- for the step away from the current table. ---qPathFromHere :: forall ren sch t path path' path'' tabPath p' k. forall (p :: Symbol)-  -> PathCheck ('Just 'FromHere) p ren sch t path path' path'' tabPath p' k+qPathFromHere :: forall ren sch t path path' tabPath p' k. forall (p :: Symbol)+  -> PathCheck ('Just 'FromHere) p ren sch t path path' tabPath p' k   => MonadQP ren sch t path' -> MonadQP ren sch t path qPathFromHere _p m = put . fst . execRWS m Proxy =<< get @@ -87,52 +86,23 @@ -- The 'Symbol' must name the foreign-key constraint (edge from the child) -- for the step away from the current table. ---qPathToHere :: forall ren sch t path path' path'' tabPath p' k. forall (p :: Symbol)-  -> PathCheck ('Just 'ToHere) p ren sch t path path' path'' tabPath p' k+qPathToHere :: forall ren sch t path path' tabPath p' k. forall (p :: Symbol)+  -> PathCheck ('Just 'ToHere) p ren sch t path path' tabPath p' k   => MonadQP ren sch t path' -> MonadQP ren sch t path qPathToHere _p m = put . fst . execRWS m Proxy =<< get -type family ResolvePathKind-  (pathKind :: Maybe PathKind) (hasFrom :: Bool) (hasTo :: Bool) (tab :: NameNSK) (name :: Symbol)-  :: PathKind where-    ResolvePathKind ('Just 'FromHere) 'True _ _ _ = 'FromHere-    ResolvePathKind ('Just 'ToHere) _ 'True _ _ = 'ToHere-    ResolvePathKind 'Nothing 'True 'False _ _ = 'FromHere-    ResolvePathKind 'Nothing 'False 'True _ _ = 'ToHere-    ResolvePathKind ('Just 'FromHere) _ _ tab name = TypeError-      (TE.Text "Relation is not available in from-here direction."-      :$$: TE.Text "Use qPathToHere or qPath."-      :$$: TE.Text ""-      :$$: TE.Text "Table: " :<>: ShowType tab-      :$$: TE.Text "Relation: " :<>: ShowType name-      :$$: TE.Text "" )-    ResolvePathKind ('Just 'ToHere) _ _ tab name = TypeError-      (TE.Text "Relation is not available in to-here direction."-      :$$: TE.Text "Use qPathFromHere or qPath."-      :$$: TE.Text ""-      :$$: TE.Text "Table: " :<>: ShowType tab-      :$$: TE.Text "Relation: " :<>: ShowType name-      :$$: TE.Text "" )-    ResolvePathKind 'Nothing _ _ tab name = TypeError-      (TE.Text "qPath cannot be used for self-reference relation."-      :$$: TE.Text "Use qPathFromHere or qPathToHere."-      :$$: TE.Text ""-      :$$: TE.Text "Table: " :<>: ShowType tab-      :$$: TE.Text "Relation: " :<>: ShowType name-      :$$: TE.Text "" )--type PathCheck pathKind p ren sch t path path' path'' tabPath p' k =+type PathCheck pathKind p ren sch t path path' tabPath p' k =   ( p' ~ ApplyRenamer ren p-  , tabPath ~ TabOnDPath sch t (MapRenPath ren path)+  , tabPath ~ TabOnDPathRen ren sch t path   , k ~ ResolvePathKind pathKind (HasFromStep sch tabPath p') (HasToStep sch tabPath p') tabPath p'-  , path' ~ (path ++ '[ '(p, k)])-  , path'' ~ MapRenPath ren (path ++ '[ '(p, k)])-  , ToStar path'', TabDPath sch t path'' )+  , path' ~ (path ++ '[ 'PathElem p p' k ])+  , PathCtx ren sch t path'+  ) -- | Add @WHERE@ condition for the current table. -- -- If several 'qWhere' exist they are composed according to the 'Monoid' instance for 'Cond', i.e. with '(&&&)'-qWhere :: forall ren sch t path. Cond ren sch (TabOnDPath sch t (MapRenPath ren path)) -> MonadQP ren sch t path-qWhere c = modify \qp -> qp { qpConds = CondWithPath @path c : qp.qpConds }+qWhere :: forall ren sch t path. Cond ren sch (TabOnDPathRen ren sch t path) -> MonadQP ren sch t path+qWhere c = modify \qp -> qp { qpConds = CondWithPath @path (demote @path) c : qp.qpConds }  -- | Add @ORDER BY@ condition for the current table --@@ -149,14 +119,14 @@ -- -- we get @ORDER BY t1.f1, t2.f2 DESC, t1.f3@ ---qOrderBy :: forall ren sch t path. [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> MonadQP ren sch t path-qOrderBy ofs = modify \qp -> qp { qpOrds = OrdWithPath @path ofs : qp.qpOrds }+qOrderBy :: forall ren sch t path. [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> MonadQP ren sch t path+qOrderBy ofs = modify \qp -> qp { qpOrds = OrdWithPath @path (demote @path) ofs : qp.qpOrds }  -- | Add `DISTINCT` condition for the current table. -- It is applied only to "root" or "children" tables.-qDistinct :: forall ren sch t path t'. TabOnDPath2 sch t path ~ '(t', 'RelMany) =>+qDistinct :: forall ren sch t path. PathEndsMany sch t path =>   MonadQP ren sch t path-qDistinct = modify \qp -> qp { qpDistinct = DistWithPath @path Distinct : qp.qpDistinct }+qDistinct = modify \qp -> qp { qpDistinct = DistWithPath @path (demote @path) Distinct : qp.qpDistinct }  -- | Add @DISTINCT ON@ condition for the current table. --@@ -176,60 +146,60 @@ -- -- we get @DISTINCT ON (t1.f1, t2.f2, t1.f3) ... ORDER BY t1.f1, t2.f2 DESC, t1.f3, t1.f0 DESC@ ---qDistinctOn :: forall ren sch t path. [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> MonadQP ren sch t path-qDistinctOn ofs = modify \qp -> qp { qpDistinct = DistWithPath @path (DistinctOn ofs) : qp.qpDistinct }+qDistinctOn :: forall ren sch t path. [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> MonadQP ren sch t path+qDistinctOn ofs =+  modify \qp -> qp { qpDistinct = DistWithPath @path (demote @path) (DistinctOn ofs) : qp.qpDistinct }  -- | Add `LIMIT` condition for the current table. -- It is applied only to "root" or "children" tables. -- -- If 'qLimit' is applied several times on the same path, only the last one is used-qLimit :: forall ren sch t path. Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ RelMany-  => Natural -> MonadQP ren sch t path+qLimit :: forall ren sch t path. PathEndsMany sch t path =>+  Natural -> MonadQP ren sch t path qLimit n = modify \qp -> qp { qpLOs = mk qp.qpLOs }   where     mk xs = case L.break eq xs of       (xs1, []) -> new : xs1       (xs1, x:xs2) -> xs1 <> [upd x] <> xs2-    eq (LimOffWithPath @p _) = demote @(MapRenPath ren p) == demote @(MapRenPath ren path)-    upd (LimOffWithPath @p lo) = LimOffWithPath @p lo{ limit = Just n }-    new = LimOffWithPath @path LO { limit = Just n, offset = Nothing }+    eq (LimOffWithPath p _) = pathsEqual p (demote @path)+    upd (LimOffWithPath @p' p lo) = LimOffWithPath @p' p lo{ limit = Just n }+    new = LimOffWithPath @path (demote @path) LO { limit = Just n, offset = Nothing }  -- | Add `OFFSET` condition for the current table. -- It is applied only to "root" or "children" tables. -- -- If 'qOffset' is applied several times on the same path, only the last one is used-qOffset :: forall ren sch t path. Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ RelMany-  => Natural -> MonadQP ren sch t path+qOffset :: forall ren sch t path. PathEndsMany sch t path =>+  Natural -> MonadQP ren sch t path qOffset n = modify \qp -> qp { qpLOs = mk qp.qpLOs }   where     mk xs = case L.break eq xs of       (xs1, []) -> new : xs1       (xs1, x:xs2) -> xs1 <> [upd x] <> xs2-    eq (LimOffWithPath @p _) = demote @(MapRenPath ren p) == demote @(MapRenPath ren path)-    upd (LimOffWithPath @p lo) = LimOffWithPath @p lo{offset = Just n}-    new = LimOffWithPath @path LO { offset = Just n, limit = Nothing }+    eq (LimOffWithPath p _) = pathsEqual p (demote @path)+    upd (LimOffWithPath @p' p lo) = LimOffWithPath @p' p lo{offset = Just n}+    new = LimOffWithPath @path (demote @path) LO { offset = Just n, limit = Nothing }  -- | GADT to safely set `where` condition data CondWithPath ren sch t where-  CondWithPath ::  forall (path :: [(Symbol, PathKind)]) ren sch t. ToStar (MapRenPath ren path)-    => Cond ren sch (TabOnDPath sch t (MapRenPath ren path)) -> CondWithPath ren sch t+  CondWithPath :: forall (path :: [PathElemK]) ren sch t. PathCtx ren sch t path+    => [PathElem] -> Cond ren sch (TabOnDPathRen ren sch t path) -> CondWithPath ren sch t  -- | GADT to safely set `order by` clauses data OrdWithPath ren sch t where-  OrdWithPath :: forall (path :: [(Symbol, PathKind)]) ren sch t. ToStar (MapRenPath ren path)-    => [OrdFld ren sch (TabOnDPath sch t (MapRenPath ren path))] -> OrdWithPath ren sch t+  OrdWithPath :: forall (path :: [PathElemK]) ren sch t. PathCtx ren sch t path+    => [PathElem] -> [OrdFld ren sch (TabOnDPathRen ren sch t path)] -> OrdWithPath ren sch t  -- | GADT to safely set `distinct/distinct on` clauses data DistWithPath ren sch t where-  DistWithPath :: forall (path :: [(Symbol, PathKind)]) ren sch t. ToStar (MapRenPath ren path)-    => Dist ren sch (TabOnDPath sch t (MapRenPath ren path)) -> DistWithPath ren sch t+  DistWithPath :: forall (path :: [PathElemK]) ren sch t. PathCtx ren sch t path+    => [PathElem] -> Dist ren sch (TabOnDPathRen ren sch t path) -> DistWithPath ren sch t  -- | GADT to safely set `limit/offset` clauses data LimOffWithPath ren sch t where-  LimOffWithPath :: forall (path :: [(Symbol, PathKind)]) ren sch t.-    ( TabDPath sch t (MapRenPath ren path), ToStar (MapRenPath ren path)-    , Snd (TabOnDPath2 sch t (MapRenPath ren path)) ~ 'RelMany )-    => LO -> LimOffWithPath ren sch t+  LimOffWithPath :: forall (path :: [PathElemK]) ren sch t.+    ( PathCtx ren sch t path, PathEndsMany sch t path )+    => [PathElem] -> LO -> LimOffWithPath ren sch t  -- | Comparison constructors; each is paired with its corresponding operator -- (e.g. '(:=)' with '(=?)').@@ -352,7 +322,7 @@   , order :: [OrdFld ren sch tab]   , lo :: LO } --- | Default empty 'TabParam'.+-- | Default empty 'TabParam': no conditions, no ordering, no limits. defTabParam :: TabParam ren sch tab defTabParam = TabParam mempty mempty defLO @@ -465,17 +435,20 @@   DistinctOn :: [OrdFld ren sch tab] -> Dist ren sch tab  {-# INLINE ordf #-}+-- | Sort by field in the given direction. ordf   :: forall fld -> forall sch tab. CDBField sch tab (ApplyRenamer ren fld)   => OrdDirection -> OrdFld ren sch tab ordf fld = OrdFld @fld  {-# INLINE ascf #-}+-- | Sort ascending by field. ascf :: forall fld -> forall sch tab. CDBField sch tab (ApplyRenamer ren fld)   => OrdFld ren sch tab ascf fld = ordf fld Asc  {-# INLINE descf #-}+-- | Sort descending by field. descf :: forall fld -> forall sch tab. CDBField sch tab (ApplyRenamer ren fld)   => OrdFld ren sch tab descf fld = ordf fld Desc@@ -485,8 +458,10 @@   , offset :: Maybe Natural }   deriving Show +-- | No limit and no offset. defLO :: LO defLO = LO Nothing Nothing +-- | Limit 1 and no offset. lo1 :: LO lo1 = LO (Just 1) Nothing
src/PgSchema/DML/Update.hs view
@@ -1,22 +1,112 @@-module PgSchema.DML.Update where+{-# LANGUAGE AllowAmbiguousTypes #-}+module PgSchema.DML.Update+  ( updateByKey, updateByKey_, updateByKeyText, updateByKeyText_+  , updateByCond, updateByCond_, updateText, updateText_+  , updateByKeyRowParams ) where  import Data.String import Data.Text as T import Database.PostgreSQL.Simple+import Database.PostgreSQL.Simple.ToField (Action)+import Database.PostgreSQL.Simple.ToRow (ToRow(..)) import GHC.Int import PgSchema.Ann import PgSchema.DML.Select import PgSchema.DML.Select.Types+import Control.Monad (forM)+import Data.List as L+import Data.Maybe import PgSchema.DML.Insert.Types-import PgSchema.Schema+import PgSchema.DML.KeyedWrite+import Data.Map qualified as M+import PgSchema.Schema (CSchema, qualName, tabInfoMap) import PgSchema.Types import PgSchema.Utils.Internal import Prelude as P --- TODO:--- updateByKey Connection -> [r] -> IO [r']--- updateByKeyJSON Connection -> [r] -> IO [r']--- updateExp (e.q. update t set a = a + c + 1 where b > 10)+newtype KeyedUpdateParams = KeyedUpdateParams { keyedUpdateActions :: [Action] }++instance ToRow KeyedUpdateParams where+  toRow = keyedUpdateActions++updateByKeyRowParams+  :: forall ann r+  . (CRecInfo ann r, CSchema (AnnSch ann), ToRow (PgTag ann r))+  => r -> KeyedUpdateParams+updateByKeyRowParams rec =+  let+    ri = getRecordInfo @ann @r+    ti = tabInfoMap @(AnnSch ann) M.! ri.tabName+    keyNames = fromMaybe [] $ pickKeyNames ti [fi.fieldDbName | fi <- ri.fields]+    (fldKeys, fldOthers) =+      L.partition ((`L.elem` keyNames) . (.fieldDbName)) ri.fields+    allActs = toRow (PgTag @ann @r rec)+    pick fi =+      allActs+        !! fromMaybe (error "updateByKeyRowParams: field")+          (L.findIndex ((== fi.fieldDbName) . (.fieldDbName)) ri.fields)+  in KeyedUpdateParams $ P.map pick (fldOthers ++ fldKeys)++-- | Update rows by primary / unique key from record fields (never inserts).+--+-- Returning type @r'@ is a bare row; the result is @IO ([Maybe r'], Text)@+-- with @Nothing@ when no row matched the key.+updateByKey+  :: forall ann -> forall r r'. UpdateByKeyReturning ann r r'+  => Connection -> [r] -> IO ([Maybe r'], Text)+updateByKey ann @r @r' conn recs =+  let sql = updateByKeyText ann @r @r' in+  trace' (T.unpack sql) do+    rs <- forM recs \rec -> do+      rows <- query conn (fromString $ T.unpack sql) (updateByKeyRowParams @ann @r rec)+      pure $ case rows of+        [x] -> Just (unPgTag @ann @r' x)+        _ -> Nothing+    pure (rs, sql)++-- | Update rows by key without @RETURNING@.+updateByKey_+  :: forall ann -> forall r. UpdateByKeyNonReturning ann r+  => Connection -> [r] -> IO (Int64, Text)+updateByKey_ ann @r conn recs =+  let sql = updateByKeyText_ ann @r in+  trace' (T.unpack sql) do+    n <- executeMany conn (fromString $ T.unpack sql)+      $ fmap (updateByKeyRowParams @ann @r) recs+    pure (n, sql)++updateByKeyText+  :: forall ann -> forall r r' s+  . ( CRecInfo ann r, CRecInfo ann r', IsString s, Monoid s, HasSchema ann ) => s+updateByKeyText ann @r @r' =+  updateByKeyText_ ann @r <> " returning " <> fs'+  where+    ri' = getRecordInfo @ann @r'+    fs' = fromString $ T.unpack $ T.intercalate "," [fi.fieldDbName | fi <- ri'.fields]++updateByKeyText_+  :: forall ann -> forall r s+  . (IsString s, Monoid s, CRecInfo ann r, HasSchema ann) => s+updateByKeyText_ ann @r = fromString $ T.unpack $ updateByKeyStmt ann @r++updateByKeyStmt+  :: forall ann -> forall r. (CRecInfo ann r, HasSchema ann) => T.Text+updateByKeyStmt ann @r =+  let+    ri = getRecordInfo @ann @r+    ti = tabInfoMap @(AnnSch ann) M.! ri.tabName+    plains = [ (fromText fi.fieldDbName, "?") | fi <- ri.fields ]+    (plainsKey, plainsOthers) = keyedUpdateSetAndKeys ti plains+    nameVal n v = n <> " = " <> v+    setClause =+      case plainsOthers of+        [] -> case plainsKey of+          (n, _) : _ -> n <> " = " <> n+          [] -> "id = id"+        xs -> T.intercalate ", " (uncurry nameVal <$> xs)+  in "update " <> qualName ri.tabName+    <> " set " <> setClause+    <> keyWhereClause plainsKey  -- | Update rows matching a condition; the result type selects which columns are returned. updateByCond :: forall ann -> forall r r'.
+ src/PgSchema/DML/UpsertByKey.hs view
@@ -0,0 +1,67 @@+{-# LANGUAGE OverloadedRecordDot #-}+module PgSchema.DML.UpsertByKey+  ( upsertByKey, upsertByKey_, upsertByKeyText, upsertByKeyText_ ) where++import Data.List as L+import Data.Maybe+import Data.String+import Data.Text as T+import Database.PostgreSQL.Simple+import GHC.Int+import PgSchema.Ann+import PgSchema.DML.Insert.Types+import PgSchema.DML.KeyedWrite+import Data.Map qualified as M+import PgSchema.Schema (qualName, tabInfoMap)+import PgSchema.Types+import PgSchema.Utils.Internal+import Prelude as P+++upsertByKey+  :: forall ann -> forall r r'. UpsertByKeyReturning ann r r'+  => Connection -> [r] -> IO ([r'], Text)+upsertByKey ann @r @r' conn recs = let sql = upsertByKeyText ann @r @r' in+  trace' (T.unpack sql) do+    (, sql)+      <$> fmap (fmap (unPgTag @ann @r'))+      ( returning conn (fromString $ T.unpack sql)+      $ fmap (PgTag @ann @r) recs )++upsertByKey_+  :: forall ann -> forall r. UpsertByKeyNonReturning ann r+  => Connection -> [r] -> IO (Int64, Text)+upsertByKey_ ann @r conn recs = let sql = upsertByKeyText_ ann @r in+  trace' (T.unpack sql)+    $ fmap (, sql)+    . executeMany conn (fromString $ T.unpack sql)+    . fmap (PgTag @ann @r) $ recs++upsertByKeyText+  :: forall ann -> forall r r' s+  . (UpsertByKeyReturning ann r r', IsString s, Monoid s) => s+upsertByKeyText ann @r @r' = upsertByKeyText_ ann @r <> " returning " <> fs'+  where+    ri' = getRecordInfo @ann @r'+    fs' = fromText $ T.intercalate "," [fi.fieldDbName | fi <- ri'.fields]++upsertByKeyText_+  :: forall ann -> forall r s+  . (UpsertByKeyNonReturning ann r, IsString s, Monoid s) => s+upsertByKeyText_ ann @r = fromString $ T.unpack $ upsertByKeyStmt ann @r++upsertByKeyStmt :: forall ann -> forall r. UpsertByKeyNonReturning ann r => Text+upsertByKeyStmt ann @r+  | L.null plainsOthers = ins <> " on conflict do nothing"+  | otherwise = ins <> upsertOnConflict plainsOthers keyNames+  where+    ri = getRecordInfo @ann @r+    ti = tabInfoMap @(AnnSch ann) M.! ri.tabName+    flds = [ (fromText fi.fieldDbName, "?") | fi <- ri.fields ]+    srcFlds = fst <$> flds+    keyNames = fromMaybe [] $ pickKeyNames ti srcFlds+    (_, plainsOthers) = keyedUpdateSetAndKeys ti flds+    ins =+      "insert into " <> qualName ri.tabName+      <> "(" <> intercalate' ", " srcFlds <> ") values ("+      <> intercalate' ", " (snd <$> flds) <> ")"
src/PgSchema/Schema.hs view
@@ -2,7 +2,6 @@ {-# LANGUAGE ParallelListComp #-} module PgSchema.Schema where -import Data.Kind import Data.List as L import Data.Map as M import Data.Singletons@@ -61,6 +60,13 @@ -- | Direction of one path step relative to the current table. data PathKind = FromHere | ToHere deriving (Show, Eq) +-- | One step of a 'SELECT' query path: logical branch name, DB fk name, direction.+data PathElem' s = PathElem+  { peName   :: s+  , peDbName :: s+  , peKind   :: PathKind+  } deriving (Show, Eq)+ -- | Field of a logical record: plain column, aggregate, or relation hop. data RecField' s p   = RFEmpty s -- ^ Placeholder / unnamed slot (depending on schema codegen).@@ -88,7 +94,7 @@  genSingletons   [ ''AggrFun, ''NameNS', ''TypDef', ''FldDef', ''TabDef', ''RelDef', ''RelType-  , ''PathKind+  , ''PathKind, ''PathElem'   , ''RecField', ''Ref' ]  type NameNSK = NameNS' Symbol@@ -104,7 +110,41 @@ type FldDef = FldDef' Text type TabDef = TabDef' Text type RelDef = RelDef' Text+type PathElemK = PathElem' Symbol+type PathElem = PathElem' Text +mkPathStep :: Text -> Text -> PathKind -> PathElem+mkPathStep name db kind = PathElem{peName = name, peDbName = db, peKind = kind}++-- | Broadcast step: 'peName' is the db/fk key from 'qPath', not a Haskell field.+pathStepBroadcast :: PathElem -> Bool+pathStepBroadcast s = s.peName == s.peDbName++-- | Whether a query-path step applies to a runtime tree-path step.+pathStepMatches :: PathElem -> PathElem -> Bool+pathStepMatches cond rt =+  cond.peKind == rt.peKind+  && cond.peDbName == rt.peDbName+  && ( cond.peName == rt.peName+    || cond.peName == cond.peDbName+    )++pathsEqual :: [PathElem] -> [PathElem] -> Bool+pathsEqual xs ys =+  L.length xs == L.length ys+  && and (L.zipWith pathStepMatches xs ys)++pathIsPrefixOfBy :: (PathElem -> PathElem -> Bool) -> [PathElem] -> [PathElem] -> Bool+pathIsPrefixOfBy _ [] _ = True+pathIsPrefixOfBy f (x : xs) (y : ys) = f x y && pathIsPrefixOfBy f xs ys+pathIsPrefixOfBy _ _ [] = False++pathIsPrefixOf :: [PathElem] -> [PathElem] -> Bool+pathIsPrefixOf = pathIsPrefixOfBy $ flip pathStepMatches++parentPathMatches :: [PathElem] -> [PathElem] -> [PathElem] -> Bool+parentPathMatches p basePath piPath = pathsEqual p (basePath <> piPath)+ infixr 9 ->> (->>) :: Text -> Text -> NameNS (->>) = NameNS@@ -113,14 +153,6 @@  type SimpleType c = 'TypDef c 'Nothing '[] -type family GetRelTab (froms :: [(NameNSK, RelDefK)])-  (tos :: [(NameNSK, RelDefK)]) (s :: Symbol) :: (NameNSK, RelType) where-    GetRelTab '[] '[] s = TypeError ('Text "No relation by name" ':$$: 'ShowType s)-    GetRelTab ('(a,b) ':xs) ys s =-      If (NnsName a == s) '(RdTo b, RelOne) (GetRelTab xs ys s)-    GetRelTab '[] ('(c,d) ':ys) s =-      If (NnsName c == s) '(RdFrom d, RelMany) (GetRelTab '[] ys s)- type IsMandatory fd = Not (FdNullable fd || FdHasDefault fd) type IsMandatory' sch tab fld = IsMandatory (GetFldDef sch tab fld) @@ -151,9 +183,10 @@   AppendPK '[] uks = uks   AppendPK pk uks = pk ': uks --- | Keys accepted by 'CheckAllMandatoryOrHasKey': PK plus every unique+-- | Keys accepted by 'CheckAllMandatoryOrHasKey' (tree upsert) and+-- 'CheckHasKey' (flat 'updateByKey' / 'upsertByKey'): PK plus every unique -- constraint from the schema (catalog order). Runtime key choice uses--- 'PgSchema.DML.InsertJSON.identityCandidatesFromTab' and its priority rules.+-- 'PgSchema.DML.KeyedWrite.identityCandidatesFromTab' and its priority rules. type family IdentityCandidates sch tab :: [[Symbol]] where   IdentityCandidates sch tab =     AppendPK (TdKey (TTabDef sch tab)) (TdUniq (TTabDef sch tab))@@ -288,10 +321,6 @@ typDefMap = M.fromList $ L.zip   (demote @(TTypes sch)) (demote @(Map1 (TTypDefSym1 sch) (TTypes sch))) -type TRelTab sch t name = GetRelTab-  (Map2 (TRelDefSym1 sch) (TFrom sch t)) (Map2 (TRelDefSym1 sch) (TTo sch t))-  name- type family GetRelDef (rels :: [(NameNSK, RelDefK)]) (s :: Symbol) :: RelDefK where   GetRelDef '[] s = TypeError ('Text "No relation by name" ':$$: 'ShowType s)   GetRelDef ('(a, b) ': xs) s = If (NnsName a == s) b (GetRelDef xs s)@@ -302,32 +331,26 @@ type family TRelToTab sch t name :: NameNSK where   TRelToTab sch t name = RdFrom (GetRelDef (Map2 (TRelDefSym1 sch) (TTo sch t)) name) -type family TabOnPath2 sch (t :: NameNSK) (path :: [Symbol]) :: (NameNSK, RelType) where-  TabOnPath2 sch t '[] = '(t, 'RelMany)-  TabOnPath2 sch t '[x] = TRelTab sch t x-  TabOnPath2 sch t (x ': xs) = TabOnPath2 sch (Fst (TRelTab sch t x)) xs--type TabOnPath sch (t :: NameNSK) (path :: [Symbol]) = Fst (TabOnPath2 sch t path)-+-- | Walk a directed @path@ from root table @t@. ---type family TabPath sch (t :: NameNSK) (path :: [Symbol]) :: Constraint where-  TabPath sch t '[] = ()-  TabPath sch t (x ': xs) = TabPath sch (Fst (TRelTab sch t x)) xs--type family TabOnDPath2 sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) :: (NameNSK, RelType) where+-- Each step is a 'PathElem' (schema walk uses 'peDbName'). Result: end table and+-- its cardinality ('RelOne' for a single parent step, 'RelMany' for a single child).+type family TabOnDPath2 sch (t :: NameNSK) (path :: [PathElemK]) :: (NameNSK, RelType) where   TabOnDPath2 sch t '[] = '(t, 'RelMany)-  TabOnDPath2 sch t '[ '(name, 'FromHere)] = '(TRelFromTab sch t name, 'RelOne)-  TabOnDPath2 sch t '[ '(name, 'ToHere)] = '(TRelToTab sch t name, 'RelMany)-  TabOnDPath2 sch t ('(name, 'FromHere) ': xs) = TabOnDPath2 sch (TRelFromTab sch t name) xs-  TabOnDPath2 sch t ('(name, 'ToHere) ': xs) = TabOnDPath2 sch (TRelToTab sch t name) xs+  TabOnDPath2 sch t '[ 'PathElem _ name 'FromHere] =+    '(TRelFromTab sch t name, 'RelOne)+  TabOnDPath2 sch t '[ 'PathElem _ name 'ToHere] =+    '(TRelToTab sch t name, 'RelMany)+  TabOnDPath2 sch t ('PathElem _ name 'FromHere ': xs) =+    TabOnDPath2 sch (TRelFromTab sch t name) xs+  TabOnDPath2 sch t ('PathElem _ name 'ToHere ': xs) =+    TabOnDPath2 sch (TRelToTab sch t name) xs -type family TabOnDPath sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) :: NameNSK where-  TabOnDPath sch t path = Fst (TabOnDPath2 sch t path)+-- | Table reached after walking directed @path@ from @t@ ('Fst' of 'TabOnDPath2').+type TabAtPath sch t path = Fst (TabOnDPath2 sch t path) -type family TabDPath sch (t :: NameNSK) (path :: [(Symbol, PathKind)]) :: Constraint where-  TabDPath sch t '[] = ()-  TabDPath sch t ('(name, 'FromHere) ': xs) = TabDPath sch (TRelFromTab sch t name) xs-  TabDPath sch t ('(name, 'ToHere) ': xs) = TabDPath sch (TRelToTab sch t name) xs+-- | Current position has child cardinality ('RelMany'): root or child table.+type PathEndsMany sch t path = Snd (TabOnDPath2 sch t path) ~ 'RelMany  type RecField = RecField' Text type Ref = Ref' Text@@ -355,26 +378,30 @@ type HasFromStep sch tab name = HasRelName (TFrom sch tab) name type HasToStep   sch tab name = HasRelName (TTo sch tab) name -type family CheckStep (pathKind :: (Maybe PathKind)) (hasFrom :: Bool) (hasTo :: Bool) sch tab name :: Constraint where-  CheckStep ('Just FromHere) 'True _ sch tab name = ()-  CheckStep ('Just 'ToHere)   _ 'True sch tab name = ()-  CheckStep 'Nothing 'True 'False sch tab name = ()-  CheckStep 'Nothing 'False 'True sch tab name = ()-  CheckStep ('Just 'FromHere) _ _ sch tab name = TypeError+-- | Resolve 'PathKind' for 'qPath' / 'qPathFromHere' / 'qPathToHere'.+type family ResolvePathKind+  (pathKind :: Maybe PathKind) (hasFrom :: Bool) (hasTo :: Bool) (tab :: NameNSK)+  (name :: Symbol)+  :: PathKind where+  ResolvePathKind ('Just 'FromHere) 'True _ _ _ = 'FromHere+  ResolvePathKind ('Just 'ToHere) _ 'True _ _ = 'ToHere+  ResolvePathKind 'Nothing 'True 'False _ _ = 'FromHere+  ResolvePathKind 'Nothing 'False 'True _ _ = 'ToHere+  ResolvePathKind ('Just 'FromHere) _ _ tab name = TypeError     ( TL.Text "Relation is not available in from-here direction."     :$$: TL.Text "Use qPathToHere or qPath."     :$$: TL.Text ""     :$$: TL.Text "Table: " :<>: ShowType tab     :$$: TL.Text "Relation: " :<>: ShowType name     :$$: TL.Text "" )-  CheckStep ('Just 'ToHere) _ _ sch tab name = TypeError+  ResolvePathKind ('Just 'ToHere) _ _ tab name = TypeError     ( TL.Text "Relation is not available in to-here direction."     :$$: TL.Text "Use qPathFromHere or qPath."     :$$: TL.Text ""     :$$: TL.Text "Table: " :<>: ShowType tab     :$$: TL.Text "Relation: " :<>: ShowType name     :$$: TL.Text "" )-  CheckStep 'Nothing _ _ sch tab name = TypeError+  ResolvePathKind 'Nothing _ _ tab name = TypeError     ( TL.Text "qPath cannot be used for self-reference relation."     :$$: TL.Text "Use qPathFromHere or qPathToHere."     :$$: TL.Text ""
src/PgSchema/Types.hs view
@@ -66,9 +66,10 @@ #endif  --- | Introduce `enum` database types.+-- | Representation of `enum` database types.+-- -- Data instances are produced by schema generation.--- You can use these data instances in you records to @SELECT@/@INSERT@/@UPSERT@ data+-- You can use these data instances in your records to @SELECT@ | @INSERT@ | @UPSERT@ data. data family PGEnum sch (name :: NameNSK) :: Type  instance@@ -111,9 +112,9 @@   size = F.size . P.show #endif --- | Introduce aggregate functions.+-- | Representation of aggregate functions. ----- I.e. @"fld" := Aggr AMin (Maybe Int32)@ means "minimum value of the field `fld`"+-- E.g. @\"fld\" := Aggr AMin (Maybe Int32)@ means "minimum value of the field `fld`" -- -- 'Aggr' requires a 'Maybe' argument for all functions except @count@. --
test-pgs/Main.hs view
@@ -12,6 +12,7 @@ import Tests.Hierarchy import Tests.InsertJSONTransaction import Tests.UpsertUniqueKey+import Tests.KeyedDML import Tests.Conditions import Tests.Aggregates @@ -48,6 +49,28 @@       , testProperty "Failure rolls back standalone transaction" $           prop_insertJSON_rolls_back_on_failure pool       ]+    , testGroup "Keyed DML (test_dml)"+      [ testProperty "upsertByKey insert then key patch" $+          prop_upsert_by_key_insert_then_update pool+      , testProperty "upsertByKey composite unique" $+          prop_upsert_by_key_composite_unique pool+      , testProperty "upsertByKey always INSERT ON CONFLICT SQL" $+          prop_upsert_by_key_never_pure_update pool+      , testProperty "updateByKey found" $ prop_update_by_key_found pool+      , testProperty "updateByKey not found" $+          prop_update_by_key_not_found pool+      , testProperty "updateByKey never inserts" $+          prop_update_by_key_never_inserts pool+      , testProperty "updateJSON bad root id" $+          prop_update_json_not_found_returns_nothing pool+      , testProperty "updateJSON found" $ prop_update_json_found pool+      , testProperty "upsertJSON returning positions" $+          prop_upsert_json_returning_positions pool+      , testProperty "upsertJSON full row bare returning" $+          prop_upsert_json_full_row_bare_returning pool+      , testProperty "updateJSON child key miss" $+          prop_update_json_child_maybe pool+      ]     , testGroup "Upsert by unique key (test_dml)"       [ testProperty "upsertJSON on composite unique without PK in payload" $           prop_upsert_json_by_composite_unique pool@@ -61,6 +84,10 @@     , testGroup "Query (test_dml)"       [ testProperty "'Simple' queries" $ prop_cond_query pool       , testProperty "Conditions by duplicated path" $ prop_cond_by_dup_path pool+      , testProperty "Renamer: independent filters per aliased fk field" $+          prop_renamer_alias_dual_fields pool+      , testProperty "Renamer: broadcast qPath by shared db fk" $+          prop_renamer_broadcast_dual_fields pool       ]     , testGroup "Aggregates (test_dml)"       [ testProperty "Aggr' on plain column is not in GROUP BY" $
test-pgs/Tests/BaseConverts.hs view
@@ -61,11 +61,12 @@ prop_base_converts pool = withTests 30 $ property do   recs <- forAll (genData BaseConverts)   (resSel, resIns, resUpd) <- evalIO $ withPool pool \conn -> do+    void $ delByCond "base_converts" conn mempty     void $ insSch_ "base_converts" conn recs     (res, _) <- selSch "base_converts" conn qpEmpty     res'' <- updByCond "base_converts" conn       ("cint4" =: Just (10 :: Int32)) ("cboolean" =? Just False)-    (res', _) <- insSch "base_converts" conn recs+    (res' :: [BaseConverts], _) <- insSch "base_converts" conn recs     pure (res, res', res'')   L.sort resSel === L.sort recs   resIns === recs@@ -77,11 +78,12 @@   recs <- forAll (genData BaseArrConverts)   ds <- forAll $ Gen.list (Range.linear 0 20) genUTCTime   (resSel, resIns, resUpd::[BaseArrConverts]) <- evalIO $ withPool pool \conn -> do+    void $ delByCond "base_arr_converts" conn mempty     void $ insSch_ "base_arr_converts" conn recs     (res, _) <- selSch "base_arr_converts" conn qpEmpty     res'' <- updByCond "base_arr_converts" conn       ("ctimestamptz" =: Just (pgArr' ds)) mempty-    (res', _) <- insSch "base_arr_converts" conn recs+    (res' :: [BaseArrConverts], _) <- insSch "base_arr_converts" conn recs     pure (res, res', res'')   L.sort resSel === L.sort recs   resIns === recs@@ -91,11 +93,12 @@ prop_ext_converts pool = withTests 30 $ property do   recs <- forAll (genData ExtConverts)   (resSel, resIns, resUpd) <- evalIO $ withPool pool \conn -> do+    void $ delByCond "ext_converts" conn mempty     void $ insSch_ "ext_converts" conn recs     (res, _) <- selSch "ext_converts" conn qpEmpty     res'' <- updByCond "ext_converts" conn       ("ccitext" =: Just ("CaSes" :: CI Text)) ("ccolor" =? Just Color_red)-    (res', _) <- insSch "ext_converts" conn recs+    (res' :: [ExtConverts], _) <- insSch "ext_converts" conn recs     pure (res, res', res'')   L.sort resSel === L.sort recs   resIns === recs
test-pgs/Tests/Conditions.hs view
@@ -6,6 +6,7 @@  module Tests.Conditions where +import Control.Monad (forM_) import Control.Monad.IO.Class import Data.Functor import Data.Function@@ -14,7 +15,7 @@ import Data.Pool as Pool import Data.Text as T import Database.PostgreSQL.Simple-import GHC.Generics+import GHC.Generics (Generic) import GHC.Int import GHC.Stack (HasCallStack) import Hedgehog@@ -22,20 +23,10 @@ import Utils  -data RootI = MkRootI-  { code :: Text-  , grp :: Int32-  , someEmpty :: ()-  , name :: Text }-  deriving (Show, Generic)-  deriving anyclass GenDefault--data Mid1I = MkMid1I-  { pos :: Int32-  , flag :: Bool-  , sort_key :: Int32-  , payload :: Maybe Text }-  deriving (Show, Generic)+data LeafI = MkLeafI+  { leafNo :: Int32+  , value :: Double }+  deriving stock (Eq, Ord, Show, Generic)   deriving anyclass GenDefault  data Mid2I = MkMid2I@@ -43,28 +34,51 @@   , kind :: Text   , flag :: Bool   , priority :: Int32 }-  deriving (Show, Generic, Eq, Ord)+  deriving stock (Eq, Ord, Show, Generic)   deriving anyclass GenDefault -data LeafI = MkLeafI-  { leaf_no :: Int32-  , value :: Double }-  deriving (Generic, Eq, Ord, Show)+data Mid1I = MkMid1I+  { pos :: Int32+  , flag :: Bool+  , sortKey :: Int32+  , payload :: Maybe Text }+  deriving stock (Eq, Ord, Show, Generic)   deriving anyclass GenDefault -eqRoot :: RootI -> RootI -> Bool-eqRoot r1 r2 = (r1.code, r1.grp) == (r2.code, r2.grp)+data RootI = MkRootI+  { code :: Text+  , grp :: Int32+  , someEmpty :: ()+  , name :: Text }+  deriving stock (Show, Generic)+  deriving anyclass GenDefault  type InsData = "mid1_root_fk" := [Mid1I]   :. "mid2_root_fk" := ["leaf_mid2_fk" := [LeafI] :. Mid2I]   :. RootI +-- | Two edges in the result type that renamer maps to the same DB fk+-- ('mid1_root_fk2' -> 'mid1_root_fk'); each branch gets its own 'qPath' filter.+data RootDualMid1 = MkRootDualMid1+  { code :: Text+  , grp :: Int32+  , someEmpty :: ()+  , name :: Text+  , mid1RootFk :: [Mid1I] -- Renamer renames it to "mid1_root_fk"+  , mid1_root_fk2 :: [Mid1I] -- Renamer renames it to "mid1_root_fk"+  }+  deriving stock (Show, Generic)+  deriving anyclass GenDefault++eqRoot :: RootI -> RootI -> Bool+eqRoot r1 r2 = (r1.code, r1.grp) == (r2.code, r2.grp)+ insData :: MonadIO m => Pool Connection -> PropertyT m [InsData] insData pool = do   rootsIn <- forAll (L.nubBy eqRoot <$> genData' RootI 1 200)   mid1In <- forAll (genData' Mid1I 0 10)   mid2In <- forAll (L.nubBy ((==) `on` (.seq)) <$> genData' Mid2I 0 10)-  leafIn <- forAll (L.nubBy ((==) `on` (.leaf_no)) <$> genData' LeafI 0 10)+  leafIn <- forAll (L.nubBy ((==) `on` (.leafNo)) <$> genData' LeafI 0 10)   let     inIns = rootsIn <&> \r -> "mid1_root_fk" =: mid1In       :. "mid2_root_fk" =: (mid2In <&> \m2 -> "leaf_mid2_fk" =: leafIn :. m2)@@ -94,27 +108,73 @@           qWhere $ pparent (TS "leaf_mid2_fk")             $ pparent (TS "mid2_root_fk") $ "grp" >? (0::Int32)         qOrderBy [descf "kind"]-  L.length (L.filter (\(PgTag m1s :. _ :. r) ->-    (r.grp > 100 || r.grp `L.elem` [0..70])-    && L.any ((<100) . (.sort_key)) (L.take 2 $ L.sortBy (comparing (.pos)) m1s)-    )-    inIns) === L.length res+  L.length (L.filter rootMatches inIns) === L.length res+  where+    rootMatches (PgTag m1s :. _ :. r) =+      (r.grp > 100 || r.grp `L.elem` [0..70])+      && L.any ((<100) . (.sortKey))+        (L.take 2 $ L.sortBy (comparing (.pos)) m1s) -type OutData = "mid2_root_fk" := [Mid2I] :. InsData+-- | 'qPath' / 'qWhere' on 'mid1_root_fk' vs 'mid1_root_fk2' must not be merged+-- just because both edges share one fk in the database.+prop_renamer_alias_dual_fields :: Pool Connection -> Property+prop_renamer_alias_dual_fields pool = withTests 30 $ property do+  inIns <- insData pool+  (sel :: [RootDualMid1], _) <- evalIO $ withResource pool \conn ->+    selSch "root" conn $ qRoot do+      qPath "mid1RootFk" do+        qWhere $ "flag" =? True+      qPath "mid1_root_fk2" do+        qWhere $ "pos" >? (5 :: Int32)+  forM_ sel \root -> do+    let+      expectFk = [ m1+        | PgTag m1s :. _ :. r <- inIns+        , (r.code, r.grp) == (root.code, root.grp)+        , m1 <- m1s+        , m1.flag+        ]+      expectFk2 = [ m1+        | PgTag m1s :. _ :. r <- inIns+        , (r.code, r.grp) == (root.code, root.grp)+        , m1 <- m1s+        , m1.pos > 5+        ]+    L.sort root.mid1RootFk === L.sort expectFk+    L.sort root.mid1_root_fk2 === L.sort expectFk2 --- We can have similar pathes in select.--- Params are applied for all branches with this path+-- | 'qPath' by shared db fk name applies one 'qWhere' to every matching branch.+prop_renamer_broadcast_dual_fields :: Pool Connection -> Property+prop_renamer_broadcast_dual_fields pool = withTests 30 $ property do+  inIns <- insData pool+  (sel :: [RootDualMid1], _) <- evalIO $ withResource pool \conn ->+    selSch "root" conn $ qRoot do+      qPath "mid1_root_fk" do+        qWhere $ "flag" =? True+  forM_ sel \root -> do+    let+      expectFlag = [ m1+        | PgTag m1s :. _ :. r <- inIns+        , (r.code, r.grp) == (root.code, root.grp)+        , m1 <- m1s+        , m1.flag+        ]+    L.sort root.mid1RootFk === L.sort expectFlag+    L.sort root.mid1_root_fk2 === L.sort expectFlag++-- | Duplicate 'mid2_root_fk' in the result type: filters on the path+-- and on nested 'leaf_mid2_fk' apply per branch and may diverge. prop_cond_by_dup_path :: Pool Connection -> Property prop_cond_by_dup_path pool = withTests 30 $ property do   inIns <- insData pool-  (sel :: [OutData], _) <- evalIO $ withResource pool \conn ->+  (sel :: ["mid2_root_fk" := [Mid2I] :. InsData], _) <- evalIO $ withResource pool \conn ->     selSch "root" conn $ qRoot-      $ qPath "mid2_root_fk" do+      $ qPath "mid2_root_fk" do -- there are two "mid2_root_fk" branches         qWhere $ "flag" =? True-        qPath "leaf_mid2_fk" do-          qWhere $ "leaf_no" >? (100::Int32)+        qPath "leaf_mid2_fk" do -- only one "leaf_mid2_fk" branch+          qWhere $ "leaf_no" >? (97::Int32)   L.sort [m2 | (PgTag m2s :. _) <- sel, m2 <- m2s]     === L.sort [m2 | (_ :. PgTag m2s :. _) <- inIns, (_ :. m2) <- m2s, m2.flag]   L.sort [(m2, L.length ls) | (_ :. _ :. PgTag m2s :. _) <- sel, (PgTag ls :. m2) <- m2s]-    === L.sort [(m2, L.length $ L.filter ((>100) . (.leaf_no)) ls)+    === L.sort [(m2, L.length $ L.filter ((>97) . (.leafNo)) ls)       | (_ :. PgTag m2s :. _) <- inIns, (PgTag ls :. m2) <- m2s, m2.flag]
test-pgs/Tests/Hierarchy.hs view
@@ -11,7 +11,7 @@ import Data.Functor import Data.Int (Int32, Int64) import Data.List qualified as L-import Data.Maybe (fromMaybe, isJust)+import Data.Maybe (fromMaybe, isJust, mapMaybe, catMaybes) import Data.Pool as Pool import Data.Proxy (Proxy(..)) import Data.Text (Text)@@ -120,25 +120,26 @@       insJSON "root" conn inIns     (fst -> (outSel1' :: ["id" := Int32 :. "mid1RootFk" := [Mid1Rec] :. RootRec])) <-       selSch "root" conn qpEmpty-    (outUps' :: ["id" := Int32 :. "mid1_root_fk" := [Mid1Rec] :. RootRec], _txt) <-+    (outUps' :: [Maybe ("id" := Int32 :. "mid1_root_fk" := [Mid1Rec] :. RootRec)], _txt) <-       upsJSON "root" conn $ outIns' <&> \(ir :. PgTag ms) -> "mid1_root_fk" =:         (ms <&> \(i :. flag :. x) -> i :. (not <$> flag) :. x) :. ir-    -- T.putStrLn $ "\n\n" <> txt <> "\n\n"     (fst -> (outSel2' :: ["id" := Int32 :. "mid1_root_fk" := [Mid1Rec] :. RootRec])) <-       selSch "root" conn qpEmpty     pure (outIns', outSel1', outUps', outSel2')+  assert $ all isJust outUps && not (null outUps)+  let outUpsBare = catMaybes outUps   L.sort (inIns <&> \(ms :. _) -> L.length ms) ===     L.sort (outIns <&> \(_ :. ms) -> L.length ms)   L.length outIns === L.length outSel1   L.length outIns === L.length outSel2-  L.length outIns === L.length outUps+  L.length outIns === L.length outUpsBare   (L.sort outIns <&> \(ir :. ms) -> ir :. length ms)     === (L.sort outSel2 <&> \(ir :. ms :. _) -> ir :. length ms)   (L.sort outIns <&> \(ir :. ms) -> ir :. length ms)-    === (L.sort outUps <&> \(ir :. ms :. _) -> ir :. length ms)+    === (L.sort outUpsBare <&> \(ir :. ms :. _) -> ir :. length ms)   (L.sort outIns <&> \(ir :. PgTag ms) -> ir :. length (filter (\(_ :. PgTag b :. _) -> b) ms))     === (L.sort outSel2 <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (\(PgTag b :. _) -> not b) ms))-  (L.sort outUps <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (\(PgTag b :. _) -> b) ms))+  (L.sort outUpsBare <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (\(PgTag b :. _) -> b) ms))     === (L.sort outSel2 <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (\(PgTag b :. _) -> b) ms))  prop_hier_insert_composite_fk :: Pool Connection -> Property@@ -153,25 +154,26 @@       insJSON "root" conn inIns     (fst -> (outSel1' :: ["id" := Int32 :. "mid2_root_fk" := [Mid2Rec] :. RootRec])) <-       selSch "root" conn qpEmpty-    (outUps' :: ["id" := Int32 :. "mid2_root_fk" := [Mid2Rec] :. RootRec], _txt) <-+    (outUps' :: [Maybe ("id" := Int32 :. "mid2_root_fk" := [Mid2Rec] :. RootRec)], _txt) <-       upsJSON "root" conn $ outIns' <&> \(ir :. PgTag ms) -> "mid2_root_fk" =:         (ms <&> \m -> m { flag = not m.flag }) :. ir-    -- T.putStrLn $ "\n\n" <> txt <> "\n\n"     (fst -> (outSel2' :: ["id" := Int32 :. "mid2_root_fk" := [Mid2Rec] :. RootRec])) <-       selSch "root" conn qpEmpty     pure (outIns', outSel1', outUps', outSel2')+  assert $ all isJust outUps && not (null outUps)+  let outUpsBare = catMaybes outUps   L.sort (inIns <&> \(ms :. _) -> L.length ms) ===     L.sort (outIns <&> \(_ :. ms) -> L.length ms)   L.length outIns === L.length outSel1   L.length outIns === L.length outSel2-  L.length outIns === L.length outUps+  L.length outIns === L.length outUpsBare   (L.sort outIns <&> \(ir :. ms) -> ir :. length ms)     === (L.sort outSel2 <&> \(ir :. ms :. _) -> ir :. length ms)   (L.sort outIns <&> \(ir :. ms) -> ir :. length ms)-    === (L.sort outUps <&> \(ir :. ms :. _) -> ir :. length ms)+    === (L.sort outUpsBare <&> \(ir :. ms :. _) -> ir :. length ms)   (L.sort outIns <&> \(ir :. PgTag ms) -> ir :. length (filter (.flag) ms))     === (L.sort outSel2 <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (not . (.flag)) ms))-  (L.sort outUps <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (.flag) ms))+  (L.sort outUpsBare <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (.flag) ms))     === (L.sort outSel2 <&> \(ir :. PgTag ms :. _) -> ir :. length (filter (.flag) ms))  prop_hier_select_child_with_parent :: Pool Connection -> Property
+ test-pgs/Tests/KeyedDML.hs view
@@ -0,0 +1,308 @@+{-# LANGUAGE BlockArguments #-}+-- |+-- Flat keyed DML: 'upsertByKey' needs mandatory fields and a full key+-- ('INSERT … ON CONFLICT'); key-only patches use 'updateByKey' only.+module Tests.KeyedDML where++import Control.Monad (void, when, unless)+import Data.Int (Int32)+import Data.List qualified as L+import Data.Maybe (Maybe(..))+import Data.Pool as Pool+import Data.Text (Text)+import Data.Text qualified as T+import Database.PostgreSQL.Simple (Connection)+import Hedgehog+import Hedgehog.Gen qualified as Gen+import Hedgehog.Range qualified as Range+import PgSchema.DML+import Sch+import Utils++type RootRec =+  "code" := Text :. "grp" := Int32 :. "name" := Text :. "someEmpty" := ()++type RootRetBare = "id" := Int32 :. RootRec++type RootKeyPatch =+  "code" := Text :. "grp" := Int32 :. "name" := Text :. "someEmpty" := ()++type RootKeyRet = "id" := Int32 :. "name" := Text++type RootSel = "code" := Text :. "grp" := Int32 :. "name" := Text++type NullableRow =+  "code" := Text :. "suffix" := Maybe Text :. "name" := Text+    :. "note" := Maybe Text :. "someEmpty" := ()++type NullableKeyPatch =+  "code" := Text :. "suffix" := Maybe Text :. "note" := Maybe Text+    :. "someEmpty" := ()++type NullableKeyRet = "id" := Int32 :. "note" := Maybe Text++type RootPatchOnly =+  "id" := Int32 :. "code" := Text :. "grp" := Int32 :. "name" := Text+    :. "someEmpty" := ()++type RootKeyOnlyPatch = "id" := Int32 :. "name" := Text++type RootUpsOutMaybe = Maybe ("id" := Int32 :. "name" := Text)++type RootUpdOutMaybe = RootUpsOutMaybe++type RootUpsBare = "id" := Int32 :. RootRec++type Mid1Rec =+  "flag" := Bool :. "pos" := Int32 :. "sortKey" := Int32 :. "payload" := Maybe Text++type RootWithMid1 = "mid1_root_fk" := [Mid1Rec] :. RootRec++type RootInsWithMid =+  "id" := Int32 :. "mid1_root_fk" := ["id" := Int32 :. Mid1Rec]++type Mid1KeyPatch = "id" := Int32 :. Mid1Rec++type Mid1KeyRet = "id" := Int32 :. "flag" := Bool++expectAbsent :: Show a => [Maybe a] -> IO ()+expectAbsent [Nothing] = pure ()+expectAbsent xs = fail $ "expected [Nothing], got " <> show xs++expectUpsertSql :: Text -> IO ()+expectUpsertSql sql = do+  let lower = T.toLower sql+  unless ("insert into" `T.isInfixOf` lower) $+    fail $ "expected INSERT upsert SQL, got: " <> T.unpack sql+  unless ("on conflict" `T.isInfixOf` lower) $+    fail $ "expected ON CONFLICT in upsert SQL, got: " <> T.unpack sql+  when (T.stripPrefix "update " (T.stripStart lower) == Just "") $+    fail $ "upsertByKey must not emit UPDATE-only SQL: " <> T.unpack sql++prop_upsert_by_key_insert_then_update :: Pool Connection -> Property+prop_upsert_by_key_insert_then_update pool = withTests 10 $ property do+  code <- forAll (Gen.text (Range.linear 3 20) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 10000))+  name1 <- forAll (Gen.text (Range.linear 1 20) Gen.alphaNum)+  name2 <- forAll (Gen.text (Range.linear 1 20) Gen.alphaNum)+  when (name1 == name2) discard+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn ("code" =? code)+    let+      full :: RootRec+      full = "code" =: code :. "grp" =: grp :. "name" =: name1 :. "someEmpty" =: ()+      row2 :: RootRec+      row2 = "code" =: code :. "grp" =: grp :. "name" =: name2 :. "someEmpty" =: ()+    ([_ :. _], sql1) <- upsertByKey (AnnSch "root") @RootRec @RootRetBare conn [full]+    expectUpsertSql sql1+    (ret, sql2) <- upsertByKey (AnnSch "root") @RootRec @RootKeyRet conn [row2]+    expectUpsertSql sql2+    case ret of+      [_ :. n] -> when (unPgTag n /= name2) $+        fail $ "expected name " <> show name2+      _ -> fail "expected one row from upsertByKey on conflict update"+    (rows :: [RootSel], _) <- selSch "root" conn $+      qRoot $ qWhere $ "code" =? code &&& "grp" =? grp+    case rows of+      [_ :. _ :. n] -> when (unPgTag n /= name2) $ fail "DB name not updated"+      _ -> fail "expected one row"++prop_upsert_by_key_composite_unique :: Pool Connection -> Property+prop_upsert_by_key_composite_unique pool = withTests 5 $ property do+  code <- forAll (Gen.text (Range.linear 3 24) Gen.alphaNum)+  name1 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)+  name2 <- forAll (Gen.text (Range.linear 1 24) Gen.alphaNum)+  when (name1 == name2) discard+  let grp = 5151 :: Int32+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn ("code" =? code)+    let+      row1 :: RootRec+      row1 = "code" =: code :. "grp" =: grp :. "name" =: name1 :. "someEmpty" =: ()+      row2 :: RootRec+      row2 = "code" =: code :. "grp" =: grp :. "name" =: name2 :. "someEmpty" =: ()+    (_, sql1) <- upsByKey_ "root" conn [row1]+    expectUpsertSql sql1+    (_, sql2) <- upsByKey_ "root" conn [row2]+    expectUpsertSql sql2+    (rows :: [RootSel], _) <- selSch "root" conn $+      qRoot $ qWhere $ "code" =? code &&& "grp" =? grp+    case rows of+      [_ :. _ :. n] ->+        when (unPgTag n /= name2) $+          fail $ "expected updated name " <> show name2+      _ -> fail $ "expected 1 row, got " <> show (L.length rows)++prop_upsert_by_key_never_pure_update :: Pool Connection -> Property+prop_upsert_by_key_never_pure_update pool = withTests 3 $ property do+  code <- forAll (Gen.text (Range.linear 3 20) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 10000))+  name <- forAll (Gen.text (Range.linear 1 20) Gen.alphaNum)+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn ("code" =? code)+    let+      row :: RootRec+      row = "code" =: code :. "grp" =: grp :. "name" =: name :. "someEmpty" =: ()+    (_, sqlExec) <- upsByKey_ "root" conn [row]+    expectUpsertSql sqlExec+    let sqlText = upsertByKeyText_ (AnnSch "root") @RootRec+    expectUpsertSql sqlText++prop_update_by_key_found :: Pool Connection -> Property+prop_update_by_key_found pool = withTests 10 $ property do+  code <- forAll (Gen.text (Range.linear 3 20) Gen.alphaNum)+  note1 <- forAll (Gen.text (Range.linear 1 20) Gen.alphaNum)+  note2 <- forAll (Gen.text (Range.linear 1 20) Gen.alphaNum)+  when (note1 == note2) discard+  let sfx = Just ("sfx" :: Text)+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "nullable_uq_row" conn ("code" =? code)+    let+      seed :: NullableRow+      seed =+        "code" =: code :. "suffix" =: sfx :. "name" =: "n"+          :. "note" =: Just note1 :. "someEmpty" =: ()+      patch :: NullableKeyPatch+      patch =+        "code" =: code :. "suffix" =: sfx :. "note" =: Just note2+          :. "someEmpty" =: ()+    void $ insSch_ "nullable_uq_row" conn [seed]+    (ret, _) <- updateByKey (AnnSch "nullable_uq_row") @NullableKeyPatch+      @NullableKeyRet conn [patch]+    case ret of+      [Just (_ :. n)] -> when (unPgTag n /= Just note2) $ fail "note not updated"+      _ -> fail "expected [Just]"+    (rows :: ["note" := Maybe Text], _) <- selSch "nullable_uq_row" conn $+      qRoot $ qWhere $ "code" =? code &&& "suffix" =?? sfx+    case rows of+      [n] -> when (unPgTag n /= Just note2) $ fail "DB note mismatch"+      _ -> fail "expected one row"++prop_update_by_key_not_found :: Pool Connection -> Property+prop_update_by_key_not_found pool = withTests 5 $ property do+  code <- forAll (Gen.text (Range.linear 3 20) Gen.alphaNum)+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "nullable_uq_row" conn ("code" =? code)+    let+      patch :: NullableKeyPatch+      patch =+        "code" =: code :. "suffix" =: Just "x" :. "note" =: Just "y"+          :. "someEmpty" =: ()+    (ret, _) <- updateByKey (AnnSch "nullable_uq_row") @NullableKeyPatch+      @NullableKeyRet conn [patch]+    expectAbsent ret+    (rows :: [NullableRow], _) <- selSch "nullable_uq_row" conn $+      qRoot $ qWhere $ "code" =? code+    unless (null rows) $ fail $ "expected 0 rows, got " <> show rows++prop_update_by_key_never_inserts :: Pool Connection -> Property+prop_update_by_key_never_inserts pool = withTests 3 $ property do+  code <- forAll (Gen.text (Range.linear 3 20) Gen.alphaNum)+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn mempty+    let+      patch :: RootKeyPatch+      patch =+        "code" =: code :. "grp" =: (1 :: Int32) :. "name" =: "x"+          :. "someEmpty" =: ()+    (ret, _) <- updateByKey (AnnSch "root") @RootKeyPatch @RootKeyRet conn [patch]+    expectAbsent ret+    (rows :: [RootSel], _) <- selSch "root" conn qpEmpty+    unless (null rows) $ fail $ "expected 0 rows, got " <> show rows++prop_update_json_not_found_returns_nothing :: Pool Connection -> Property+prop_update_json_not_found_returns_nothing pool = withTests 5 $ property do+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn mempty+    let+      bad :: RootPatchOnly+      bad =+        "id" =: (999999999 :: Int32) :. "code" =: "x" :. "grp" =: 1+          :. "name" =: "y" :. "someEmpty" =: ()+    (rets, _) <- updateJSON (AnnSch "root") @RootPatchOnly @RootUpdOutMaybe conn [bad]+    expectAbsent rets++prop_update_json_found :: Pool Connection -> Property+prop_update_json_found pool = withTests 10 $ property do+  code <- forAll (Gen.text (Range.linear 3 16) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 5000))+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn mempty+    let+      row :: RootRec+      row = "code" =: code :. "grp" =: grp :. "name" =: "seed" :. "someEmpty" =: ()+    (insOut, _) <- insertJSON (AnnSch "root") @RootRec @RootRetBare conn [row]+    insHead <- case insOut of+      (x : _) -> pure x+      [] -> fail "insertJSON returned no rows"+    let+      (PgTag rid :. _) = insHead+      patch :: RootPatchOnly+      patch =+        "id" =: rid :. "code" =: code :. "grp" =: grp :. "name" =: "patched"+          :. "someEmpty" =: ()+    (ret, _) <- updateJSON (AnnSch "root") @RootPatchOnly @RootUpdOutMaybe conn [patch]+    case ret of+      [Just (_ :. PgTag n)] -> when (n /= "patched") $ fail "name not updated"+      _ -> fail $ "unexpected updateJSON result: " <> show ret++prop_upsert_json_returning_positions :: Pool Connection -> Property+prop_upsert_json_returning_positions pool = withTests 5 $ property do+  code <- forAll (Gen.text (Range.linear 3 16) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 5000))+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn mempty+    let+      row :: RootRec+      row = "code" =: code :. "grp" =: grp :. "name" =: "n" :. "someEmpty" =: ()+    (insOut, _) <- insertJSON (AnnSch "root") @RootRec @RootRetBare conn [row]+    insHead <- case insOut of+      (x : _) -> pure x+      [] -> fail "insertJSON returned no rows"+    let+      (PgTag rid :. _) = insHead+      good :: RootKeyOnlyPatch+      good = "id" =: rid :. "name" =: "ok"+      bad :: RootKeyOnlyPatch+      bad = "id" =: (888888888 :: Int32) :. "name" =: "z"+    (rets, _) <- upsertJSON (AnnSch "root") @RootKeyOnlyPatch @RootUpsOutMaybe conn [good, bad]+    when (length rets /= 2) $ fail $ "expected 2 results, got " <> show rets+    case rets of+      [Just _, Nothing] -> pure ()+      _ -> fail $ "expected [Just, Nothing], got " <> show rets++prop_upsert_json_full_row_bare_returning :: Pool Connection -> Property+prop_upsert_json_full_row_bare_returning pool = withTests 5 $ property do+  code <- forAll (Gen.text (Range.linear 3 16) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 5000))+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "root" conn mempty+    let+      row :: RootRec+      row = "code" =: code :. "grp" =: grp :. "name" =: "n" :. "someEmpty" =: ()+    (rets, _) <- upsertJSON (AnnSch "root") @RootRec @RootUpsBare conn [row]+    case rets of+      [PgTag _ :. _] -> pure ()+      _ -> fail "expected bare upsert returning row"++-- | Root @Just@, key-only upsert patch on child table via flat @updateByKey@.+prop_update_json_child_maybe :: Pool Connection -> Property+prop_update_json_child_maybe pool = withTests 5 $ property do+  code <- forAll (Gen.text (Range.linear 3 16) Gen.alphaNum)+  grp <- forAll (Gen.int32 (Range.linear 1 5000))+  evalIO $ Pool.withResource pool \conn -> do+    void $ delByCond "mid1" conn mempty+    void $ delByCond "root" conn mempty+    let+      row :: RootRec+      row = "code" =: code :. "grp" =: grp :. "name" =: "n" :. "someEmpty" =: ()+    ([_ :. PgTag mids], _) <- insertJSON (AnnSch "root") @RootWithMid1 @RootInsWithMid conn+      [ "mid1_root_fk" =: [ "flag" =: True :. "pos" =: 1 :. "sortKey" =: 1+          :. "payload" =: Nothing ] :. row ]+    let+      midId = case mids of [PgTag i :. _] -> i; _ -> error "one mid1"+      patch :: Mid1KeyPatch+      patch = "id" =: (midId + 99999) :. "flag" =: False :. "pos" =: 1+        :. "sortKey" =: 1 :. "payload" =: Nothing+    (ret, _) <- updateByKey (AnnSch "mid1") @Mid1KeyPatch @Mid1KeyRet conn [patch]+    expectAbsent ret
test-pgs/Utils.hs view
@@ -102,6 +102,36 @@   => Connection -> [r] -> IO ([r'], Text) upsJSON tn = upsertJSON (AnnSch tn) +updJSON_+  :: forall tn -> forall r. UpdateTreeNonReturning (AnnSch tn) r+  => Connection -> [r] -> IO Text+updJSON_ tn = updateJSON_ (AnnSch tn)++updJSON+  :: forall tn -> forall r r'. UpdateTreeReturning (AnnSch tn) r r'+  => Connection -> [r] -> IO ([r'], Text)+updJSON tn = updateJSON (AnnSch tn)++upsByKey+  :: forall tn -> forall r r'. UpsertByKeyReturning (AnnSch tn) r r'+  => Connection -> [r] -> IO ([r'], Text)+upsByKey tn = upsertByKey (AnnSch tn)++upsByKey_+  :: forall tn -> forall r. UpsertByKeyNonReturning (AnnSch tn) r+  => Connection -> [r] -> IO (Int64, Text)+upsByKey_ tn = upsertByKey_ (AnnSch tn)++updByKey+  :: forall tn -> forall r r'. UpdateByKeyReturning (AnnSch tn) r r'+  => Connection -> [r] -> IO ([Maybe r'], Text)+updByKey tn = updateByKey (AnnSch tn)++updByKey_+  :: forall tn -> forall r. UpdateByKeyNonReturning (AnnSch tn) r+  => Connection -> [r] -> IO (Int64, Text)+updByKey_ tn = updateByKey_ (AnnSch tn)+ genDay :: Gen Day genDay = ModifiedJulianDay . fromIntegral <$> Gen.int (Range.linear 50000 80000)