Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Error Reference

AxiomDB returns structured errors with a SQLSTATE code, a human-readable message, and optional detail fields. Understanding these codes allows applications to handle specific failure scenarios correctly (for example: catching a uniqueness violation to show a “email already taken” message rather than a generic crash page).


Error Format

Every error from AxiomDB is represented as an ErrorResponse struct with these fields:

FieldTypeAlways present?Description
sqlstatestring (5 chars)YesSQLSTATE code for programmatic handling (e.g. "23505")
severitystringYes"ERROR", "WARNING", or "NOTICE"
messagestringYesShort human-readable description. Do not parse this — use sqlstate
detailstringSometimesExtended context about the failure (offending value, referenced row)
hintstringSometimesActionable suggestion for how to fix the error
positionintegerSometimes0-based byte offset of the unexpected token in the SQL string (parse errors only)
{
  "sqlstate": "23505",
  "severity": "ERROR",
  "message": "unique key violation on index 'users_email_idx'",
  "detail": "Key (value)=(alice@example.com) is already present in index users_email_idx.",
  "hint": "A row with the same value already exists in index users_email_idx. Use INSERT ... ON CONFLICT to handle duplicates."
}
{
  "sqlstate": "42601",
  "severity": "ERROR",
  "message": "SQL syntax error: unexpected token 'FORM'",
  "position": 9
}

Always use sqlstate for programmatic handling. The message text may change between versions; SQLSTATE codes are stable.

When using the MySQL wire protocol, the error is delivered as a MySQL error packet with the SQLSTATE code in the sql_state field (5 bytes following the # marker).

JSON Error Format

For clients that need structured errors without screen-scraping message strings, AxiomDB supports a JSON error format that carries all ErrorResponse fields in the MySQL ERR packet:

SET error_format = 'json';

After this, every ERR packet carries a JSON string instead of plain text:

{"code":1064,"sqlstate":"42601","severity":"ERROR","message":"SQL syntax error: unexpected token 'FORM'","position":9}
{"code":1062,"sqlstate":"23505","severity":"ERROR","message":"unique key violation on index 'users_email_idx'","detail":"Key (value)=(alice@example.com) is already present in index users_email_idx."}

Reset to plain text with SET error_format = 'text'. This setting is per-connection and does not persist after disconnect.

⚙️
Design Decision — JSON on MySQL Wire The MySQL wire protocol has no structured error field beyond a plain string message. AxiomDB encodes the full ErrorResponse as a JSON string in that message field when error_format = 'json' is set. This mirrors how PostgreSQL's ErrorResponse packet carries detail, hint, and position in separate fields — achieving the same semantics over MySQL's more limited protocol.

Integrity Constraint Violations (Class 23)

These errors indicate that an INSERT, UPDATE, or DELETE violated a declared constraint. The application should handle them and return a user-facing message.

💡
Constraint Enforcement Status (Phase 4.16) The following constraints are parsed and stored in the schema but are not yet enforced at INSERT/UPDATE time:
  • NOT NULL — declared columns accept NULL without error
  • UNIQUE — duplicate values are allowed
  • CHECK — expressions are not evaluated at write time
As a result, 23502, 23505, and 23514 are not raised by DML in the current release. Enforcement will be added in a future phase. PRIMARY KEY uniqueness is enforced via the B+ tree index.

23505 — unique_violation

A row with the same value already exists in a column or set of columns declared UNIQUE or PRIMARY KEY.

CREATE TABLE users (email TEXT NOT NULL UNIQUE);
INSERT INTO users VALUES ('alice@example.com');
INSERT INTO users VALUES ('alice@example.com');  -- ERROR 23505

The error message identifies both the index and the offending value:

Duplicate entry 'alice@example.com' for key 'users_email_uq'

The detail field (available in JSON format) provides a PostgreSQL-style message:

Key (value)=(alice@example.com) is already present in index users_email_uq.

Typical application response: Show “An account with this email already exists.”

try:
    db.execute("INSERT INTO users (email) VALUES (?)", [email])
except AxiomDbError as e:
    if e.sqlstate == '23505':
        return {"error": "Email already taken"}
    raise

23503 — foreign_key_violation

Child insert / update — parent key does not exist

An INSERT or UPDATE references a value in the FK column that has no matching row in the parent table.

INSERT INTO orders (user_id, total) VALUES (99999, 100);
-- ERROR 23503: Foreign key constraint fails: 'orders.user_id' = '99999'

Typical response: Validate that the referenced entity exists before inserting, or surface “Referenced record not found.”

Parent delete — children still reference it (RESTRICT / NO ACTION)

A DELETE on the parent table was blocked because child rows reference the row being deleted and the FK action is RESTRICT or NO ACTION (the default).

-- orders.user_id REFERENCES users(id) ON DELETE RESTRICT
DELETE FROM users WHERE id = 1;
-- ERROR 23503: foreign key constraint "fk_orders_user": orders.user_id references this row

Typical response: Either delete child rows first, use ON DELETE CASCADE, or prevent parent deletion in the application layer.

Cascade depth exceeded

A chain of ON DELETE CASCADE constraints exceeded the maximum depth of 10 levels.

-- If table chain A→B→C→...→K (11 levels all with CASCADE) and you delete from A:
DELETE FROM a WHERE id = 1;
-- ERROR 23503: foreign key cascade depth exceeded limit of 10

Typical response: Restructure the schema to reduce cascade depth, or perform the deletes manually level-by-level.

SET NULL on a NOT NULL column

ON DELETE SET NULL is defined on a foreign key column that was declared NOT NULL.

-- orders.user_id is NOT NULL, but ON DELETE SET NULL is declared
DELETE FROM users WHERE id = 1;
-- ERROR 23503: cannot set FK column orders.user_id to NULL: column is NOT NULL

Typical response: Either remove the NOT NULL constraint from the FK column, or change the action to ON DELETE RESTRICT or ON DELETE CASCADE.

23502 — not_null_violation

An INSERT or UPDATE attempted to store NULL in a NOT NULL column.

INSERT INTO users (name, email) VALUES (NULL, 'bob@example.com');
-- ERROR 23502: null value in column "name" violates not-null constraint

Typical application response: Validate required fields on the client before submitting.

23514 — check_violation

A row failed a CHECK constraint.

INSERT INTO products (name, price) VALUES ('Widget', -5.00);
-- ERROR 23514: new row for relation "products" violates check constraint "chk_price_positive"

Startup / Open Errors

These errors happen before a SQL statement runs. They are returned by Db::open(...), Db::open_dsn(...), AsyncDb::open(...), or server startup, so there is no SQLSTATE-bearing result set yet.

IndexIntegrityFailure — open refused because an index is not trustworthy

On every open, AxiomDB now verifies each catalog-visible index against the heap-visible rows reconstructed after WAL recovery.

  • If an index is readable but missing entries or contains extra entries, AxiomDB rebuilds it automatically before accepting traffic.
  • If the index tree cannot be traversed safely, open fails with DbError::IndexIntegrityFailure.

Example Rust handling:

#![allow(unused)]
fn main() {
match axiomdb_embedded::Db::open("./data.db") {
    Ok(db) => { /* ready */ }
    Err(axiomdb_core::DbError::IndexIntegrityFailure { table, index, reason }) => {
        eprintln!("database refused to open: {table}.{index}: {reason}");
    }
    Err(other) => return Err(other),
}
}
⚙️
Design Decision — Repair What Is Readable AxiomDB borrows PostgreSQL amcheck's “fail if the tree is not safely readable” discipline, but borrows SQLite's “rebuild from table contents” recovery idea for readable divergence. A readable-but-wrong index is rebuilt automatically; an unreadable tree blocks open.

Cardinality Errors (Class 21)

21000 — cardinality_violation

A scalar subquery returned more than one row. Scalar subqueries (a SELECT used where a single value is expected) must return exactly one row. Zero rows yield NULL; more than one row is an error.

-- Suppose users contains Alice and Bob
SELECT (SELECT name FROM users) AS single_name FROM orders;
-- ERROR 21000: subquery must return exactly one row, but returned 2 rows

Fix: add a WHERE condition that makes the result unique, or use LIMIT 1 if you intentionally want only the first row:

-- Safe: guaranteed single row via primary key
SELECT (SELECT name FROM users WHERE id = o.user_id) AS customer_name
FROM orders o;

-- Safe: explicit LIMIT 1 when you want "any one" result
SELECT (SELECT name FROM users ORDER BY created_at LIMIT 1) AS oldest_user
FROM config;
try:
    db.execute("SELECT (SELECT name FROM users) FROM orders")
except AxiomDbError as e:
    if e.sqlstate == '21000':
        # The subquery returned multiple rows — add a WHERE clause
        ...

Undefined Object Errors (Class 42)

These errors indicate a reference to an object (table, column, index) that does not exist in the catalog. They are typically programming errors caught in development.

42P01 — undefined_table

A statement referenced a table or view that does not exist.

SELECT * FROM nonexistent_table;
-- ERROR 42P01: relation "nonexistent_table" does not exist

42703 — undefined_column

A statement referenced a column that does not exist in the specified table.

SELECT typo_column FROM users;
-- ERROR 42703: column "typo_column" does not exist in table "users"

42P07 — duplicate_table

CREATE TABLE was called for a table that already exists (without IF NOT EXISTS).

CREATE TABLE users (...);
CREATE TABLE users (...);
-- ERROR 42P07: relation "users" already exists

42701 — duplicate_column

ALTER TABLE ... ADD COLUMN was called for a column that already exists in the table.

CREATE TABLE users (id BIGINT PRIMARY KEY, email TEXT NOT NULL);
ALTER TABLE users ADD COLUMN email TEXT;
-- ERROR 42701: column "email" already exists in table "users"

Fix: Use a different column name, or check the current schema with DESCRIBE users before adding the column.

42702 — ambiguous_column

An unqualified column name appears in multiple tables in the FROM clause.

-- Both users and orders have a column named "id"
SELECT id FROM users JOIN orders ON orders.user_id = users.id;
-- ERROR 42702: column reference "id" is ambiguous

-- Fix: qualify the column
SELECT users.id FROM users JOIN orders ON orders.user_id = users.id;

Database Catalog Errors

These errors are surfaced primarily through the MySQL wire protocol when a client uses CREATE DATABASE, DROP DATABASE, USE, the handshake database, or COM_INIT_DB.

1049 (42000) — Unknown database

The requested database does not exist in the persisted catalog.

USE missing_db;
-- ERROR 1049 (42000): Unknown database 'missing_db'

This same error is returned if a client connects with database=missing_db in the initial MySQL handshake.

Fix: create the database first with CREATE DATABASE missing_db, or switch to an existing one from SHOW DATABASES.

1007 (HY000) — Database already exists

CREATE DATABASE was called for a name already present in the catalog.

CREATE DATABASE analytics;
CREATE DATABASE analytics;
-- ERROR 1007 (HY000): Can't create database 'analytics'; database exists

Fix: choose a different name, or treat the existing database as reusable.

1105 (HY000) — Active database cannot be dropped

The current connection attempted to drop the database it has selected.

USE analytics;
DROP DATABASE analytics;
-- ERROR 1105 (HY000): Can't drop database 'analytics'; database is currently selected

Fix: switch to another database such as axiomdb, then run DROP DATABASE.


Transaction Errors (Class 40)

40001 — serialization_failure

A concurrent write conflict was detected. The transaction must be retried.

-- Two transactions try to update the same row simultaneously.
-- The second one receives:
-- ERROR 40001: could not serialize access due to concurrent update

The application must catch this and retry the transaction. This is normal and expected behavior under high concurrency, not a bug.

40P01 — deadlock_detected

Two transactions are each waiting for a lock held by the other.

-- Txn A holds lock on row 1, waiting for row 2
-- Txn B holds lock on row 2, waiting for row 1
-- → AxiomDB detects the cycle and aborts one transaction with 40P01
-- ERROR 40P01: deadlock detected

Prevention: Access rows in a consistent order across all transactions. If you always acquire locks on (accounts with lower id) before (accounts with higher id), deadlocks cannot form between two such transactions.


I/O and System Errors (Class 58)

58030 — io_error

The storage engine encountered an operating system I/O error.

ERROR 58030: could not write to file "axiomdb.db": No space left on device

Possible causes:

  • Disk full — free space or expand the volume
  • File permissions — ensure the AxiomDB process can write to the data directory
  • Hardware error — check dmesg / system logs for disk errors

Syntax and Parse Errors (Class 42)

42601 — syntax_error

The SQL statement is not syntactically valid.

SELECT FORM users;  -- 'FORM' is not a keyword
-- ERROR 42601: syntax error at or near "FORM"
-- Position: 8

42883 — undefined_function

A function name was called that does not exist.

SELECT unknown_function(1);
-- ERROR 42883: function "unknown_function" does not exist

Data Errors (Class 22)

22001 — string_data_right_truncation

A TEXT or VARCHAR value exceeds the column’s declared length.

CREATE TABLE codes (code CHAR(3));
INSERT INTO codes VALUES ('TOOLONG');
-- ERROR 22001: value too long for type CHAR(3)

22003 — numeric_value_out_of_range

A numeric value exceeds the range of its declared type.

INSERT INTO users (age) VALUES (99999);  -- age is SMALLINT
-- ERROR 22003: integer out of range for type SMALLINT

22012 — division_by_zero

Division by zero in an arithmetic expression.

SELECT 10 / 0;
-- ERROR 22012: division by zero

22018 — invalid_character_value_for_cast

A value cannot be implicitly coerced to the target type. This error is raised when AxiomDB is in strict mode (the default) and a conversion is attempted that would discard data or is not defined.

-- Text with non-numeric characters inserted into an INT column (strict mode):
INSERT INTO users (age) VALUES ('42abc');
-- ERROR 22018: cannot coerce '42abc' (Text) to INT: '42abc' is not a valid integer

-- A type pair with no implicit conversion:
SELECT 3.14 + DATE '2026-01-01';
-- ERROR 22018: cannot coerce 3.14 (Real) to Date: no implicit numeric promotion between these types

Hint: Use explicit CAST for conversions that AxiomDB does not apply automatically:

INSERT INTO users (age) VALUES (CAST('42' AS INT));   -- explicit — always works
SELECT CAST(3 AS REAL) + 1.5;                         -- explicit widening

Permissive mode: if your application requires MySQL-style lenient coercion ('42abc' silently converted to 42), disable strict mode for the session:

SET strict_mode = OFF;   -- or: SET sql_mode = ''

In permissive mode, failed coercions fall back to a best-effort conversion and emit warning 1265 instead of returning 22018. Use SHOW WARNINGS after bulk loads to audit any truncated values. See Strict Mode for full details.

Implicit coercions that always succeed (no error)

The following conversions happen automatically without raising 22018:

FromToExample
INTBIGINT1 + 9999999999BIGINT
INTREAL5 + 1.5Real(6.5)
INTDECIMAL2 + 3.14Decimal(5.14)
BIGINTREAL100 + 1.5Real(101.5)
BIGINTDECIMAL100 + 3.14Decimal(103.14)
BIGINTINTonly if value fits in INT range
TEXTINT / BIGINT'42'42 (strict: entire string must be a number)
TEXTREAL'3.14'3.14
TEXTDECIMAL'3.14'Decimal(314, 2)
DATETIMESTAMPmidnight UTC of the given date
NULLanyalways passes through as NULL

Connection Protocol Errors (Class 08)

MySQL 1153 / 08S01 — ER_NET_PACKET_TOO_LARGE

Returned when an incoming MySQL logical command payload exceeds the connection’s current max_allowed_packet limit.

ERROR 1153 (08S01): Got a packet bigger than 'max_allowed_packet' bytes

What triggers it:

  • A COM_QUERY whose SQL text exceeds @@max_allowed_packet bytes.
  • A COM_STMT_PREPARE or COM_STMT_EXECUTE packet above the limit.
  • A HandshakeResponse41 above the default 64 MiB limit (rare in practice).
  • A multi-packet logical command whose total reassembled payload exceeds the limit, even if each individual physical fragment is below the limit.

What happens after the error: The server closes the connection immediately. The stream cannot be safely reused because the framing layer cannot determine where the next command begins.

Fix: Raise max_allowed_packet before sending the large command:

SET max_allowed_packet = 134217728;  -- 128 MiB

Or reconnect after the error — the new connection starts with the server default.

💡
Session Scope SET max_allowed_packet affects only the current connection. Use it before any statement whose payload may be large (e.g., bulk INSERT with many values, or a BLOB upload via COM_STMT_EXECUTE).

Disk-Full Errors (Class 53)

53100 — disk_full

Returned when the OS reports that the volume is full (ENOSPC) or over quota (EDQUOT) during a durable write — a WAL append, WAL fsync, storage grow, or mmap flush.

ERROR 53100: disk full during 'wal commit fsync': no space left on device
HINT: The database volume is full or over quota. Free disk space and restart
      the server to restore write access. The database is now in read-only
      degraded mode.

What happens after the error:

AxiomDB enters read-only degraded mode immediately. In this mode:

Statement typeAllowed?
SELECT, SHOW, EXPLAIN✅ Yes
SET (session variables)✅ Yes
INSERT, UPDATE, DELETE, TRUNCATE❌ No — returns 53100
CREATE TABLE, DROP TABLE, DDL❌ No — returns 53100
BEGIN, COMMIT, ROLLBACK❌ No — returns 53100

The mode persists until the server process is restarted. There is no way to return to read-write mode without restarting.

Fix:

  1. Free disk space or remove the quota restriction.
  2. Restart the server — AxiomDB will reopen in read-write mode if space is available.
💡
Reads Are Always Safe In degraded mode, all read traffic continues uninterrupted. Applications can continue serving queries while the operator resolves the disk space issue — no connection restart needed for reads.

Complete SQLSTATE Reference

SQLSTATENameCommon Cause
21000cardinality_violationScalar subquery returned more than 1 row
23505unique_violationDuplicate value in UNIQUE / PK column
23503foreign_key_violationReferencing non-existent FK target
23502not_null_violationNULL inserted into NOT NULL column
23514check_violationRow failed a CHECK constraint
40001serialization_failureWrite-write conflict; retry the txn
40P01deadlock_detectedCircular lock dependency
42P01undefined_tableTable does not exist
42703undefined_columnColumn does not exist
42702ambiguous_columnUnqualified column name is ambiguous
42P07duplicate_tableTable already exists
42701duplicate_columnColumn already exists in table
42601syntax_errorMalformed SQL
42883undefined_functionUnknown function name
22001string_data_right_truncationValue too long for column type
22003numeric_value_out_of_rangeNumber exceeds type bounds
22012division_by_zeroDivision by zero in expression
22018invalid_character_value_for_castImplicit type coercion failed
22P02invalid_text_representationInvalid literal value
42501insufficient_privilegePermission denied on object
42702ambiguous_columnUnqualified column matches in 2+ tables
42804datatype_mismatchType mismatch in expression
25001active_sql_transactionBEGIN inside an active transaction
25P01no_active_sql_transactionCOMMIT/ROLLBACK with no active transaction
25006read_only_sql_transactionTransaction expired
0A000feature_not_supportedSQL feature not yet implemented
08S01connection_failure (MySQL ext)Incoming packet exceeds max_allowed_packet
53100disk_fullStorage volume is full
58030io_errorOS-level I/O failure (disk, permissions)