SynapCores v1.15.0 — a restart switched off your primary key

Published on September 11, 2026

SynapCores v1.15.0 — a restart switched off your primary key

Index data was not rebuilt when the engine started. Constraint indexes are how PRIMARY KEY is enforced. So after the first restart, it wasn't.

INSERT INTO accounts (id, owner) VALUES (1, 'original');

INSERT INTO accounts (id, owner) VALUES (1, 'duplicate');
-- Constraint violation: Duplicate key: row violates a UNIQUE or
-- PRIMARY KEY constraint on table 'accounts'

Correct. Now restart the engine and run the same statement:

INSERT INTO accounts (id, owner) VALUES (1, 'clobbered');
-- rows_affected: 1

SELECT * FROM accounts WHERE id = 1;
-- (1, 'clobbered')

Not a duplicate row. Not an error. The original row is gone.

That transcript is from v1.14.5-ce, which was :latest until today. If you run SynapCores in production and have ever restarted it, this is the fix to care about.

CREATE INDEX never backfilled an existing table

CREATE INDEX did not read the table. The production build_index was this, with the comment included:

async fn build_index(&self, name: &str) -> Result<()> {
    // This would scan the table and populate the index
    // For now, we assume indexes are maintained incrementally
    Ok(())
}

The assumption was doing real work: indexes were populated by INSERT-time maintenance, not by a build step. Which means it depended entirely on the order you did things in.

Create the index over a table that already holds rows, and you get an empty index. The optimizer picks it, IndexScanExec finds it empty, correctly declines to treat an empty lookup as authoritative, and falls back to a full scan. Correct answers, no acceleration, no error anywhere.

There was a second way to land an empty index: INSERT-time maintenance was gated on the table's constraint list rather than its index catalog. On a table with no PRIMARY KEY and no UNIQUE column, even rows inserted after the index existed never reached it.

Measured on v1.14.5-ce, 20,000 rows, same box, same query:

scenario v1.14.5-ce v1.15.0-ce
PK table, index created before the load 1.0 ms 1.0 ms
PK table, index created after the load 66.6 ms 1.0 ms
no-PK table, index created before the load 66.3 ms 1.0 ms
unindexed column (control) 72.1 ms unchanged

Read the first row before the others: if your table had a primary key and you created the index before loading the data, your indexes were already working. That is the common path, and it was fine. The bug is conditional, and the conditions are the two above.

The control row not moving is what makes the rest of the table mean anything — if everything had improved, we would have measured a warm cache.

Each bug was hiding the next one

Once CREATE INDEX started backfilling, a second bug became reachable: index selection resolved table names without the database prefix. An index in a database called analytics was invisible, and main's same-named index was used instead.

The symptom is the one you least want:

-- running against `analytics`
SELECT * FROM orders WHERE region = 'us-west';
-- returns rows where region <> 'us-west'

Queries returning rows that violate their own WHERE clause. That bug stayed latent for as long as those indexes were empty — our own backfill fix is what activated it. It is fixed in the same release, which is the only reason we are comfortable shipping the first one.

That pattern repeated. Eleven defects, found in sequence, because each fix exposed the next.

Every HNSW index was flat

VectorOperations::create_space had no index_type parameter. VectorStorage::create_space hardcoded IndexType::Flat. The REST API accepted an index_type, stored nothing, and echoed your request back in the response.

So it looked configured from every angle available to you — the request succeeded, the response said HNSW, the collection reported HNSW. It was a flat index doing a linear scan.

recall@10    0.776  ->  0.976

This also improves GraphRAG SIMILAR_TO traversals, which ran on the same flat path.

Three more in the same area: vector collections were never persisted and did not survive a restart at all; filter and threshold were accepted by vector search and silently dropped; and there is now an upsert.

At the SQL level, USING HNSW is still rejected — there is no SQL-level vector index, and we would rather say so than build you a B-tree over an embedding. An index on a VECTOR column with no USING clause is refused for the same reason: it accelerates nothing while looking indexed.

Your immutable audit table was storing everything as text

CREATE IMMUTABLE TABLE discarded declared types. Every column — INTEGER, DOUBLE, TIMESTAMP — was persisted as TEXT.

CREATE IMMUTABLE TABLE ledger (id INTEGER, amount DOUBLE);

SELECT amount FROM ledger ORDER BY amount DESC;
-- 100, 25, 9        (lexicographic)

SELECT SUM(amount) FROM ledger;
-- NULL

SELECT * FROM ledger WHERE amount > 50;
-- Cannot compare String and Int64

On disk the row held "777.25" where a row table held 888.25. For a compliance feature whose entire purpose is a trustworthy record, a table you cannot sum or compare is not much of a record.

Columnar tables: what we fixed, and what we are telling you instead of fixing

Fixed: a PRIMARY KEY predicate on a columnar table matched zero rows.

Not fixed, deliberately: PRIMARY KEY and UNIQUE are not enforced on columnar tables. They never have been. Snowflake, BigQuery, Redshift, ClickHouse, Iceberg and Delta Lake all behave the same way — a uniqueness probe on every insert removes the bulk-ingest advantage that is the reason to choose columnar storage. BigQuery makes you write NOT ENFORCED out loud.

What was actually wrong was accepting the declaration in silence. So now:

CREATE TABLE events (id INT PRIMARY KEY, ...) ENGINE = SYNAPCORES_COLUMNAR;
-- warning: PRIMARY KEY is not enforced on columnar tables; it is used as a
-- clustering and metadata hint. Duplicate keys will be accepted.

The same statement behaves differently depending on ENGINE, and a row table in this engine does enforce it. That difference should be visible at the moment you write the DDL, not discovered later.

The rest

  • CREATE TABLE ... WITH ( ... ) silently corrupted the schema.
  • SELECT t.* — qualified wildcard projection.
  • The optimizer no longer pushes subquery and AI-function predicates into a scan that cannot evaluate them.
  • CUDA builds ran on the CPU while logging "will use GPU acceleration".
  • An unescaped apostrophe broke the SPA build, and with it every binary.

What these have in common

Every one of these is code written when there was only one storage engine, one optimizer, or one projection form — still being applied unconditionally after a second one arrived. None of them is an exotic failure. They are all a default that stopped being the only case.

That is worth saying plainly because it predicts where the next ones are, and we have written that down rather than waiting to trip over it.

Validation

gate result
feature_validator.py, state-asserting 216 passed, 0 failed
Recipe certification, pristine vs pristine 158/164, identical failing set to v1.14.5-ce, zero regressions
Non-AVX-512 canary (i5-10400F) boots, serves, EMBED works, no SIGILL
Published-artifact re-validation checksum, canary, validator 215/0/1

What is still broken

Shipping the list matters as much as shipping the fixes.

  • No CUDA binary. This release contains the fix for CUDA builds running on the CPU, and linux-x86_64-cuda is not in the build matrix. The fix ships unbuilt.
  • Immutable and timeseries tables still discard column constraints. An immutable table accepts a duplicate primary key and silently replaces the row. VERIFY TABLE catches it afterwards — chain holds 3 record(s) but only 2 row(s) remain — but the write should never have been accepted. This predates v1.15.0.
  • Immutable tables created on v1.15.0-ce or earlier keep their TEXT schema permanently. The type fix applies to new tables only. We verified there is no mixed-representation hazard across upgrade and rollback, and VERIFY TABLE still passes — but an existing table stays wrong, with no way to detect or repair it yet.
  • TIMESTAMP columns are reported as TEXT over REST, on every table type.
  • Six catalog recipes fail certification, unchanged from v1.14.5-ce. Five are duplicate-key failures, and we now suspect they were relying on primary keys not being enforced across a restart.

Get it

curl -fsSL https://get.synapcores.com | bash
docker pull synapcores/community:v1.15.0-ce

Linux x86_64 and aarch64 (glibc and Ubuntu 24.04 builds), macOS aarch64, and multi-arch Docker on GHCR and Docker Hub.

Full notes and checksums: github.com/SynapCores/synapcores-releases