This document lists the internal technical limitations of VelesDB Core. These are distinct from the product-level scope boundaries in the README "Known Limitations" section (single-writer, no distributed replication, WASM hop-limit, etc.). Each entry below is either:
- Tracked by a GitHub issue and scheduled for resolution, OR
- An explicit design approximation whose trade-off is documented in the source and covered by regression tests.
None of the items below is a correctness bug. They are transparency notes so operators, integrators, and code reviewers understand the bounds of the current implementation.
Status: documented trade-off. Source: crates/velesdb-core/src/velesql/explain/node_stats.rs (COST_UNIT_TO_MS = 0.001, with TODO noting empirical calibration).
Once a collection has been analyzed, EXPLAIN.estimated_cost_ms is derived from the calibrated CostEstimator (real histogram-based selectivity + I/O / CPU weights). Before ANALYZE, the same query uses the legacy heuristic (fixed coefficients).
These two code paths produce values in different magnitude ranges:
Example (10 K rows, VectorSearch ef=100, k=10) |
estimated_cost_ms reported |
|---|---|
Before ANALYZE (heuristic) |
≈ 0.1 |
After ANALYZE (calibrated, COST_UNIT_TO_MS = 0.001) |
≈ 2.2 |
The jump between the two estimates is not a regression; it reflects that the calibrated path counts more operations per unit (probe visits, comparisons, I/O page reads) whereas the heuristic uses rule-of-thumb constants directly. Users comparing EXPLAIN output across an ANALYZE boundary should expect this jump.
Resolution path: pin COST_UNIT_TO_MS empirically via a micro-benchmark that times a known plan shape on reference hardware, then rescale the constant so pre/post-ANALYZE costs align at the same operating point. Not blocker for correctness — both paths rank the same plan shape consistently within their own range.
Status: partial integration (scope-reduced). Tracked by issue #467. Source: crates/velesdb-core/src/collection/query_cost/plan_generator.rs (PlanGenerator::CandidatePlan).
compute_cbo_strategy in collection/search/query/select_dispatch.rs now routes SELECT queries through two calibrated planner entry points:
QueryPlanner::choose_hybrid_strategyfor queries carryingORDER BY similarity()— forcesVectorFirstto preserve HNSW natural ordering regardless of cost estimates.QueryPlanner::choose_strategy_with_cbo_and_overfetchfor all other SELECT queries — calibrated I/O / CPU cost comparison acrossVectorFirst/GraphFirst/Parallel.
Both branches feed into the same dispatch_vector_query executor through the (ExecutionStrategy, over_fetch: usize) tuple.
What remains open: the deeper PlanGenerator::CandidatePlan enumeration (SeqScan, IndexScan, VectorSearch, GraphTraversal, hybrid combinations) is still not consumed by execute_query. The current two-path routing covers the operationally common cases — full multi-candidate enumeration would only change the decision when the cost landscape is non-trivially multimodal.
Strategy realization on SELECT (audit F-2.15 — RESOLVED, #1390): on the NEAR + metadata-filter SELECT path (execution_paths.rs, dispatch_vector_with_strategy), the ExecutionStrategy the planner selects is executed as three physically distinct plans:
VectorFirst— filtered HNSW search (search_with_filter_and_opts).GraphFirst— a full metadata scan scored by vector similarity (scan_and_score_by_vector), physically distinct from the HNSW path.Parallel— the GraphFirst scan and the VectorFirst HNSW branch run concurrently viarayon::join, then merge by best-score-per-id. OnMATCH(match_dispatch.rs,execute_match_parallel),Parallellikewise runs its GraphFirst and VectorFirst legs concurrently viarayon::joinand merges bynode_id.
The concurrent Parallel result set is identical to the former sequential one (both legs are read-only; the merge is order-insensitive); the shared EXPLAIN counters remain the sum of both legs (atomic fetch_add). The other SELECT arms (similarity() threshold, pure NEAR, metadata-only, SELECT *) each have a single sensible physical plan and deliberately ignore the strategy — see the dispatch_vector_query match arms for the per-arm rationale. SELECT graph predicates still use their own anchored pre-filter (graph_prefilter.rs) independently of ExecutionStrategy. Covered by test_parallel_strategy_returns_best_score_union_of_both_branches, the forced-GraphFirst/VectorFirst dispatch tests, and parallel_counters_sum_both_legs.
User impact: MATCH queries use the full CBO via MatchQueryPlanner::plan. SELECT queries (including ORDER BY similarity + filter) now use calibrated strategy and over-fetch selection. Covered by test_cbo_forces_vector_first_for_order_by_similarity_with_selective_filter + test_cbo_calibrated_path_still_works_without_order_by_similarity + test_filter_strategy_switches_on_selectivity.
Status: resolved (configurable). Source: crates/velesdb-core/src/velesql/explain/filter_strategy.rs (DEFAULT_FALLBACK_SELECTIVITY_THRESHOLD = 0.1, AtomicU64 runtime state).
When no calibrated CollectionStats is available (collection never analyzed, SDK path without collection handle), resolve_filter_strategy falls back to selectivity > threshold → PostFilter. The threshold defaults to 0.1 to keep the ~50 pre-existing EXPLAIN tests green (backward-compat anchor), but is tunable at runtime via velesdb_core::velesql::set_fallback_selectivity_threshold(value) (lock-free AtomicU64, validates [0.0, 1.0]). Once stats are present, the cost-based comparison (pre-filter vs post-filter with recall guardrail at selectivity >= 0.5) takes over.
User impact: for unanalyzed collections, operators can tune the fallback threshold for workloads where the calibrated pathway is unavailable without recompiling. Running ANALYZE on the collection still switches the decision to the calibrated pathway documented by BDD tests test_filter_strategy_switches_on_selectivity and test_filter_strategy_respects_ef_search.
Status: documented heuristic. Source: crates/velesdb-core/src/collection/search/query/metadata_query.rs (execute_indexed_metadata_query).
For a metadata query whose WHERE reduces to an indexed Eq, the executor uses the secondary index directly. If the index reports more than max(execution_limit × 50, 1000) matching ids, it abandons the index and does a full filtered scan instead. The 50× factor (with a 1000-id floor) is an intentional but arbitrary cutoff: past that fan-out, materializing and post-filtering the id list from the index is empirically slower than a straight scan, but the exact break-even point is workload-dependent and has not been calibrated per collection.
User impact: none on correctness (results are identical either way). On a low-selectivity Eq over a very large collection, the query may take the scan path even when a different constant would have kept the index path. The factor is a compile-time constant today; making it cost-model-driven (like the filter-strategy threshold above) is a possible follow-up.
Status: open, rework decision still pending. Source: crates/velesdb-migrate/ (workspace member).
The velesdb-migrate sub-crate ships a migration toolkit covering 10 source connectors (Supabase, Qdrant, Pinecone, Weaviate, Milvus, ChromaDB, JSON, CSV, Elasticsearch, Redis — crates/velesdb-migrate/src/connectors/). It is currently bundled in the workspace but is identified for rework or extraction in a future release: the current scope inflates the workspace surface (10 connector surfaces, most against third-party APIs) without a measured user base, and the connectors evolve at different cadences than the core engine.
Decision criteria (set during the v1.15.0 cycle; the horizon has since been re-baselined — see ROADMAP.md "Next"):
- crates.io download counts for
velesdb-migrateover the last 90 days - GitHub stars / watchers attributable to migration tooling
- Open issues count specifically scoped to migration connectors
User impact: until the rework decision lands, the crate is maintained on a best-effort basis. No migration tooling will be removed or moved without a documented sunset window — this is purely a forward-looking transparency note.
Resolution path: the candidate outcomes are (a) keep + invest, (b) extract to separate velesdb-migrate repository under the same org, or (c) archive with documented sunset window. The decision will be made in a separate planning issue once the criteria above are measurable.
Status: resolved. Source: .github/workflows/release.yml publish-pypi-wheels matrix.
A dedicated macos-13 (Intel x86_64) matrix entry was added briefly in v1.14.4 (PR #738) but the GitHub-hosted macos-13 runner availability proved unreliable: one v1.14.4 publish attempt left the wheel-build job queued for over 9 hours without a runner being assigned, blocking the rest of the release pipeline. The entry was removed in v1.14.5 to keep the release pipeline reliable.
Current state: the PyPI matrix now builds a single universal2 macOS wheel on macos-14 (arm64 + x86_64 slices in one artifact), so Intel Macs are covered without the unreliable macos-13 runner. Building from source (pip install velesdb --no-binary :all: with a Rust toolchain) also remains available for native x86_64 builds.
These are intentional hard limits introduced by the core hardening effort to bound resource use against corrupt files and adversarial queries. They are not bugs; they are the documented ceilings of the current implementation.
Status: resolved (hard limit). Source: crates/velesdb-core/src/velesql/parser/prescan.rs (MAX_NESTING_DEPTH = 64).
Before a query reaches the pest parser, a single O(n) pre-scan rejects any
query that:
- exceeds the configured
max_query_length, or - has an effective parse-recursion depth (open
()/[]brackets plus a leadingNOT NOT …run) greater than 64.
The pre-scan exists because pest builds the full recursive parse tree before
any Rust-level guard runs, so a deeply nested query (~thousands of levels) would
otherwise overflow the native stack and abort the process. Quoted strings,
backtick/double-quoted identifiers, and -- comments are skipped so the guard
never false-positives on literal bracket content.
User impact: legitimate hand-written queries nest a handful of levels and are unaffected; programmatically generated queries must keep bracket/NOT nesting at or below 64.
Status: resolved (server-side hard ceiling). Source: crates/velesdb-core/src/collection/search/query/aggregation/having.rs (DEFAULT_MAX_GROUPS = 10_000, SERVER_MAX_GROUPS_CEILING = 1_000_000).
A GROUP BY query retains at most DEFAULT_MAX_GROUPS (10,000) groups by default.
A query may use WITH (max_groups = N) (or group_limit) to lower its group
budget, but N is always clamped down to the server-side ceiling of
1,000,000 — a query can never raise the memory ceiling. Exceeding the
effective limit returns a "Too many groups" error rather than growing unbounded.
Status: resolved (hard limit). Source: crates/velesdb-core/src/collection/search/query/similarity_filter.rs (NOT_SIMILARITY_MAX_SCAN = 5_000_000).
A NOT similarity(...) predicate has no index acceleration and must full-scan
the collection. It is now a hard guard-rail (not just a warning): if the
collection holds more than 5,000,000 vectors the query is rejected with
guidance to add a selective metadata filter or use a positive similarity()
predicate (which is index-accelerated).
Status: intentional guard-rail, observable since WO-D2. Source:
crates/velesdb-core/src/collection/search/query/similarity_filter.rs
(scan_and_score_by_vector, SCAN_CAP = 100_000, #901).
When the planner picks the GraphFirst strategy for a filtered vector query
(highly selective metadata filter), the executor full-scans the metadata
matches and rescores them by exact vector similarity. To bound the work of a
pathological query, at most 100,000 metadata matches are scored per query.
When it bites: the metadata filter matches more than 100,000 points (or the scan reaches 100,000 matches while unvisited points remain). Matches beyond the cap are never scored.
Symptom: silently degraded recall — the returned top-k is the best of the
first 100,000 scanned matches, not of all matches. Results stay correctly
ordered and deduplicated; they may just miss better matches that live past the
cap. Since WO-D2 the truncation is no longer silent: one structured
tracing::warn! is emitted per affected query (never per candidate) with
the collection name, the cap, the number of matches scored, and an upper bound
on the unvisited remainder (unscanned_points).
Workaround: narrow the metadata filter so it matches fewer points —
ideally on a field with a secondary index (create_index), which lets the
executor scan only the indexed candidate set — or split the query into more
selective partitions at the application level. A broad filter with low
selectivity is better served by the VectorFirst (ANN) strategy, which the
cost-based optimizer picks automatically when statistics are available
(ANALYZE).
Status: resolved (bounded memory). Source: crates/velesdb-core/src/collection/search/query/ (set_operations, parallel_traversal, similarity_filter), database/query_engine.rs, database/query_join.rs.
Result materialization for top-k scans, JOIN, parallel graph traversal, and
set operations (UNION/INTERSECT) is bounded by the effective LIMIT via bounded
top-k rather than collect-all-then-truncate. Results are identical to the
unbounded path; only peak memory is bounded. Intermediate operators that can
legitimately drop rows fall back to the conservative server-side ceiling — this
includes a scalar (non-similarity()) ORDER BY ... LIMIT k, which must rank
the full matching set before truncating, so it fetches exhaustively rather than
bounding the fetch at k. (Capping the fetch at k first was the
ORDER BY-before-sort defect fixed 2026-06-14; the bounded==unbounded identity
above now holds for scalar ORDER BY as well, at the cost of an exhaustive
fetch for that one operator.) The similarity()-ordered HNSW path stays bounded
top-k — it is pre-sorted by score, so truncation is correct without an
exhaustive fetch and recall is unaffected.
The O(n) cost of that exhaustive scalar-ORDER BY fetch is removed by the
ordered-index pushdown (EPIC-081, docs/planning/CORE_PARITY_REMEDIATION.md):
when the single ORDER BY field has a fully-covering secondary index and the
query has no WHERE/JOIN/graph/similarity, the engine serves the top-k from the
index in O(log n + k) (create_index(field) to opt in) — ~89 ms → ~0.013 ms for
that 50k-row query — with identical results to the exhaustive path. Queries
without a covering index keep the exhaustive scalar-ORDER BY behaviour above.
The create_index(field) opt-in is persisted (recorded in config.json and
rebuilt from the stored payloads on open), so the fast path keeps firing after a
process restart instead of silently reverting to the exhaustive scan.
Status: resolved (validated in every loader and on open; the limits.*
fields are additionally enforced at runtime since 2026-06-14). Source:
crates/velesdb-core/src/config_validation.rs (range checks, called from
Config loaders and Database::open_with_config);
crates/velesdb-core/src/collection/{types,payload_size,core/crud,core/bulk_import,core/graph_api,search/vector,search/vector_filter}.rs
and database/collection_ops.rs (runtime enforcement).
VelesConfig::validate() now runs in every config loader (load,
load_from_path, from_toml) and on open_with_config. Each capacity/limit
field is range-checked against a hard ceiling:
| Field | 0 means |
Hard ceiling |
|---|---|---|
limits.max_vectors_per_collection |
rejected | 10,000,000,000 |
limits.max_collections |
rejected | 1,000,000 |
limits.max_payload_size |
rejected | 1 GiB (1,073,741,824) |
search.query_timeout_ms |
disabled | 24 h (86,400,000 ms) |
hnsw.max_layers |
auto | 64 |
storage.mmap_cache_mb |
rejected | 1 TiB (1,048,576 MiB) |
server.workers |
auto (CPU count) | 4,096 |
An out-of-range value fails the loader/open with ConfigError::InvalidValue
rather than being silently accepted (which previously allowed 0 = DoS or
absurdly large = unbounded). The per-client RateLimiter map is also bounded
with sampled eviction so a client cycling client_id values cannot OOM the
limiter.
Beyond range validation, all five limits.* fields are now enforced at
runtime (2026-06-14): max_dimensions / max_collections at collection
creation, and max_vectors_per_collection / max_payload_size /
max_perfect_mode_vectors at the cold ingest/search boundary inside the
Collection (off the hot path), covering the Point upsert, zero-copy raw
bulk, graph node-write, and filtered/unfiltered search paths. An operation
that would exceed a cap is rejected with Error::GuardRail (VELES-027)
naming the actual value and the limits.<field> to raise; the engine never
silently clamps. Two intentional scoping notes: (1) max_vectors_per_collection
is a conservative O(1) pre-count (stored + batch) that treats every incoming
point as net-new, so a collection exactly at the cap may reject a pure in-place
update batch — raise the cap to update at the limit; (2) vector-less graph node
writes do not increment the vector count, so max_vectors_per_collection does
not apply to pure-graph node ingest (only max_payload_size does there).
Status: resolved (backstop). Source: crates/velesdb-core/src/alloc_guard.rs (DEFAULT_ALLOC_BYTE_LIMIT = 1 TiB).
Every raw aligned allocation is capped at a process-wide per-allocation ceiling
of 1 TiB, configurable at runtime via set_alloc_byte_limit. This is a
deliberately high backstop against arithmetic-wrapped or pathological sizes; it
is far above any single contiguous buffer VelesDB legitimately allocates, so it
never rejects a real index. Primary defense against untrusted sizes is the
per-artifact load-time validation (file-length-bounded counts).
Status: documented design choice (not a bug). Sources: crates/velesdb-core/src/fusion/strategy.rs (fuse_maximum, fuse_average — no normalization; fuse_rsf → min_max_normalize), crates/velesdb-core/src/collection/search/text_fusion.rs (routes maximum/average/rsf to score-level fusion of the raw vector-similarity and BM25 streams).
Symptom: in a hybrid query fusing a vector branch with a text branch — e.g.
WHERE vector NEAR $v AND content MATCH '...' USING FUSION(strategy = 'maximum')
(or 'average') — the fused ranking is dominated by the text (BM25) results,
and the vector branch has little or no visible influence.
Why: maximum and average operate on the raw per-branch scores with
no normalization. Vector similarity is bounded (cosine ∈ [-1, 1]) while BM25 is
unbounded (routinely > 1, often 5–20 on longer queries), so under maximum the
BM25 score almost always wins, and under average it dwarfs the vector
contribution. These strategies are designed for branches whose scores share a
scale (e.g. two dense-vector branches over the same metric); the engine
deliberately does not second-guess the caller by normalizing behind their back.
Recommendation: for mixed-scale hybrids (vector + BM25), use
FUSION(strategy = 'rrf') — rank-based, therefore insensitive to score scale —
or FUSION(strategy = 'rsf', dense_weight = ..., sparse_weight = ...), which
min-max normalizes each branch to [0, 1] before the weighted sum. Reserve
maximum/average for branches with commensurable scores. See the
scale-mixing caveat in VELESQL_SPEC.md → USING FUSION.
Status: documented trade-off (intentional; re-audited for issue #1542,
2026-07-23). Sources: crates/velesdb-core/src/wire/stable_hash.rs
(hash_id/hash_id_bytes, the canonical FNV-1a derivation — "the single
authoritative source for VelesDB's stable string→u64 derivation");
crates/velesdb-memory/src/id.rs (stable_id/stable_id_bytes, delegates to
core); crates/velesdb-migrate/src/pipeline.rs (fnv1a64, delegates to core)
and crates/velesdb-migrate/src/pipeline_points.rs (stable_point_id);
integrations/common/src/velesdb_common/ids.py (stable_hash_id, the single
source shared by LangChain, LlamaIndex, and Haystack since 2026-06-14);
integrations/haystack/src/haystack_velesdb/document_store.py (imports
stable_hash_id — no forked copy).
VelesDB point IDs are u64. Components that ingest documents keyed by an
arbitrary string derive the numeric ID with three intentionally different
semantics:
| Component | Function | Strategy |
|---|---|---|
Core / velesdb-memory / velesdb-migrate's non-numeric fallback |
velesdb_core::hash_id (canonical); velesdb-memory::id::stable_id, velesdb-migrate::pipeline::fnv1a64 delegate to it |
FNV-1a 64-bit over the UTF-8 (or raw) bytes — full unsigned 64-bit output |
velesdb-migrate (stable_point_id) |
numeric-preserve fast-path over the FNV-1a fallback above | numeric strings ("12345") parsed directly to u64; only non-numeric strings fall through to FNV-1a |
| LangChain / LlamaIndex / Haystack | velesdb_common.ids.stable_hash_id (shared, default) |
SHA-256 of the UTF-8 string, top 8 bytes, sign bit cleared → positive 63-bit ID |
Since issue #1542, velesdb-memory and velesdb-migrate no longer
re-declare their own FNV-1a constants: both delegate to the single exported
velesdb_core::hash_id_bytes fold, so core/memory/migrate's FNV-1a output is
now provably one implementation, not three independently-maintained copies
that could silently drift from each other. This changed nothing about
the derived u64 values (see the golden-vector regression tests added
alongside the delegation in each crate) — it only removed the duplication
risk. It did not change the ecosystem-level divergence described below.
velesdb-migrate's numeric-preserve fast-path is a deliberate third
semantics layered on top of FNV-1a, not a bug: it parses numeric IDs
verbatim so a source row keyed "12345" maps to point 12345, and its
FNV-1a fallback for non-numeric IDs is frozen for checkpoint-resumable
migrations (changing either would re-key already-inserted points and corrupt
a resumed run — see the stability notes in pipeline_points.rs).
The Python integrations' SHA-256 default is a separate divergence,
distinct from the FNV-1a-vs-numeric-fast-path split above: it does not agree
with core's FNV-1a for the same string, by design — see the rationale below.
Callers who need a Python-derived ID to agree with core/velesdb-migrate's
FNV-1a output for the same string can opt in with
stable_hash_id(value, algorithm="fnv1a") (added in #1542;
velesdb_common.ids module docstring has the full contract). This is
opt-in only — the SHA-256 default is unchanged and MUST stay unchanged:
aligning the default to FNV-1a would change the u64 derived from every
string ID already stored through these integrations, silently orphaning
existing LangChain/LlamaIndex/Haystack collections. algorithm="fnv1a" does
not change point-ID collisions with velesdb-migrate's numeric fast-path
either — it only aligns with FNV-1a for non-numeric-looking strings.
User impact: the same logical document can land under different point
IDs depending on the ingestion path. A corpus loaded via velesdb-migrate
and the same corpus loaded via the LangChain/LlamaIndex/Haystack vector store
(with the SHA-256 default) will not share point IDs, so cross-referencing or
de-duplicating across the two paths by point ID is not reliable. Pick a
single ingestion path per collection, use algorithm="fnv1a" on the Python
side when the interop case calls for it (understanding it changes the IDs
the integration derives), or map on a payload field (e.g. a stored
source_id) rather than on the numeric point ID.
Possible future alignment path (not implemented, tracked as a follow-up
in issue #1542's discussion): a major-version release of the Python
integrations could flip stable_hash_id's default to "fnv1a" behind an
explicit migration story — e.g. a one-time re-key utility that re-derives
every stored point's ID from its original string under the new algorithm and
rewrites the collection, shipped alongside a deprecation window where both
algorithms' old-ID lookups are supported. This is a breaking, opt-in,
explicitly-versioned change for users with existing data — never a silent
default flip — so it is deliberately out of scope for this fix.
Status: resolved (2026-06-20). Sources: crates/velesdb-core/tests/velesql_executor_conformance.rs, crates/velesdb-cli/tests/velesql_executor_conformance.rs, crates/velesdb-wasm/src/velesql_executor_conformance_tests.rs + the shared conformance/velesql_executor_cases.json; parser layer crates/velesdb-{wasm,cli}/tests/velesql_parser_conformance.rs; server REST-contract layer crates/velesdb-server/tests/velesql_conformance_tests.rs.
The shared VelesQL conformance fixtures come in layers. The
velesql_parser_cases.json layer (does this query parse?) is checked across
core, WASM, and CLI. The velesql_contract_cases.json layer (does this
query execute and return the contracted result/error shape over REST?) is
exercised at the server runtime. The velesql_executor_cases.json layer
(does this query produce the exact result rows / counts / ordering?) is now
run by all three executors — core, CLI, and WASM — with goldens derived from
velesdb-core as the source of truth.
Architecture note: the three executors are not the same code path. The CLI
delegates SELECT execution to velesdb-core (Database::execute_query), so it
inherits core behaviour directly. WASM runs its own independent SELECT/ORDER
BY pipeline (velesql_select / velesql_orderby), sharing only the AST/parser
with core — which is exactly why a per-runtime executor golden has real value:
it pins the WASM result set against core rather than assuming shared-executor
equivalence.
Coverage (conformance/velesql_executor_cases.json, cases X001–X010 plus
the B001 regression lock): scalar string-equality and integer-range WHERE
filters, conjunctive (AND) filters, single- and multi-column ORDER BY in mixed
directions, the deterministic ascending-id tie-break, and bounded top-k
(ORDER BY ... LIMIT k, both ASC and DESC). A result-shape divergence specific
to the WASM or CLI surface now fails CI rather than going unnoticed.
Extended coverage (issue #1544, fixture v2.0.0): the original 10 cases were
all scalar WHERE + ORDER BY + LIMIT — a thin slice next to the 134
parser-conformance cases. The fixture now also carries, checked against
core and WASM (the CLI layer is unchanged — it only reads dataset /
cases / known_bugs, so it keeps validating the original cases plus the new
scalar-expression ones below, which use the same single-collection dataset):
casesX011–X014: WHERE-expression evaluation (BETWEEN, parenthesizedAND/OR,NOT,IN) — safe for CLI's strict-order comparison, so these run on all three executors.join_casesJ001–J005:JOIN ... ON(both condition-side orders on both engines — J005 locks the joined-table-first order, issue #1555) andJOIN ... USING (col).aggregate_casesG001–G005:GROUP BY/HAVING/COUNT/SUM/LIMIT, checked viaDatabase::execute_aggregateon core (notexecute_query, which only groups when combined with vectorNEARsearch) and via the default SELECT pipeline on WASM. G005 (issue #1556) locksLIMITtruncation on grouped results — formerly D006 below, now resolved on both engines.setops_casesS001–S004:UNION/INTERSECT/EXCEPT, compared as a sorted set of ids, not an ordered list (see D003 below).match_casesM001–M002: 1- and 2-hopMATCH, with per-executor query text (see D004 below).
documented_divergences (informational entries in the same JSON file, not
pass/fail cases) record real, currently-existing core↔WASM behavioural gaps
discovered while building this coverage, so they're visible instead of
silently relied upon:
- D001 — unaliased aggregate output field names differ (
"count"/"sum_year"on core vs"count(*)"/"sum(year)"on WASM); sidestepped in the fixture by always using an explicitASalias. D002— fixed 2026-07-24 (issue #1555): WASM'sequality_keysnow orients theONcondition by which side names the joined table (alias or raw table name), mirroring core'snormalize_join_condition— both condition-side orders resolve identically. Locked byjoin_casesJ005 above and the parity test (test_wasm_join_condition_side_order_parity_1555incrates/velesdb-wasm/src/velesql_executor_conformance_tests.rs).- D003 —
UNION/INTERSECT/EXCEPTrow order for non-ranked (score 0.0) branches is implementation-defined on both sides (core re-sorts by score with an unstable tie-break that was observed to differ between two runs of the same binary; WASM preserves branch/ORDER BYorder via concatenate-then-dedup). Only membership and count are part of the golden contract. - D004 — core treats vector-collection points as graph nodes (MATCH is
SELECT ... FROM <coll> WHERE MATCH (...)); WASM keeps a separate in-memory graph store addressed via@collectionannotations and the standaloneMATCH ... RETURNform. An accepted architectural difference, not a bug. - D005 — cross-reference to the pre-existing
relative_score/rsfmulti-query fusion ranking divergence, already tracked indocs/reference/ECOSYSTEM_PARITY.md. D006— fixed 2026-07-24 (issue #1556):Collection::build_grouped_resultsnow appliesOFFSET/LIMITto grouped results afterORDER BY, matching the WASM aggregate pipeline (no default cap — a LIMIT-lessGROUP BYstill returns every group). Locked byaggregate_casesG005 above andcrates/velesdb-core/tests/velesql_group_by_limit.rs.
Status: documented (governance clarification). Sources: QUALITY_BAR.md Gates 5–6, .codacy.yml engines.lizard.exclude_paths.
The enforced Codacy metric is per-function: cyclomatic complexity ≤ 8 and function NLOC ≤ 50. There is no hard file-length gate — a file can be large yet fully compliant if it is a set of small functions or is comment-dense. So raw line count is not, by itself, a violation.
Several production files exceed ~900 raw lines (e.g. simd_native/x86_avx512.rs ≈ 1469 raw / ~944 NLOC — dominated by per-intrinsic // SAFETY: blocks; tauri-plugin-velesdb/src/{types,commands}.rs; velesdb-python/src/{agent.rs, collection/search.rs}; velesql/.../match_dispatch.rs; velesdb-server/src/config.rs). They fall into two buckets:
- Covered by an exclusion: the tauri-plugin files via the
crates/tauri-plugin-velesdb/src/**lizard glob. - Compliant without an exclusion: the rest — their individual functions stay within CC ≤ 8 / NLOC ≤ 50, which is why
mainships Codacy-green. They are intentionally not added to.codacy.yml, because a blanket file exclusion would suppress genuine future per-function findings in them.
The rule of thumb when adding a large file: only list it under engines.lizard.exclude_paths (with a rationale) if a specific function legitimately exceeds the per-function budget; never to silence file size alone.
Status: documented (by design). Source: crates/velesdb-mobile/src/graph.rs (MobileGraphStore); core counterparts crates/velesdb-core/src/collection/graph/edge.rs (EdgeStore) and crates/velesdb-core/src/collection/graph_collection.rs (GraphCollection). Decision recorded in docs/planning/CORE_PARITY_REMEDIATION.md (T4).
The mobile SDK's MobileGraphStore is a self-contained, purely in-memory graph
engine (node/edge maps plus BFS/DFS/degree/label helpers) rather than a delegate
to velesdb-core's graph runtime. This is an intentional design decision, not
an accidental rewrite: mobile needs a RAM-only graph with no filesystem path,
WAL, or on-disk payloads, whereas core's GraphCollection persists nodes as JSON
payloads and requires a path. Core today exposes an in-memory EdgeStore (edges
- traversal) but has no in-memory
GraphNode-object store (label + properties - vector CRUD), so the node half of
MobileGraphStorehas no core API to delegate to.
What is single-sourced is the record types: MobileGraphStore is pinned to
core via From<velesdb_core::GraphNode / GraphEdge / TraversalResult> conversions
(graph.rs), so any field drift in the core types is a compile error in mobile —
the type-shadowing risk (the in-scope half of T4) is closed.
User impact: none functionally — the mobile graph API behaves as documented.
The architectural caveat is that mobile graph semantics are maintained
independently of core's graph engine. Full delegation is gated on core first
shipping an in-memory GraphStore API (node + edge CRUD, label query, cascade
remove, BFS/DFS); until then, copying core's graph engine into the MIT-licensed
mobile crate would violate the Core License boundary, so the fork is the correct
boundary-preserving choice.
Each entry states:
- Status: open / partial / documented / resolved / pre-existing.
- Source: the file or line referenced in code.
- User impact: what an operator or integrator actually sees.
- Resolution path or workaround where applicable.
For product-level scope boundaries (single-writer, no replication, RBAC scope, WASM hop-limit, benchmark infrastructure), see the README "Known Limitations" section.