Skip to content

Core Concepts

This page gives you the mental model for ApexBase before you dive into the API reference.

Database Root

An ApexClient opens a root directory:

from apexbase import ApexClient

client = ApexClient("./data")

The root directory contains the default database and any named databases. ApexBase stores each table as a .apex file.

For an ephemeral database, pass the special path ":memory:":

client = ApexClient(":memory:")

This is a true process-local storage backend, not a temporary directory. Tables still use the normal SQL, DDL, DML, index, schema, and result paths, but no database, catalog, WAL, delta, blob-sidecar, or index files are created. Its contents disappear when the client is closed and are not shared with another independently created in-memory client.

In-memory FTS has no back-fill

On :memory:, create the FTS index before writing rows (init_fts(...) or CREATE FTS INDEX on an empty table). An index created after rows are stored does not back-fill those rows, so searches return nothing for them. Filesystem tables index existing rows either way.

Databases

ApexBase supports multiple isolated databases under one root directory. The default database maps to the root directory. Named databases live in subdirectories.

client.use_database("analytics")
client.use(database="analytics", table="events")

print(client.list_databases())
print(client.current_database)

SQL can refer to another database with database.table syntax:

client.execute("""
    SELECT u.name, e.event
    FROM default.users u
    JOIN analytics.events e ON u.id = e.user_id
""")

Tables

Tables are explicit. Create or select a table before using table-scoped methods such as store(), retrieve_all(), and list_fields().

client.create_table("users")
client.use_table("users")

You can also create tables with SQL:

client.execute("CREATE TABLE IF NOT EXISTS users")

Schemas

You may let ApexBase infer column types from the first write, or provide a schema up front for clearer contracts and faster bulk loading.

client.create_table("orders", schema={
    "order_id": "int64",
    "customer": "string",
    "total": "float64",
    "paid": "bool",
})

Query Execution And Visibility

SQL semantics do not depend on whether rows live in the persisted base file or in delta/overlay state. Supported conjunctive filters can be translated into a shared physical scan request; the backend either uses selective mmap access or an authoritative merged read, then applies grouping, HAVING, ordering, and limits in SQL order. Unsupported or inexact predicate forms fall back to the general evaluator rather than returning approximate results.

For implementation details, see Scan & Physical Execution.

Records And Columns

ApexBase accepts row-oriented dictionaries, lists of dictionaries, and columnar dictionaries. For bulk ingest, columnar data is usually the fastest path.

client.store({"name": "Alice", "age": 30})

client.store([
    {"name": "Bob", "age": 25},
    {"name": "Charlie", "age": 35},
])

client.store({
    "name": ["Diana", "Eve"],
    "age": [28, 41],
})

Every stored row has an internal _id. SQL hides _id unless you request it explicitly.

ResultView

Queries return a ResultView, which can convert to Python-native rows or columnar DataFrame formats.

result = client.execute("SELECT * FROM users")

rows = result.to_dict()
pandas_df = result.to_pandas()
polars_df = result.to_polars()
arrow_table = result.to_arrow()

Use ResultView when you want to move smoothly between SQL, Python lists, Pandas, Polars, and PyArrow.

Process-Local Default Connection

The module-level apexbase.execute() helper lazily creates one shared in-memory client inside the current Python process:

import apexbase

apexbase.execute("CREATE TABLE counters (name TEXT, value INT)")
apexbase.execute("INSERT INTO counters VALUES ('jobs', 3)")
value = apexbase.execute(
    "SELECT value FROM counters WHERE name = ?", ["jobs"]
).scalar()

Later module-level calls see the same tables. Use an explicit ApexClient when you need lifecycle control, isolation, persistence, or more than one database connection.

Durability

Durability is configured when opening the client:

Mode Best for Behavior
fast Local analytics, scratch data, benchmarks Leaves writes in the OS page cache; no fsync
safe Application data with balanced speed and safety Fsyncs on explicit flush() / close()
max Highest crash safety Fsyncs every write
client = ApexClient("./data", durability="safe")

Bounded Process Caches

ApexBase keeps several per-process caches bounded so long-running applications have a predictable memory ceiling: parsed SQL statements, planner feedback (256 query shapes per table across at most 256 tables), table statistics, CTE batches, dictionary/null caches, and the client-side analytical result cache (at most 64 results, each at most 4096 rows / 8 MiB). Caches are invalidated by epoch and file mtime, so external writers never serve stale rows. See Resource Ownership for the authoritative inventory.

Interfaces

The same storage engine can be reached through several interfaces:

  • Python API for embedded applications and notebooks.
  • Rust embedded API for native Rust applications.
  • PostgreSQL Wire server for SQL clients and database tools.
  • Arrow Flight server for high-throughput columnar result streaming.

Start with the Python API unless you already know you need a wire protocol or Rust integration.