slate
Type-safe Gleam wrapper for Erlang DETS (Disk Erlang Term Storage).
DETS provides persistent key-value storage backed by files on disk. Tables survive process crashes and node restarts. DETS is built into OTP — no external database or dependency is needed.
Table Types
slate/set— Unique keys, one value per keyslate/bag— Multiple distinct values per keyslate/duplicate_bag— Multiple values per key (duplicates allowed)
Quick Start
import gleam/dynamic/decode
import slate/set
let assert Ok(table) = set.open("data/cache.dets",
key_decoder: decode.string, value_decoder: decode.string)
let assert Ok(Nil) = set.insert(table, "key", "value")
let assert Ok(value) = set.lookup(table, key: "key")
let assert Ok(Nil) = set.close(table)
Limitations
- 2 GB maximum file size
- No
ordered_settable type (unlike ETS) - Disk I/O on every operation (use ETS for high-frequency reads)
- Tables must be closed properly or data may be lost
- Bounded table name pool: slate uses a bounded set of internal DETS table names to avoid unbounded atom growth. Opening too many distinct tables at once may fail; close unused tables promptly
Types
Access mode for opening tables.
pub type AccessMode {
ReadWrite
ReadOnly
}
Constructors
-
ReadWriteRead and write access (default)
-
ReadOnlyRead-only access — writes will return
AccessDenied
Errors that can occur during DETS operations.
Match on the explicit variants for expected cases such as NotFound,
AccessDenied(_), or TypeMismatch(_).
Treat UnexpectedError(detail) as diagnostic output for logs and debugging
only. Its string detail is not part of slate’s stable API contract. Use
error_code or error_message when you want a stable classifier or a
user-facing message.
pub type DetsError {
NotFound
FileNotFound(FileErrorContext)
AlreadyOpen(FileErrorContext)
TableDoesNotExist
FileSizeLimitExceeded(FileErrorContext)
KeyAlreadyPresent
AccessDenied(FileErrorContext)
TypeMismatch(FileErrorContext)
TableNamePoolExhausted
NotADetsFile(FileErrorContext)
NeedsRepair(FileErrorContext)
DecodeErrors(List(decode.DecodeError))
UnexpectedError(String)
}
Constructors
-
NotFoundNo value found for the given key
-
FileNotFound(FileErrorContext)Table file or a parent directory does not exist
-
AlreadyOpen(FileErrorContext)Table is already open with a different configuration
-
TableDoesNotExistThe table does not exist (not open)
-
FileSizeLimitExceeded(FileErrorContext)File exceeds the 2 GB DETS limit
-
KeyAlreadyPresentKey already exists (for insert_new)
-
AccessDenied(FileErrorContext)File access denied, or write operation attempted on a read-only table
-
TypeMismatch(FileErrorContext)Table type or key position mismatch (e.g., opening a set file as a bag)
-
TableNamePoolExhaustedAll internal table name slots are in use; close unused tables to free slots
-
NotADetsFile(FileErrorContext)File exists but is not a valid DETS file
-
NeedsRepair(FileErrorContext)File was not closed cleanly and
NoRepairwas requested -
DecodeErrors(List(decode.DecodeError))Data read from disk did not match the expected Gleam types
-
UnexpectedError(String)Unexpected OTP or Erlang-level error for logging and diagnostics only.
Diagnostic context for an expected file error.
path is the filename reported by OTP, not a table name. For a pathless
AlreadyOpen error, open operations supply the path they passed to OTP.
Opens normally report an absolute path; is_dets_file can report a relative path.
None means OTP supplied no single filename (for example, a pathless error
or a rename failure with two filenames). No table lookup is needed, so
context remains usable after the table closes.
reason is the lower-level Erlang reason formatted for diagnostics, such as
"enoent", "{error,eacces}", "access_mode", or "keypos_mismatch".
Rename failures retain both filenames in this diagnostic string.
Do not parse it as a stable classifier. Both fields can contain sensitive
details: use error_code and error_message for safe external output.
pub type FileErrorContext {
FileErrorContext(path: option.Option(String), reason: String)
}
Constructors
-
FileErrorContext(path: option.Option(String), reason: String)
Auto-repair policy for improperly closed tables.
pub type RepairPolicy {
AutoRepair
ForceRepair
NoRepair
}
Constructors
-
AutoRepairRepair automatically if needed (default)
-
ForceRepairForce repair even if file appears clean
-
NoRepairDon’t repair, return error instead
Information about an open DETS table.
file_path is the absolute path used by open, with . and ..
segments normalized. Symlinks are not resolved.
pub type TableInfo {
TableInfo(file_size: Int, object_count: Int, file_path: String)
}
Constructors
-
TableInfo(file_size: Int, object_count: Int, file_path: String)
Values
pub fn error_code(of error: DetsError) -> String
Return a stable machine-readable code for a DetsError.
This is useful when you want to log, branch on, or serialize error
categories without relying on the detail string in UnexpectedError(_).
pub fn error_message(of error: DetsError) -> String
Return a concise user-facing description for a DetsError.
File context and UnexpectedError(_) details are intentionally omitted so
callers can safely surface messages without leaking paths or OTP details.
pub fn is_dets_file(path: String) -> Result(Bool, DetsError)
Check whether the given file is a valid DETS file.
Returns Ok(True) if the file is a valid DETS file, Ok(False) if
it exists but is not a DETS file, or an error if the file cannot be read.
let assert Ok(True) = slate.is_dets_file("data/cache.dets")
let assert Ok(False) = slate.is_dets_file("README.md")