SDK
The AxialDB SDK is the axialdb command line and the engine's configuration: everything you need to connect your databases, create tables and query them.
This is reference documentation. Start with the first table below, then use the contents. Questions are welcome in the AxialDB community on Discord.
Your first table
This takes a few minutes once the engine is running and has one source. Here the source is a MongoDB replica set, the [sources.mongo] entry of the engine's config (section 2). The commands are the same on Windows and Linux.
axialdb sources
axialdb create-table sales.orders --source mongo --mode live --query "SELECT _id, total, status FROM shop.orders WHERE status <> 'draft'"
axialdb tables
axialdb query "SELECT status, COUNT(*) AS orders FROM sales.orders GROUP BY status"
sourceslists the sources the engine knows. If yours is missing, stop and read section 2.create-tablemakes a live table namedsales.ordersfrom the collectionordersin the MongoDB databaseshop, keeping the documents whose status is notdraft. The name is the AxialDB table. The MongoDB database is in the query'sFROM. Nothing is created in MongoDB. For a live table, a DBA must first turn on post-images forshop.orders(section 3, step 3).tableslists every table on the engine, oneschema.tableper line.queryruns ordinary SQL over those names. Rows are printed, and nothing is stored.
total is a text column here because the query has no CAST. To add it up, write CAST(total AS DECIMAL(12,2)) AS total in the create query. The rules are in The MongoDB create query.
1. What is in the box
There are two downloads for each system, Windows and Linux: the engine package and the optional MySQL shim package. Both are free and have no expiry. 6.0.0 was released on 2026-10-27; the zips are on the download page.
| Part | Engine package | MySQL shim package |
|---|---|---|
The engine (axialdb-engine), run as a service (on Linux with its systemd unit) | Yes | No |
The axialdb command line | Yes | No |
The engine's config axialdb.toml | Yes | No |
ODBC driver, with the Tableau customization file axialdb_odbc.tdc (its install script in the Windows zip only) | Yes | No |
| JDBC driver | Yes | No |
The documents live-tables.md and remote-engine.md | Yes | No |
| The MySQL plugin, one file for each of 9.7.0, 9.7.1 and 9.7.2, and its bridge library | No | Yes |
Helper functions for MySQL (install-axialdb-mysql-functions.sql) and the plugin's settings file axialdb-shim.toml | No | Yes |
TERMS.md, THIRD-PARTY-NOTICES.md, VERSION | Yes | Yes |
- Everyone installs the engine package, on the engine's machine. It reads MySQL and MongoDB with nothing installed on them.
- Add the MySQL shim to a MySQL server only to create and query tables from inside MySQL; the engine reads MySQL without it. Use the two packages of the same version together.
Where the files go
| What | Linux | Windows |
|---|---|---|
| Engine config | /etc/axialdb/axialdb.toml | C:\ProgramData\AxialDB\mysql\axialdb.toml |
| Shim settings (on each MySQL server with the shim) | /etc/axialdb/axialdb-shim.toml | C:\ProgramData\AxialDB\mysql\axialdb-shim.toml |
| Data folder | /var/lib/axialdb/data/ | C:\ProgramData\AxialDB\mysql\data\ |
| Engine log | /var/log/axialdb/axialdb-engine.log | C:\ProgramData\AxialDB\mysql\logs\axialdb-engine.log |
| Service name | axialdb-engine | AxialDBEngine |
The engine listens on 127.0.0.1:9742 unless you change it. The command line uses that address when you give no --engine.
2. The engine's configuration
The engine reads one file, axialdb.toml. Restart the engine after you change it. The file holds passwords, so only the service account and administrators should be able to read it. The shipped file has more sections ([data], [logging], [license] and others) with working values. This section covers the ones you change to connect databases.
[helper]
listen = "127.0.0.1:9742" # the engine's address. Beyond this machine: section 6
[engine]
live_apply_interval_secs = 1 # how often captured changes are applied to live tables
live_compact_interval_secs = 3600 # how often the stored changes of live tables are tidied
[default_naming] # where a new table lands, for every source
mirror_host_schema = true
target_schema = "analytics"
prefix_source = false
prefix_host_schema = false
[sources.orders] # a MySQL server. The name is yours
kind = "mysql"
host = "orders-db.example.com"
port = 3306
user = "axialdb"
password = "..."
capture_user = "axialdb_cdc"
capture_password = "..."
capture_server_id = 19001
[sources.mongo] # a MongoDB replica set
kind = "mongo"
uri = "mongodb://axialdb:...@mongo-eu.example.com:27017/?replicaSet=rs0"
Sources
Each database instance is a source: one [sources.<name>] section for each MySQL server and each MongoDB replica set. The addresses and passwords live here and nowhere else. A create never carries a connection string: one in a MySQL COMMENT would show in SHOW CREATE TABLE, and one on the command line would land in shell history.
- Source names are unique on the engine. A name is fixed once a table uses it, because the table's change log and position hang off the name.
- The name shows in
axialdb sources, in--source, and in table names only whenprefix_sourceputs it there. - One engine serves any number of sources of either kind, on any machines.
- If a source is later removed from the config, its tables become
failedwith a reason that names it (section 7).
A MySQL source
| Key | What it is |
|---|---|
kind | "mysql" |
host, port | The main instance: where the engine reads the source tables' columns and keys and the server's position for every live table, and runs the work of create-table and drop-table. |
user, password | The engine's own account on the main instance: SELECT on source tables (SHOW VIEW on views). Needed for every live table, also one typed in MySQL, and for command-line creates; on a server with the shim also CREATE, DROP, INSERT. Only snapshots typed inside MySQL need none. |
shim_secret | Only for a server with the MySQL shim, on another machine than the engine. The secret this server's plugin proves. See section 6. |
capture_host, capture_port | Where the binlog is read for live tables. Normally a replica. Leave them out and capture reads from the main instance. |
capture_user, capture_password | The account that reads the binlog. Live tables are possible on a source only when capture_user is set. |
capture_tls | Encrypts the binlog connection (default off). |
capture_server_id | The replication client id of the binlog reader. Unique among that server's replicas, and different for every source. |
The two accounts, for the address the engine connects from (here the engine runs on the MySQL server's machine), as in the engine's install guide:
CREATE USER 'axialdb'@'127.0.0.1' IDENTIFIED BY 'choose-a-password';
GRANT SELECT, SHOW VIEW ON *.* TO 'axialdb'@'127.0.0.1';
CREATE USER 'axialdb_cdc'@'127.0.0.1' IDENTIFIED BY 'choose-another-password';
GRANT REPLICATION SLAVE, REPLICATION CLIENT, SELECT ON *.* TO 'axialdb_cdc'@'127.0.0.1';
SELECT for axialdb may be narrowed to the databases your tables read. On a server with the MySQL shim, axialdb also needs CREATE, DROP and INSERT.
- No capture account, no live tables. A live create on a source without
capture_useris refused with "capture is not configured for source ...". Snapshots still work. - Server settings. Capture connects only to a server with
binlog_format=ROW,binlog_row_image=FULL,binlog_row_metadata=FULLandgtid_mode=ON. The engine checks at every connect. If one is wrong, capture does not start, andaxialdb sourcesshows why. - A replica needs no plugin. With the MySQL shim, AxialDB keeps its own
CREATE TABLE ... ENGINE=AXIALDBandDROP TABLEout of the binlog. A live table shows data as of what capture has read, so with a replica it trails the main instance by the replica's delay. - You point capture at the right server. The engine does not check that the capture address is a replica of the main instance.
- Each MySQL source has its own capture task and change log. A change log follows one MySQL server. See "One source is one server" in section 6.
A MongoDB source
| Key | What it is |
|---|---|
kind | "mongo" |
uri | The connection string of the replica set, with replicaSet= and the engine's account. The engine connects out to it. Nothing is installed on MongoDB. |
- MongoDB 8.0 and later, replica sets. One member is enough; a standalone server has no change stream, and sharded clusters are not supported.
- The account in the URI needs the built-in
readrole on the source database. It needs no write access. - A live table needs post-images turned on for its source collection (section 3, step 3).
- The naming keys of the next part can be set under a MongoDB source too.
Naming: where a table lands
Every table has a name schema.table, unique on the engine. BI tools show the schema. The naming keys decide it. They are four keys, set for every source in [default_naming], and set again directly under a source (next to kind) to change it for that source only. A key set under a source wins.
| Key | Meaning |
|---|---|
mirror_host_schema | true: the schema is the host database name (the MySQL database, or the MongoDB database in the query's FROM). |
target_schema | The schema used when mirror_host_schema = false. |
prefix_source | true: the table name starts with the source name. |
prefix_host_schema | true: the table name starts with the host database. Only allowed when mirror_host_schema = false. |
With sources named orders (MySQL) and mongo (MongoDB), a table created as demo_perf.order_lines_av on MySQL, and as customers with FROM crm.customers on MongoDB, lands here:
| Settings | MongoDB table | MySQL table |
|---|---|---|
| Defaults (mirror) | crm.customers | demo_perf.order_lines_av |
Mirror, prefix_source | crm.mongo_customers | demo_perf.orders_order_lines_av |
| No mirror | analytics.customers | analytics.order_lines_av |
No mirror, prefix_source | analytics.mongo_customers | analytics.orders_order_lines_av |
| No mirror, both prefixes | analytics.mongo_crm_customers | analytics.orders_demo_perf_order_lines_av |
--schema reporting, any settings | reporting.customers | reporting.order_lines_av |
On MongoDB, a schema typed with the name (create-table mongo.customers) replaces the schema the rules would pick, and nothing else: with prefix_source it becomes mongo.mongo_customers.
Inside MySQL, with the MySQL shim, a create names another schema in its comment: COMMENT='live; schema=reporting' puts the AxialDB table into the schema reporting.
The rules
- Order of precedence.
--schemawins. Next, on MongoDB, a schema typed with the name. Otherwise the source's own keys decide, and[default_naming]fills in the keys the source does not set. --schemais exact. The table lands in that schema under its own name, with no prefixes.- Names are lowercase. A name with capitals is lowercased without a message, whether you typed it or it came from the host. A MySQL table keeps the name MySQL stores (
Demo.Order_Lines); AxialDB's copy of the name isdemo.order_lines. - Allowed characters. An AxialDB schema or table name matches
^[a-z][a-z0-9_]{0,63}$. A name that would get any other character (a hyphen from a databasedemo-perf, say), or a prefixed name over 64 characters, is refused with a request for--schemaor another name. Nothing is replaced silently. This rule is about the AxialDB name only: a collection such asorders-2024is read withFROM "orders-2024"(quoted, as in the MongoDB create query); give its table a name of your own, such assales.orders_2024. - Reserved schemas.
axialdb(engine status),information_schemaandpublic. Names such asmysql_andmongo_are ordinary. - A name is taken once. A create onto a name that exists is refused, on every source. Drop the table first. If two sources have the same database or table names, separate them with a
target_schemaof their own for one source, or withprefix_source. - Computed once. The name is computed when the table is created and stored. Changing the naming config never renames an existing table. There is no rename.
- What MySQL lists, on a server with the MySQL shim.
SHOW TABLESlists MySQL tables only, in the database the create named. A schema that is not mirrored is not a MySQL database, soanalytics.order_lines_avshows in AxialDB while MySQL showsdemo_perf.order_lines_av. - With the defaults a table from MySQL has the same name in AxialDB and in Tableau as its database and table in MySQL.
Other settings
[engine]:live_apply_interval_secs(shipped value 1) is how often captured changes are applied to live tables, for every source.live_compact_interval_secs(shipped value 3600) is how often the stored changes of live tables are tidied.[access]: secrets, TLS and the network check. See section 6.[license]: leave as shipped. To lift the caps, copy the license fileaxialdb.licbesideaxialdb.tomland restart the engine. The caps are 5 tables, 2 of them live, across the whole engine, MySQL and MongoDB tables together. A license removes the caps and nothing else.
3. Several databases, step by step
This walk-through ends with one query over a MySQL table and a MongoDB table. The engine runs on a machine of its own, engine.example.com. If your engine shares a machine with MySQL, skip the secrets: a connection from the engine's own machine needs none.
- Install the engine. Put the engine package on
engine.example.comand start the service. Check it on the engine's machine withaxialdb sources. - Prepare the MySQL server (
orders-db.example.com).- Optional: to create and query tables from inside MySQL, install the MySQL shim package: the plugin file that matches
SELECT VERSION(), its bridge library, and the helper functions script. The engine reads the server without it. - For live tables, check the server settings. Expect
ROW,FULL,FULL,ON:SELECT @@binlog_format, @@binlog_row_image, @@binlog_row_metadata, @@gtid_mode; - Create the engine's account and the capture account for the engine's address (
'axialdb'@'engine.example.com','axialdb_cdc'@'engine.example.com'). - With the shim, give the plugin its settings file,
axialdb-shim.toml(section 6). Itssourcemust be the name in the engine's config, hereorders.
- Optional: to create and query tables from inside MySQL, install the MySQL shim package: the plugin file that matches
- Prepare MongoDB (nothing is installed).
- Create a user for the engine, with the
readrole on the database. - For a live table, a DBA enables post-images once on each source collection (the engine's account does not need the right):
db.runCommand({ collMod: "customers", changeStreamPreAndPostImages: { enabled: true } })
- Create a user for the engine, with the
- Write the engine's config. One
[sources.<name>]for each, plus the secrets for an engine that other machines reach:Restart the engine.[helper] listen = "0.0.0.0:9742" [access] client_secret = "a long random text" # for the axialdb command line, ODBC, JDBC # tls_cert = "engine-cert.pem" # see section 6 # tls_key = "engine-key.pem" [sources.orders] kind = "mysql" shim_secret = "another long random text" # only with the shim: what orders-db's plugin proves host = "orders-db.example.com" port = 3306 user = "axialdb" password = "..." capture_user = "axialdb_cdc" capture_password = "..." capture_server_id = 19001 [sources.mongo] kind = "mongo" uri = "mongodb://axialdb:...@mongo-eu.example.com:27017/?replicaSet=rs0" - Check the links. On a server with the shim,
SELECT axialdb_init();in MySQL opens a connection to the engine, proves the secret and says which source this server is. From your PC:Both sources should be listed, and the MySQL source's capture state should beaxialdb sources --engine engine.example.com:9742 --secret-file client.secretok. - Make a MySQL table. Inside MySQL, with the shim, as we did on our lab (the column list is shortened):
The same table can be made from the command line with
CREATE TABLE demo_perf.order_lines_av ENGINE=AXIALDB COMMENT='live' AS SELECT ol.id, ol.order_id, ol.line_total, ol.customer_id, o.order_date, o.channel FROM order_lines AS ol LEFT JOIN orders AS o ON ol.order_id = o.idaxialdb create-table demo_perf.order_lines_av --source orders --mode live --query "SELECT ...". - Make a MongoDB table.
(On Windows cmd, end the first line with
axialdb create-table mongo.customers --source mongo --mode live \ --query "SELECT _id, CAST(customer_id AS BIGINT) AS customer_id, legal_name, address.city AS city, address.state AS state FROM crm.customers"^instead of\, or put it all on one line.) - List the tables.
axialdb tables --engine engine.example.com:9742 --secret-file client.secretprints oneschema.tableper line. You should seedemo_perf.order_lines_avandmongo.customers. - Ask one question of both. This is the query we ran on our lab with sample data.
demo_perf.order_lines_avhas 3,920,000 rows;mongo.customershas 10,000 documents.Run it withSELECT c.state, COUNT(DISTINCT c.customer_id) AS customers, COUNT(*) AS order_lines, ROUND(SUM(o.line_total), 2) AS revenue FROM demo_perf.order_lines_av o JOIN mongo.customers c ON c.customer_id = o.customer_id GROUP BY c.state ORDER BY revenue DESC LIMIT 5axialdb query "..."and the same--engineand--secret-file. The result on our lab:This query was not timed by the method on the measurements page.state customers order_lines revenue FL 1037 406504 24659544.0 MO 522 204624 12741176.0 AZ 523 205016 12588688.0 VA 524 205408 12076736.0 OR 500 196000 12044984.0
Adding a second MySQL server
Add another section, with a name of its own. When that server has the MySQL shim, give its plugin the same name in its settings file:
# in the engine's axialdb.toml
[sources.billing]
kind = "mysql"
shim_secret = "a third long random text" # only with the shim
host = "billing-db.example.com"
port = 3306
user = "axialdb"
password = "..."
capture_user = "axialdb_cdc"
capture_password = "..."
capture_server_id = 19002
# in billing-db's axialdb-shim.toml, only with the shim
engine = "engine.example.com:9742"
source = "billing"
secret_file = "/etc/axialdb/shim.secret"
Tables from every source can be joined in one query. The split into schemas organizes the data; it never blocks a query. A query's result is not stored and does not count toward the caps. If both servers have a database and a table of the same name, the second create is refused as taken; separate them with the naming keys (section 2) or --schema.
4. The command line
The axialdb command line is a thin client of the engine. It needs the engine's address and nothing else. It does not read the engine's config and it never connects to MySQL or MongoDB: a create or a drop is a request to the engine, which holds the accounts and runs the work on the source.
The same help is printed by axialdb --help:
axialdb --version
axialdb sources
axialdb tables [--schema name] [--source name]
axialdb query [--schema name] "SQL"
axialdb create-table name [--source name] [--schema name] [--mode snapshot|live] --query "SELECT ..."
axialdb drop-table schema.table
axialdb export-tables > tables.json
axialdb create-tables < tables.json
Connecting
--engine host:port- Every command takes it. Without it the engine is
127.0.0.1:9742. --secret-file path- An engine on another machine asks for its client secret. This names a file that holds it. The environment variable
AXIALDB_SECRETworks too. There is no flag for the secret itself, because a flag stays in shell history and in the process list. --tls-ca path- Encrypts the connection. The file is the PEM file with the engine's certificate (or the authority that signed it).
- On the engine's own machine, neither a secret nor TLS is needed.
- A flag a command does not have is an error. A flag given twice is an error. Both
--name valueand--name=valuework. - Output goes to standard output. Counts and messages go to standard error. Exit code 0 is success, 1 is a refusal or an unreachable engine, 2 is a wrong command line.
- AxialDB names are lowercase: capitals you type are lowercased.
axialdb --versionprints the version and build of the command line itself, not of the engine.
sources
axialdb sources [--engine host:port]Lists the sources the engine knows, in seven columns: name, kind, address, identity, capture (the address changes are read from), capture_status and tables (how many tables come from the source). No secrets are shown. The capture state is ok, not configured, or down since <time>: <reason>. The same list is the table axialdb.sources. The output is a header line and then one tab-separated line for each source.
tables
axialdb tables [--schema name] [--source name]Prints every table on the engine, one schema.table per line, from every source. --schema and --source only narrow the list, and they combine.
create-table
axialdb create-table name [--source name] [--schema name] [--mode snapshot|live] --query "SELECT ..."name- The table that is created. The form depends on the source (below).
--source name- One of the names in
axialdb sources. You can leave it out when the engine has exactly one source. --mode snapshot|livesnapshot(the default) copies once.livekeeps the table current. Any other value is refused.--query "SELECT ..."- Required. The rows to keep. On MySQL it is MySQL SQL. On MongoDB it is AxialDB's small dialect (below).
--schema name- The AxialDB schema, exactly. It wins over the naming rules and over a schema typed with the name, and the table gets no name prefix.
On a MySQL source
- The name is
database.table: the MySQL database and table, as on that server. A name without a database is refused. The query may read any databases. MySQL runs it in its own dialect. - On a server with the MySQL shim the engine runs
CREATE TABLE <database>.<table> ENGINE=AXIALDB [COMMENT='live'] AS <query>there, with the source'suser, and the table also appears in MySQL. On a server without it the engine copies the query's result itself, and the table exists only in AxialDB. - The AxialDB table gets the same name unless the naming rules or
--schemasay otherwise. - A live create returns when the table is
healthy, or says which status it was left in. - A live MySQL table can be one table (InnoDB or MyISAM), or a fact table with
LEFT JOINor innerJOINto dimension tables on their primary keys; source columns only; a primary key on every source table; a view or a subquery only when it is a plain select;ORDER BYonly withLIMIT; noGROUP BY. See the architecture page andlive-tables.md.
axialdb create-table demo_perf.orders_wide --source orders --mode live \
--query "SELECT o.id, o.customer_id, o.amount, c.segment FROM demo_perf.orders o LEFT JOIN crm.customers c ON c.id = o.customer_id" \
--schema reporting
This makes the AxialDB table reporting.orders_wide. On a server with the MySQL shim, MySQL also gets the table demo_perf.orders_wide. The fact table's key o.id and the join column o.customer_id are in the select list, as a live table needs.
On a MongoDB source
- Nothing is created in MongoDB, so the name is the AxialDB table:
schema.table, or the table name alone to let the naming rules pick the schema. - The MongoDB database is in the query:
FROM database.collection. AFROMwithout the database is refused. - The engine reads the query, connects with the source's URI, copies the collection, and for
livefollows the change stream.
axialdb create-table sales.orders --source mongo --mode live --query "SELECT _id, total, status FROM shop.orders WHERE status <> 'draft'"
Checks at create
- A name that already exists, on every source. Drop the table first.
- A sixth table, or a third live table, on the free version. The create is refused with a clear message and nothing else changes.
- A live create on a MySQL source with no
capture_user. - A live MySQL query outside what is listed above. There is no silent fallback to a snapshot.
ENUMandSETcolumns work in a snapshot and are refused in a live table. - A column type AxialDB does not hold (binary columns,
BIT, spatial types, aDECIMALwider than 38 digits). The message names the column.TIMEis kept as text.
Who may create. Everyone who can reach the engine acts on MySQL under the one account in the source's config. MySQL's per-user privileges no longer decide who may create or drop through AxialDB. A statement typed inside MySQL still runs under that user's own privileges.
Column types
An AxialDB table has seven column types: integer (64-bit), double, decimal up to 38 digits, boolean, timestamp, date and text. A MySQL column keeps its own type (DATE is a date, DATETIME and TIMESTAMP are timestamps, DECIMAL is exact, TINYINT(1) is a boolean). A time with a zone is converted to UTC, and a time without one is taken as UTC. Through the command line, ODBC and JDBC you see UTC. A query that arrives through a MySQL session is evaluated in that session's time zone.
The MongoDB create query
For a MongoDB source, --query is not MongoDB's language and not MySQL's. It is a small SQL dialect that AxialDB reads and turns into a MongoDB read and a column list. Anything outside it is refused at create, with a message that names the construct.
SELECT _id,
CAST(customer_id AS BIGINT) AS customer_id,
legal_name,
address.city AS city,
contacts[0].first_name AS customer_0_first_name
FROM crm.customers
WHERE address.state = 'GA' AND CAST(customer_id AS BIGINT) >= 100
SELECT- Field paths only. A dot steps into a nested object.
[n]reads an array element, counting from 0 as MongoDB does.ASnames the column. A nested path withoutASbecomes the path with_between steps (address_city). A missing field, or a step into a value of the wrong kind, isNULL. A nested object or array named as a whole column is stored as plain JSON text. FROM- One collection, written
database.collection. A standard MongoDB view is accepted for a snapshot only. Joins and reshaping stay in MongoDB: build the collection you want there and read it. _id- Required, under that name. It is the key of the table. It is never cast and never renamed; both are refused. It is stored as text: an ObjectId as its 24 hex characters, a string as is, a number as its digits, a document as plain JSON. For a typed copy, select the field a second time:
SELECT _id, CAST(_id AS BIGINT) AS customer_id, .... WHERE=,<>,<,<=,>,>=,IN,IS [NOT] NULL,AND,OR,NOT, on field paths (with an optionalCAST) and literals. A missing field isNULL.- Refused
SELECT *, joins,GROUP BY,ORDER BY,LIMIT, and functions other thanCAST. Group and sort when you query the table.
Types
Types are declared with CAST(path AS BIGINT | DOUBLE | DECIMAL(p,s) | BOOLEAN | TIMESTAMP | DATE | VARCHAR). Each cast gives a real column of that type. A path without a cast is a text column. No schema is guessed from the data.
| MongoDB type | Without a cast (text) | Casts that accept it |
|---|---|---|
| String | As is | Any, if the text converts |
| Int32, Int64 | Digits | BIGINT, DOUBLE, DECIMAL |
| Double | Digits | DOUBLE, DECIMAL; BIGINT only for a whole number |
| Decimal128 | Digits | DECIMAL, DOUBLE; BIGINT only for a whole number |
| Boolean | true or false | BOOLEAN |
| Date | 2026-01-02T03:04:05.000Z | TIMESTAMP |
| ObjectId | 24-character hex text | VARCHAR only |
| Object, Array | JSON text | VARCHAR only |
| Binary and other rare types | Refused, with a message | None |
- A failed cast is an error, not
NULL. A missing field or a BSONnullgivesNULL. A value that is present and does not convert does not:'abc'or3.5asBIGINT, or text that is not a timestamp. At create, the create is refused and nothing is published. On a live table, the table fails closed (section 7). The message names the_id, the column and the value. - Dates in text. Only ISO forms are read. With
Zor an offset, text is that instant. Without a zone it is that time in UTC.2026-01-31is a day: it casts toDATE, notTIMESTAMP.1/31/2026and the like fail. - Nothing is rounded. A value with more digits or decimal places than its
DECIMAL(p,s)is a failed cast. WHEREis typed and checked at create. A path has the type of its cast, or text without one. A literal of another kind is refused, and the message shows the fix:n = 5is refused;CAST(n AS BIGINT) = 5andn = '5'are accepted. A numeric_idneeds the cast too.- Literals. A number for
BIGINT,DOUBLEandDECIMAL;TRUEorFALSEforBOOLEAN;'2026-01-31'forDATE. ATIMESTAMPliteral is a complete instant with its zone:'2026-02-01T00:00:00Z'or'2026-02-01T00:00:00-05:00'. - A period includes its start and excludes the next start:
CAST(at AS TIMESTAMP) >= '2026-01-01T00:00:00-05:00' AND CAST(at AS TIMESTAMP) < '2026-02-01T00:00:00-05:00'. - A whole array or object is not compared. Pick an element (
tags[0]) or a nested field (address.city). - Left to right. Conditions are evaluated in the order written and stop as soon as the result is known, so a guard written first protects a cast written after it:
kind = 'order' AND CAST(amount AS BIGINT) > 5. The casts in theSELECTlist run only for documents theWHEREkeeps. - Column names are lowercase, and lowercased silently (
AS firstNamegivesfirstname). Two columns of the same name are refused, and the message names both paths. A field or collection name with other characters is written in double quotes:SELECT _id, "first-name" AS first_name, address."zip code" AS zip FROM "orders-2024". A quoted name that is not a valid column name needsAS. - Note. Two documents whose
_idgives the same stored key (the number1and the text"1") refuse the create, and the message names both. After the create of a live table this is not checked: a later document with the stored key of another replaces that row.
drop-table
axialdb drop-table schema.table- The name is the AxialDB name, as
axialdb tableslists it. It is the same on every source, and there is no--source. - A drop removes everything the table consists of. For a MySQL table, when the server has the shim, the engine drops the MySQL table too. For a MongoDB table there is only the AxialDB table.
- It prints
dropped schema.table, orno such table schema.table. A drop removes whatever exists and reports success; a table half there is not refused. - Inside MySQL, with the shim,
DROP TABLEon an AxialDB table removes both halves. It must be a statement of its own: aDROP TABLEthat names an AxialDB table together with other tables is refused. ALTER TABLE,RENAME TABLEandTRUNCATE TABLEon an AxialDB table are refused, each with a message that says to drop the table and create it again. AxialDB tables are read-only copies: you neverINSERT,UPDATEorLOADinto them.
query
axialdb query [--schema name] "SQL"- Runs engine SQL over
schema.tablenames. Rows are returned and nothing is stored. It does not use a table slot or a live slot. - Filters, joins and aggregates work across any schemas, from any source.
--schemalets the tables of one schema be written without the schema. It does not hide the other schemas.- Output is a header line, then one tab-separated line per row. A
NULLis an empty cell. The row count goes to standard error asN rows.
axialdb query "SELECT c.segment, SUM(o.amount) FROM app.orders_av AS o JOIN demo_perf.customers_av AS c ON c._id = o.customer_id WHERE o.amount > 0 GROUP BY c.segment"
axialdb query "SELECT 'eu' AS region, COUNT(*) FROM app_eu.orders_av UNION ALL SELECT 'us', COUNT(*) FROM app_us.orders_av"
Two engine tables describe the engine itself, in the reserved schema axialdb: axialdb.tables (with the columns schema_name, table_name, mode, status, source, host_database, host_object and shims) and axialdb.sources.
axialdb query "SELECT schema_name, table_name, mode, status FROM axialdb.tables"
export-tables and create-tables
axialdb export-tables > tables.json
axialdb create-tables < tables.json
These two are the recovery pair. They are described in section 8.
5. Connecting a BI tool
ODBC and JDBC are built on the same client as the command line, so they see the same tables, with the same schema.table names, from every source. Both are read-only. They are in the free download, not an add-on.
- Date, timestamp and decimal columns arrive as real types. A timestamp has no zone, and values are in UTC.
- The engine's client secret is the password. The user name is ignored. An engine on the same machine asks for none; a password that a BI tool sends anyway is not used there.
ODBC
Driver={AxialDB};Server=127.0.0.1;Port=9742;DbIdentity=demo_perf;
For an engine on another machine:
Driver={AxialDB};Server=engine.example.com;Port=9742;DbIdentity=demo_perf;AllowRemote=1;PWD=<client secret>;TlsCa=C:\certs\engine-cert.pem;
| Keyword | Meaning |
|---|---|
Driver | {AxialDB}, the name the driver is registered under. |
Server, Port | The engine's address. The port is 9742 unless you changed it. |
DbIdentity | Optional. The AxialDB schema this connection works in. Schema and Database mean the same. With it, tables of that schema are listed and are named without the schema. Without it, the connection lists the tables of every schema, and a query names a table as schema.table. Either way a query can name a table of any schema as schema.table, so one query can join tables of two schemas. |
AllowRemote=1 | Required to connect to an engine that is not on this machine. |
PWD | The engine's client secret, for an engine on another machine. |
TlsCa | A PEM file with the engine's certificate. It encrypts the connection. |
ConnectTimeout | Seconds to wait for the engine. The default is 10. |
In a DSN, set DbIdentity in the AxialDB setup dialog.
Register the driver once, with the script that ships in its folder. It is registered under the name AxialDB, which is the name Tableau's customization file expects.
- Windows: put the folder where it will stay, then run
.\register-axialdb-odbc.ps1from an elevated PowerShell. Windows records the path ofaxialdb_odbc.dll. If PowerShell answers that the script is not digitally signed, the zip was unpacked with the mark Windows puts on a browser download:Unblock-File .\*.ps1and run it again, orpowershell -ExecutionPolicy Bypass -File .\register-axialdb-odbc.ps1. Unblocking the zip before you unpack it (right-click, Properties, Unblock) avoids this for every file in it. - Linux: install unixODBC, then run
sudo bash register-axialdb-odbc.sh. The driver is copied to/usr/local/lib/axialdb/and registered for every user.
The driver is 64-bit.
JDBC
jdbc:axialdb://engine.example.com:9742
jdbc:axialdb://engine.example.com:9742/demo_perf
- The optional path is the default schema for unqualified names.
schema.tableworks without it. - The driver class is
com.axialdb.jdbc.AxialDbDriver. - The password field is the engine's client secret. The user name is ignored.
- The driver property
tlsCanames a PEM file with the engine's certificate and encrypts the connection. - The driver needs two files side by side:
axialdb-jdbc.jarand the native library (axialdb_jdbc.dllon Windows,libaxialdb_jdbc.soon Linux). It loads the native library from the folder that holds the jar. If that fails, name the file with the Java option-Daxialdb.native=<full path of the library>. Java 17 or later. - A date, timestamp or decimal column comes back as
java.sql.Date,java.sql.Timestamporjava.math.BigDecimal. A timestamp has no zone and the driver does not shift it.
Tableau
Tested by hand with Tableau Desktop on 6.0.0.
- Install the Tableau customization file
axialdb_odbc.tdcthat ships with the ODBC driver: run.\install-tableau-tdc.ps1from the driver's folder (the unblock note above applies to it too), or copy the file yourself. Tableau reads it from theDatasourcesfolder of your Tableau repository, for exampleDocuments\My Tableau Repository\Datasources. Without this file, Tableau does not work well with the driver. - Restart Tableau Desktop completely. Restart it again whenever you replace the .tdc file or the driver.
- Connect, choose Other Databases (ODBC), pick the AxialDB driver. To work in one schema, put
DbIdentity=<schema>;in String Extras; leave it out to see every schema, so a join of a MySQL table and a MongoDB table runs in the engine. For an engine on another machine, the same string also carriesAllowRemote=1;,PWD=andTlsCa=. - Pick tables from the list, or use Custom SQL.
Prefer Live connections: a live worksheet pushes its GROUP BY to the engine, so a large table can be aggregated in the view. Drag-and-drop, Custom SQL, Live and Extract on small tables were the flows tested.
Power BI
Tested by hand with Power BI Desktop on 6.0.0.
- Choose Get Data, then ODBC.
- Give the connection string, a DSN, or the advanced options.
DbIdentity=<schema>;is optional, as for Tableau. - Use Custom SQL, or a narrow query, for large or wide tables.
Power BI Import has no Live mode like Tableau's. Do not Import a full wide table: write an aggregate, or select fewer columns.
DBeaver
Tested by hand on 6.0.0, with the JDBC driver.
Put axialdb-jdbc.jar and the native library in the same folder. Then:
- Open Database, then Driver Manager, then New.
- Class name:
com.axialdb.jdbc.AxialDbDriver. - URL template:
jdbc:axialdb://{host}:{port}. - Libraries: add
axialdb-jdbc.jar. - Make a new connection with the engine's host and port 9742. Any user name works. The password is the engine's client secret, when it has one.
6. The engine on another machine
The engine can run on a machine of its own, and one engine can serve several MySQL servers. The shim's plugin goes on each MySQL server that should create and query tables from inside MySQL, and each server is one [sources.<name>] entry in the engine's config. MongoDB needs nothing: the engine connects out to it.
What crosses the network
- The shim's plugin opens connections to the engine's port, 9742. The rows of a
CREATE TABLE ... ENGINE=AXIALDBgo to the engine over them, and query results come back. - The engine opens connections to MySQL: to the main instance when a live table is created (it reads the source tables' columns and keys and the server's position there, also for a
CREATEtyped in MySQL) and forcreate-tableanddrop-table, and to the main instance or a replica to read the binlog for live tables. It connects out to MongoDB. - Clients (the command line, ODBC, JDBC) connect to the engine's port.
Without TLS all of it is clear text: table rows, query results and SQL. The secrets below are not sent, but someone who can change traffic inside your network could take over a connection after it proved its secret. Turn TLS on unless the machines are on a network you trust the way you trust the machines themselves.
On the engine's machine
[helper]
listen = "0.0.0.0:9742" # or one address of this machine
[access]
client_secret = "a long random text" # for the axialdb command line, ODBC, JDBC
# tls_cert = "engine-cert.pem" # both, to accept TLS
# tls_key = "engine-key.pem"
[sources.orders] # one entry per MySQL server; the name is yours
kind = "mysql"
shim_secret = "another long random text" # only with the shim: what this server's plugin proves
host = "orders-db.example.com"
port = 3306
user = "axialdb"
password = "..."
capture_user = "axialdb_cdc" # live tables only
capture_password = "..."
capture_server_id = 19001 # different for every source
- A connection from the engine's own machine needs no secret.
- A connection from another machine proves a secret before it may do anything. The secret is not sent: the engine sends a random challenge, and the caller answers with a keyed hash of it. The engine answers with a proof of its own, so each side knows the other holds the secret. A wrong secret, or none, is refused.
client_secretopens everything a client does. A source'sshim_secretopens only what that server's plugin does, and only for that source's tables.- Treat
client_secretas an administrator's secret. Whoever holds it can create and drop tables. The engine writes every request that changes something and does not come from a plugin to its log asadmin request, with the caller's address, how it was let in (this machine, client secret, open network), and the table. Queries are not logged. - The engine does not start with
listenbeyond its own machine and no secret in the config.allow_open_network = trueunder[access]turns that check off: anyone who reaches the port may then query, create and drop, and through the engine acts on MySQL under the source's account. Use it on a closed lab network only. - With
tls_certandtls_keyset, every connection from another machine has to use TLS: each plugin (tls = true) and each client. The engine's own machine may still connect in the clear. - Open port 9742 on the engine's machine for the MySQL servers and the clients. On each MySQL server, let the engine's machine reach MySQL's port, and create the accounts for its address with the grants listed in section 2. Restart the engine after changing its config.
On each MySQL server with the shim
Install the plugin, the bridge and the helper functions, as the MySQL shim package's guide says. There is no engine and no axialdb.toml on this machine. The server has a small file of its own, axialdb-shim.toml, found through the AXIALDB_CONFIG variable of the MySQL service:
engine = "engine.example.com:9742" # the engine's machine
source = "orders" # this server's [sources.<name>] on the engine
secret_file = "/etc/axialdb/shim.secret" # one line: the source's shim_secret
# tls = true
# tls_ca = "/etc/axialdb/engine-cert.pem" # the engine's certificate, or the authority that signed it
query_timeout_secs = 300
publish_retry_secs = 15
log_level = "info"
- With the engine on the same machine,
engineis127.0.0.1:9742and the secret file is not read. - A key the file does not have is an error. If you point
AXIALDB_CONFIGat the engine's config by mistake, the message says so. - A server whose
sourceis not a MySQL source of the engine is refused, with a message that names it.
Linux:
sudo mkdir -p /etc/axialdb
sudo cp axialdb-shim.toml /etc/axialdb/axialdb-shim.toml # then edit engine and source
printf '%s\n' 'another long random text' | sudo tee /etc/axialdb/shim.secret >/dev/null
sudo chown mysql:mysql /etc/axialdb/shim.secret
sudo chmod 600 /etc/axialdb/shim.secret
sudo mkdir -p /etc/systemd/system/mysql.service.d
sudo tee /etc/systemd/system/mysql.service.d/axialdb.conf >/dev/null <<'EOF'
[Service]
Environment=AXIALDB_CONFIG=/etc/axialdb/axialdb-shim.toml
EOF
sudo systemctl daemon-reload
sudo systemctl restart mysql
Windows: put axialdb-shim.toml and shim.secret in C:\ProgramData\AxialDB\mysql\. Keep the Machine variable AXIALDB_CONFIG at C:\ProgramData\AxialDB. Let only the MySQL service account and administrators read shim.secret. Restart MySQL.
Check from MySQL:
SELECT axialdb_init();It opens a connection to the engine, proves the secret, and says which source this server is. An error names what is wrong: the engine cannot be reached, the secret does not match, or the source is not in the engine's config.
One source is one server
The engine learns a server's id (@@server_uuid) from its plugin. Two servers must not use the same source name. Every table records the server it was made on, snapshot and live alike. While a source has any table, the engine refuses a different server under that name and names both ids. After a main instance has really been replaced by a server with a new id, drop the old server's tables, snapshot tables too, and create them again (section 8). A server restored with its data folder keeps its id and needs none of this.
Clients on another machine
| Client | The secret | TLS |
|---|---|---|
axialdb command line | --secret-file path, or the environment variable AXIALDB_SECRET. No flag for the secret itself. | --tls-ca path |
| ODBC | PWD= in the DSN or the connection string, with AllowRemote=1. The user name is ignored. | TlsCa=path |
| JDBC | The password field. The user name is ignored. | The driver property tlsCa |
tls_ca is a PEM file with the engine's certificate, or with the certificate of the authority that signed it. A self-signed certificate works. The certificate has to name the address the clients use for the engine (a DNS name or an IP address in its subject alternative names).
When the connection is lost during a create in MySQL
- Before MySQL has committed the
CREATE TABLE: the statement fails and MySQL has no table. The engine removes what it had received. - After MySQL has committed and before the engine was told to publish: the plugin keeps trying for
publish_retry_secs. If that fails too, the statement ends with an error that says the table is created in MySQL and not published. The next statement that uses the table publishes it. Until then a query on it fails with a message, andDROP TABLEremoves both halves. When nobody uses the table, the engine settles it itself about once an hour, which takes between one and two hours after the create.
Upgrades
The plugin and the engine do not have to be replaced in the same moment. The plugin tells the engine which protocol it speaks. When the two differ, the engine refuses that server's plugin with a message that says which side is older. The release notes say when the protocol changes and which side to upgrade first.
7. Statuses, and what to do
A live table reports its state in the status column of axialdb.tables. A source reports the state of its capture in axialdb sources.
axialdb query "SELECT schema_name, table_name, mode, status FROM axialdb.tables"
axialdb sources
| Status | Does it answer? | What it means, and what to do |
|---|---|---|
healthy | Yes | Nothing to do. For a live table it is current with what capture has read. |
catching_up, with a percentage | No, until healthy | The table is being built. Wait. A query on the table, MySQL or MongoDB, fails until it is healthy. |
updating N% of a change of R rows | Yes, from the table as it was before the change | A large change to its source (10,000 rows or more) is being applied, or waits for its turn. The change shows all at once when it is applied. Wait. |
updating (receiving a change: R rows so far) | Yes, as above | A large transaction on its source is still arriving. Wait. |
stale since <time> | Yes, from the last data received | The table's source cannot be reached. The engine keeps retrying, and the table returns to healthy by itself. Check the source, the network, and axialdb sources, where the capture state reads down since <time>: <reason>. |
failed: <reason>, a statement on the source | No | The source table was truncated, dropped or renamed, its database was dropped, or a column the table uses was dropped or changed, or the primary key changed. The status names the statement. Drop the table and create it again. |
failed: <reason>, a change that cannot be applied | No | A change could not be applied, or on MongoDB a value no longer converts to its declared type. The status names the document, the column and the value. Fix the data or the cast, then drop the table and create it again. |
failed: <reason>, the stream cannot continue | No | On MongoDB, the collection or database was dropped or renamed, or the engine was down for longer than the oplog keeps, so the change stream cannot resume. Drop the table and create it again. |
failed: source '...' is not in the engine config | Yes, from the last data | The source was removed from axialdb.toml. Put it back and the status clears, or drop the table. A BI tool is not cut off at once. |
What a live MySQL table keeps following
Adding a column, adding or changing an index, changing a column the table does not use, another table, a view, a user, and CREATE DATABASE leave a live table following its source. A statement stops a table only when it can change the table's rows or the columns it uses.
Capture states in axialdb sources
ok: capture is running (or has nothing to capture).not configured: the source has nocapture_user. Snapshots work; a live create is refused.down since <time>: <reason>: capture cannot reach the source or the server settings are wrong. The reason names which. Tables of that source showstale since.
Common messages and what to do
- "already exists": the name is taken. Drop the table first, or separate the sources with naming keys or
--schema. - "capture is not configured for source ...": set
capture_userandcapture_passwordfor that source and restart the engine. - A command line error that names a flag: the command does not take that flag. Run
axialdb --help. - A MongoDB create refused: the message names the path or construct and shows what to write (a
CAST, a literal,AS, the database inFROM).
8. Recovery
Everything in the engine is derived data. A table can be built again from its definition and its source, so only the list of definitions has to survive. AxialDB has a manual recovery procedure. It is manual, and supported. There is no automatic failover.
Save the definitions
axialdb export-tables > tables.jsonThis writes one entry for every table that came from a source: source, table (the name as create-table takes it), schema only when --schema was given, mode and query. Keep the file off the engine's machine, for example with a nightly job. If the engine's machine is lost, the file is what you need.
Create the tables again
axialdb create-tables < tables.json- It creates each table through the engine, as
create-tablewould, and prints each result. - A table that fails does not stop the others. A line on standard error names it and says why, and the last line reads
N created, M failed. If any failed, the exit code is 1. - Names stay. They are computed from the same config, so each table comes back with the same
schema.table. BI workbooks and ODBC or JDBC connections need no change. - The cost is time, not data. Each table is unavailable while it is copied again in full.
- A file written by a build before 6.0.0 with MongoDB entries is refused, because its
FROMnames no database. Export again with the new build.
After a MySQL main server is lost
- Promote the replica, or restore the server. When the server had the shim, install the shim's plugin on the new main instance.
- In the engine's config, point the source at the new main instance, and capture at its new replica (or at the new main instance when there is no replica yet).
- Restart the engine and drop the old tables, which can no longer be followed.
- Run
axialdb create-tables < tables.json.
If the new server has a new id, the old server's tables, snapshot tables too, are dropped first (section 6, "One source is one server"). A change log follows one MySQL server: a live create from a new server is refused while live tables of the old one are left.
After the engine's machine is lost
Install a new engine, restore axialdb.toml, and run create-tables from the saved file. The same steps move tables to another engine, or to another server in a planned move.