From bbd1be12087775453dae1893c09c0bd71ebd4c54 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 16 Aug 2025 14:39:52 -0300 Subject: [PATCH 01/37] fsync to fix intermittent failure in MacOS --- codd.cabal | 1 + src/Codd/Representations/Disk.hs | 10 +++++++++- test/WritingReadingRepresentationsSpec.hs | 4 +--- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/codd.cabal b/codd.cabal index 83dfdaf4..fcb214dd 100644 --- a/codd.cabal +++ b/codd.cabal @@ -110,6 +110,7 @@ library , text , time , transformers + , unix , unliftio , unliftio-core , unordered-containers diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 98594299..b329da66 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -27,6 +27,7 @@ import Data.List (sortOn) import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) +import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath @@ -34,9 +35,11 @@ import System.FilePath (), ) import System.IO.Error (isDoesNotExistError) +import System.Posix (Fd (..), OpenFileFlags (directory), OpenMode (..), closeFd, defaultFileFlags, openFd) import UnliftIO ( MonadIO (..), MonadUnliftIO, + bracket, evaluate, handle, throwIO, @@ -196,7 +199,7 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- We want this function to be fast and as atomic as it can be, but: -- 1. `renameFile` throws if the target path is in a different partition from the source path -- 2. The user might not have permissions to delete the target folder but does have permissions to modify its contents - -- 3. Different operating systems can have different behaviours, like Windows, where renameDirectory fails on Windows if the target directory exists: https://hackage.haskell.org/package/directory-1.3.9.0/docs/System-Directory.html#v:renameDirectory + -- 3. Different operating systems can have different behaviours, like Windows, where renameDirectory fails if the target directory exists: https://hackage.haskell.org/package/directory-1.3.9.0/docs/System-Directory.html#v:renameDirectory -- 4. Windows and I think even Linux can have ACLs that have more than just a binary "can-write" privilege, with "can-delete" being -- separated from "can-create", IIRC. errBestScenario <- tryJust (\(e :: IOError) -> if ioe_type e `elem` [NoSuchThing, UnsatisfiedConstraints, PermissionDenied, IllegalOperation, UnsupportedOperation] then Just () else Nothing) $ do @@ -243,10 +246,15 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = obj ( \parentDir sobj -> do createDirectoryIfMissing True (dir parentDir) + -- TODO: Ignore exception (let's keep trying if fsync fails) + bracket (openFd (dir parentDir) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd >>= print) writeRec (dir parentDir) sobj ) (\fn jsonRep -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) +foreign import ccall safe "fsync" + c_fsync :: CInt -> IO CInt + readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = SchemaRep (readObjName dir) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 3ee1916c..c65ae3ee 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -27,9 +27,7 @@ spec = do describe "Writing and reading representations" $ do it "persistRepsToDisk is inverse of readRepsFromDisk" $ do property $ \(DbRepsGen dbHashes pgVersion) -> do - -- /dev/shm is shared memory so should be faster, if it exists (MacOS doesn't have it) - shmExists <- doesDirectoryExist "/dev/shm" - baseFolder :: FilePath <- if shmExists then pure "/dev/shm" else getEmptyTempDir + baseFolder <- getEmptyTempDir writeSchemaAndReadSchemaRoundtrip pgVersion dbHashes (baseFolder "inverse-test-sql-folder") modifyMaxSuccess (const 1) $ it From 2efeff2e3ddf08efbf86c38a97e90daf011b3052 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 14:03:54 -0300 Subject: [PATCH 02/37] Write schema to disk as a function of `toFiles` We might be able to get rid of the typeclass, and optimise this --- src/Codd/Representations/Disk.hs | 31 +++++++++++++++---------------- 1 file changed, 15 insertions(+), 16 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index b329da66..df277958 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -24,14 +24,18 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) +import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) +import Data.Ord (comparing) +import Debug.Trace import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath - ( takeFileName, + ( takeDirectory, + takeFileName, (), ) import System.IO.Error (isDoesNotExistError) @@ -47,6 +51,7 @@ import UnliftIO ) import UnliftIO.Directory ( canonicalizePath, + createDirectory, createDirectoryIfMissing, doesDirectoryExist, listDirectory, @@ -239,21 +244,15 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = forM_ entries $ \x -> removePathForcibly (path x) else createDirectoryIfMissing True path - writeRec :: (DbDiskObj a) => FilePath -> a -> IO () - writeRec dir obj = - void $ - appr - obj - ( \parentDir sobj -> do - createDirectoryIfMissing True (dir parentDir) - -- TODO: Ignore exception (let's keep trying if fsync fails) - bracket (openFd (dir parentDir) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd >>= print) - writeRec (dir parentDir) sobj - ) - (\fn jsonRep -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) - -foreign import ccall safe "fsync" - c_fsync :: CInt -> IO CInt + writeRec :: FilePath -> DbRep -> IO () + writeRec dir obj = do + forM_ (NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj)) $ \filesPerFolder -> do + let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder + createDirectoryIfMissing True (dir relFolderToCreate) + forM_ filesPerFolder $ \(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep) + +-- foreign import ccall safe "fsync" +-- c_fsync :: CInt -> IO CInt readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = From e2b994debcc8988cdfe3ca51297c1a335c9610f8 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 14:09:11 -0300 Subject: [PATCH 03/37] Use parallelism to improve the performance of `codd write-schema` Improved by at least 10% according to tests --- src/Codd/Representations/Disk.hs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index df277958..9f37b95e 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -46,6 +46,7 @@ import UnliftIO bracket, evaluate, handle, + pooledMapConcurrentlyN_, throwIO, tryJust, ) @@ -249,7 +250,8 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = forM_ (NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj)) $ \filesPerFolder -> do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder createDirectoryIfMissing True (dir relFolderToCreate) - forM_ filesPerFolder $ \(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep) + -- Codd only has 2 capabilities + pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder -- foreign import ccall safe "fsync" -- c_fsync :: CInt -> IO CInt From 2650f5d3f8c060f01adb03ee35f7c00c0b1191fa Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 14:20:29 -0300 Subject: [PATCH 04/37] Import from different namespace to avoid MacOS compilation error --- src/Codd/Representations/Disk.hs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 9f37b95e..e32b8487 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -12,7 +12,6 @@ import Control.Monad ( forM, forM_, join, - void, when, ) import Control.Monad.Identity (runIdentity) @@ -28,8 +27,6 @@ import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) -import Data.Ord (comparing) -import Debug.Trace import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) @@ -39,7 +36,8 @@ import System.FilePath (), ) import System.IO.Error (isDoesNotExistError) -import System.Posix (Fd (..), OpenFileFlags (directory), OpenMode (..), closeFd, defaultFileFlags, openFd) +import System.Posix (Fd (..), OpenMode (..), closeFd, defaultFileFlags, openFd) +import System.Posix.IO (OpenFileFlags (directory)) import UnliftIO ( MonadIO (..), MonadUnliftIO, @@ -52,7 +50,6 @@ import UnliftIO ) import UnliftIO.Directory ( canonicalizePath, - createDirectory, createDirectoryIfMissing, doesDirectoryExist, listDirectory, @@ -253,8 +250,11 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- Codd only has 2 capabilities pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder --- foreign import ccall safe "fsync" --- c_fsync :: CInt -> IO CInt + -- TODO: Ignore exception (let's keep trying if fsync fails) + bracket (openFd (dir relFolderToCreate) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd >>= print) + +foreign import ccall safe "fsync" + c_fsync :: CInt -> IO CInt readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = From 336d7027fdf49ee0a027f7d96b988d2940c059d4 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 14:40:30 -0300 Subject: [PATCH 05/37] Run MacOS tests a large number of times --- .github/workflows/main.yml | 28 ++++++++++++++++++++++++++++ src/Codd/Representations/Disk.hs | 3 ++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index eed91de9..d8c061ee 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -160,6 +160,34 @@ jobs: run: | echo "Running tests that don't depend on a database" scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 1' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 2' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 3' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 4' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 5' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 6' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 7' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 8' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 9' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 10' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 11' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 12' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 13' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 14' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 15' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 16' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 17' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 18' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 19' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 20' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 21' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 22' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 23' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 24' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 25' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 26' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 27' + scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 28' # Postgres-version dependent tests for each possible version next # We test the last version with the vanilla nixpkgs-built derivation, diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index e32b8487..653e00b2 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -251,7 +251,8 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder -- TODO: Ignore exception (let's keep trying if fsync fails) - bracket (openFd (dir relFolderToCreate) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd >>= print) + fsyncResult <- bracket (openFd (dir relFolderToCreate) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) + when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult foreign import ccall safe "fsync" c_fsync :: CInt -> IO CInt From 6197eb777b28a2d61cebfafe8113ff857ae2013a Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 14:58:42 -0300 Subject: [PATCH 06/37] Fsync every file too --- src/Codd/Representations/Disk.hs | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 653e00b2..abbb2ace 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -248,11 +248,20 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder createDirectoryIfMissing True (dir relFolderToCreate) -- Codd only has 2 capabilities - pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder - - -- TODO: Ignore exception (let's keep trying if fsync fails) - fsyncResult <- bracket (openFd (dir relFolderToCreate) ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) - when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult + pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep) >> fsyncFile (dir fn)) filesPerFolder + fsyncFolder (dir relFolderToCreate) + +fsyncFolder :: FilePath -> IO () +fsyncFolder dir = do + -- TODO: Ignore exception (let's keep trying if fsync fails) + fsyncResult <- bracket (openFd dir ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) + when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult + +fsyncFile :: FilePath -> IO () +fsyncFile fn = do + -- TODO: Ignore exception (let's keep trying if fsync fails) + fsyncResult <- bracket (openFd fn ReadOnly defaultFileFlags) closeFd (\(Fd fd) -> c_fsync fd) + when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult foreign import ccall safe "fsync" c_fsync :: CInt -> IO CInt From 7f875f5503dc6d40a29f43bd9e5bf7a117a62c04 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 15:12:02 -0300 Subject: [PATCH 07/37] Try `sync` in tests --- src/Codd/Representations/Disk.hs | 24 +++++++++++++---------- test/WritingReadingRepresentationsSpec.hs | 5 +++++ 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index abbb2ace..e2b46a7e 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -252,19 +252,23 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = fsyncFolder (dir relFolderToCreate) fsyncFolder :: FilePath -> IO () -fsyncFolder dir = do - -- TODO: Ignore exception (let's keep trying if fsync fails) - fsyncResult <- bracket (openFd dir ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) - when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult +fsyncFolder _dir = do + pure () + +-- -- TODO: Ignore exception (let's keep trying if fsync fails) +-- fsyncResult <- bracket (openFd dir ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) +-- when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult fsyncFile :: FilePath -> IO () -fsyncFile fn = do - -- TODO: Ignore exception (let's keep trying if fsync fails) - fsyncResult <- bracket (openFd fn ReadOnly defaultFileFlags) closeFd (\(Fd fd) -> c_fsync fd) - when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult +fsyncFile _fn = pure () + +-- do +-- -- TODO: Ignore exception (let's keep trying if fsync fails) +-- fsyncResult <- bracket (openFd fn ReadOnly defaultFileFlags) closeFd (\(Fd fd) -> c_fsync fd) +-- when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult -foreign import ccall safe "fsync" - c_fsync :: CInt -> IO CInt +-- foreign import ccall safe "fsync" +-- c_fsync :: CInt -> IO CInt readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index c65ae3ee..cf4cc820 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -12,6 +12,7 @@ import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUIDv4 import DbUtils (getEmptyTempDir) +import Foreign.C (CInt (..)) import System.FilePath ( (), ) @@ -72,6 +73,7 @@ spec = do writeSchemaAndReadSchemaRoundtrip :: PgMajorVersion -> DbRep -> FilePath -> IO () writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do persistRepsToDisk pgVersion dbReps expectedSchemaDir + c_sync readDbSchema <- readRepsFromDisk pgVersion @@ -79,3 +81,6 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do let diffs = schemaDifferences dbReps readDbSchema diffs `shouldBe` Map.empty readDbSchema `shouldBe` dbReps + +foreign import ccall safe "sync" + c_sync :: IO () From 2ad1f53dc21c240fcf31d9173fd17ac88f259a06 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 15:53:33 -0300 Subject: [PATCH 08/37] In-memory cache of existing folders This avoid `createDirectoryIfMissing`, but there's no change in measured runtime --- src/Codd/Representations/Disk.hs | 41 ++++++++++++++++++----- test/WritingReadingRepresentationsSpec.hs | 4 --- 2 files changed, 33 insertions(+), 12 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index e2b46a7e..8cbd78f3 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -12,6 +12,7 @@ import Control.Monad ( forM, forM_, join, + unless, when, ) import Control.Monad.Identity (runIdentity) @@ -23,10 +24,12 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) +import qualified Data.List as List import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) +import qualified Data.Set as Set import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) @@ -44,12 +47,15 @@ import UnliftIO bracket, evaluate, handle, + modifyMVar_, + newMVar, pooledMapConcurrentlyN_, throwIO, tryJust, ) import UnliftIO.Directory ( canonicalizePath, + createDirectory, createDirectoryIfMissing, doesDirectoryExist, listDirectory, @@ -244,23 +250,42 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeRec :: FilePath -> DbRep -> IO () writeRec dir obj = do + createDirectoryIfMissing True dir + createdDirs <- newMVar (Set.singleton dir) + let createDirAndAncestors fp = modifyMVar_ createdDirs $ \cdirs -> do + allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do + unless (d `Set.member` cdirs) $ + createDirectory d + pure d + evaluate $ Set.fromList allDirs `Set.union` cdirs forM_ (NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj)) $ \filesPerFolder -> do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder - createDirectoryIfMissing True (dir relFolderToCreate) + createDirAndAncestors (dir relFolderToCreate) -- Codd only has 2 capabilities - pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep) >> fsyncFile (dir fn)) filesPerFolder - fsyncFolder (dir relFolderToCreate) + pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder -fsyncFolder :: FilePath -> IO () -fsyncFolder _dir = do - pure () +-- fsyncFolder (dir relFolderToCreate) + +dirAndAncestorsBetween :: FilePath -> FilePath -> [FilePath] +dirAndAncestorsBetween ancestor' dir' = go ancestor' dir' [] + where + go ancestor dir res + | dir == ancestor = dir : res + | not (ancestor `List.isPrefixOf` dir) = res + | otherwise = + let parent = takeDirectory dir + in go ancestor parent [dir] + +-- fsyncFolder :: FilePath -> IO () +-- fsyncFolder _dir = do +-- pure () -- -- TODO: Ignore exception (let's keep trying if fsync fails) -- fsyncResult <- bracket (openFd dir ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) -- when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult -fsyncFile :: FilePath -> IO () -fsyncFile _fn = pure () +-- fsyncFile :: FilePath -> IO () +-- fsyncFile _fn = pure () -- do -- -- TODO: Ignore exception (let's keep trying if fsync fails) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index cf4cc820..374f7484 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -73,7 +73,6 @@ spec = do writeSchemaAndReadSchemaRoundtrip :: PgMajorVersion -> DbRep -> FilePath -> IO () writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do persistRepsToDisk pgVersion dbReps expectedSchemaDir - c_sync readDbSchema <- readRepsFromDisk pgVersion @@ -81,6 +80,3 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do let diffs = schemaDifferences dbReps readDbSchema diffs `shouldBe` Map.empty readDbSchema `shouldBe` dbReps - -foreign import ccall safe "sync" - c_sync :: IO () From 9009e865e20d7db3df5bc45ead9f4114c7d5647a Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 16:10:23 -0300 Subject: [PATCH 09/37] Parallelism for the whole process -> some performance improvements --- src/Codd/Representations/Disk.hs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 8cbd78f3..2e0212d1 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -29,6 +29,7 @@ import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) +import Data.Set (Set) import qualified Data.Set as Set import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) @@ -49,7 +50,9 @@ import UnliftIO handle, modifyMVar_, newMVar, + newTVar, pooledMapConcurrentlyN_, + pooledMapConcurrently_, throwIO, tryJust, ) @@ -252,17 +255,22 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeRec dir obj = do createDirectoryIfMissing True dir createdDirs <- newMVar (Set.singleton dir) + -- concWriters <- newTVar (mempty :: Set FilePath) let createDirAndAncestors fp = modifyMVar_ createdDirs $ \cdirs -> do - allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do - unless (d `Set.member` cdirs) $ - createDirectory d - pure d - evaluate $ Set.fromList allDirs `Set.union` cdirs - forM_ (NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj)) $ \filesPerFolder -> do - let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder - createDirAndAncestors (dir relFolderToCreate) - -- Codd only has 2 capabilities - pooledMapConcurrentlyN_ 2 (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) filesPerFolder + createDirectoryIfMissing True fp + pure cdirs + -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do + -- unless (d `Set.member` cdirs || takeFileName d == ".") $ + -- createDirectory d + -- pure d + -- evaluate $ Set.fromList allDirs `Set.union` cdirs + -- Codd only has 2 capabilities + pooledMapConcurrentlyN_ 2 (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) + writeFolder :: (FilePath -> IO ()) -> FilePath -> NE.NonEmpty (FilePath, Value) -> IO () + writeFolder createDirAndAncestors dir filesPerFolder = do + let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder + createDirAndAncestors (dir relFolderToCreate) + forM_ filesPerFolder (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) -- fsyncFolder (dir relFolderToCreate) From 2ad94e601d519cfe8e18793c53d5b116c1934e4e Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 16:54:09 -0300 Subject: [PATCH 10/37] STM is not worth it --- src/Codd/Representations/Disk.hs | 45 +++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 10 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 2e0212d1..6e382f4f 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -2,6 +2,7 @@ module Codd.Representations.Disk ( persistRepsToDisk, readRepsFromDisk, toFiles, + dirAndAncestorsBetween, ) where @@ -31,7 +32,9 @@ import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import Data.Set (Set) import qualified Data.Set as Set +import Debug.Trace import Foreign.C (CInt (..)) +import qualified GHC.Conc as STM import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath @@ -45,6 +48,7 @@ import System.Posix.IO (OpenFileFlags (directory)) import UnliftIO ( MonadIO (..), MonadUnliftIO, + atomically, bracket, evaluate, handle, @@ -254,18 +258,39 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeRec :: FilePath -> DbRep -> IO () writeRec dir obj = do createDirectoryIfMissing True dir - createdDirs <- newMVar (Set.singleton dir) - -- concWriters <- newTVar (mempty :: Set FilePath) - let createDirAndAncestors fp = modifyMVar_ createdDirs $ \cdirs -> do - createDirectoryIfMissing True fp - pure cdirs + dirsCreatedT <- STM.newTVarIO (Set.singleton dir) + concWritersT <- STM.newTVarIO (mempty :: Set FilePath) + let createDirAndAncestors fp = do + -- Check-and-add to list of dirs being created + let foldersInBetween = Set.fromList $ dirAndAncestorsBetween dir fp + alreadyCreated <- STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + -- let alreadyCreated = traceShow (fp, foldersInBetween) $ all (`Set.member` dirsCreated) foldersInBetween + let alreadyCreated = all (`Set.member` dirsCreated) foldersInBetween + wouldCompete = any (`Set.member` foldersBeingCreated) foldersInBetween + if wouldCompete + then STM.retry + else + if alreadyCreated + then pure True + else do + STM.writeTVar concWritersT ((foldersBeingCreated `Set.union` foldersInBetween) `Set.difference` dirsCreated) + pure False + unless alreadyCreated $ createDirectoryIfMissing True fp + -- Update lists again + STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + STM.writeTVar concWritersT $ foldersBeingCreated `Set.difference` foldersInBetween + STM.writeTVar dirsCreatedT $ dirsCreated `Set.union` foldersInBetween -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do -- unless (d `Set.member` cdirs || takeFileName d == ".") $ -- createDirectory d -- pure d -- evaluate $ Set.fromList allDirs `Set.union` cdirs - -- Codd only has 2 capabilities - pooledMapConcurrentlyN_ 2 (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) + -- Limit concurrent writers to 2 to avoid too many open file descriptors at once + pooledMapConcurrently_ (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) writeFolder :: (FilePath -> IO ()) -> FilePath -> NE.NonEmpty (FilePath, Value) -> IO () writeFolder createDirAndAncestors dir filesPerFolder = do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder @@ -275,14 +300,14 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- fsyncFolder (dir relFolderToCreate) dirAndAncestorsBetween :: FilePath -> FilePath -> [FilePath] -dirAndAncestorsBetween ancestor' dir' = go ancestor' dir' [] +dirAndAncestorsBetween ancestor dir' = filter ((/= ".") . takeFileName) $ go dir' [] where - go ancestor dir res + go dir res | dir == ancestor = dir : res | not (ancestor `List.isPrefixOf` dir) = res | otherwise = let parent = takeDirectory dir - in go ancestor parent [dir] + in go parent $ dir : res -- fsyncFolder :: FilePath -> IO () -- fsyncFolder _dir = do From a4b6b519f36881605d5d6d3260ef6660bbed00a2 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 17:06:08 -0300 Subject: [PATCH 11/37] Smaller STM transactions don't help much --- src/Codd/Representations/Disk.hs | 41 +++++++++++++++----------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 6e382f4f..a0b9490c 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -262,28 +262,25 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = concWritersT <- STM.newTVarIO (mempty :: Set FilePath) let createDirAndAncestors fp = do -- Check-and-add to list of dirs being created - let foldersInBetween = Set.fromList $ dirAndAncestorsBetween dir fp - alreadyCreated <- STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - -- let alreadyCreated = traceShow (fp, foldersInBetween) $ all (`Set.member` dirsCreated) foldersInBetween - let alreadyCreated = all (`Set.member` dirsCreated) foldersInBetween - wouldCompete = any (`Set.member` foldersBeingCreated) foldersInBetween - if wouldCompete - then STM.retry - else - if alreadyCreated - then pure True - else do - STM.writeTVar concWritersT ((foldersBeingCreated `Set.union` foldersInBetween) `Set.difference` dirsCreated) - pure False - unless alreadyCreated $ createDirectoryIfMissing True fp - -- Update lists again - STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - STM.writeTVar concWritersT $ foldersBeingCreated `Set.difference` foldersInBetween - STM.writeTVar dirsCreatedT $ dirsCreated `Set.union` foldersInBetween + let foldersInBetween = dirAndAncestorsBetween dir fp + forM_ foldersInBetween $ \folder -> do + doCreate <- STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + if folder `Set.member` dirsCreated + then pure False + else do + when (folder `Set.member` foldersBeingCreated) STM.retry + STM.writeTVar concWritersT (Set.insert folder foldersBeingCreated) + pure True + when doCreate $ do + createDirectory folder + -- Update lists again + STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + STM.writeTVar concWritersT $ Set.delete folder foldersBeingCreated + STM.writeTVar dirsCreatedT $ Set.insert folder dirsCreated -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do -- unless (d `Set.member` cdirs || takeFileName d == ".") $ -- createDirectory d From 85c8e15bb9a2e00833eaab8124f6e6ccd0e63d1a Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 17:06:38 -0300 Subject: [PATCH 12/37] Revert "Smaller STM transactions don't help much" This reverts commit a4b6b519f36881605d5d6d3260ef6660bbed00a2. --- src/Codd/Representations/Disk.hs | 41 +++++++++++++++++--------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index a0b9490c..6e382f4f 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -262,25 +262,28 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = concWritersT <- STM.newTVarIO (mempty :: Set FilePath) let createDirAndAncestors fp = do -- Check-and-add to list of dirs being created - let foldersInBetween = dirAndAncestorsBetween dir fp - forM_ foldersInBetween $ \folder -> do - doCreate <- STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - if folder `Set.member` dirsCreated - then pure False - else do - when (folder `Set.member` foldersBeingCreated) STM.retry - STM.writeTVar concWritersT (Set.insert folder foldersBeingCreated) - pure True - when doCreate $ do - createDirectory folder - -- Update lists again - STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - STM.writeTVar concWritersT $ Set.delete folder foldersBeingCreated - STM.writeTVar dirsCreatedT $ Set.insert folder dirsCreated + let foldersInBetween = Set.fromList $ dirAndAncestorsBetween dir fp + alreadyCreated <- STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + -- let alreadyCreated = traceShow (fp, foldersInBetween) $ all (`Set.member` dirsCreated) foldersInBetween + let alreadyCreated = all (`Set.member` dirsCreated) foldersInBetween + wouldCompete = any (`Set.member` foldersBeingCreated) foldersInBetween + if wouldCompete + then STM.retry + else + if alreadyCreated + then pure True + else do + STM.writeTVar concWritersT ((foldersBeingCreated `Set.union` foldersInBetween) `Set.difference` dirsCreated) + pure False + unless alreadyCreated $ createDirectoryIfMissing True fp + -- Update lists again + STM.atomically $ do + foldersBeingCreated <- STM.readTVar concWritersT + dirsCreated <- STM.readTVar dirsCreatedT + STM.writeTVar concWritersT $ foldersBeingCreated `Set.difference` foldersInBetween + STM.writeTVar dirsCreatedT $ dirsCreated `Set.union` foldersInBetween -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do -- unless (d `Set.member` cdirs || takeFileName d == ".") $ -- createDirectory d From d8d4ebf1a1114f985013c76faa7524f05d4640ec Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 17:06:40 -0300 Subject: [PATCH 13/37] Revert "STM is not worth it" This reverts commit 2ad94e601d519cfe8e18793c53d5b116c1934e4e. --- src/Codd/Representations/Disk.hs | 45 +++++++------------------------- 1 file changed, 10 insertions(+), 35 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 6e382f4f..2e0212d1 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -2,7 +2,6 @@ module Codd.Representations.Disk ( persistRepsToDisk, readRepsFromDisk, toFiles, - dirAndAncestorsBetween, ) where @@ -32,9 +31,7 @@ import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import Data.Set (Set) import qualified Data.Set as Set -import Debug.Trace import Foreign.C (CInt (..)) -import qualified GHC.Conc as STM import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath @@ -48,7 +45,6 @@ import System.Posix.IO (OpenFileFlags (directory)) import UnliftIO ( MonadIO (..), MonadUnliftIO, - atomically, bracket, evaluate, handle, @@ -258,39 +254,18 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeRec :: FilePath -> DbRep -> IO () writeRec dir obj = do createDirectoryIfMissing True dir - dirsCreatedT <- STM.newTVarIO (Set.singleton dir) - concWritersT <- STM.newTVarIO (mempty :: Set FilePath) - let createDirAndAncestors fp = do - -- Check-and-add to list of dirs being created - let foldersInBetween = Set.fromList $ dirAndAncestorsBetween dir fp - alreadyCreated <- STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - -- let alreadyCreated = traceShow (fp, foldersInBetween) $ all (`Set.member` dirsCreated) foldersInBetween - let alreadyCreated = all (`Set.member` dirsCreated) foldersInBetween - wouldCompete = any (`Set.member` foldersBeingCreated) foldersInBetween - if wouldCompete - then STM.retry - else - if alreadyCreated - then pure True - else do - STM.writeTVar concWritersT ((foldersBeingCreated `Set.union` foldersInBetween) `Set.difference` dirsCreated) - pure False - unless alreadyCreated $ createDirectoryIfMissing True fp - -- Update lists again - STM.atomically $ do - foldersBeingCreated <- STM.readTVar concWritersT - dirsCreated <- STM.readTVar dirsCreatedT - STM.writeTVar concWritersT $ foldersBeingCreated `Set.difference` foldersInBetween - STM.writeTVar dirsCreatedT $ dirsCreated `Set.union` foldersInBetween + createdDirs <- newMVar (Set.singleton dir) + -- concWriters <- newTVar (mempty :: Set FilePath) + let createDirAndAncestors fp = modifyMVar_ createdDirs $ \cdirs -> do + createDirectoryIfMissing True fp + pure cdirs -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do -- unless (d `Set.member` cdirs || takeFileName d == ".") $ -- createDirectory d -- pure d -- evaluate $ Set.fromList allDirs `Set.union` cdirs - -- Limit concurrent writers to 2 to avoid too many open file descriptors at once - pooledMapConcurrently_ (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) + -- Codd only has 2 capabilities + pooledMapConcurrentlyN_ 2 (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) writeFolder :: (FilePath -> IO ()) -> FilePath -> NE.NonEmpty (FilePath, Value) -> IO () writeFolder createDirAndAncestors dir filesPerFolder = do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder @@ -300,14 +275,14 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- fsyncFolder (dir relFolderToCreate) dirAndAncestorsBetween :: FilePath -> FilePath -> [FilePath] -dirAndAncestorsBetween ancestor dir' = filter ((/= ".") . takeFileName) $ go dir' [] +dirAndAncestorsBetween ancestor' dir' = go ancestor' dir' [] where - go dir res + go ancestor dir res | dir == ancestor = dir : res | not (ancestor `List.isPrefixOf` dir) = res | otherwise = let parent = takeDirectory dir - in go parent $ dir : res + in go ancestor parent [dir] -- fsyncFolder :: FilePath -> IO () -- fsyncFolder _dir = do From d75d747275481c8f19c6c6f88d6451cdcdba687f Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 17:09:07 -0300 Subject: [PATCH 14/37] Tidy up --- codd.cabal | 1 - src/Codd/Representations/Disk.hs | 65 +++-------------------- test/WritingReadingRepresentationsSpec.hs | 1 - 3 files changed, 6 insertions(+), 61 deletions(-) diff --git a/codd.cabal b/codd.cabal index fcb214dd..83dfdaf4 100644 --- a/codd.cabal +++ b/codd.cabal @@ -110,7 +110,6 @@ library , text , time , transformers - , unix , unliftio , unliftio-core , unordered-containers diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 2e0212d1..594a23bf 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -12,7 +12,6 @@ import Control.Monad ( forM, forM_, join, - unless, when, ) import Control.Monad.Identity (runIdentity) @@ -24,14 +23,10 @@ import Data.Bifunctor (first) import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) -import qualified Data.List as List import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) -import Data.Set (Set) -import qualified Data.Set as Set -import Foreign.C (CInt (..)) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath @@ -40,25 +35,17 @@ import System.FilePath (), ) import System.IO.Error (isDoesNotExistError) -import System.Posix (Fd (..), OpenMode (..), closeFd, defaultFileFlags, openFd) -import System.Posix.IO (OpenFileFlags (directory)) import UnliftIO ( MonadIO (..), MonadUnliftIO, - bracket, evaluate, handle, - modifyMVar_, - newMVar, - newTVar, pooledMapConcurrentlyN_, - pooledMapConcurrently_, throwIO, tryJust, ) import UnliftIO.Directory ( canonicalizePath, - createDirectory, createDirectoryIfMissing, doesDirectoryExist, listDirectory, @@ -254,55 +241,15 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeRec :: FilePath -> DbRep -> IO () writeRec dir obj = do createDirectoryIfMissing True dir - createdDirs <- newMVar (Set.singleton dir) - -- concWriters <- newTVar (mempty :: Set FilePath) - let createDirAndAncestors fp = modifyMVar_ createdDirs $ \cdirs -> do - createDirectoryIfMissing True fp - pure cdirs - -- allDirs <- forM (dirAndAncestorsBetween dir fp) $ \d -> do - -- unless (d `Set.member` cdirs || takeFileName d == ".") $ - -- createDirectory d - -- pure d - -- evaluate $ Set.fromList allDirs `Set.union` cdirs - -- Codd only has 2 capabilities - pooledMapConcurrentlyN_ 2 (writeFolder createDirAndAncestors dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) - writeFolder :: (FilePath -> IO ()) -> FilePath -> NE.NonEmpty (FilePath, Value) -> IO () - writeFolder createDirAndAncestors dir filesPerFolder = do + -- Limit number of open file descriptors even if in the future we + -- increase codd's capabilities beyond the current 2 + pooledMapConcurrentlyN_ 2 (writeFolder dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) + writeFolder :: FilePath -> NE.NonEmpty (FilePath, Value) -> IO () + writeFolder dir filesPerFolder = do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder - createDirAndAncestors (dir relFolderToCreate) + createDirectoryIfMissing True (dir relFolderToCreate) forM_ filesPerFolder (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) --- fsyncFolder (dir relFolderToCreate) - -dirAndAncestorsBetween :: FilePath -> FilePath -> [FilePath] -dirAndAncestorsBetween ancestor' dir' = go ancestor' dir' [] - where - go ancestor dir res - | dir == ancestor = dir : res - | not (ancestor `List.isPrefixOf` dir) = res - | otherwise = - let parent = takeDirectory dir - in go ancestor parent [dir] - --- fsyncFolder :: FilePath -> IO () --- fsyncFolder _dir = do --- pure () - --- -- TODO: Ignore exception (let's keep trying if fsync fails) --- fsyncResult <- bracket (openFd dir ReadOnly defaultFileFlags {directory = True}) closeFd (\(Fd fd) -> c_fsync fd) --- when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult - --- fsyncFile :: FilePath -> IO () --- fsyncFile _fn = pure () - --- do --- -- TODO: Ignore exception (let's keep trying if fsync fails) --- fsyncResult <- bracket (openFd fn ReadOnly defaultFileFlags) closeFd (\(Fd fd) -> c_fsync fd) --- when (fsyncResult /= 0) $ putStrLn $ "Got bad fsync result: " ++ show fsyncResult - --- foreign import ccall safe "fsync" --- c_fsync :: CInt -> IO CInt - readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = SchemaRep (readObjName dir) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 374f7484..c65ae3ee 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -12,7 +12,6 @@ import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUIDv4 import DbUtils (getEmptyTempDir) -import Foreign.C (CInt (..)) import System.FilePath ( (), ) From 5b6dbd7e56ddd899527e9371e38a4368885de2d8 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 17 Aug 2025 17:11:13 -0300 Subject: [PATCH 15/37] Try to threadDelay before reading again --- test/WritingReadingRepresentationsSpec.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index c65ae3ee..a15f5274 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -7,6 +7,7 @@ import Codd.Representations schemaDifferences, ) import Codd.Types (PgMajorVersion) +import Control.Concurrent (threadDelay) import qualified Data.Map as Map import Data.UUID (UUID) import qualified Data.UUID as UUID @@ -72,6 +73,7 @@ spec = do writeSchemaAndReadSchemaRoundtrip :: PgMajorVersion -> DbRep -> FilePath -> IO () writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do persistRepsToDisk pgVersion dbReps expectedSchemaDir + threadDelay 50_000 readDbSchema <- readRepsFromDisk pgVersion From 373248cf0bb03b06fad6ac68e7cac9ac999dd8df Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Mon, 18 Aug 2025 17:09:41 -0300 Subject: [PATCH 16/37] Call sync in addition to waiting 50ms --- test/WritingReadingRepresentationsSpec.hs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index a15f5274..4530975a 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -73,6 +73,15 @@ spec = do writeSchemaAndReadSchemaRoundtrip :: PgMajorVersion -> DbRep -> FilePath -> IO () writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do persistRepsToDisk pgVersion dbReps expectedSchemaDir + -- Tests fail intermittently on MacOS because apparently directory entry metadata + -- might not be flushed to disk without fsync. + -- I have tried fsync'ing the directory after the last file is written to it, but + -- it didn't work. fsync'ing every file also didn't work. Granted, I didn't try + -- fsync'ing after the `rename` operation for the whole of the written expected-schema + -- folder takes place, so maybe that's what I'm missing. + -- Anyway, just calling "sync" also doesn't work. We call "sync" and wait some time. + -- This is all terrible. + c_sync threadDelay 50_000 readDbSchema <- readRepsFromDisk @@ -81,3 +90,6 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do let diffs = schemaDifferences dbReps readDbSchema diffs `shouldBe` Map.empty readDbSchema `shouldBe` dbReps + +foreign import ccall unsafe "sync" + c_sync :: IO () From 83eceddb2c433adf53d3d3ade5eab81fc5f0a8cb Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Mon, 18 Aug 2025 17:47:47 -0300 Subject: [PATCH 17/37] Two syncs and 100ms --- test/WritingReadingRepresentationsSpec.hs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 4530975a..40a35eb6 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -82,7 +82,8 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do -- Anyway, just calling "sync" also doesn't work. We call "sync" and wait some time. -- This is all terrible. c_sync - threadDelay 50_000 + threadDelay 100_000 + c_sync readDbSchema <- readRepsFromDisk pgVersion From 11261ea7ef6b0b76c4a61e658127d2617a661719 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Tue, 19 Aug 2025 17:12:19 -0300 Subject: [PATCH 18/37] Wait 200ms --- test/WritingReadingRepresentationsSpec.hs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 40a35eb6..f1e9a11f 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -84,6 +84,8 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do c_sync threadDelay 100_000 c_sync + threadDelay 100_000 + c_sync readDbSchema <- readRepsFromDisk pgVersion From d40945e8249ee5fb72cb67a7fae4c84d346dc08d Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Tue, 19 Aug 2025 17:30:21 -0300 Subject: [PATCH 19/37] Wait 200 ms more --- test/WritingReadingRepresentationsSpec.hs | 1 + 1 file changed, 1 insertion(+) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index f1e9a11f..71cf974d 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -86,6 +86,7 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do c_sync threadDelay 100_000 c_sync + threadDelay 200_000 readDbSchema <- readRepsFromDisk pgVersion From 637f3f94bb4dafeca8b3d2e12386d4fce25ec7e7 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Tue, 19 Aug 2025 17:53:17 -0300 Subject: [PATCH 20/37] Run sync in many threads --- test/WritingReadingRepresentationsSpec.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 71cf974d..489b5ea9 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -7,7 +7,8 @@ import Codd.Representations schemaDifferences, ) import Codd.Types (PgMajorVersion) -import Control.Concurrent (threadDelay) +import Control.Concurrent (forkIO, threadDelay) +import Control.Monad (forM_) import qualified Data.Map as Map import Data.UUID (UUID) import qualified Data.UUID as UUID @@ -83,7 +84,7 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do -- This is all terrible. c_sync threadDelay 100_000 - c_sync + forM_ [1 .. 1000] $ \_ -> forkIO c_sync threadDelay 100_000 c_sync threadDelay 200_000 From b9b8f405cc105ea2c6433ba913bf5b3730e9f17d Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Thu, 21 Aug 2025 16:18:31 -0300 Subject: [PATCH 21/37] Why is MacOS so difficult --- test/WritingReadingRepresentationsSpec.hs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 489b5ea9..70aa3228 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -82,12 +82,9 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do -- folder takes place, so maybe that's what I'm missing. -- Anyway, just calling "sync" also doesn't work. We call "sync" and wait some time. -- This is all terrible. - c_sync - threadDelay 100_000 - forM_ [1 .. 1000] $ \_ -> forkIO c_sync + forM_ [1 .. 10] $ \_ -> forkIO c_sync threadDelay 100_000 c_sync - threadDelay 200_000 readDbSchema <- readRepsFromDisk pgVersion From 7d23195112c8823715038db9aa3708c3a375efef Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Thu, 21 Aug 2025 16:58:01 -0300 Subject: [PATCH 22/37] Print exceptions --- src/Codd/Representations/Disk.hs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 594a23bf..62c48313 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -27,6 +27,7 @@ import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) +import Debug.Trace import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) import System.FilePath @@ -201,7 +202,7 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- 3. Different operating systems can have different behaviours, like Windows, where renameDirectory fails if the target directory exists: https://hackage.haskell.org/package/directory-1.3.9.0/docs/System-Directory.html#v:renameDirectory -- 4. Windows and I think even Linux can have ACLs that have more than just a binary "can-write" privilege, with "can-delete" being -- separated from "can-create", IIRC. - errBestScenario <- tryJust (\(e :: IOError) -> if ioe_type e `elem` [NoSuchThing, UnsatisfiedConstraints, PermissionDenied, IllegalOperation, UnsupportedOperation] then Just () else Nothing) $ do + errBestScenario <- tryJust (\(e :: IOError) -> if ioe_type (traceShowId e) `elem` [NoSuchThing, UnsatisfiedConstraints, PermissionDenied, IllegalOperation, UnsupportedOperation] then Just () else Nothing) $ do let nonCanonTempDir = schemaDir "../.temp-codd-dir-you-can-remove" -- We don't try to be too smart if the schema dir is a symlink so we preserve that symlink isLink <- pathIsSymbolicLink schemaDir @@ -355,7 +356,7 @@ readMultiple dir f = do return $ listToMap objList Left () -> pure Map.empty where - checkDoesNotExist (e :: IOError) = if isDoesNotExistError e then pure (Left ()) else throwIO e + checkDoesNotExist (e :: IOError) = if isDoesNotExistError (traceShowId e) then pure (Left ()) else throwIO e readRepsFromDisk :: (MonadUnliftIO m) => From 43dc2883296668ea407e17524f55541f738c837b Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Thu, 21 Aug 2025 17:24:10 -0300 Subject: [PATCH 23/37] Try strict ByteString --- src/Codd/Representations/Disk.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 62c48313..82756034 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -20,7 +20,7 @@ import Data.Aeson decode, ) import Data.Bifunctor (first) -import qualified Data.ByteString.Lazy as BS +import qualified Data.ByteString as BS import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) import qualified Data.List.NonEmpty as NE @@ -249,7 +249,7 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = writeFolder dir filesPerFolder = do let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder createDirectoryIfMissing True (dir relFolderToCreate) - forM_ filesPerFolder (\(fn, jsonRep) -> BS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) + forM_ filesPerFolder (\(fn, jsonRep) -> BS.writeFile (dir fn) (BS.toStrict $ detEncodeJSONByteString jsonRep)) readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = From f1b44ff728488bf1b0ecbc5d96a798fd33dd1097 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Thu, 21 Aug 2025 19:07:41 -0300 Subject: [PATCH 24/37] Test macos filesystem capabilities --- .github/workflows/main.yml | 6 ++++++ test/TypesGen.hs | 42 +++++++++++++++++++++----------------- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index d8c061ee..87a818c0 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -156,6 +156,12 @@ jobs: - name: Build codd's tests run: nix-build --no-out-link -A coddtests + - name: Test filesystem capabilities + run: | + mkdir "0123456789$" + echo something > "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + cat "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + - name: Run tests run: | echo "Running tests that don't depend on a database" diff --git a/test/TypesGen.hs b/test/TypesGen.hs index 02b72745..f4018fcf 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -2,6 +2,7 @@ module TypesGen where import Codd.Representations import Codd.Types (PgMajorVersion (..)) +import qualified Data.Aeson as Aeson import Data.Function (on) import Data.List (nubBy) import Data.Map.Strict (Map) @@ -15,7 +16,7 @@ instance Arbitrary DbRepsGen where arbitrary = let repsGen = DbRep - <$> arbitrary + <$> arbJson <*> uniqueMapOf 3 schemaHashGen objName <*> uniqueMapOf 2 roleHashGen objName versionGen = PgMajorVersion <$> arbitrary @@ -24,39 +25,42 @@ instance Arbitrary DbRepsGen where schemaHashGen = SchemaRep <$> genObjName - <*> arbitrary + <*> arbJson <*> uniqueMapOf 20 tableGen objName <*> uniqueMapOf 5 viewGen objName <*> uniqueMapOf 10 routineGen objName <*> uniqueMapOf 15 sequenceGen objName <*> uniqueMapOf 2 collationGen objName <*> uniqueMapOf 5 typeGen objName - roleHashGen = RoleRep <$> genObjName <*> arbitrary + roleHashGen = RoleRep <$> genObjName <*> arbJson -- Per-schema object generators tableGen = TableRep <$> genObjName - <*> arbitrary + <*> arbJson <*> uniqueMapOf 20 colGen objName <*> uniqueMapOf 5 constraintGen objName <*> uniqueMapOf 1 triggerGen objName <*> uniqueMapOf 2 policyGen objName <*> uniqueMapOf 3 indexGen objName <*> uniqueMapOf 1 stxGen objName - viewGen = ViewRep <$> genObjName <*> arbitrary - routineGen = RoutineRep <$> genObjName <*> arbitrary - sequenceGen = SequenceRep <$> genObjName <*> arbitrary - collationGen = CollationRep <$> genObjName <*> arbitrary - typeGen = TypeRep <$> genObjName <*> arbitrary + viewGen = ViewRep <$> genObjName <*> arbJson + routineGen = RoutineRep <$> genObjName <*> arbJson + sequenceGen = SequenceRep <$> genObjName <*> arbJson + collationGen = CollationRep <$> genObjName <*> arbJson + typeGen = TypeRep <$> genObjName <*> arbJson -- Per-table object generators - colGen = TableColumnRep <$> genObjName <*> arbitrary - constraintGen = TableConstraintRep <$> genObjName <*> arbitrary - triggerGen = TableTriggerRep <$> genObjName <*> arbitrary - policyGen = TablePolicyRep <$> genObjName <*> arbitrary - indexGen = TableIndexRep <$> genObjName <*> arbitrary - stxGen = TableStatisticsRep <$> genObjName <*> arbitrary + colGen = TableColumnRep <$> genObjName <*> arbJson + constraintGen = TableConstraintRep <$> genObjName <*> arbJson + triggerGen = TableTriggerRep <$> genObjName <*> arbJson + policyGen = TablePolicyRep <$> genObjName <*> arbJson + indexGen = TableIndexRep <$> genObjName <*> arbJson + stxGen = TableStatisticsRep <$> genObjName <*> arbJson + +arbJson :: Gen Aeson.Value +arbJson = arbitrary uniqueListOf :: (Eq b) => Int -> Gen a -> (a -> b) -> Gen [a] uniqueListOf size gen uniqBy = @@ -73,10 +77,10 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉÍÓÚñ_" - validLowerOtherChars = validLowerFirstChars ++ "0123456789$" - validUpperOtherChars = validUpperFirstChars ++ "0123456789$" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyz" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZ" + validLowerOtherChars = validLowerFirstChars ++ "0123456789" + validUpperOtherChars = validUpperFirstChars ++ "0123456789" genLower = do c <- elements validLowerFirstChars -- Max Length 63 bytes of UTF8-Encoded name From d05e22542a218299d7eb4e51f1bb6137e0657797 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Fri, 22 Aug 2025 17:49:55 -0300 Subject: [PATCH 25/37] Start binary search of bad characters --- .github/workflows/main.yml | 6 +++--- test/TypesGen.hs | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 87a818c0..e0ec8460 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,9 +158,9 @@ jobs: - name: Test filesystem capabilities run: | - mkdir "0123456789$" - echo something > "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - cat "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + mkdir -p "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + echo something > "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + cat "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - name: Run tests run: | diff --git a/test/TypesGen.hs b/test/TypesGen.hs index f4018fcf..2e846a4a 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,10 +77,10 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyz" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZ" - validLowerOtherChars = validLowerFirstChars ++ "0123456789" - validUpperOtherChars = validUpperFirstChars ++ "0123456789" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáé" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉ" + validLowerOtherChars = validLowerFirstChars ++ "0123456789$" + validUpperOtherChars = validUpperFirstChars ++ "0123456789$" genLower = do c <- elements validLowerFirstChars -- Max Length 63 bytes of UTF8-Encoded name From 6e50fb37deaf8b9cdfaa2fd4ee69239662db8be3 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 23 Aug 2025 09:18:15 -0300 Subject: [PATCH 26/37] Binary search to find error - part 2 --- test/TypesGen.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index 2e846a4a..594dec08 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,8 +77,8 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáé" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉ" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzíóúñ_" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÍÓÚñ_" validLowerOtherChars = validLowerFirstChars ++ "0123456789$" validUpperOtherChars = validUpperFirstChars ++ "0123456789$" genLower = do From cf2dfcae5c666dc5b4c7621ceaa6a0d75fbc7c32 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 23 Aug 2025 17:43:07 -0300 Subject: [PATCH 27/37] Remove `traceShowId` --- src/Codd/Representations/Disk.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 82756034..c2a7d15d 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -202,7 +202,7 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = -- 3. Different operating systems can have different behaviours, like Windows, where renameDirectory fails if the target directory exists: https://hackage.haskell.org/package/directory-1.3.9.0/docs/System-Directory.html#v:renameDirectory -- 4. Windows and I think even Linux can have ACLs that have more than just a binary "can-write" privilege, with "can-delete" being -- separated from "can-create", IIRC. - errBestScenario <- tryJust (\(e :: IOError) -> if ioe_type (traceShowId e) `elem` [NoSuchThing, UnsatisfiedConstraints, PermissionDenied, IllegalOperation, UnsupportedOperation] then Just () else Nothing) $ do + errBestScenario <- tryJust (\(e :: IOError) -> if ioe_type e `elem` [NoSuchThing, UnsatisfiedConstraints, PermissionDenied, IllegalOperation, UnsupportedOperation] then Just () else Nothing) $ do let nonCanonTempDir = schemaDir "../.temp-codd-dir-you-can-remove" -- We don't try to be too smart if the schema dir is a symlink so we preserve that symlink isLink <- pathIsSymbolicLink schemaDir @@ -356,7 +356,7 @@ readMultiple dir f = do return $ listToMap objList Left () -> pure Map.empty where - checkDoesNotExist (e :: IOError) = if isDoesNotExistError (traceShowId e) then pure (Left ()) else throwIO e + checkDoesNotExist (e :: IOError) = if isDoesNotExistError e then pure (Left ()) else throwIO e readRepsFromDisk :: (MonadUnliftIO m) => From 90e014adada2276016f3de4a64960d58c2e7f905 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 23 Aug 2025 17:45:21 -0300 Subject: [PATCH 28/37] Binary search - more --- test/TypesGen.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index 594dec08..644be9a8 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,8 +77,8 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzíóúñ_" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÍÓÚñ_" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzúñ_" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÚñ_" validLowerOtherChars = validLowerFirstChars ++ "0123456789$" validUpperOtherChars = validUpperFirstChars ++ "0123456789$" genLower = do From 7107c826f29c34fe662b8721b350736823778cda Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 23 Aug 2025 18:38:45 -0300 Subject: [PATCH 29/37] Binary search - remove more chars --- test/TypesGen.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index 644be9a8..7a36714e 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,8 +77,8 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzúñ_" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÚñ_" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzñ" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZñ" validLowerOtherChars = validLowerFirstChars ++ "0123456789$" validUpperOtherChars = validUpperFirstChars ++ "0123456789$" genLower = do From 961c7c4ceed33625f652937794e5ee2ee895b93a Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 23 Aug 2025 19:26:28 -0300 Subject: [PATCH 30/37] Binary search - Remove `$` character --- test/TypesGen.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index 7a36714e..f40b82ac 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -79,8 +79,8 @@ genObjName = -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzñ" validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZñ" - validLowerOtherChars = validLowerFirstChars ++ "0123456789$" - validUpperOtherChars = validUpperFirstChars ++ "0123456789$" + validLowerOtherChars = validLowerFirstChars ++ "0123456789" + validUpperOtherChars = validUpperFirstChars ++ "0123456789" genLower = do c <- elements validLowerFirstChars -- Max Length 63 bytes of UTF8-Encoded name From fc306e431f3fcb3c277d99e3e7e27739ee87040d Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 24 Aug 2025 10:59:13 -0300 Subject: [PATCH 31/37] =?UTF-8?q?Confirm=20`=C3=B1`=20is=20the=20problem?= =?UTF-8?q?=20by=20removing=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/TypesGen.hs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index f40b82ac..f4018fcf 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,8 +77,8 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzñ" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZñ" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyz" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZ" validLowerOtherChars = validLowerFirstChars ++ "0123456789" validUpperOtherChars = validUpperFirstChars ++ "0123456789" genLower = do From 24b91c884cd2d31b7d588e251a07959c537dfdc8 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 24 Aug 2025 11:08:54 -0300 Subject: [PATCH 32/37] =?UTF-8?q?Bring=20back=20`=C3=B1`=20and=20print=20f?= =?UTF-8?q?ilesystem=20encoding?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/main.yml | 7 ++++--- test/TypesGen.hs | 4 ++-- test/WritingReadingRepresentationsSpec.hs | 17 +++-------------- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e0ec8460..102b9142 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -158,9 +158,10 @@ jobs: - name: Test filesystem capabilities run: | - mkdir -p "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - echo something > "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - cat "0123456789$/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_/abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + echo "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" > testfile + echo other > "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" + FILE=$(cat testfile) + cat "$FILE" - name: Run tests run: | diff --git a/test/TypesGen.hs b/test/TypesGen.hs index f4018fcf..f40b82ac 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -77,8 +77,8 @@ genObjName = [(100, genLower), (5, genMixed)] where -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyz" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZ" + validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzñ" + validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZñ" validLowerOtherChars = validLowerFirstChars ++ "0123456789" validUpperOtherChars = validUpperFirstChars ++ "0123456789" genLower = do diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 70aa3228..893d393c 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -14,6 +14,7 @@ import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUIDv4 import DbUtils (getEmptyTempDir) +import GHC.IO.Encoding import System.FilePath ( (), ) @@ -30,6 +31,8 @@ spec = do it "persistRepsToDisk is inverse of readRepsFromDisk" $ do property $ \(DbRepsGen dbHashes pgVersion) -> do baseFolder <- getEmptyTempDir + enc <- getFileSystemEncoding + print enc writeSchemaAndReadSchemaRoundtrip pgVersion dbHashes (baseFolder "inverse-test-sql-folder") modifyMaxSuccess (const 1) $ it @@ -74,17 +77,6 @@ spec = do writeSchemaAndReadSchemaRoundtrip :: PgMajorVersion -> DbRep -> FilePath -> IO () writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do persistRepsToDisk pgVersion dbReps expectedSchemaDir - -- Tests fail intermittently on MacOS because apparently directory entry metadata - -- might not be flushed to disk without fsync. - -- I have tried fsync'ing the directory after the last file is written to it, but - -- it didn't work. fsync'ing every file also didn't work. Granted, I didn't try - -- fsync'ing after the `rename` operation for the whole of the written expected-schema - -- folder takes place, so maybe that's what I'm missing. - -- Anyway, just calling "sync" also doesn't work. We call "sync" and wait some time. - -- This is all terrible. - forM_ [1 .. 10] $ \_ -> forkIO c_sync - threadDelay 100_000 - c_sync readDbSchema <- readRepsFromDisk pgVersion @@ -92,6 +84,3 @@ writeSchemaAndReadSchemaRoundtrip pgVersion dbReps expectedSchemaDir = do let diffs = schemaDifferences dbReps readDbSchema diffs `shouldBe` Map.empty readDbSchema `shouldBe` dbReps - -foreign import ccall unsafe "sync" - c_sync :: IO () From 0efdf4e975a8909cf682fa92bf014e8cb6eadc81 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Mon, 25 Aug 2025 14:01:13 -0300 Subject: [PATCH 33/37] Try to use `OsPath` --- codd.cabal | 2 + nix/nixpkgs.nix | 1 + src/Codd/Representations.hs | 121 +++++++++++++----------- src/Codd/Representations/Disk.hs | 152 +++++++++++++++--------------- src/Codd/Representations/Types.hs | 13 ++- 5 files changed, 153 insertions(+), 136 deletions(-) diff --git a/codd.cabal b/codd.cabal index 83dfdaf4..22a2fbf4 100644 --- a/codd.cabal +++ b/codd.cabal @@ -97,6 +97,8 @@ library , containers , deepseq , dlist + , directory + , file-io , filepath , formatting , hashable diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index ccb23254..409f841e 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -5,6 +5,7 @@ let haskellPatchesOverlay = final: prev: { haxl = final.haskell.lib.doJailbreak (final.haskell.lib.markUnbroken hsSuper.haxl); postgresql-query = final.haskell.lib.dontCheck (final.haskell.lib.markUnbroken hsSuper.postgresql-query); + file-io = final.haskell.lib.doJailbreak (final.haskell.lib.markUnbroken hsSuper.file-io); }; }; }; diff --git a/src/Codd/Representations.hs b/src/Codd/Representations.hs index 7e33722e..d2597f8e 100644 --- a/src/Codd/Representations.hs +++ b/src/Codd/Representations.hs @@ -1,71 +1,78 @@ module Codd.Representations - ( module Codd.Representations.Database - , module Codd.Representations.Types - , module Codd.Representations.Disk - , logSchemasComparison - , schemaDifferences - ) where + ( module Codd.Representations.Database, + module Codd.Representations.Types, + module Codd.Representations.Disk, + logSchemasComparison, + schemaDifferences, + ) +where -import Codd.Representations.Database ( readRepresentationsFromDbWithSettings - , readSchemaFromDatabase - ) -import Codd.Representations.Disk -import Codd.Representations.Types - -import Codd.Logging ( CoddLogger - , logError - , logInfo - ) -import Data.List ( sortOn ) -import Data.Map ( Map ) -import qualified Data.Map as Map -import Data.Maybe ( mapMaybe ) +import Codd.Logging + ( CoddLogger, + logError, + logInfo, + ) +import Codd.Representations.Database + ( readRepresentationsFromDbWithSettings, + readSchemaFromDatabase, + ) +import Codd.Representations.Disk +import Codd.Representations.Types +import Data.Bifunctor (first) +import Data.List (sortOn) +import Data.Map (Map) +import qualified Data.Map as Map +import Data.Maybe (mapMaybe) +import System.IO.Unsafe (unsafePerformIO) +import qualified System.OsPath as OS -- | Takes the DB and the expected schemas and logError's the differences, if any, -- or logInfo that they match otherwise. -logSchemasComparison - :: CoddLogger m - => DbRep - -- ^ Database schema - -> DbRep - -- ^ Expected schema - -> m () -logSchemasComparison dbSchema expectedSchemas = if dbSchema /= expectedSchemas +logSchemasComparison :: + (CoddLogger m) => + -- | Database schema + DbRep -> + -- | Expected schema + DbRep -> + m () +logSchemasComparison dbSchema expectedSchemas = + if dbSchema /= expectedSchemas then - logError - $ "DB and expected schemas do not match. Differing objects and their current DB schemas are: " - <> detEncodeSingleLineJSON (schemaDifferences dbSchema expectedSchemas) - else logInfo + logError $ + "DB and expected schemas do not match. Differing objects and their current DB schemas are: " + <> detEncodeSingleLineJSON (schemaDifferences dbSchema expectedSchemas) + else + logInfo "Comparing actual and expected schemas... [match]" schemaDifferences :: DbRep -> DbRep -> Map FilePath DiffType schemaDifferences l r = - let matches = matchOrd fst (toFiles l) (toFiles r) - in Map.fromList $ mapMaybe - (\case - (Nothing, Nothing) -> Nothing - (Just (name, foundInDB), Nothing) -> - Just (name, NotExpectedButFound foundInDB) - (Nothing, Just (name, _)) -> Just (name, ExpectedButNotFound) - (Just (name, foundInDB), Just (_, expected)) - | foundInDB == expected -> Nothing - | otherwise -> Just (name, BothButDifferent foundInDB) - ) - matches - + let matches = matchOrd fst (map (first (unsafePerformIO . OS.decodeFS)) $ toFiles l) (map (first (unsafePerformIO . OS.decodeFS)) $ toFiles r) + in Map.fromList $ + mapMaybe + ( \case + (Nothing, Nothing) -> Nothing + (Just (name, foundInDB), Nothing) -> + Just (name, NotExpectedButFound foundInDB) + (Nothing, Just (name, _)) -> Just (name, ExpectedButNotFound) + (Just (name, foundInDB), Just (_, expected)) + | foundInDB == expected -> Nothing + | otherwise -> Just (name, BothButDifferent foundInDB) + ) + matches -matchOrd :: Ord b => (a -> b) -> [a] -> [a] -> [(Maybe a, Maybe a)] +matchOrd :: (Ord b) => (a -> b) -> [a] -> [a] -> [(Maybe a, Maybe a)] matchOrd f (sortOn f -> lsub) (sortOn f -> lsup) = go lsub lsup where - go [] [] = [] - go (x : xs) [] = (Just x, Nothing) : go xs [] - go [] (y : ys) = (Nothing, Just y) : go [] ys + go [] [] = [] + go (x : xs) [] = (Just x, Nothing) : go xs [] + go [] (y : ys) = (Nothing, Just y) : go [] ys go (x : xs) (y : ys) = - let xf = f x - yf = f y - in if xf < yf - then (Just x, Nothing) : go xs (y : ys) - else if xf == yf - then (Just x, Just y) : go xs ys - else (Nothing, Just y) : go (x : xs) ys - + let xf = f x + yf = f y + in if xf < yf + then (Just x, Nothing) : go xs (y : ys) + else + if xf == yf + then (Just x, Just y) : go xs ys + else (Nothing, Just y) : go (x : xs) ys diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index c2a7d15d..4b7b03a0 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE QuasiQuotes #-} + module Codd.Representations.Disk ( persistRepsToDisk, readRepsFromDisk, @@ -20,22 +22,21 @@ import Data.Aeson decode, ) import Data.Bifunctor (first) -import qualified Data.ByteString as BS -import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) -import Debug.Trace import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) +import qualified System.Directory.OsPath as OS +import qualified System.File.OsPath as OS import System.FilePath - ( takeDirectory, - takeFileName, - (), + ( (), ) import System.IO.Error (isDoesNotExistError) +import System.OsPath (OsPath, osp) +import qualified System.OsPath as OS import UnliftIO ( MonadIO (..), MonadUnliftIO, @@ -80,22 +81,22 @@ class DbDiskObj a where -- | When recursing into a new sub-structure, this function -- will be called with a relative folder where that substructure's -- root belongs to. - (forall b. (DbDiskObj b) => FilePath -> b -> m d) -> + (forall b. (DbDiskObj b) => OsPath -> b -> m d) -> -- | This function will be called when writing to a file. A relative path -- to this structure's root will be passed. - (FilePath -> Value -> m d) -> + (OsPath -> Value -> m d) -> m [d] simpleDbDiskObj :: - (Functor m) => ObjName -> Value -> (FilePath -> Value -> m d) -> m [d] + (Functor m) => ObjName -> Value -> (OsPath -> Value -> m d) -> m [d] simpleDbDiskObj oname orep ffile = (: []) <$> ffile (mkPathFrag oname) orep instance DbDiskObj DbRep where appr (DbRep dbSettingsRep schemas roles) frec ffile = do - x <- ffile "db-settings" dbSettingsRep - rs <- forM (Map.elems roles) $ frec "roles" + x <- ffile [osp|db-settings|] dbSettingsRep + rs <- forM (Map.elems roles) $ frec [osp|roles|] ss <- forM (Map.toList schemas) $ \(schemaName, schema) -> - frec ("schemas" mkPathFrag schemaName) schema + frec ([osp|schemas|] OS. mkPathFrag schemaName) schema pure $ x : rs ++ ss instance DbDiskObj RoleRep where @@ -104,29 +105,29 @@ instance DbDiskObj RoleRep where instance DbDiskObj SchemaRep where appr (SchemaRep _schemaName namespaceRep tables views routines seqs colls types) frec ffile = do - x <- ffile "objrep" namespaceRep + x <- ffile [osp|objrep|] namespaceRep tbls <- forM (Map.toList tables) $ \(tableName, table) -> - frec ("tables" mkPathFrag tableName) table - vs <- forM (Map.elems views) $ frec "views" - rs <- forM (Map.elems routines) $ frec "routines" - ss <- forM (Map.elems seqs) $ frec "sequences" - cs <- forM (Map.elems colls) $ frec "collations" - ts <- forM (Map.elems types) $ frec "types" + frec ([osp|tables|] OS. mkPathFrag tableName) table + vs <- forM (Map.elems views) $ frec [osp|views|] + rs <- forM (Map.elems routines) $ frec [osp|routines|] + ss <- forM (Map.elems seqs) $ frec [osp|sequences|] + cs <- forM (Map.elems colls) $ frec [osp|collations|] + ts <- forM (Map.elems types) $ frec [osp|types|] pure $ x : tbls ++ vs ++ rs ++ ss ++ cs ++ ts instance DbDiskObj TableRep where appr (TableRep _tblName tblRep columns constraints triggers policies indexes statistics) frec ffile = do let mkpath p = p - x <- ffile (mkpath "objrep") tblRep - cols <- forM (Map.elems columns) $ frec (mkpath "cols") + x <- ffile (mkpath [osp|objrep|]) tblRep + cols <- forM (Map.elems columns) $ frec (mkpath [osp|cols|]) constrs <- forM (Map.elems constraints) $ - frec (mkpath "constraints") - trgrs <- forM (Map.elems triggers) $ frec (mkpath "triggers") - pols <- forM (Map.elems policies) $ frec (mkpath "policies") - idxs <- forM (Map.elems indexes) $ frec (mkpath "indexes") - stats <- forM (Map.elems statistics) $ frec (mkpath "statistics") + frec (mkpath [osp|constraints|]) + trgrs <- forM (Map.elems triggers) $ frec (mkpath [osp|triggers|]) + pols <- forM (Map.elems policies) $ frec (mkpath [osp|policies|]) + idxs <- forM (Map.elems indexes) $ frec (mkpath [osp|indexes|]) + stats <- forM (Map.elems statistics) $ frec (mkpath [osp|statistics|]) pure $ x : cols ++ constrs ++ trgrs ++ pols ++ idxs ++ stats instance DbDiskObj TableColumnRep where @@ -166,10 +167,10 @@ instance DbDiskObj CollationRep where instance DbDiskObj TypeRep where appr (TypeRep typeName typeRep) _ = simpleDbDiskObj typeName typeRep -toFiles :: DbRep -> [(FilePath, Value)] +toFiles :: DbRep -> [(OsPath, Value)] toFiles = sortOn fst . frec where - frec :: (DbDiskObj a) => a -> [(FilePath, Value)] + frec :: (DbDiskObj a) => a -> [(OsPath, Value)] frec sobj = concat $ runIdentity $ @@ -177,7 +178,7 @@ toFiles = sortOn fst . frec sobj (\parentDir obj -> pure $ prependDir parentDir $ frec obj) (\fn h -> pure [(fn, h)]) - prependDir dir = map (first (dir )) + prependDir dir = map (first (dir OS.)) -- | Wipes out completely the supplied folder and writes the representations of the Database's structures to it again. persistRepsToDisk :: forall m. (HasCallStack, MonadUnliftIO m) => PgMajorVersion -> DbRep -> FilePath -> m () @@ -240,52 +241,53 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = else createDirectoryIfMissing True path writeRec :: FilePath -> DbRep -> IO () - writeRec dir obj = do - createDirectoryIfMissing True dir + writeRec dir' obj = do + createDirectoryIfMissing True dir' + dir <- OS.encodeFS dir' -- Limit number of open file descriptors even if in the future we -- increase codd's capabilities beyond the current 2 - pooledMapConcurrentlyN_ 2 (writeFolder dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) - writeFolder :: FilePath -> NE.NonEmpty (FilePath, Value) -> IO () + pooledMapConcurrentlyN_ 2 (writeFolder dir) $ NE.groupBy (\(f1, _) (f2, _) -> OS.takeDirectory f1 == OS.takeDirectory f2) (sortOn fst $ toFiles obj) + writeFolder :: OsPath -> NE.NonEmpty (OsPath, Value) -> IO () writeFolder dir filesPerFolder = do - let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder - createDirectoryIfMissing True (dir relFolderToCreate) - forM_ filesPerFolder (\(fn, jsonRep) -> BS.writeFile (dir fn) (BS.toStrict $ detEncodeJSONByteString jsonRep)) + let relFolderToCreate = OS.takeDirectory $ fst $ NE.head filesPerFolder + OS.createDirectoryIfMissing True (dir OS. relFolderToCreate) + forM_ filesPerFolder (\(fn, jsonRep) -> OS.writeFile (dir OS. fn) (detEncodeJSONByteString jsonRep)) -readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep +readNamespaceRep :: (MonadUnliftIO m) => OsPath -> m SchemaRep readNamespaceRep dir = SchemaRep (readObjName dir) - <$> readFileRep (dir "objrep") - <*> readMultiple (dir "tables") readTable - <*> readMultiple (dir "views") readView - <*> readMultiple (dir "routines") readRoutine - <*> readMultiple (dir "sequences") readSequence - <*> readMultiple (dir "collations") readCollation - <*> readMultiple (dir "types") readType - -readTable :: (MonadUnliftIO m) => FilePath -> m TableRep -readView :: (MonadUnliftIO m) => FilePath -> m ViewRep -readRoutine :: (MonadUnliftIO m) => FilePath -> m RoutineRep -readSequence :: (MonadUnliftIO m) => FilePath -> m SequenceRep -readCollation :: (MonadUnliftIO m) => FilePath -> m CollationRep -readType :: (MonadUnliftIO m) => FilePath -> m TypeRep + <$> readFileRep (dir OS. [osp|objrep|]) + <*> readMultiple (dir OS. [osp|tables|]) readTable + <*> readMultiple (dir OS. [osp|views|]) readView + <*> readMultiple (dir OS. [osp|routines|]) readRoutine + <*> readMultiple (dir OS. [osp|sequences|]) readSequence + <*> readMultiple (dir OS. [osp|collations|]) readCollation + <*> readMultiple (dir OS. [osp|types|]) readType + +readTable :: (MonadUnliftIO m) => OsPath -> m TableRep +readView :: (MonadUnliftIO m) => OsPath -> m ViewRep +readRoutine :: (MonadUnliftIO m) => OsPath -> m RoutineRep +readSequence :: (MonadUnliftIO m) => OsPath -> m SequenceRep +readCollation :: (MonadUnliftIO m) => OsPath -> m CollationRep +readType :: (MonadUnliftIO m) => OsPath -> m TypeRep readTable dir = TableRep (readObjName dir) - <$> readFileRep (dir "objrep") - <*> readMultiple (dir "cols") (simpleObjRepFileRead TableColumnRep) + <$> readFileRep (dir OS. [osp|objrep|]) + <*> readMultiple (dir OS. [osp|cols|]) (simpleObjRepFileRead TableColumnRep) <*> readMultiple - (dir "constraints") + (dir OS. [osp|constraints|]) (simpleObjRepFileRead TableConstraintRep) <*> readMultiple - (dir "triggers") + (dir OS. [osp|triggers|]) (simpleObjRepFileRead TableTriggerRep) <*> readMultiple - (dir "policies") + (dir OS. [osp|policies|]) (simpleObjRepFileRead TablePolicyRep) <*> readMultiple - (dir "indexes") + (dir OS. [osp|indexes|]) (simpleObjRepFileRead TableIndexRep) <*> readMultiple - (dir "statistics") + (dir OS. [osp|statistics|]) (simpleObjRepFileRead TableStatisticsRep) readView = simpleObjRepFileRead ViewRep @@ -298,23 +300,23 @@ readCollation = simpleObjRepFileRead CollationRep readType = simpleObjRepFileRead TypeRep -readObjName :: FilePath -> ObjName -readObjName = fromPathFrag . takeFileName +readObjName :: OsPath -> ObjName +readObjName = fromPathFrag . OS.takeFileName readFileRep :: - forall m. (MonadUnliftIO m) => FilePath -> m Value + forall m. (MonadUnliftIO m) => OsPath -> m Value readFileRep filepath = rethrowIfNotExists $ do -- Careful, LBS.readFile is lazy and does not close -- the file handle unless we force the thunk, and not closing -- file handles can make shells with low ulimits barf. -- MacOS has particularly low ulimits. - !fileContents <- liftIO $ LBS.readFile filepath + !fileContents <- liftIO $ OS.readFile filepath !decodedJson <- evaluate $ fromMaybe ( error $ "File '" - ++ filepath + ++ show filepath ++ "' was supposed to contain a JSON value" ) $ force @@ -330,7 +332,7 @@ readFileRep filepath = rethrowIfNotExists $ do throwIO $ userError $ "File " - <> filepath + <> show filepath <> " was expected but does not exist" else throwIO e ) @@ -338,21 +340,21 @@ readFileRep filepath = rethrowIfNotExists $ do simpleObjRepFileRead :: (MonadUnliftIO m) => (ObjName -> Value -> a) -> - FilePath -> + OsPath -> m a simpleObjRepFileRead f filepath = f (readObjName filepath) <$> readFileRep filepath readMultiple :: (MonadUnliftIO m, HasName o) => - FilePath -> - (FilePath -> m o) -> + OsPath -> + (OsPath -> m o) -> m (Map ObjName o) readMultiple dir f = do - foldersOrNotOne <- handle checkDoesNotExist $ Right . filter (/= "objrep") <$> listDirectory dir + foldersOrNotOne <- handle checkDoesNotExist $ Right . filter (/= [osp|objrep|]) <$> liftIO (OS.listDirectory dir) case foldersOrNotOne of Right folders -> do - objList <- traverse (f . (dir )) folders + objList <- traverse (f . (dir OS.)) folders return $ listToMap objList Left () -> pure Map.empty where @@ -364,12 +366,12 @@ readRepsFromDisk :: -- | The path of CODD_EXPECTED_SCHEMA_DIR, without any major version numbers FilePath -> m DbRep -readRepsFromDisk pgVersion schemaDir = - let dir = schemaDir show pgVersion - in DbRep - <$> readFileRep (dir "db-settings") - <*> readMultiple (dir "schemas") readNamespaceRep - <*> readMultiple (dir "roles") (simpleObjRepFileRead RoleRep) +readRepsFromDisk pgVersion schemaDir = do + dir <- liftIO $ OS.encodeFS $ schemaDir show pgVersion + DbRep + <$> readFileRep (dir OS. [osp|db-settings|]) + <*> readMultiple (dir OS. [osp|schemas|]) readNamespaceRep + <*> readMultiple (dir OS. [osp|roles|]) (simpleObjRepFileRead RoleRep) whenM :: (Monad m) => m Bool -> m () -> m () whenM s r = s >>= flip when r diff --git a/src/Codd/Representations/Types.hs b/src/Codd/Representations/Types.hs index 22d6a8d1..5f56bd4a 100644 --- a/src/Codd/Representations/Types.hs +++ b/src/Codd/Representations/Types.hs @@ -50,6 +50,11 @@ import Database.PostgreSQL.Simple.ToField ( ToField, ) import GHC.Generics (Generic) +import qualified System.Directory.OsPath as OS +import qualified System.File.OsPath as OS +import System.IO.Unsafe (unsafePerformIO) +import System.OsPath (OsPath) +import qualified System.OsPath as OS data ObjectRep = HDatabaseSettings | HSchema | HTable | HView | HRoutine | HColumn | HIndex | HTableConstraint | HTrigger | HRole | HSequence | HPolicy | HCollation | HType | HStatistics deriving stock (Eq, Ord, Show, Generic) @@ -156,11 +161,11 @@ data TypeRep = TypeRep ObjName Value deriving anyclass (FromJSON, ToJSON) -- | TODO: Make sure valid DB characters are replaced by valid on-disk characters when necessary -mkPathFrag :: ObjName -> FilePath -mkPathFrag (ObjName n) = Text.unpack n +mkPathFrag :: ObjName -> OsPath +mkPathFrag (ObjName n) = unsafePerformIO $ OS.encodeFS $ Text.unpack n -fromPathFrag :: FilePath -> ObjName -fromPathFrag fp = ObjName $ Text.pack fp +fromPathFrag :: OsPath -> ObjName +fromPathFrag fp = ObjName $ Text.pack $ unsafePerformIO $ OS.decodeFS fp newtype ObjName = ObjName {unObjName :: Text} deriving newtype (FromField, ToField, Eq, Ord, Show, Hashable, FromJSON, FromJSONKey, ToJSON, ToJSONKey) From 7e178a9c54a7a710420062f23f9cbdf5a3327e6f Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Fri, 29 Aug 2025 16:35:40 -0300 Subject: [PATCH 34/37] =?UTF-8?q?Remove=20`=C3=B1`=20for=20Darwin=20only?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/TypesGen.hs | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index f40b82ac..dfbea934 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -1,3 +1,5 @@ +{-# LANGUAGE CPP #-} + module TypesGen where import Codd.Representations @@ -70,17 +72,33 @@ uniqueMapOf :: (Ord k) => Int -> Gen a -> (a -> k) -> Gen (Map k a) uniqueMapOf size gen uniqBy = Map.fromList . map (\v -> (uniqBy v, v)) <$> resize size (listOf gen) +-- | Valid first characters for database object names. +validLowerFirstChars, validUpperFirstChars :: [Char] +#ifdef darwin_HOST_OS +-- Tests fail intermittenly on Darwin, and ñ seems to be the offending character +-- in filesystem operations. See https://github.com/mzabani/codd/pull/220 for +-- a long attempt at fixing it nicely. +-- I tried using 'OsPath' and encoding and decoding carefully, but it did not work. +-- My best hypothesis still is some weird encoding that does not roundtrip, +-- despite github actions seemingly using UTF8 encoding in their filesystem. +-- https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/ +-- https://hasufell.github.io/posts/2022-06-29-fixing-haskell-filepaths.html +validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáéíóú_" +validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉÍÓÚ_" +#else +validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" +validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉÍÓÚñ_" +#endif + genObjName :: Gen ObjName genObjName = ObjName . Text.pack <$> frequency [(100, genLower), (5, genMixed)] where - -- Docs: https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS - validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzñ" - validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZñ" - validLowerOtherChars = validLowerFirstChars ++ "0123456789" - validUpperOtherChars = validUpperFirstChars ++ "0123456789" + validLowerOtherChars = validLowerFirstChars ++ "0123456789$" + validUpperOtherChars = validUpperFirstChars ++ "0123456789$" + genLower = do c <- elements validLowerFirstChars -- Max Length 63 bytes of UTF8-Encoded name From 3857974c22ebf7dd2768ebe0545d9c761cbe9096 Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Fri, 29 Aug 2025 16:49:56 -0300 Subject: [PATCH 35/37] Undo `OsPath` --- codd.cabal | 2 - nix/nixpkgs.nix | 1 - src/Codd/Representations.hs | 121 ++++++++--------- src/Codd/Representations/Disk.hs | 150 +++++++++++----------- src/Codd/Representations/Types.hs | 13 +- test/TypesGen.hs | 3 + test/WritingReadingRepresentationsSpec.hs | 5 - 7 files changed, 137 insertions(+), 158 deletions(-) diff --git a/codd.cabal b/codd.cabal index 22a2fbf4..83dfdaf4 100644 --- a/codd.cabal +++ b/codd.cabal @@ -97,8 +97,6 @@ library , containers , deepseq , dlist - , directory - , file-io , filepath , formatting , hashable diff --git a/nix/nixpkgs.nix b/nix/nixpkgs.nix index 409f841e..ccb23254 100644 --- a/nix/nixpkgs.nix +++ b/nix/nixpkgs.nix @@ -5,7 +5,6 @@ let haskellPatchesOverlay = final: prev: { haxl = final.haskell.lib.doJailbreak (final.haskell.lib.markUnbroken hsSuper.haxl); postgresql-query = final.haskell.lib.dontCheck (final.haskell.lib.markUnbroken hsSuper.postgresql-query); - file-io = final.haskell.lib.doJailbreak (final.haskell.lib.markUnbroken hsSuper.file-io); }; }; }; diff --git a/src/Codd/Representations.hs b/src/Codd/Representations.hs index d2597f8e..7e33722e 100644 --- a/src/Codd/Representations.hs +++ b/src/Codd/Representations.hs @@ -1,78 +1,71 @@ module Codd.Representations - ( module Codd.Representations.Database, - module Codd.Representations.Types, - module Codd.Representations.Disk, - logSchemasComparison, - schemaDifferences, - ) -where + ( module Codd.Representations.Database + , module Codd.Representations.Types + , module Codd.Representations.Disk + , logSchemasComparison + , schemaDifferences + ) where -import Codd.Logging - ( CoddLogger, - logError, - logInfo, - ) -import Codd.Representations.Database - ( readRepresentationsFromDbWithSettings, - readSchemaFromDatabase, - ) -import Codd.Representations.Disk -import Codd.Representations.Types -import Data.Bifunctor (first) -import Data.List (sortOn) -import Data.Map (Map) -import qualified Data.Map as Map -import Data.Maybe (mapMaybe) -import System.IO.Unsafe (unsafePerformIO) -import qualified System.OsPath as OS +import Codd.Representations.Database ( readRepresentationsFromDbWithSettings + , readSchemaFromDatabase + ) +import Codd.Representations.Disk +import Codd.Representations.Types + +import Codd.Logging ( CoddLogger + , logError + , logInfo + ) +import Data.List ( sortOn ) +import Data.Map ( Map ) +import qualified Data.Map as Map +import Data.Maybe ( mapMaybe ) -- | Takes the DB and the expected schemas and logError's the differences, if any, -- or logInfo that they match otherwise. -logSchemasComparison :: - (CoddLogger m) => - -- | Database schema - DbRep -> - -- | Expected schema - DbRep -> - m () -logSchemasComparison dbSchema expectedSchemas = - if dbSchema /= expectedSchemas +logSchemasComparison + :: CoddLogger m + => DbRep + -- ^ Database schema + -> DbRep + -- ^ Expected schema + -> m () +logSchemasComparison dbSchema expectedSchemas = if dbSchema /= expectedSchemas then - logError $ - "DB and expected schemas do not match. Differing objects and their current DB schemas are: " - <> detEncodeSingleLineJSON (schemaDifferences dbSchema expectedSchemas) - else - logInfo + logError + $ "DB and expected schemas do not match. Differing objects and their current DB schemas are: " + <> detEncodeSingleLineJSON (schemaDifferences dbSchema expectedSchemas) + else logInfo "Comparing actual and expected schemas... [match]" schemaDifferences :: DbRep -> DbRep -> Map FilePath DiffType schemaDifferences l r = - let matches = matchOrd fst (map (first (unsafePerformIO . OS.decodeFS)) $ toFiles l) (map (first (unsafePerformIO . OS.decodeFS)) $ toFiles r) - in Map.fromList $ - mapMaybe - ( \case - (Nothing, Nothing) -> Nothing - (Just (name, foundInDB), Nothing) -> - Just (name, NotExpectedButFound foundInDB) - (Nothing, Just (name, _)) -> Just (name, ExpectedButNotFound) - (Just (name, foundInDB), Just (_, expected)) - | foundInDB == expected -> Nothing - | otherwise -> Just (name, BothButDifferent foundInDB) - ) - matches + let matches = matchOrd fst (toFiles l) (toFiles r) + in Map.fromList $ mapMaybe + (\case + (Nothing, Nothing) -> Nothing + (Just (name, foundInDB), Nothing) -> + Just (name, NotExpectedButFound foundInDB) + (Nothing, Just (name, _)) -> Just (name, ExpectedButNotFound) + (Just (name, foundInDB), Just (_, expected)) + | foundInDB == expected -> Nothing + | otherwise -> Just (name, BothButDifferent foundInDB) + ) + matches + -matchOrd :: (Ord b) => (a -> b) -> [a] -> [a] -> [(Maybe a, Maybe a)] +matchOrd :: Ord b => (a -> b) -> [a] -> [a] -> [(Maybe a, Maybe a)] matchOrd f (sortOn f -> lsub) (sortOn f -> lsup) = go lsub lsup where - go [] [] = [] - go (x : xs) [] = (Just x, Nothing) : go xs [] - go [] (y : ys) = (Nothing, Just y) : go [] ys + go [] [] = [] + go (x : xs) [] = (Just x, Nothing) : go xs [] + go [] (y : ys) = (Nothing, Just y) : go [] ys go (x : xs) (y : ys) = - let xf = f x - yf = f y - in if xf < yf - then (Just x, Nothing) : go xs (y : ys) - else - if xf == yf - then (Just x, Just y) : go xs ys - else (Nothing, Just y) : go (x : xs) ys + let xf = f x + yf = f y + in if xf < yf + then (Just x, Nothing) : go xs (y : ys) + else if xf == yf + then (Just x, Just y) : go xs ys + else (Nothing, Just y) : go (x : xs) ys + diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 4b7b03a0..487bb951 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -1,5 +1,3 @@ -{-# LANGUAGE QuasiQuotes #-} - module Codd.Representations.Disk ( persistRepsToDisk, readRepsFromDisk, @@ -22,6 +20,7 @@ import Data.Aeson decode, ) import Data.Bifunctor (first) +import qualified Data.ByteString.Lazy as LBS import Data.List (sortOn) import qualified Data.List.NonEmpty as NE import Data.Map.Strict (Map) @@ -29,14 +28,12 @@ import qualified Data.Map.Strict as Map import Data.Maybe (fromMaybe) import GHC.IO.Exception (IOErrorType (..), ioe_type) import GHC.Stack (HasCallStack) -import qualified System.Directory.OsPath as OS -import qualified System.File.OsPath as OS import System.FilePath - ( (), + ( takeDirectory, + takeFileName, + (), ) import System.IO.Error (isDoesNotExistError) -import System.OsPath (OsPath, osp) -import qualified System.OsPath as OS import UnliftIO ( MonadIO (..), MonadUnliftIO, @@ -81,22 +78,22 @@ class DbDiskObj a where -- | When recursing into a new sub-structure, this function -- will be called with a relative folder where that substructure's -- root belongs to. - (forall b. (DbDiskObj b) => OsPath -> b -> m d) -> + (forall b. (DbDiskObj b) => FilePath -> b -> m d) -> -- | This function will be called when writing to a file. A relative path -- to this structure's root will be passed. - (OsPath -> Value -> m d) -> + (FilePath -> Value -> m d) -> m [d] simpleDbDiskObj :: - (Functor m) => ObjName -> Value -> (OsPath -> Value -> m d) -> m [d] + (Functor m) => ObjName -> Value -> (FilePath -> Value -> m d) -> m [d] simpleDbDiskObj oname orep ffile = (: []) <$> ffile (mkPathFrag oname) orep instance DbDiskObj DbRep where appr (DbRep dbSettingsRep schemas roles) frec ffile = do - x <- ffile [osp|db-settings|] dbSettingsRep - rs <- forM (Map.elems roles) $ frec [osp|roles|] + x <- ffile "db-settings" dbSettingsRep + rs <- forM (Map.elems roles) $ frec "roles" ss <- forM (Map.toList schemas) $ \(schemaName, schema) -> - frec ([osp|schemas|] OS. mkPathFrag schemaName) schema + frec ("schemas" mkPathFrag schemaName) schema pure $ x : rs ++ ss instance DbDiskObj RoleRep where @@ -105,29 +102,29 @@ instance DbDiskObj RoleRep where instance DbDiskObj SchemaRep where appr (SchemaRep _schemaName namespaceRep tables views routines seqs colls types) frec ffile = do - x <- ffile [osp|objrep|] namespaceRep + x <- ffile "objrep" namespaceRep tbls <- forM (Map.toList tables) $ \(tableName, table) -> - frec ([osp|tables|] OS. mkPathFrag tableName) table - vs <- forM (Map.elems views) $ frec [osp|views|] - rs <- forM (Map.elems routines) $ frec [osp|routines|] - ss <- forM (Map.elems seqs) $ frec [osp|sequences|] - cs <- forM (Map.elems colls) $ frec [osp|collations|] - ts <- forM (Map.elems types) $ frec [osp|types|] + frec ("tables" mkPathFrag tableName) table + vs <- forM (Map.elems views) $ frec "views" + rs <- forM (Map.elems routines) $ frec "routines" + ss <- forM (Map.elems seqs) $ frec "sequences" + cs <- forM (Map.elems colls) $ frec "collations" + ts <- forM (Map.elems types) $ frec "types" pure $ x : tbls ++ vs ++ rs ++ ss ++ cs ++ ts instance DbDiskObj TableRep where appr (TableRep _tblName tblRep columns constraints triggers policies indexes statistics) frec ffile = do let mkpath p = p - x <- ffile (mkpath [osp|objrep|]) tblRep - cols <- forM (Map.elems columns) $ frec (mkpath [osp|cols|]) + x <- ffile (mkpath "objrep") tblRep + cols <- forM (Map.elems columns) $ frec (mkpath "cols") constrs <- forM (Map.elems constraints) $ - frec (mkpath [osp|constraints|]) - trgrs <- forM (Map.elems triggers) $ frec (mkpath [osp|triggers|]) - pols <- forM (Map.elems policies) $ frec (mkpath [osp|policies|]) - idxs <- forM (Map.elems indexes) $ frec (mkpath [osp|indexes|]) - stats <- forM (Map.elems statistics) $ frec (mkpath [osp|statistics|]) + frec (mkpath "constraints") + trgrs <- forM (Map.elems triggers) $ frec (mkpath "triggers") + pols <- forM (Map.elems policies) $ frec (mkpath "policies") + idxs <- forM (Map.elems indexes) $ frec (mkpath "indexes") + stats <- forM (Map.elems statistics) $ frec (mkpath "statistics") pure $ x : cols ++ constrs ++ trgrs ++ pols ++ idxs ++ stats instance DbDiskObj TableColumnRep where @@ -167,10 +164,10 @@ instance DbDiskObj CollationRep where instance DbDiskObj TypeRep where appr (TypeRep typeName typeRep) _ = simpleDbDiskObj typeName typeRep -toFiles :: DbRep -> [(OsPath, Value)] +toFiles :: DbRep -> [(FilePath, Value)] toFiles = sortOn fst . frec where - frec :: (DbDiskObj a) => a -> [(OsPath, Value)] + frec :: (DbDiskObj a) => a -> [(FilePath, Value)] frec sobj = concat $ runIdentity $ @@ -178,7 +175,7 @@ toFiles = sortOn fst . frec sobj (\parentDir obj -> pure $ prependDir parentDir $ frec obj) (\fn h -> pure [(fn, h)]) - prependDir dir = map (first (dir OS.)) + prependDir dir = map (first (dir )) -- | Wipes out completely the supplied folder and writes the representations of the Database's structures to it again. persistRepsToDisk :: forall m. (HasCallStack, MonadUnliftIO m) => PgMajorVersion -> DbRep -> FilePath -> m () @@ -241,53 +238,52 @@ persistRepsToDisk pgVersion dbSchema schemaDirBeforeVersions = else createDirectoryIfMissing True path writeRec :: FilePath -> DbRep -> IO () - writeRec dir' obj = do - createDirectoryIfMissing True dir' - dir <- OS.encodeFS dir' + writeRec dir obj = do + createDirectoryIfMissing True dir -- Limit number of open file descriptors even if in the future we -- increase codd's capabilities beyond the current 2 - pooledMapConcurrentlyN_ 2 (writeFolder dir) $ NE.groupBy (\(f1, _) (f2, _) -> OS.takeDirectory f1 == OS.takeDirectory f2) (sortOn fst $ toFiles obj) - writeFolder :: OsPath -> NE.NonEmpty (OsPath, Value) -> IO () + pooledMapConcurrentlyN_ 2 (writeFolder dir) $ NE.groupBy (\(f1, _) (f2, _) -> takeDirectory f1 == takeDirectory f2) (sortOn fst $ toFiles obj) + writeFolder :: FilePath -> NE.NonEmpty (FilePath, Value) -> IO () writeFolder dir filesPerFolder = do - let relFolderToCreate = OS.takeDirectory $ fst $ NE.head filesPerFolder - OS.createDirectoryIfMissing True (dir OS. relFolderToCreate) - forM_ filesPerFolder (\(fn, jsonRep) -> OS.writeFile (dir OS. fn) (detEncodeJSONByteString jsonRep)) + let relFolderToCreate = takeDirectory $ fst $ NE.head filesPerFolder + createDirectoryIfMissing True (dir relFolderToCreate) + forM_ filesPerFolder (\(fn, jsonRep) -> LBS.writeFile (dir fn) (detEncodeJSONByteString jsonRep)) -readNamespaceRep :: (MonadUnliftIO m) => OsPath -> m SchemaRep +readNamespaceRep :: (MonadUnliftIO m) => FilePath -> m SchemaRep readNamespaceRep dir = SchemaRep (readObjName dir) - <$> readFileRep (dir OS. [osp|objrep|]) - <*> readMultiple (dir OS. [osp|tables|]) readTable - <*> readMultiple (dir OS. [osp|views|]) readView - <*> readMultiple (dir OS. [osp|routines|]) readRoutine - <*> readMultiple (dir OS. [osp|sequences|]) readSequence - <*> readMultiple (dir OS. [osp|collations|]) readCollation - <*> readMultiple (dir OS. [osp|types|]) readType - -readTable :: (MonadUnliftIO m) => OsPath -> m TableRep -readView :: (MonadUnliftIO m) => OsPath -> m ViewRep -readRoutine :: (MonadUnliftIO m) => OsPath -> m RoutineRep -readSequence :: (MonadUnliftIO m) => OsPath -> m SequenceRep -readCollation :: (MonadUnliftIO m) => OsPath -> m CollationRep -readType :: (MonadUnliftIO m) => OsPath -> m TypeRep + <$> readFileRep (dir "objrep") + <*> readMultiple (dir "tables") readTable + <*> readMultiple (dir "views") readView + <*> readMultiple (dir "routines") readRoutine + <*> readMultiple (dir "sequences") readSequence + <*> readMultiple (dir "collations") readCollation + <*> readMultiple (dir "types") readType + +readTable :: (MonadUnliftIO m) => FilePath -> m TableRep +readView :: (MonadUnliftIO m) => FilePath -> m ViewRep +readRoutine :: (MonadUnliftIO m) => FilePath -> m RoutineRep +readSequence :: (MonadUnliftIO m) => FilePath -> m SequenceRep +readCollation :: (MonadUnliftIO m) => FilePath -> m CollationRep +readType :: (MonadUnliftIO m) => FilePath -> m TypeRep readTable dir = TableRep (readObjName dir) - <$> readFileRep (dir OS. [osp|objrep|]) - <*> readMultiple (dir OS. [osp|cols|]) (simpleObjRepFileRead TableColumnRep) + <$> readFileRep (dir "objrep") + <*> readMultiple (dir "cols") (simpleObjRepFileRead TableColumnRep) <*> readMultiple - (dir OS. [osp|constraints|]) + (dir "constraints") (simpleObjRepFileRead TableConstraintRep) <*> readMultiple - (dir OS. [osp|triggers|]) + (dir "triggers") (simpleObjRepFileRead TableTriggerRep) <*> readMultiple - (dir OS. [osp|policies|]) + (dir "policies") (simpleObjRepFileRead TablePolicyRep) <*> readMultiple - (dir OS. [osp|indexes|]) + (dir "indexes") (simpleObjRepFileRead TableIndexRep) <*> readMultiple - (dir OS. [osp|statistics|]) + (dir "statistics") (simpleObjRepFileRead TableStatisticsRep) readView = simpleObjRepFileRead ViewRep @@ -300,23 +296,23 @@ readCollation = simpleObjRepFileRead CollationRep readType = simpleObjRepFileRead TypeRep -readObjName :: OsPath -> ObjName -readObjName = fromPathFrag . OS.takeFileName +readObjName :: FilePath -> ObjName +readObjName = fromPathFrag . takeFileName readFileRep :: - forall m. (MonadUnliftIO m) => OsPath -> m Value + forall m. (MonadUnliftIO m) => FilePath -> m Value readFileRep filepath = rethrowIfNotExists $ do -- Careful, LBS.readFile is lazy and does not close -- the file handle unless we force the thunk, and not closing -- file handles can make shells with low ulimits barf. -- MacOS has particularly low ulimits. - !fileContents <- liftIO $ OS.readFile filepath + !fileContents <- liftIO $ LBS.readFile filepath !decodedJson <- evaluate $ fromMaybe ( error $ "File '" - ++ show filepath + ++ filepath ++ "' was supposed to contain a JSON value" ) $ force @@ -332,7 +328,7 @@ readFileRep filepath = rethrowIfNotExists $ do throwIO $ userError $ "File " - <> show filepath + <> filepath <> " was expected but does not exist" else throwIO e ) @@ -340,21 +336,21 @@ readFileRep filepath = rethrowIfNotExists $ do simpleObjRepFileRead :: (MonadUnliftIO m) => (ObjName -> Value -> a) -> - OsPath -> + FilePath -> m a simpleObjRepFileRead f filepath = f (readObjName filepath) <$> readFileRep filepath readMultiple :: (MonadUnliftIO m, HasName o) => - OsPath -> - (OsPath -> m o) -> + FilePath -> + (FilePath -> m o) -> m (Map ObjName o) readMultiple dir f = do - foldersOrNotOne <- handle checkDoesNotExist $ Right . filter (/= [osp|objrep|]) <$> liftIO (OS.listDirectory dir) + foldersOrNotOne <- handle checkDoesNotExist $ Right . filter (/= "objrep") <$> listDirectory dir case foldersOrNotOne of Right folders -> do - objList <- traverse (f . (dir OS.)) folders + objList <- traverse (f . (dir )) folders return $ listToMap objList Left () -> pure Map.empty where @@ -366,12 +362,12 @@ readRepsFromDisk :: -- | The path of CODD_EXPECTED_SCHEMA_DIR, without any major version numbers FilePath -> m DbRep -readRepsFromDisk pgVersion schemaDir = do - dir <- liftIO $ OS.encodeFS $ schemaDir show pgVersion - DbRep - <$> readFileRep (dir OS. [osp|db-settings|]) - <*> readMultiple (dir OS. [osp|schemas|]) readNamespaceRep - <*> readMultiple (dir OS. [osp|roles|]) (simpleObjRepFileRead RoleRep) +readRepsFromDisk pgVersion schemaDir = + let dir = schemaDir show pgVersion + in DbRep + <$> readFileRep (dir "db-settings") + <*> readMultiple (dir "schemas") readNamespaceRep + <*> readMultiple (dir "roles") (simpleObjRepFileRead RoleRep) whenM :: (Monad m) => m Bool -> m () -> m () whenM s r = s >>= flip when r diff --git a/src/Codd/Representations/Types.hs b/src/Codd/Representations/Types.hs index 5f56bd4a..22d6a8d1 100644 --- a/src/Codd/Representations/Types.hs +++ b/src/Codd/Representations/Types.hs @@ -50,11 +50,6 @@ import Database.PostgreSQL.Simple.ToField ( ToField, ) import GHC.Generics (Generic) -import qualified System.Directory.OsPath as OS -import qualified System.File.OsPath as OS -import System.IO.Unsafe (unsafePerformIO) -import System.OsPath (OsPath) -import qualified System.OsPath as OS data ObjectRep = HDatabaseSettings | HSchema | HTable | HView | HRoutine | HColumn | HIndex | HTableConstraint | HTrigger | HRole | HSequence | HPolicy | HCollation | HType | HStatistics deriving stock (Eq, Ord, Show, Generic) @@ -161,11 +156,11 @@ data TypeRep = TypeRep ObjName Value deriving anyclass (FromJSON, ToJSON) -- | TODO: Make sure valid DB characters are replaced by valid on-disk characters when necessary -mkPathFrag :: ObjName -> OsPath -mkPathFrag (ObjName n) = unsafePerformIO $ OS.encodeFS $ Text.unpack n +mkPathFrag :: ObjName -> FilePath +mkPathFrag (ObjName n) = Text.unpack n -fromPathFrag :: OsPath -> ObjName -fromPathFrag fp = ObjName $ Text.pack $ unsafePerformIO $ OS.decodeFS fp +fromPathFrag :: FilePath -> ObjName +fromPathFrag fp = ObjName $ Text.pack fp newtype ObjName = ObjName {unObjName :: Text} deriving newtype (FromField, ToField, Eq, Ord, Show, Hashable, FromJSON, FromJSONKey, ToJSON, ToJSONKey) diff --git a/test/TypesGen.hs b/test/TypesGen.hs index dfbea934..e30af45e 100644 --- a/test/TypesGen.hs +++ b/test/TypesGen.hs @@ -81,8 +81,11 @@ validLowerFirstChars, validUpperFirstChars :: [Char] -- I tried using 'OsPath' and encoding and decoding carefully, but it did not work. -- My best hypothesis still is some weird encoding that does not roundtrip, -- despite github actions seemingly using UTF8 encoding in their filesystem. +-- Of course, ideally we would fix this, but without MacOS to test with, +-- pushing to CI and checking what happens there is too terrible a feedback loop. -- https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/ -- https://hasufell.github.io/posts/2022-06-29-fixing-haskell-filepaths.html +-- https://www.postgresql.org/docs/12/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS validLowerFirstChars = "abcdefghijklmnopqrstuvxwyzçáéíóú_" validUpperFirstChars = "ABCDEFGHIJKLMNOPQRSTUVXWYZÇÁÉÍÓÚ_" #else diff --git a/test/WritingReadingRepresentationsSpec.hs b/test/WritingReadingRepresentationsSpec.hs index 893d393c..c65ae3ee 100644 --- a/test/WritingReadingRepresentationsSpec.hs +++ b/test/WritingReadingRepresentationsSpec.hs @@ -7,14 +7,11 @@ import Codd.Representations schemaDifferences, ) import Codd.Types (PgMajorVersion) -import Control.Concurrent (forkIO, threadDelay) -import Control.Monad (forM_) import qualified Data.Map as Map import Data.UUID (UUID) import qualified Data.UUID as UUID import qualified Data.UUID.V4 as UUIDv4 import DbUtils (getEmptyTempDir) -import GHC.IO.Encoding import System.FilePath ( (), ) @@ -31,8 +28,6 @@ spec = do it "persistRepsToDisk is inverse of readRepsFromDisk" $ do property $ \(DbRepsGen dbHashes pgVersion) -> do baseFolder <- getEmptyTempDir - enc <- getFileSystemEncoding - print enc writeSchemaAndReadSchemaRoundtrip pgVersion dbHashes (baseFolder "inverse-test-sql-folder") modifyMaxSuccess (const 1) $ it From ab476002f16cd036d87275f6ad883a0d95030a9b Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sat, 30 Aug 2025 12:13:40 -0300 Subject: [PATCH 36/37] Simplify write-schema code significantly --- src/Codd/Representations/Disk.hs | 157 ++++++++----------------------- 1 file changed, 38 insertions(+), 119 deletions(-) diff --git a/src/Codd/Representations/Disk.hs b/src/Codd/Representations/Disk.hs index 487bb951..0d8e8330 100644 --- a/src/Codd/Representations/Disk.hs +++ b/src/Codd/Representations/Disk.hs @@ -9,12 +9,10 @@ import Codd.Representations.Types import Codd.Types (PgMajorVersion (..)) import Control.DeepSeq (force) import Control.Monad - ( forM, - forM_, + ( forM_, join, when, ) -import Control.Monad.Identity (runIdentity) import Data.Aeson ( Value, decode, @@ -57,125 +55,46 @@ import Prelude hiding writeFile, ) -{- -This module contains functions and data models to write and read schema representations -from disk. It has been rewritten a few times in attempt to find a model -that is sufficiently clean and robust. - -The unsolved challenge at this point is to unify both reading and writing -under a unique model that represents how files and directories are laid out -on disk, but it doesn't seem worth the trouble, honestly. --} - --- | This class is equivalent to some form of monadic unfolding, mixed --- with a mockable "writeFile" function. It allows us to derive both --- `toFiles` and `persistRepsToDisk`, for example, but isn't a very elegant --- model otherwise. -class DbDiskObj a where - appr :: - (Monad m) => - a -> - -- | When recursing into a new sub-structure, this function - -- will be called with a relative folder where that substructure's - -- root belongs to. - (forall b. (DbDiskObj b) => FilePath -> b -> m d) -> - -- | This function will be called when writing to a file. A relative path - -- to this structure's root will be passed. - (FilePath -> Value -> m d) -> - m [d] - simpleDbDiskObj :: - (Functor m) => ObjName -> Value -> (FilePath -> Value -> m d) -> m [d] -simpleDbDiskObj oname orep ffile = (: []) <$> ffile (mkPathFrag oname) orep - -instance DbDiskObj DbRep where - appr (DbRep dbSettingsRep schemas roles) frec ffile = do - x <- ffile "db-settings" dbSettingsRep - rs <- forM (Map.elems roles) $ frec "roles" - ss <- forM (Map.toList schemas) $ \(schemaName, schema) -> - frec ("schemas" mkPathFrag schemaName) schema - pure $ x : rs ++ ss - -instance DbDiskObj RoleRep where - appr (RoleRep roleName roleRep) _ = simpleDbDiskObj roleName roleRep - -instance DbDiskObj SchemaRep where - appr (SchemaRep _schemaName namespaceRep tables views routines seqs colls types) frec ffile = - do - x <- ffile "objrep" namespaceRep - tbls <- forM (Map.toList tables) $ \(tableName, table) -> - frec ("tables" mkPathFrag tableName) table - vs <- forM (Map.elems views) $ frec "views" - rs <- forM (Map.elems routines) $ frec "routines" - ss <- forM (Map.elems seqs) $ frec "sequences" - cs <- forM (Map.elems colls) $ frec "collations" - ts <- forM (Map.elems types) $ frec "types" - pure $ x : tbls ++ vs ++ rs ++ ss ++ cs ++ ts - -instance DbDiskObj TableRep where - appr (TableRep _tblName tblRep columns constraints triggers policies indexes statistics) frec ffile = - do - let mkpath p = p - x <- ffile (mkpath "objrep") tblRep - cols <- forM (Map.elems columns) $ frec (mkpath "cols") - constrs <- - forM (Map.elems constraints) $ - frec (mkpath "constraints") - trgrs <- forM (Map.elems triggers) $ frec (mkpath "triggers") - pols <- forM (Map.elems policies) $ frec (mkpath "policies") - idxs <- forM (Map.elems indexes) $ frec (mkpath "indexes") - stats <- forM (Map.elems statistics) $ frec (mkpath "statistics") - pure $ x : cols ++ constrs ++ trgrs ++ pols ++ idxs ++ stats - -instance DbDiskObj TableColumnRep where - appr (TableColumnRep colName colRep) _ = simpleDbDiskObj colName colRep - -instance DbDiskObj TableConstraintRep where - appr (TableConstraintRep constrName constrRep) _ = - simpleDbDiskObj constrName constrRep - -instance DbDiskObj TableTriggerRep where - appr (TableTriggerRep triggerName triggerRep) _ = - simpleDbDiskObj triggerName triggerRep - -instance DbDiskObj TablePolicyRep where - appr (TablePolicyRep polName polRep) _ = simpleDbDiskObj polName polRep - -instance DbDiskObj TableStatisticsRep where - appr (TableStatisticsRep stxName stxRep) _ = simpleDbDiskObj stxName stxRep - -instance DbDiskObj TableIndexRep where - appr (TableIndexRep idxName indexRep) _ = simpleDbDiskObj idxName indexRep - -instance DbDiskObj ViewRep where - appr (ViewRep viewName viewRep) _ = simpleDbDiskObj viewName viewRep - -instance DbDiskObj RoutineRep where - appr (RoutineRep routineName routineRep) _ = - simpleDbDiskObj routineName routineRep - -instance DbDiskObj SequenceRep where - appr (SequenceRep seqName seqRep) _ = simpleDbDiskObj seqName seqRep - -instance DbDiskObj CollationRep where - appr (CollationRep collName collationRep) _ = - simpleDbDiskObj collName collationRep - -instance DbDiskObj TypeRep where - appr (TypeRep typeName typeRep) _ = simpleDbDiskObj typeName typeRep + ObjName -> Value -> (FilePath, Value) +simpleDbDiskObj oname orep = (mkPathFrag oname, orep) + +apprDbRep :: DbRep -> [(FilePath, Value)] +apprDbRep (DbRep dbSettingsRep schemas roles) = + let x = ("db-settings", dbSettingsRep) + rs = prependDir "roles" $ flip map (Map.elems roles) $ \(RoleRep n v) -> simpleDbDiskObj n v + ss = prependDir "schemas" $ mconcat $ flip map (Map.toList schemas) $ \(schemaName, schema) -> + prependDir (mkPathFrag schemaName) (apprSchemaRep schema) + in x : rs ++ ss + +apprSchemaRep :: SchemaRep -> [(FilePath, Value)] +apprSchemaRep (SchemaRep _schemaName namespaceRep tables views routines seqs colls types) = + let x = ("objrep", namespaceRep) + tbls = prependDir "tables" $ mconcat $ flip map (Map.toList tables) $ \(tableName, table) -> + prependDir (mkPathFrag tableName) (apprTableRep table) + vs = prependDir "views" $ map (\(ViewRep n v) -> simpleDbDiskObj n v) $ Map.elems views + rs = prependDir "routines" $ map (\(RoutineRep n v) -> simpleDbDiskObj n v) (Map.elems routines) + ss = prependDir "sequences" $ map (\(SequenceRep n v) -> simpleDbDiskObj n v) (Map.elems seqs) + cs = prependDir "collations" $ map (\(CollationRep n v) -> simpleDbDiskObj n v) (Map.elems colls) + ts = prependDir "types" $ map (\(TypeRep n v) -> simpleDbDiskObj n v) (Map.elems types) + in x : tbls ++ vs ++ rs ++ ss ++ cs ++ ts + +apprTableRep :: TableRep -> [(FilePath, Value)] +apprTableRep (TableRep _tblName tblRep columns constraints triggers policies indexes statistics) = + let x = ("objrep", tblRep) + cols = prependDir "cols" $ flip map (Map.elems columns) $ \(TableColumnRep colName colRep) -> simpleDbDiskObj colName colRep + constrs = prependDir "constraints" $ flip map (Map.elems constraints) $ \(TableConstraintRep n v) -> simpleDbDiskObj n v + trgrs = prependDir "triggers" $ flip map (Map.elems triggers) $ \(TableTriggerRep n v) -> simpleDbDiskObj n v + pols = prependDir "policies" $ flip map (Map.elems policies) $ \(TablePolicyRep n v) -> simpleDbDiskObj n v + idxs = prependDir "indexes" $ flip map (Map.elems indexes) $ \(TableIndexRep n v) -> simpleDbDiskObj n v + stats = prependDir "statistics" $ flip map (Map.elems statistics) $ \(TableStatisticsRep n v) -> simpleDbDiskObj n v + in x : cols ++ constrs ++ trgrs ++ pols ++ idxs ++ stats + +prependDir :: FilePath -> [(FilePath, Value)] -> [(FilePath, Value)] +prependDir dir = map (first (dir )) toFiles :: DbRep -> [(FilePath, Value)] -toFiles = sortOn fst . frec - where - frec :: (DbDiskObj a) => a -> [(FilePath, Value)] - frec sobj = - concat $ - runIdentity $ - appr - sobj - (\parentDir obj -> pure $ prependDir parentDir $ frec obj) - (\fn h -> pure [(fn, h)]) - prependDir dir = map (first (dir )) +toFiles = sortOn fst . apprDbRep -- | Wipes out completely the supplied folder and writes the representations of the Database's structures to it again. persistRepsToDisk :: forall m. (HasCallStack, MonadUnliftIO m) => PgMajorVersion -> DbRep -> FilePath -> m () From 03794aac130652430b0bfb96319dd4f75131ffac Mon Sep 17 00:00:00 2001 From: Marcelo Zabani Date: Sun, 31 Aug 2025 12:17:25 -0300 Subject: [PATCH 37/37] Undo CI changes --- .github/workflows/main.yml | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 102b9142..eed91de9 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -156,45 +156,10 @@ jobs: - name: Build codd's tests run: nix-build --no-out-link -A coddtests - - name: Test filesystem capabilities - run: | - echo "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" > testfile - echo other > "abcdefghijklmnopqrstuvxwyzçáéíóúñ_" - FILE=$(cat testfile) - cat "$FILE" - - name: Run tests run: | echo "Running tests that don't depend on a database" scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 1' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 2' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 3' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 4' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 5' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 6' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 7' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 8' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 9' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 10' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 11' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 12' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 13' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 14' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 15' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 16' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 17' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 18' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 19' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 20' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 21' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 22' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 23' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 24' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 25' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 26' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 27' - scripts/ci/run-tests-and-annotate-with-error.sh local/tests-no-db.txt nix-build --no-out-link -A testsNoDb --argstr hspecArgs '--match Writing --seed 28' # Postgres-version dependent tests for each possible version next # We test the last version with the vanilla nixpkgs-built derivation,