Limitations
ElyraSQL 1.0 is stable and broadly MySQL-compatible. This page is an honest inventory of what is not yet implemented (or differs from MySQL), so you can judge fit before deploying.
SQL surface
- Subqueries (
WHEREand SELECT-list, uncorrelated and correlated, including over joins), derived tables, CTEs (WITH, includingWITH RECURSIVE),HAVING, window functions with explicitROWS/RANGEframes, set operations, andFROM-lessSELECTare supported. - Stored procedures support
IN/OUT/INOUTparameters, session@uservariables (SET @x = ...), local variables (DECLARE,SET), and control flow:IF/ELSEIF/ELSE,WHILE,LOOP,REPEAT ... UNTIL, with labeledLEAVE/ITERATE.OUT/INOUTarguments must be@uservariables (written back on return). Cursors (DECLARE ... CURSOR FOR,OPEN,FETCH ... INTO,CLOSE) and condition handlers (DECLARE {CONTINUE|EXIT} HANDLER FOR {NOT FOUND | SQLEXCEPTION | SQLSTATE '...' | <code>} <action>) are supported; a handler action is a single statement (not aBEGIN ... ENDblock), and handlers are scoped to the whole procedure body.OPENbuffers the cursor's full result set in memory (it is not a streaming server-side cursor), so cursors are intended for the modest result sets typical of procedural logic, not for iterating huge tables. - Row-level triggers are supported:
CREATE TRIGGER name {BEFORE|AFTER} {INSERT|UPDATE|DELETE} ON t FOR EACH ROW <body>, withNEW.col/OLD.col. BEFORE bodies supportSET NEW.col = expr; AFTER bodies run arbitrary DML. Firing is depth-guarded against runaway recursion. Triggers fire on single-table INSERT/UPDATE/DELETE (not on multi-table or the upsert variants REPLACE/ON DUPLICATE/IGNORE). - Materialized views:
CREATE MATERIALIZED VIEW v AS <select>stores the result as a real table;REFRESH MATERIALIZED VIEW vrecomputes it;DROP MATERIALIZED VIEW vremoves it. Views auto-refresh on read when a base table has changed since the last refresh (detected via per-table write counters); this is a full recompute, not incremental delta maintenance. - Named windows are supported:
... OVER w ... WINDOW w AS (PARTITION BY ... ORDER BY ...), includingOVER (w ...)inheriting a named window. - Numeric value-offset
RANGEframes and peer-offsetGROUPSframes are supported. TemporalRANGEoffsets are not yet supported. Other gaps include correlated subqueries combined with aggregation over a join, user-defined functions, and events. INSERT ... SET col = val, ...(MySQL shorthand) is supported — it is rewritten toINSERT ... (cols) VALUES (...)before parsing, includingON DUPLICATE KEY UPDATE.- Comma-style multi-table
UPDATE t1, t2 SET ... WHERE ...is supported — it is rewritten toUPDATE t1 CROSS JOIN t2 SET ... WHERE ...(the WHERE supplies the join condition) before parsing. GROUP BY ... WITH ROLLUPis supported: it adds a subtotal row for each grouping prefix and a grand-total row (dropped group columns are NULL), re-aggregating base rows per level soAVG/MIN/MAXstay correct.ORDER BY/LIMITapply to the combined result (NULLs sort first).- All the bitwise operators are supported:
&,|,^,<<,>>, and unary~. They compute on 64-bit unsigned integers and returnBIGINT UNSIGNED(Value::UInt), matching MySQL exactly — e.g.~5is18446744073709551610and1 << 63is9223372036854775808. Unsigned integer arithmetic (+,-,*,%) on such values is also exact. (~is bridged by rewriting~xto(x ^ 18446744073709551615)since no SQL dialect parses the prefix.) - Supported beyond the basics: multi-table
UPDATE/DELETEviaJOIN,INSERT ... SELECT,CREATE TABLE ... AS SELECT,COUNT(DISTINCT ...),UNION ALL/INTERSECT/EXCEPT,WITH RECURSIVE, row/tupleIN, window functions incl.LAG/LEAD/NTILE/FIRST_VALUE/LAST_VALUE/NTH_VALUE, the<=>null-safe operator,IS [NOT] TRUE/FALSE/UNKNOWN,LAST_INSERT_ID()/ROW_COUNT(),@@system variables,CONVERT(),MD5/SHA*/SOUNDEX/REGEXP_REPLACE, and statistical/bitwise aggregates.
Constraints & integrity
- Enforced:
PRIMARY KEY,UNIQUE,NOT NULL,CHECK, andFOREIGN KEY. - Foreign keys reference a primary key or unique index; both
ON DELETEandON UPDATERESTRICT/NO ACTION/CASCADE/SET NULLare enforced. - Multi-level
ON DELETEcascades are supported, including self-referencing hierarchies. Delete cascades run to a fixed point with cycle detection and a depth guard.ON UPDATEcascades are currently single-level, and deferred constraint checking is not yet supported.
Query planning
- Range scans and index nested-loop joins are single-column; composite ranges fall back to a scan.
- Equi joins (INNER/LEFT/RIGHT) use a hash join with a cost-based build side
(the smaller relation for INNER; an index nested-loop join when the driving
side is small and the partner is indexed). Large INNER equi-joins whose inputs
are already sorted on the join key (e.g. clustered primary-key scans) use a
streaming merge join (no hash table, ordered output). Non-equi joins pair
every row and apply the
ONcondition per pair, streamed (see below);FULLjoins use a materialising nested loop. - Explicit INNER-join chains over base tables are reordered cost-based: the planner drives from the smallest relation and always extends along an equi-join predicate, keeping intermediate results small. Reordering is alias-aware and applies only when every join is a single equi-connector; outer joins, non-equi/multi-condition ON, and derived tables keep the written order.
ANALYZE TABLErecords row-count and per-column statistics (NDV, null count, min/max, and an equi-height histogram built from a reservoir sample), surfaced asinformation_schema.tables.TABLE_ROWSandinformation_schema.column_statistics(including a JSONHISTOGRAM). The planner estimates WHERE-predicate selectivity from the histograms to order comma cross-joins by estimated (not just raw) row counts, reorders explicit INNER-join chains cost-based, and picks hash-join build sides by live size. Multi-column/correlated histograms are not modelled.- Indexed
ORDER BY ... LIMIT(top-N without a sort). With noWHEREfilter, an orderedLIMITis served by an ordered index/clustered walk that stops afterOFFSET + LIMITrows — no full scan, no sort:ORDER BY <primary-key prefix> ASC|DESC LIMIT nwalks the clustered keyspace forward (ASC) or backward (DESC).ORDER BY <indexed column(s)> ASC|DESC LIMIT nwalks a secondary index in key order and follows each entry to its row. ACOLLATEoverride or an expression key skips it. A composite index must have every columnNOT NULL(indexes omit NULL tuples, so a NULL in any key column would drop a row from the walk).- A primary-key tiebreaker stays on the fast path: since a non-unique
secondary index stores
(value, clustered pk),ORDER BY <indexed col> [DESC], id [DESC](a grid's stable-sort tiebreaker) walks the index directly. All terms must share a direction and any trailing terms must be the primary-key columns in order. - A nullable single-column index is fully supported in both directions
(including with a PK tiebreaker). Single-column indexes built on 1.4.7+ store
NULL-keyed rows under a companion
indexnull::keyspace, so the ordered walk is a complete MySQL ordering — NULLs first forASC, last forDESC, each ordered by the clustered PK — with no data scan and no fallback. (Indexes built before 1.4.7 use the older NULL scan / sorter fallback until rebuilt; a composite index still requires every column to beNOT NULL.) The NULL block is fetched by a budgeted clustered scan; if NULLs are so rare that the budget (ELYRASQL_ORDER_SCAN_BUDGET) is exhausted before the block is known (mainly anASCconcern), it falls back to the sorter below. - A
WHEREfilter is applied as a residual during the ordered walk, so a filtered grid page (WHERE ... ORDER BY <col> LIMIT n) is still served without a full sort. To stay safe when the filter is very selective, the walk is capped by the same examine budget; if it cannot fillnrows within budget it falls back to the sorter below (a selective filter has few matches, so that sort is cheap). - Deep
OFFSETon the fast path (no residual filter) steps over the leadingOFFSETrows at the index/clustered level without reading their rows, so paging deep into a result stays cheap (index steps, not row reads). With a residual filter the pre-offset rows must still be read to be counted. - An ordered
LIMITinside a transaction falls back to the sorter below (correct, not yet index-accelerated).
- Single-table
ORDER BY(the fallback) is memory-bounded:ORDER BY ... LIMITuses a top-N heap and large unbounded sorts spill sorted runs to temp files (external merge sort,ELYRASQL_SORT_MAX_ROWS). This spilling path now runs inside transactions too (streaming the snapshot+overlay via the session cursor), not just in autocommit.GROUP BYwith many distinct groups uses partitioned spill aggregation (rows routed to partitions by group-key hash, spilled to temp files); when column statistics predict a large group count the planner goes straight to the spilling path instead of running the in-memory pass, hitting the cap, and re-scanning (which previously cost two full scans). A single skewed partition pastELYRASQL_GROUP_MAX_GROUPSstill errors. When a table has no statistics (neverANALYZEd) and turns out to have a huge group count, the in-memory pass can still overflow and fall back to the spilling path, costing a second scan; runningANALYZE TABLEavoids this. Spill files are read back with a size-guarded length prefix, so a corrupt temp file is rejected rather than triggering a giant allocation, and stale spill files left by a killed process (SIGKILL) are reclaimed at startup (only files owned by confirmed-dead PIDs are removed). - Join +
GROUP BYon an indexed partner streams: for the common shapeFROM driving JOIN partner ON driving.k = partner.<pk|indexed> [WHERE] GROUP BY ...(INNER or LEFT), the driving table is scanned incrementally, the partner is probed by index, and joined rows feed the spilling aggregator directly -- so a large fact-to-dimension join with grouping is bounded by the group state (which spills), not the join output size. Also streamed: the same join shape withLIMITand no grouping (early-stop index nested-loop). - Left-deep
JOINchains stream (two or more tables): each partner is built into a hash table and the driving table is scanned incrementally; joined rows feed straight into the spilling sorter (ORDER BY, top-N heap / external merge) or the spilling aggregator (GROUP BY). The join output is never fully materialised, so a large fact-to-dimensions join with sorting or grouping is bounded by the partner hash tables plus the sorter/aggregator, not|driving| x fanout. INNER, LEFT and RIGHT are supported (autocommit only). A two-tableRIGHT JOINstreams by rewriting it to the equivalentLEFT JOINwith the output columns reordered back to the query's order. - INNER comma joins stream too:
FROM a, b, c WHERE a.k = b.k AND b.j = c.jis normalised to an explicitJOINchain (using the WHERE equi-predicates asON), so it gets the same cost-based reordering and streaming as explicit joins when every table is connected by an equi-predicate. - Non-equi joins stream, but cost
O(n x m):ON a.id < b.id, aBETWEENband join, or anyONwith no equality to hash on now pairs every row and applies the condition per pair, streamed into the spilling sorter/aggregator. Memory is flat (a 20,000-row three-wayON a.id < b.idkeeps the server at its 34 MB idle footprint), but the work is quadratic or worse and the join row ceilings no longer bound it -- bound the time instead withELYRASQL_QUERY_TIMEOUT_MS, which interrupts such a join promptly and leaves the session usable. When theONmixes an equality with other conditions (ON a.k = b.k AND a.x > b.x) the equality is still hashed and the rest is applied as a residual, so that shape staysO(n+m). There is no index-driven inequality (band) join yet, which is where MySQL wins these outright. - Remaining materialising joins:
FULLjoins, derived-table joins,RIGHTjoins that are part of a multi-table chain (rather than a single two-tableRIGHT JOIN), and any join whose output is neither aggregated nor ordered (a plain projection has no streaming consumer to feed) still build the full join result (correct, but not memory-bounded on very large such joins; theELYRASQL_JOIN_MAX_ROWS/_BYTESceilings apply there).FULLneeds unmatched-build-side tracking; a derived table is materialised first. These shapes are rare and the materialising path is correct. Left-deep chains of more than two tables do stream; a join expression the chain builder cannot analyse takes the materialisingjoin_selectpath. WHERE col IN (SELECT ...)collection is in-memory: the subquery's value list is buffered in RAM. To stay fail-safe rather than OOM, anIN (SELECT ...)over more thanELYRASQL_IN_SUBQUERY_MAXrows (default 1,000,000) errors with a clear message (rewrite it as aJOIN/EXISTS).SELECT DISTINCTdoes spill: it keeps up toELYRASQL_DISTINCT_MAXdistinct rows in its in-memory fast path (default 5,000,000), then switches to external sorting. A narrow correlatedEXISTS/NOT EXISTSshape is decorrelated into one-time key membership: one plain outer table, a top-levelANDconjunct, one plain inner table, and one type- and collation-compatible column equality. NULL keys retain SQL semi/anti-join semantics. Other correlated shapes execute as a nested loop (re-run per driving row,O(N×M)).- Uncommitted transaction writes are buffered in memory (not spilled to disk)
until
COMMIT/ROLLBACK. To keep this bounded, a transaction that stages more thanELYRASQL_TXN_MAX_BYTES(default 1 GiB) of writes has its next write rejected with an error rather than exhausting server memory.SAVEPOINTis cheap: it records an undo-log marker (O(1)) rather than copying the staged write set, andROLLBACK TOreverts only the changes made since the savepoint (reads/locked rows are kept, which only makes commit-time validation more conservative, never incorrect). - Spilled
GROUP BYoutput is ordered per partition (addORDER BYfor a defined order).
Partitioning
CREATE TABLE ... PARTITION BY RANGE|LIST|HASH (<pk column>) (...)records a partitioning scheme over the primary key, exposed ininformation_schema.partitions. Partitions are managed primary-key ranges (metadata over the clustered PK), not physically separate files:ALTER TABLE t DROP PARTITION p/TRUNCATE PARTITION pcheaply delete a partition's rows (a range/INdelete, with index cleanup), and queries with a PK predicate prune automatically via clustered range scans. Boundaries are not enforced on INSERT, and this is single-node (partitioning does not shard writes across nodes — horizontal write scale-out would require distributed sharding, which is out of scope by design).
Transactions & locking
- Snapshot isolation (default, first-committer-wins) and serializable
isolation (opt-in), both optimistic (validate-on-commit; conflicts abort with
error
1213rather than blocking). Serializable validates every range the transaction scanned by re-reading it at commit, so commit cost scales with the read set; a single scanned range overELYRASQL_SERIALIZABLE_MAX_RANGErows (default 5,000,000) aborts the commit (fail-safe against unbounded memory) rather than materializing without limit. SAVEPOINT/ROLLBACK TO SAVEPOINT/RELEASE SAVEPOINTare supported.SELECT ... FOR UPDATE/FOR SHAREprovide optimistic row locking: a locked row that another transaction changes aborts your commit. Row locking is applied to single-table locking selects.- Pessimistic table locking is also available:
LOCK TABLES t READ|WRITE/UNLOCK TABLEStake blocking table locks (aWRITElock blocks other readers and writers; aREADlock blocks writers). While an explicit lock is held, conflicting statements from other sessions block until it is released, or fail with1205(lock wait timeout).LOCK IN SHARE MODEis accepted as a synonym forFOR SHARE. MVCC reads are not blocked by table locks (they read a consistent snapshot). When no explicit lock is held, locking adds no overhead. - A single writer serializes all commits (an inherent property of the ACID single-file engine — there are no parallel writers or write sharding). Throughput under high write concurrency comes from group commit: many pending writes — now including validated transactional commits — are folded into one transaction (one fsync), so N concurrent transactions cost one fsync rather than N. First-committer-wins ordering and write-write conflict detection are preserved within a batch. The expensive per-statement work (parsing, constraint checks, encoding, index maintenance) runs in the connection tasks in parallel; only the final commit is serialized.
Types & text
- Text is case-insensitive by default. A column can opt into case-sensitive
behavior with
COLLATE ..._bin/BINARY, which applies to equality/range comparisons (WHERE),UNIQUE,PRIMARY KEY, secondary indexes, and nowORDER BY,GROUP BY,DISTINCTand equi-join keys (a_bincolumn sorts, groups, de-duplicates and joins by exact bytes, case-sensitively; the default column stays case-insensitive). Accent sensitivity and alternate charsets are not implemented. - Full-text search:
MATCH(col, ...) AGAINST('terms' [IN BOOLEAN MODE])(natural-language OR-of-terms, or boolean+/-, with relevance scoring).CREATE FULLTEXT INDEXbuilds a persistent inverted index that is maintained on INSERT/UPDATE/DELETE and used to accelerate MATCH; without one, MATCH falls back to a scan. Stemming uses the Snowball algorithms (rust-stemmers), so it is linguistically correct (running->run,studies->study, whilestring/singare left alone) and supports many languages viaELYRASQL_FULLTEXT_LANGUAGE(defaultenglish;nonedisables stemming). It still doesn't handle synonyms, and truly irregular forms (e.g.wolves) aren't unified. Changing the language invalidates an existing index (rebuild withCREATE FULLTEXT INDEX). Vector (ANN) search is also available. ENUMandSETvalues are validated against their declared members. ACREATE TABLEENUM('a','b',...)column is enforced via a synthesizedCHECK col IN (...), and aSET('a','b',...)column via a synthesized REGEXP CHECK that accepts any comma-separated subset (or the empty set); a non-member INSERT/UPDATE is rejected, and NULL is allowed on a nullable column.- Basic spatial support:
POINT/GEOMETRYcolumns are stored as WKT text, withPOINT(x,y),ST_X,ST_Y,ST_Distance(Euclidean),ST_AsText, andST_GeomFromText. Only 2D points are supported; there is no spatial index or SRID/geodesic distance.
Security & operations
- Resource limits (denial-of-service surface). Several bounds exist:
expression depth, frame size,
IN (SELECT)/DISTINCTrow caps, recursive-CTE and stored-procedure loop caps, aSERIALIZABLErange cap, and a byte budget for string-expanding functions (ELYRASQL_MAX_ALLOWED_PACKET, returningNULLpast it as MySQL does pastmax_allowed_packet). The behaviour that matters most in practice:-
CPU-heavy queries no longer monopolise the server. Long synchronous stretches (join products, sorts, aggregation over materialised rows) run via
block_in_place, and the streaming join loops yield periodically, so the listener and other sessions keep being polled. This needs no configuration: measured with no query timeout set, 32 concurrent runaway queries (2x the core count) still left a new connection answered in under 0.1 s, where previously it timed out entirely. SettingELYRASQL_QUERY_TIMEOUT_MSadds a deadline on top, which the engine enforces inside its row loops so a runaway statement is aborted and stops consuming CPU (including work already handed to blocking threads). -
A materialising join buffers its output in memory (
FULL, derived-table and multi-tableRIGHTjoins, plus any join whose output is neither aggregated nor ordered — the shapes with no streaming consumer to feed). It is bounded byELYRASQL_JOIN_MAX_ROWSper join andELYRASQL_JOIN_MAX_ROWS_TOTAL/ELYRASQL_JOIN_MAX_BYTESacross all of them, so an unconstrained join fails with a clear error instead of growing until the process is killed. These are fail-safe caps, not spilling — such a join must raise the limit or be reshaped.Since 1.6.0, cross and non-equi joins no longer materialise at all, so they never reach those ceilings: a three-way
ON a.id < b.idover 20,000 rows used to fill 2136 MB and then be refused, and now holds the server at its 34 MB idle footprint. The trade is that their time is unbounded — useELYRASQL_QUERY_TIMEOUT_MSto bound that. -
Connections are capped by
ELYRASQL_MAX_CONNECTIONS(default 151, as in MySQL); surplus connections get error 1040, and — as in MySQL — one extra slot is reserved forAdminaccounts so an operator is not locked out. Note that a non-admin can still occupy that reserved slot for the duration of its handshake before being refused, so a flood of connection attempts can briefly delay an administrator (MySQL behaves the same way). Prepared statements and streamed parameter data are bounded too (ELYRASQL_MAX_PREPARED_STMTS,ELYRASQL_MAX_ALLOWED_PACKET).
-
- Multiple persistent accounts with
CREATE USER/GRANT/REVOKE. Privileges are tracked and enforced per action: the individual DML privilegesINSERT,UPDATEandDELETEare checked separately, per target table, so a user granted onlyINSERTcannotUPDATE/DELETE, and revoking one write privilege leaves the others intact. Grants apply globally or per table. (Reads are still allowed at the global baseline — see below; DDL such asCREATE/DROP/ALTER/CREATE INDEXand administrative statements are gated at theADMINtier rather than by their individualCREATE/DROP/ALTERprivileges.) Roles are supported:CREATE ROLE/DROP ROLE,GRANT <role> TO <user>/REVOKE <role> FROM <user>; a user inherits the global and per-table grants of every role granted to them.GRANT ... ON db.*is accepted and maps to a global grant (single default database). Reads are allowed at the global baseline (table-levelSELECTis not required for an authenticated user). Per-column SELECT grants are enforced (GRANT SELECT(col, ...) ON t TO u): a column-restricted user may only read those columns oft— querying an ungranted column (including viaSELECT *or aWHERE/ORDER BYreference) is denied. Enforcement covers single-base-table selects; a column-restricted table used in a join or subquery is denied (deny-safe). - An optional audit log (
--audit-log <path>) appends one tab-separated line per executed statement (timestamp conn_id user OK|ERR sql). - Cluster/replication authentication. Set
ELYRASQL_CLUSTER_SECRET(the same value on every node) to require a challenge-response handshake (SHA1(secret‖nonce), constant-time) on every Raft control and replication connection, so an unauthenticated peer cannot inject fake writes or votes. Password hashes are compared in constant time. The replication endpoint requires a secret on every bind address (loopback included) — it is refused otherwise unlessELYRASQL_ALLOW_OPEN_AUTH=1— because any process that can reach the port receives a full copy of the database. Replication authentication is mutual: the primary must also prove knowledge of the secret to the replica before the replica applies any data, and a replica refuses to start without a secret. The replication transport can be encrypted with TLS: setELYRASQL_CLUSTER_TLS_CERT/_KEYon the primary andELYRASQL_CLUSTER_TLS_CAon the replica (which then verifies the primary's certificate — a wrong/self-signed-mismatch cert is rejected), giving confidentiality + server authentication, with the shared secret authenticating the replica (mutual auth). The Raft control plane (votes/AppendEntries) uses the sameELYRASQL_CLUSTER_TLS_*settings and is now likewise TLS-encrypted with peer-certificate verification, so all inter-node cluster traffic (replication + consensus) can be encrypted. - Password hardening. New passwords (
CREATE USER/ALTER USER/SET PASSWORD) must satisfy a strength policy: minimum length (ELYRASQL_PASSWORD_MIN_LEN, default 8) and a letters+digits requirement (ELYRASQL_PASSWORD_REQUIRE_MIXED, default on); setELYRASQL_PASSWORD_POLICY=offto disable. Repeated failed logins trigger a temporary account lockout (ELYRASQL_AUTH_MAX_FAILURES, default 10;ELYRASQL_AUTH_LOCKOUT_SECS, default 60) to blunt brute-force attacks; failures and lockouts are logged. Two auth plugins are supported:mysql_native_password(default, works with every client) andcaching_sha2_password(MySQL 8's default; opt-in viaELYRASQL_AUTH_PLUGIN, full authentication over TLS or via an RSA public-key exchange on a plaintext connection). Both verify against the storedSHA1(SHA1(password))digest; the password is never persisted in the clear. - Hot and offline backup/restore, plus an append-only binlog for point-in-time
recovery (
--binlog+elyrasql binlog-replay). Binlog rotation/pruning is manual; there is no incremental (block-level) backup. - Primary → replica replication (read replicas, warm standby), asynchronous by
default with semi-synchronous (
--semi-sync-ms) and quorum / synchronous modes (--sync-replicas N, optional--sync-strict). A commit waits untilNreplicas acknowledge; in strict mode a timeout fails the commit-confirmation instead of silently degrading. The barrier runs after the local commit (which is always durable), so it shrinks — but does not fully close — the failover data-loss window (there is no pre-commit 2-phase replication / multi-primary). Automatic failover is available inclustermode via Raft-style leader election (majority quorum, leader-only writes/fencing) with the election restriction: a node only votes for a candidate at least as up-to-date (by LSN) as itself, so an elected leader has every quorum-acknowledged write. Together with--sync-strictthis gives no-data-loss failover for acknowledged writes (the sync barrier still runs after the local commit, so it is not a pre-commit 2-phase protocol). A reconnecting replica catches up incrementally from the binlog (streaming only the delta since its last applied LSN), falling back to a full snapshot only when the binlog is disabled or the needed segments were purged; the LSN counter resumes from the binlog across restarts. Cluster membership is dynamic:elyrasql cluster-ctl --action add|removechanges membership at runtime (send to the leader, which propagates it to followers via heartbeats); add one node at a time and start a new node before adding it. An even-node cluster can, rarely, need an extra election round to break a tie — run an odd number of nodes. Election state (current term + vote) is persisted to a<data>.raftstatefile so a restarted node never double-votes in a term (a Raft safety requirement). Inclustermode the live write path runs through the Raft replicated log: the leader appends each write to the log, replicates it viaAppendEntries, commits it once a quorum has it, and only then applies it and acknowledges the client (pre-commit / 2-phase). Followers append (with the consistency check + conflicting-suffix truncation) and apply up to the leader's commit index. With the §5.4.1 election restriction this is no-data-loss failover: an acknowledged write is on a quorum's durable log and any new leader has it. A write cannot be acknowledged without a quorum. The replication path is batched for throughput: the leader holds persistentAppendEntriesconnections to followers, appends are fsynced once per round (not per write), and committed entries are applied together through the DB's group commit — so concurrent writers reach hundreds of committed writes/second even though a single sequential write is fsync-latency-bound. The leader holds a lease: it renews leadership each round it confirms contact with a quorum, and steps down if it cannot for the lease window (below the minimum election timeout). A leader partitioned from its quorum therefore relinquishes leadership — its in-flight writes fail fast and a healthy majority elects a new leader — rather than hanging. Because the lease is shorter than the election timeout, a lease-valid leader is guaranteed to still be the leader, so its local reads are linearizable without a quorum round-trip. The Raft log is compacted: once entries are applied and replicated to every member, each node discards them (keeping only the snapshot boundary term for the consistency check), so the log does not grow unbounded — the applied state machine is the snapshot. Compaction advances only to the slowest member's replicated index, so a permanently lagging/dead member holds it back until the member catches up or is removed from membership. The olderprimary/replicamode remains asynchronous (semi-sync/quorum barrier).
Wire protocol
- The MySQL wire layer is a first-party crate (
elyra-wire), forked fromopensrv-mysql, so protocol behaviour is ours to fix and extend (this is what enabled rustls 0.23 andcaching_sha2_password). - Binary (native) prepared statements work for the common shapes, including
repeated prepares on one connection (a packet-reader desync that affected
drivers pipelining commands — e.g. PDO/mysqlnd with
PDO::ATTR_EMULATE_PREPARES => false— is fixed).describe_queryreports an exact result-column count atPREPARE(enable withELYRASQL_STMT_DESCRIBE) for single and joined/multi-table SELECTs, soSELECT *over a join resolves its columns. Qualified wildcards (SELECT a.*) and projections overinformation_schemaare supported at both prepare and execution time. Client-side (emulated) prepared statements remain the widest-compatibility default; PyMySQL and sqlx bind client-side and are unaffected. LOAD DATA INFILEreads a server-side file and bulk-inserts it (requires ADMIN, like MySQL'sFILEprivilege):LOAD DATA INFILE '<path>' INTO TABLE t [FIELDS TERMINATED BY '...'] [ENCLOSED BY '...'] [LINES TERMINATED BY '...'] [IGNORE n LINES] [(cols)], with\Nfor NULL. Rows are grouped into bounded 50,000-row insert units to amortize parsing and durable commits without allowing an individual statement to grow indefinitely. Client- sideLOAD DATA LOCAL INFILE(streaming the file over the wire) is not supported.- Authentication offers
mysql_native_password(default) andcaching_sha2_password(MySQL 8's default; opt-in viaELYRASQL_AUTH_PLUGIN). Connection salts come from the OS CSPRNG.caching_sha2_passwordruns full authentication — cleartext over TLS, or an RSA public-key exchange on a plaintext connection.
Have a need that isn't listed? Open an issue on GitHub.