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-cudais 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 TABLEcatches 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 TABLEstill passes — but an existing table stays wrong, with no way to detect or repair it yet. TIMESTAMPcolumns are reported asTEXTover 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