aster.Web Data / Docs Aster UI ↗

Aster Web Data / Connect your data

MariaDB

How Aster Web Data uses MariaDB, and how MariaDB's types, collations and results shape what clients receive.

If your data is in MariaDB 10.5 or later, Aster Web Data serves it from your existing InnoDB tables. Filters, sorts, pages, counts and most $expand requests run in MariaDB as one statement, under the column's own collation, so your indexes serve them. Where MariaDB's answer differs from OData's, MariaDB's stands; Departures from OData lists those places.

The choice with the most effect is the collation of your text columns. A _ci collation makes name eq 'smith' match 'Smith', and a PAD SPACE collation ignores trailing spaces, because the comparison is MariaDB's own. The other choices are how the connection verifies TLS and which indexes you add for descending sorts and navigations.

Connecting

Declare the database once under databases:, and name it from each data source stored on it:

databases:
  main:
    kind: mariadb
    url_env: MAIN_DB_URL        # mysql://user:password@host:3306/shop
    tls: verify-full
    ca_file: ca.pem             # optional: otherwise the Mozilla roots

The URL comes from an environment variable (url_env) or a file (url_file), or you give its parts: host, port, user, password_env or password_file, and database. It is read again on every attempt to connect. Secrets explains the rules, and the configuration reference lists every key with its default, including the pool and the retry schedule.

These keys behave in a way particular to MariaDB:

Two databases: entries are two connection pools, even on one server, and a request that reads from both is combined in the service, as Reads across databases describes.

The service checks the server version when it opens a connection. It refuses a server older than 10.5, the first with INSERT ... RETURNING, and refuses MySQL, naming the version it found. The tests run on MariaDB 12.3.

Start-up check

When the service opens the database, it checks each data source's table against its configuration. Names are matched without regard to case, as MariaDB does on file systems that ignore case. The check refuses a missing table or column, a column type that does not hold the field's values exactly (the accepted types are listed under Types), and a column whose precision, scale, fractional digits, collation or default differ from the configuration. The message says what to declare.

A failed check makes only that database's sets unavailable, and the check runs again on the retry schedule, so a fix takes effect without a restart. A database goes down describes what clients see meanwhile. To run the check on its own, use the check command.

Types

When setup: recreate creates a table, each field gets the column type below. An existing table must use one of the column types the start-up check accepts, listed after the table.

OData type Column created by recreate Notes
Edm.String VARCHAR(n) up to 16,000 characters, LONGTEXT above that, VARCHAR(255) without a length With the declared collation, if any.
Edm.Boolean BOOLEAN
Edm.Byte, Edm.SByte, Edm.Int16, Edm.Int32 INT
Edm.Int64, Edm.Duration BIGINT A duration is signed nanoseconds.
Edm.Single, Edm.Double DOUBLE Holds no INF, -INF or NaN. A seed value of that kind is refused.
Edm.Decimal(p, s) DECIMAL(p, s), or DECIMAL(65,30) without facets A value too large for a 28-digit decimal fails when it is read, and is never rounded.
Edm.Date DATE Years 1 to 9999.
Edm.TimeOfDay TIME(6), or TIME(p) with a declared precision
Edm.DateTimeOffset DATETIME(6), or DATETIME(p) Stored and read as UTC, with microseconds, for years 1 to 9999. A value with finer precision than the column fails visibly.
Edm.Guid CHAR(36) Lowercase, hyphenated.
Edm.Binary LONGBLOB
128-bit unsigned integer DECIMAL(39,0) Compared, sorted and summed in the database. The sum is range-checked.

A 28-digit decimal here is one whose digits fit a 96-bit mantissa: 28 digits always fit, and 29 only up to 79,228,162,514,264,337,593,543,950,335. A non-zero digit past the 28th decimal place also fails.

The start-up check accepts these column types, and refuses any other:

Every session uses utf8mb4 and the UTC time zone, with a SQL mode that keeps backslashes as escapes and adds SIMULTANEOUS_ASSIGNMENT, so each assignment in an UPDATE sees the row as it was before the update.

MariaDB computes with an UNSIGNED operand as BIGINT UNSIGNED, so 3 - 5 over INT UNSIGNED columns would raise an error. The service writes CAST(x AS SIGNED) around a 64-bit field stored in a narrower or UNSIGNED column, so the arithmetic is done at OData's width.

Collation and case

A comparison, a sort and like use the column's own collation. name eq 'x' is name = ? and $orderby=name is ORDER BY name, so an ordinary index on the column serves them. $metadata publishes each text field's collation as AsterData.Collation: the field's own, else the database's collation: key, else the column's. The start-up check refuses a column whose collation differs from the one declared. The server's default for utf8mb4 is utf8mb4_uca1400_ai_ci from MariaDB 11.6, and utf8mb4_general_ci before.

Where each operation runs

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

1 statement on MariaDB

MariaDB, statement 1 of 1

/* aster read customers */ SELECT c.`id`, c.`name`, c.`email`, c.`region`, (SELECT JSON_ARRAYAGG(JSON_ARRAY(i.`issued`, CAST(i.`amount` AS CHAR), i.`id`) ORDER BY i.`amount` DESC, i.`id` LIMIT ?, ?) FROM `test_aster_docs_capture`.`invoices` AS i WHERE i.`customerId` = c.`id`) AS `invoices`, (SELECT COUNT(*) FROM `test_aster_docs_capture`.`invoices` AS i2 WHERE i2.`customerId` = c.`id`) AS c6 FROM `test_aster_docs_capture`.`customers` AS c WHERE c.`id` = ?
-- binds: 0, 2, 3

The invoices come from a sub-query per customer that builds them as a JSON array, with the nested sort and $top as its ORDER BY and LIMIT. MariaDB reads an $expand this way where the navigation's foreign key leads an index and the table has at most 48 columns. Otherwise each level is read by a second statement, by the distinct keys of the parent rows in IN (...) lists padded to a power of two, with the same answer.

A decimal or a double inside that JSON is written as text with CAST(x AS CHAR), because a JSON number would round it through a double. When the service connects, it asks whether the server writes doubles in their shortest round-trip text; where it does not, a table with a double column is always read by key batches. Sessions set group_concat_max_len to its maximum, so the only bound on one aggregated value is max_allowed_packet. A statement that reaches it fails with 400 QueryTooExpensive instead of returning a cut value.

These run in MariaDB:

These run in the service, within the limits on rows it may hold: 128-bit arithmetic, date, time and duration arithmetic, other regular expressions, hour, minute and second, casts from string, other lambda expressions over collections, scalar sub-queries, and full-text search.

Errors

Case Response
Integer overflow +, - and * on BIGINT raise error 1690 in every SQL mode: 400 EvaluationFailed.
Division by zero DIV, / and MOD by zero return null in a SELECT, with a warning, and so does divby. A filter on the result keeps no row for it.
Double overflow A DOUBLE result past the finite range raises 1690: 400 EvaluationFailed.
Decimals A quotient has div_precision_increment more places than the dividend, 4 by default, so 1.5 div 2 over a DECIMAL(28,10) column is 0.75000000000000. A result too large for a 28-digit decimal is refused when read, never rounded.
Evaluation order MariaDB chooses it.
Unique, foreign key, not-null or check violation 409 Conflict.
Deadlock or record changed 409 TransactionConflict, after the atomic group's retries.
Data exception, cardinality error, or two columns of different collations compared 400 EvaluationFailed.
Statement timeout 503 Timeout.
Connection lost 503 ConnectionLost.
Any other database error 500 InternalError. The client gets a generic message, and the database's detail goes to the log.

Nulls and ordering

OData puts nulls first in ascending order and last in descending order. MariaDB has no NULLS FIRST clause, so the service writes an IS NULL key ahead of each sort key. That key is left out for a sort key that reads no row or is a bare literal, because MariaDB reads an integer constant in ORDER BY as a column position.

There is no NaN to place, and -0.0 equals 0.0.

A next page is a keyset query with the same collation, null placement and direction as the sort. $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 MariaDB gives another, MariaDB's result stands. These are the places:

Writes and transactions

A connection that fails in a way that shows it is unusable is discarded, not returned to the pool.

Indexes and sort order

Indexes gives the indexes that serve a sorted page and a navigation. The reason a descending sort needs its own index is particular to InnoDB. Paging adds the entity key to every sort, so $orderby=T desc sorts by T DESC, ID. A secondary index on T stores the primary key after T, which serves T, ID and T DESC, ID DESC but not the mixed T DESC, ID, so that order is a sort of every matching row. Index (T DESC, ID) for a descending sort; descending indexes need MariaDB 10.8 or later.

Scaffold

The scaffold command reads information_schema and writes a starting data-source configuration for the columns it can map exactly, leaving the others as comments with the reason. --schema names MariaDB databases on the one connection, so a foreign key to a table in another selected database becomes a navigation. The account needs a privilege on each table, because information_schema shows a table only to an account with one. It runs no DDL and no write. Connect your own database walks through it, and Model your data covers what to add once it has run.