ExSQL.Table (exsql v0.2.1)

Copy Markdown

In-memory table storage.

Rows live in a map keyed by rowid, mirroring SQLite's model where every table is a B-tree keyed by a 64-bit rowid. Each row is stored as a positional tuple in columns order (~63% smaller than a per-row key-bearing map); the executor's hot single-table scan reads columns by position via the frame's column_index, while other call sites widen a row back to a key => value map at the read boundary (scan/1, fetch_row/2). A single INTEGER PRIMARY KEY column is detected as the rowid alias, with SQLite's semantics: inserting NULL into it auto-assigns the next rowid, and the column's value is the row's key.

All functions are pure — they return an updated table or an error tuple. Constraint enforcement (NOT NULL, UNIQUE, PRIMARY KEY) happens here, by scanning; real indexes can replace the scans later without changing the interface.

Summary

Types

A CHECK constraint: {constraint_name | nil, expr}.

A composite PK or UNIQUE constraint: {constraint_name | nil, [column_key]}.

A table-level FK: {child_keys, parent_table, parent_keys, actions}, where actions is %{on_delete: action, on_update: action, deferred: boolean}.

t()

Functions

Reads a positional row tuple at index. An out-of-range index reads as nil, matching the old sparse-map behavior for columns a given row never stored.

Reads a column from a positional row by key (resolves the index via the table).

Returns the ColumnDef for name, or nil.

Column key → positional index, in columns order (matches a positional row tuple). Cached.

Deletes the rows with the given rowids.

Fetches one stored row as a key => value map, or :error.

Like fetch_row/2 but raises if the rowid is absent.

The {key, name, affinity, collate} tuples used to build a query frame. Returns the cached value, recomputing if the cache was invalidated by a column mutation.

Reads one stored row as a map, or nil if absent.

Inserts a row given as a map of column key => value (already evaluated and affinity-coerced by the executor). Missing columns get their DEFAULT or NULL.

Case-insensitive column lookup key, since SQL identifiers fold case.

Re-narrows all stored rows — currently held as key => value maps — to positional tuples against the table's current columns. Used after an ALTER TABLE rebuilds rows as maps under a changed column layout.

Creates a table from a name, parsed column definitions, and optional table-level constraints.

Builds a positional row tuple from a key => value map (missing keys → nil).

Rebuilds a key => value map from a positional row tuple (compat for un-migrated callers).

All rows in rowid order, as {rowid, row} pairs.

Like scan/1 but yields rows as raw positional tuples (no widening to maps). The hot single-table scan reads columns positionally via the frame's column_index, skipping the per-row map build that scan/1 does.

Replaces the row at rowid with row (a full row map), re-checking constraints. If the rowid-alias column changed, the row is re-keyed. Takes the same :on_conflict option as insert/3.

Types

check_constraint()

@type check_constraint() :: {String.t() | nil, term()}

A CHECK constraint: {constraint_name | nil, expr}.

composite_constraint()

@type composite_constraint() :: {String.t() | nil, [String.t()]}

A composite PK or UNIQUE constraint: {constraint_name | nil, [column_key]}.

foreign_key()

@type foreign_key() ::
  {[String.t()], String.t(), [String.t()], ExSQL.AST.CreateTable.fk_actions()}

A table-level FK: {child_keys, parent_table, parent_keys, actions}, where actions is %{on_delete: action, on_update: action, deferred: boolean}.

index()

@type index() :: %{name: String.t(), columns: [String.t()], unique: boolean()}

row()

@type row() :: %{required(String.t()) => ExSQL.Value.t()}

t()

@type t() :: %ExSQL.Table{
  autoincrement: boolean(),
  autoindexes: [index()],
  checks: [check_constraint()],
  column_index: term(),
  columns: [ExSQL.AST.ColumnDef.t()],
  composite_keys: [composite_constraint()],
  composite_uniques: [composite_constraint()],
  foreign_keys: [foreign_key()],
  frame_columns: term(),
  indexes: [index()],
  name: String.t(),
  next_rowid: pos_integer(),
  rowid_alias: String.t() | nil,
  rows: %{required(integer()) => row()},
  schema: String.t() | nil,
  sequence: integer(),
  sequence_row: boolean(),
  strict: boolean(),
  without_rowid: boolean()
}

Functions

cell(row, index)

@spec cell(tuple(), non_neg_integer()) :: ExSQL.Value.t()

Reads a positional row tuple at index. An out-of-range index reads as nil, matching the old sparse-map behavior for columns a given row never stored.

cell(table, row, key)

@spec cell(t(), tuple(), String.t()) :: ExSQL.Value.t()

Reads a column from a positional row by key (resolves the index via the table).

column(table, name)

@spec column(t(), String.t()) :: ExSQL.AST.ColumnDef.t() | nil

Returns the ColumnDef for name, or nil.

column_index(table)

@spec column_index(t()) :: %{required(String.t()) => non_neg_integer()}

Column key → positional index, in columns order (matches a positional row tuple). Cached.

delete_rows(table, rowids)

@spec delete_rows(t(), [integer()]) :: t()

Deletes the rows with the given rowids.

fetch_row(table, rowid)

@spec fetch_row(t(), integer()) :: {:ok, row()} | :error

Fetches one stored row as a key => value map, or :error.

fetch_row!(table, rowid)

@spec fetch_row!(t(), integer()) :: row()

Like fetch_row/2 but raises if the rowid is absent.

frame_columns(table)

@spec frame_columns(t()) :: [{String.t(), String.t(), atom(), String.t() | nil}]

The {key, name, affinity, collate} tuples used to build a query frame. Returns the cached value, recomputing if the cache was invalidated by a column mutation.

get_row(table, rowid)

@spec get_row(t(), integer()) :: row() | nil

Reads one stored row as a map, or nil if absent.

insert(table, values, opts \\ [])

@spec insert(t(), row(), keyword()) ::
  {:ok, t(), integer()} | :ignore | {:error, String.t()}

Inserts a row given as a map of column key => value (already evaluated and affinity-coerced by the executor). Missing columns get their DEFAULT or NULL.

Options:

  • :rowid — an explicit rowid (from inserting into rowid by name)
  • :on_conflict:abort (default) errors, :replace deletes the conflicting rows first, :ignore skips the insert and returns :ignore

key(name)

@spec key(String.t()) :: String.t()

Case-insensitive column lookup key, since SQL identifiers fold case.

narrow_all_rows(table)

@spec narrow_all_rows(t()) :: t()

Re-narrows all stored rows — currently held as key => value maps — to positional tuples against the table's current columns. Used after an ALTER TABLE rebuilds rows as maps under a changed column layout.

new(name, columns, opts \\ [])

@spec new(String.t(), [ExSQL.AST.ColumnDef.t()], keyword()) :: t()

Creates a table from a name, parsed column definitions, and optional table-level constraints.

row_from_map(table, map)

@spec row_from_map(t(), row()) :: tuple()

Builds a positional row tuple from a key => value map (missing keys → nil).

row_to_map(table, row)

@spec row_to_map(t(), tuple()) :: row()

Rebuilds a key => value map from a positional row tuple (compat for un-migrated callers).

scan(table)

@spec scan(t()) :: [{integer(), row()}]

All rows in rowid order, as {rowid, row} pairs.

scan_positional(table)

@spec scan_positional(t()) :: [{integer(), tuple()}]

Like scan/1 but yields rows as raw positional tuples (no widening to maps). The hot single-table scan reads columns positionally via the frame's column_index, skipping the per-row map build that scan/1 does.

update_row(table, rowid, row, opts \\ [])

@spec update_row(t(), integer(), row(), keyword()) ::
  {:ok, t()} | :ignore | {:error, String.t()}

Replaces the row at rowid with row (a full row map), re-checking constraints. If the rowid-alias column changed, the row is re-keyed. Takes the same :on_conflict option as insert/3.