lsm-tree-1.0.0.0: src-core/Database/LSMTree/Internal/Paths.hs
{-# LANGUAGE PatternSynonyms #-}
{-# OPTIONS_HADDOCK not-home #-}
module Database.LSMTree.Internal.Paths (
SessionRoot (..)
, lockFile
, lockFileName
, metadataFile
, ActiveDir (..)
, activeDir
, runPath
, SnapshotName
, toSnapshotName
, isValidSnapshotName
, InvalidSnapshotNameError (..)
, snapshotsDir
, NamedSnapshotDir (..)
, namedSnapshotDir
, SnapshotMetaDataFile (..)
, snapshotMetaDataFile
, SnapshotMetaDataChecksumFile (..)
, snapshotMetaDataChecksumFile
-- * Table paths
, tableBlobPath
-- * Run paths
, RunFsPaths (..)
, pathsForRunFiles
, runKOpsPath
, runBlobPath
, runFilterPath
, runIndexPath
, runChecksumsPath
-- * Checksums for Run files
, checksumFileNamesForRunFiles
, toChecksumsFile
, fromChecksumsFile
-- * Checksums for WriteBuffer files
, toChecksumsFileForWriteBufferFiles
, fromChecksumsFileForWriteBufferFiles
-- * ForRunFiles abstraction
, ForKOps (..)
, ForBlob (..)
, ForFilter (..)
, ForIndex (..)
, ForRunFiles (..)
, forRunKOpsRaw
, forRunBlobRaw
, forRunFilterRaw
, forRunIndexRaw
-- * WriteBuffer paths
, WriteBufferFsPaths (WrapRunFsPaths, WriteBufferFsPaths, writeBufferDir, writeBufferNumber)
, writeBufferKOpsPath
, writeBufferBlobPath
, writeBufferChecksumsPath
, writeBufferFilePathWithExt
) where
import Control.Applicative (Applicative (..))
import Control.DeepSeq (NFData (..))
import Control.Exception.Base (throw)
import Control.Monad.Class.MonadThrow (Exception)
import qualified Data.ByteString.Char8 as BS
import Data.Foldable (toList)
import qualified Data.Map as Map
import Data.String (IsString (..))
import Data.Traversable (for)
import qualified Database.LSMTree.Internal.CRC32C as CRC
import Database.LSMTree.Internal.RunNumber
import Database.LSMTree.Internal.UniqCounter
import Prelude hiding (Applicative (..))
import qualified System.FilePath.Posix
import qualified System.FilePath.Windows
import System.FS.API
newtype SessionRoot = SessionRoot { getSessionRoot :: FsPath }
deriving stock Eq
lockFile :: SessionRoot -> FsPath
lockFile (SessionRoot dir) = dir </> mkFsPath [lockFileName]
lockFileName :: String
lockFileName = "lock"
metadataFile :: SessionRoot -> FsPath
metadataFile (SessionRoot dir) = dir </> mkFsPath ["metadata"]
newtype ActiveDir = ActiveDir { getActiveDir :: FsPath }
activeDir :: SessionRoot -> ActiveDir
activeDir (SessionRoot dir) = ActiveDir (dir </> mkFsPath ["active"])
runPath :: ActiveDir -> RunNumber -> RunFsPaths
runPath (ActiveDir dir) = RunFsPaths dir
{-------------------------------------------------------------------------------
Snapshot name
-------------------------------------------------------------------------------}
newtype SnapshotName = SnapshotName FilePath
deriving stock (Eq, Ord)
instance Show SnapshotName where
showsPrec d (SnapshotName p) = showsPrec d p
-- | The given string must satisfy 'isValidSnapshotName'.
-- Otherwise, 'fromString' throws an 'InvalidSnapshotNameError'.
instance IsString SnapshotName where
fromString :: String -> SnapshotName
fromString = toSnapshotName
data InvalidSnapshotNameError
= ErrInvalidSnapshotName !String
deriving stock (Show, Eq)
deriving anyclass (Exception)
-- | Check if a 'String' would be a valid snapshot name.
--
-- Snapshot names consist of lowercase characters, digits, dashes @-@,
-- and underscores @_@, and must be between 1 and 64 characters long.
-- >>> isValidSnapshotName "main"
-- True
--
-- >>> isValidSnapshotName "temporary-123-test_"
-- True
--
-- >>> isValidSnapshotName "UPPER"
-- False
-- >>> isValidSnapshotName "dir/dot.exe"
-- False
-- >>> isValidSnapshotName ".."
-- False
-- >>> isValidSnapshotName "\\"
-- False
-- >>> isValidSnapshotName ""
-- False
-- >>> isValidSnapshotName (replicate 100 'a')
-- False
--
-- Snapshot names must be valid directory on both POSIX and Windows.
-- This rules out the following reserved file and directory names on Windows:
--
-- >>> isValidSnapshotName "con"
-- False
-- >>> isValidSnapshotName "prn"
-- False
-- >>> isValidSnapshotName "aux"
-- False
-- >>> isValidSnapshotName "nul"
-- False
-- >>> isValidSnapshotName "com1" -- "com2", "com3", etc.
-- False
-- >>> isValidSnapshotName "lpt1" -- "lpt2", "lpt3", etc.
-- False
--
-- See, e.g., [the VBA docs for the "Bad file name or number" error](https://learn.microsoft.com/en-us/office/vba/language/reference/user-interface-help/bad-file-name-or-number-error-52).
isValidSnapshotName :: String -> Bool
isValidSnapshotName str =
and [ all isValidChar str
, strLength >= 1
, strLength <= 64
, System.FilePath.Posix.isValid str
, System.FilePath.Windows.isValid str
]
where
strLength :: Int
strLength = length str
isValidChar :: Char -> Bool
isValidChar c = ('a' <= c && c <= 'z') || ('0' <= c && c <= '9' ) || c `elem` "-_"
-- | Create snapshot name.
--
-- The given string must satisfy 'isValidSnapshotName'.
--
-- Throws the following exceptions:
--
-- ['InvalidSnapshotNameError']:
-- If the given string is not a valid snapshot name.
--
toSnapshotName :: String -> SnapshotName
toSnapshotName str
| isValidSnapshotName str = SnapshotName str
| otherwise = throw (ErrInvalidSnapshotName str)
snapshotsDir :: SessionRoot -> FsPath
snapshotsDir (SessionRoot dir) = dir </> mkFsPath ["snapshots"]
-- | The directory for a specific, /named/ snapshot.
--
-- Not to be confused with the snapshot/s/ directory, which holds all named
-- snapshot directories.
newtype NamedSnapshotDir = NamedSnapshotDir { getNamedSnapshotDir :: FsPath }
namedSnapshotDir :: SessionRoot -> SnapshotName -> NamedSnapshotDir
namedSnapshotDir root (SnapshotName name) =
NamedSnapshotDir (snapshotsDir root </> mkFsPath [name])
newtype SnapshotMetaDataFile = SnapshotMetaDataFile FsPath
snapshotMetaDataFile :: NamedSnapshotDir -> SnapshotMetaDataFile
snapshotMetaDataFile (NamedSnapshotDir dir) =
SnapshotMetaDataFile (dir </> mkFsPath ["metadata"])
newtype SnapshotMetaDataChecksumFile = SnapshotMetaDataChecksumFile FsPath
snapshotMetaDataChecksumFile :: NamedSnapshotDir -> SnapshotMetaDataChecksumFile
snapshotMetaDataChecksumFile (NamedSnapshotDir dir) =
SnapshotMetaDataChecksumFile (dir </> mkFsPath ["metadata.checksum"])
{-------------------------------------------------------------------------------
Table paths
-------------------------------------------------------------------------------}
-- | The file name for a table's write buffer blob file
tableBlobPath :: SessionRoot -> Unique -> FsPath
tableBlobPath session n =
getActiveDir (activeDir session) </> mkFsPath [show (uniqueToInt n)] <.> "wbblobs"
{-------------------------------------------------------------------------------
Run paths
-------------------------------------------------------------------------------}
-- | The (relative) file path locations of all the files used by the run:
--
-- The following files exist for a run:
--
-- 1. @${n}.keyops@: the sorted run of key\/operation pairs
-- 2. @${n}.blobs@: the blob values associated with the key\/operations
-- 3. @${n}.filter@: a Bloom filter of all the keys in the run
-- 4. @${n}.index@: an index from keys to disk page numbers
-- 5. @${n}.checksums@: a file listing the crc32c checksums of the other
-- files
--
-- The representation doesn't store the full, name, just the number @n@. Use
-- the accessor functions to get the actual names.
--
data RunFsPaths = RunFsPaths {
-- | The directory that run files live in.
runDir :: !FsPath
, runNumber :: !RunNumber }
deriving stock Show
instance NFData RunFsPaths where
rnf (RunFsPaths x y) = rnf x `seq` rnf y
-- | Paths to all files associated with this run, except 'runChecksumsPath'.
pathsForRunFiles :: RunFsPaths -> ForRunFiles FsPath
pathsForRunFiles fsPaths = fmap (runFilePathWithExt fsPaths) runFileExts
runKOpsPath :: RunFsPaths -> FsPath
runKOpsPath = unForKOps . forRunKOps . pathsForRunFiles
runBlobPath :: RunFsPaths -> FsPath
runBlobPath = unForBlob . forRunBlob . pathsForRunFiles
runFilterPath :: RunFsPaths -> FsPath
runFilterPath = unForFilter . forRunFilter . pathsForRunFiles
runIndexPath :: RunFsPaths -> FsPath
runIndexPath = unForIndex . forRunIndex . pathsForRunFiles
runChecksumsPath :: RunFsPaths -> FsPath
runChecksumsPath = flip runFilePathWithExt "checksums"
runFilePathWithExt :: RunFsPaths -> String -> FsPath
runFilePathWithExt (RunFsPaths dir (RunNumber n)) ext =
dir </> mkFsPath [show n] <.> ext
runFileExts :: ForRunFiles String
runFileExts = ForRunFiles {
forRunKOps = ForKOps "keyops"
, forRunBlob = ForBlob "blobs"
, forRunFilter = ForFilter "filter"
, forRunIndex = ForIndex "index"
}
{-------------------------------------------------------------------------------
Checksums For Run Files
-------------------------------------------------------------------------------}
checksumFileNamesForRunFiles :: ForRunFiles CRC.ChecksumsFileName
checksumFileNamesForRunFiles = fmap (CRC.ChecksumsFileName . BS.pack) runFileExts
toChecksumsFile :: ForRunFiles CRC.CRC32C -> CRC.ChecksumsFile
toChecksumsFile = Map.fromList . toList . liftA2 (,) checksumFileNamesForRunFiles
fromChecksumsFile :: CRC.ChecksumsFile -> Either String (ForRunFiles CRC.CRC32C)
fromChecksumsFile file = for checksumFileNamesForRunFiles $ \name ->
case Map.lookup name file of
Just crc -> Right crc
Nothing -> Left ("key not found: " <> show name)
{-------------------------------------------------------------------------------
Marker newtypes for individual elements of the ForRunFiles and the
ForWriteBufferFiles abstractions
-------------------------------------------------------------------------------}
newtype ForKOps a = ForKOps {unForKOps :: a}
deriving stock (Show, Foldable, Functor, Traversable)
newtype ForBlob a = ForBlob {unForBlob :: a}
deriving stock (Show, Foldable, Functor, Traversable)
newtype ForFilter a = ForFilter {unForFilter :: a}
deriving stock (Show, Foldable, Functor, Traversable)
newtype ForIndex a = ForIndex {unForIndex :: a}
deriving stock (Show, Foldable, Functor, Traversable)
{-------------------------------------------------------------------------------
ForRunFiles abstraction
-------------------------------------------------------------------------------}
-- | Stores something for each run file (except the checksums file), allowing to
-- easily do something for all of them without mixing them up.
data ForRunFiles a = ForRunFiles {
forRunKOps :: !(ForKOps a)
, forRunBlob :: !(ForBlob a)
, forRunFilter :: !(ForFilter a)
, forRunIndex :: !(ForIndex a)
}
deriving stock (Show, Foldable, Functor, Traversable)
forRunKOpsRaw :: ForRunFiles a -> a
forRunKOpsRaw = unForKOps . forRunKOps
forRunBlobRaw :: ForRunFiles a -> a
forRunBlobRaw = unForBlob . forRunBlob
forRunFilterRaw :: ForRunFiles a -> a
forRunFilterRaw = unForFilter . forRunFilter
forRunIndexRaw :: ForRunFiles a -> a
forRunIndexRaw = unForIndex . forRunIndex
instance Applicative ForRunFiles where
pure x = ForRunFiles (ForKOps x) (ForBlob x) (ForFilter x) (ForIndex x)
ForRunFiles (ForKOps f1) (ForBlob f2) (ForFilter f3) (ForIndex f4) <*> ForRunFiles (ForKOps x1) (ForBlob x2) (ForFilter x3) (ForIndex x4) =
ForRunFiles (ForKOps $ f1 x1) (ForBlob $ f2 x2) (ForFilter $ f3 x3) (ForIndex $ f4 x4)
{-------------------------------------------------------------------------------
WriteBuffer paths
-------------------------------------------------------------------------------}
newtype WriteBufferFsPaths = WrapRunFsPaths RunFsPaths
pattern WriteBufferFsPaths :: FsPath -> RunNumber -> WriteBufferFsPaths
pattern WriteBufferFsPaths {writeBufferDir, writeBufferNumber} = WrapRunFsPaths (RunFsPaths writeBufferDir writeBufferNumber)
{-# COMPLETE WriteBufferFsPaths #-}
writeBufferKOpsExt :: String
writeBufferKOpsExt = unForKOps . forRunKOps $ runFileExts
writeBufferBlobExt :: String
writeBufferBlobExt = unForBlob . forRunBlob $ runFileExts
writeBufferKOpsPath :: WriteBufferFsPaths -> FsPath
writeBufferKOpsPath = flip writeBufferFilePathWithExt writeBufferKOpsExt
writeBufferBlobPath :: WriteBufferFsPaths -> FsPath
writeBufferBlobPath = flip writeBufferFilePathWithExt writeBufferBlobExt
writeBufferChecksumsPath :: WriteBufferFsPaths -> FsPath
writeBufferChecksumsPath = flip writeBufferFilePathWithExt "checksums"
writeBufferFilePathWithExt :: WriteBufferFsPaths -> String -> FsPath
writeBufferFilePathWithExt (WriteBufferFsPaths dir (RunNumber n)) ext =
dir </> mkFsPath [show n] <.> ext
{-------------------------------------------------------------------------------
Checksums For Run Files
-------------------------------------------------------------------------------}
toChecksumsFileForWriteBufferFiles :: (ForKOps CRC.CRC32C, ForBlob CRC.CRC32C) -> CRC.ChecksumsFile
toChecksumsFileForWriteBufferFiles (ForKOps kOpsChecksum, ForBlob blobChecksum) =
Map.fromList
[ (toChecksumsFileName writeBufferKOpsExt, kOpsChecksum)
, (toChecksumsFileName writeBufferBlobExt, blobChecksum)
]
where
toChecksumsFileName = CRC.ChecksumsFileName . BS.pack
fromChecksumsFileForWriteBufferFiles :: CRC.ChecksumsFile -> Either String (ForKOps CRC.CRC32C, ForBlob CRC.CRC32C)
fromChecksumsFileForWriteBufferFiles file = do
(,) <$> (ForKOps <$> fromChecksumFile writeBufferKOpsExt) <*> (ForBlob <$> fromChecksumFile writeBufferBlobExt)
where
fromChecksumFile key =
maybe (Left $ "key not found: " <> key) Right $
Map.lookup (CRC.ChecksumsFileName . fromString $ key) file