slate
Type-safe Gleam wrapper for Erlang DETS (Disk Erlang Term Storage).
DETS provides persistent key-value storage backed by files on disk. Stored data persists across node restarts. An abnormal node shutdown can lose pending writes and leave the file needing repair. DETS is built into OTP. No separate database service is required.
Table types
slate/set: One value per keyslate/bag: Multiple distinct values per keyslate/duplicate_bag: Multiple values per key, including duplicates
An entry is one key-value pair. The table modules share a core API.
Set and bag tables also provide insert_new, with different duplicate
rules. Only set tables provide update_counter.
Quick start
import gleam/dynamic/decode
import slate/set
let assert Ok(table) = set.open("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-backed access is slower than ETS for frequent 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 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.
Open operations 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 to identify the error category.
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
-
AutoRepairAttempt repair if needed (default)
-
ForceRepairRepair even if the file was closed properly
-
NoRepairReturn an error if the file needs repair
Information about an open DETS table.
file_size is the size of the table file in bytes.
object_count is the number of stored entries, not the number of distinct
keys. In a duplicate bag, each copy of a key-value pair counts as an entry.
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.
This identifies the file type. It does not check every entry or guarantee that opening or decoding will succeed. Apply your own path and access rules before using a path from an untrusted source.
let assert Ok(True) = slate.is_dets_file("data/cache.dets")
let assert Ok(False) = slate.is_dets_file("README.md")