aster.Web Data / Docs Aster UI ↗

Aster Web Data / Connect your data

SQLite

How Aster Web Data uses a SQLite file, and how SQLite's storage, single writer and results shape what clients receive.

SQLite suits a service whose data lives in one file on the same machine: a small deployment, a test suite, or a first try of Aster Web Data, which is why Get started uses it. The service opens the file inside its own process, so there is no database server to run, no network connection and no TLS. Filters, sorts, pages, counts and $expand run in SQLite, in tables the service creates as STRICT, so SQLite itself refuses a value of the wrong type. Where SQLite's answer differs from OData's, SQLite's stands; Departures from OData lists those places.

Two things decide whether SQLite fits. It allows one writer at a time, so the service serialises every write to a file. And decimals are stored as scaled 64-bit integers, so their precision is at most 18 digits.

Connecting

Name the file from an environment variable or a file that holds its path:

databases:
  local:
    kind: sqlite
    url_file: db.path      # a file holding the path, or ':memory:'; url_env also works

The path is relative to the working directory, and :memory: gives a database held in memory for as long as the service runs. A file: URI is refused. The configuration reference lists every key with its default. These behave in a way particular to SQLite:

Two entries that name the same file, after the path is resolved, share one open database, one pool and one writer. Each :memory: entry is a separate database.

SQLite 3.53.2 is bundled with the service; no system library is used. When it opens a file, the service checks that the engine allows at least 30,000 bound parameters.

Start-up check

The configuration is checked before the file opens: decimals need a declared precision of at most 18 and a declared scale, and times and timestamps a precision of at most six fractional digits.

When an existing file opens, each field's column must have the affinity of the field's type (INTEGER, REAL, TEXT or BLOB), and temporal columns must use BINARY collation. Every value of a decimal column must be an integer, and the whole column is read to check it. The service also decodes 128 rows of every other column, so a malformed date, or a fractional value in an integer field, is refused with the column named. This matters for a file that another program wrote without STRICT tables, where a column keeps whatever a writer stored under its affinity.

A file that is missing or fails the check makes only its own sets unavailable; A database goes down describes what clients see. To run the check on its own, use the check command.

Types

Each field is stored in the form below. Tables the service creates are STRICT.

OData type Column created by recreate Storage form
Edm.Boolean INTEGER 0 or 1. Any other stored value is refused when it is read.
Edm.Byte, Edm.SByte, Edm.Int16, Edm.Int32 INTEGER with a CHECK on the declared range 64-bit integers, kept within the range by the CHECK.
Edm.Int64, enumerations INTEGER 64-bit integers.
Edm.Duration INTEGER Signed nanoseconds.
Edm.Decimal(p, s) INTEGER with a CHECK on the precision The value multiplied by 10 to the power of s. Exact, and ordered correctly. A write is rescaled exactly to s, and refused if that would round.
Edm.Single, Edm.Double REAL IEEE binary64. INF and -INF are stored and read back. NaN cannot be written, because SQLite would store null.
128-bit unsigned integer TEXT with a CHECK 39 zero-padded decimal digits, compared as text. Arithmetic on them runs in the service.
Edm.String, GUID, JSON TEXT, under BINARY or the declared NOCASE UTF-8. A NUL is allowed. GUIDs are lowercase and hyphenated.
Edm.Date TEXT YYYY-MM-DD.
Edm.TimeOfDay TEXT HH:MM:SS.ffffff.
Edm.DateTimeOffset TEXT YYYY-MM-DDTHH:MM:SS.ffffffZ, in UTC, with six fractional digits.
Edm.Binary BLOB Bytes.
Collections of text, integers or booleans TEXT A JSON array.

Dates, times and timestamps sort correctly as text because every value has the same width, which is why the start-up check samples those columns and requires BINARY collation on them.

A decimal is an integer with the scale fixed by the column definition, so 18 digits is the most that fits. A comparison with a constant is exact at every scale: the constant is rounded in the direction that keeps the comparison's result, so an index on the column can serve it.

Collation and case

Where each operation runs

Each OData operator becomes the SQLite operator with the same meaning, so SQLite's comparison, arithmetic and errors give the answer. This request reads a customer with their two largest invoices and their invoice count:

1 statement on SQLite

SQLite, statement 1 of 1

/* aster read customers */ SELECT c."id", c."name", c."email", c."region", (SELECT json_group_array(json(c1)) FROM (SELECT json_object('cells', JSON_ARRAY(CAST(i."issued" AS TEXT), CAST(i."amount" AS TEXT), CAST(i."id" AS TEXT))) AS c1 FROM "main"."invoices" AS i WHERE i."customerId" = c."id" ORDER BY i."amount" DESC, i."id" LIMIT ?1)) AS "invoices", (SELECT COUNT(*) FROM "main"."invoices" AS i2 WHERE i2."customerId" = c."id") AS c6 FROM "main"."customers" AS c WHERE c."id" = ?2
-- binds: 2, 3

The invoices come from a sub-query per customer that builds them with json_group_array, with the nested sort, page and count in the same statement. A table with a double column is read by key batches instead, because the bundled JSON writes some doubles in text that is not their shortest round-trip form.

These run in SQLite:

These run in the service, within the limits on rows it may hold: like under BINARY (SQLite's LIKE would fold ASCII letters), length, substring, trim, indexof, regular expressions, divby, mod of decimals and doubles, avg of integers and decimals, round, floor and ceiling, date parts, 128-bit comparisons and arithmetic, other casts to text, and Traverse.

Errors

Case Response
Integer or decimal overflow +, - and * past 64 bits give a real number, not an error. Read back, that value is refused with 400 EvaluationFailed; in a filter it compares as the real number. For example, 99999999999999.9999 minus 0.00001 needs 19 digits at scale 5. sum past 64 bits raises "integer overflow": 400.
Division by zero / and % by zero give null, for integers, decimals and doubles. A filter on the result keeps no row for it.
Double overflow A result past the finite range is INF or -INF, stored and returned as such.
Unique, foreign key, check, not-null or type violation 409 Conflict.
Write contention, or the writer's wait runs past checkout_timeout 503 PoolExhausted, with Retry-After: 1. The database is up, and retrying after a moment succeeds.
Statement timeout 503 Timeout.
File missing, not a database, corrupt, or an I/O error 503 ConnectionLost.
A decimal written from an expression 501 NotImplemented. A decimal column takes only a value.
Anything else 500 InternalError.

Nulls and ordering

Each sort key names its null placement, and the service writes NULLS FIRST or NULLS LAST as the sort asks. The placement is left out for a column the table declares not null, so an index on it can serve the sort. NaN is never stored, and -0.0 equals 0.0.

$count=true is exact and agrees with the page, as A page with its total shows.

Departures from OData

Where OData 4.01 defines a result and SQLite gives another, SQLite's result stands. These are the places:

Writes and transactions

Across SQLite and PostgreSQL, in either direction, an $expand or a navigation takes two statements, and a per-parent page with a count takes three; Reads across databases explains how they are combined.

Indexes and sort order

Index every column you filter or sort on, and every navigation's foreign key, as Indexes describes. SQLite's EXPLAIN QUERY PLAN shows whether a filter uses its index, including a temporal column under BINARY and a decimal compared with a constant. A comparison between two columns, or one over an arithmetic result, scans the table.

Scaffold

The scaffold command reads pragma_table_xinfo, pragma_foreign_key_list and each text column's collation, and names each declared type by SQLite's affinity rules. INT types become long, REAL-like types float, text types text and BLOB binary, with foreign keys as navigations. Decimals, dates and booleans are left as comments with the reason, because their storage form is a choice you make. A scaffolded file passes check unchanged. Connect your own database walks through it.