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

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

Types

Access mode for opening tables.

pub type AccessMode {
  ReadWrite
  ReadOnly
}

Constructors

  • ReadWrite

    Read and write access (default)

  • ReadOnly

    Read-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

  • NotFound

    No 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

  • TableDoesNotExist

    The table does not exist (not open)

  • FileSizeLimitExceeded(FileErrorContext)

    File exceeds the 2 GB DETS limit

  • KeyAlreadyPresent

    Key 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)

  • TableNamePoolExhausted

    All 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 NoRepair was 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

Auto-repair policy for improperly closed tables.

pub type RepairPolicy {
  AutoRepair
  ForceRepair
  NoRepair
}

Constructors

  • AutoRepair

    Attempt repair if needed (default)

  • ForceRepair

    Repair even if the file was closed properly

  • NoRepair

    Return 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")
✨ Search Document