sqlite_graphrag/constants.rs
1//! Compile-time constants shared across the crate.
2//!
3//! Grouped into embedding configuration, length and size limits, SQLite
4//! pragmas and retrieval tuning knobs. Values are taken from the PRD and
5//! must stay in sync with the migrations under `migrations/`.
6//!
7//! ## Dynamic concurrency permit calculation
8//!
9//! The maximum number of simultaneous instances can be adjusted at runtime
10//! using the formula:
11//!
12//! ```text
13//! permits = min(cpus, available_memory_mb / LLM_WORKER_RSS_MB) * 0.5
14//! ```
15//!
16//! where `available_memory_mb` is obtained via `sysinfo::System::available_memory()`
17//! converted to MiB. The result is capped at `MAX_CONCURRENT_CLI_INSTANCES`
18//! and floored at 1.
19
20/// Default embedding vector dimensionality for a NEWLY created database.
21///
22/// Sized for `qwen/qwen3-embedding-8b`, the model the OpenRouter REST backend
23/// uses today. Matryoshka Representation Learning (MRL, arXiv 2205.13147) lets
24/// a prefix of the native vector stand on its own, so 1024 is a real truncation
25/// point rather than a lossy resize.
26///
27/// This value governs `init` only. An existing database keeps the width
28/// recorded in `schema_meta.dim`, which [`crate::storage::connection`] adopts on
29/// every open — so raising this default can never silently reinterpret vectors
30/// already on disk. Widening a populated database is a deliberate migration
31/// that must re-embed every row; the previous default was 384, generated
32/// against `multilingual-e5-small`.
33///
34/// Precedence for the active dim is documented on [`embedding_dim`].
35pub const DEFAULT_EMBEDDING_DIM: usize = 1024;
36
37/// Default tracing filter level when neither CLI `-v`/`-q` nor XDG `log.level`
38/// is set (GAP-SG-93).
39pub const DEFAULT_LOG_LEVEL: &str = "info";
40
41/// Default OpenRouter chat completions endpoint (override via XDG
42/// `network.openrouter.chat_url` or alias `network.chat_url`).
43pub const DEFAULT_OPENROUTER_CHAT_URL: &str =
44 "https://openrouter.ai/api/v1/chat/completions";
45
46/// Default OpenRouter embeddings endpoint (override via XDG
47/// `network.openrouter.embeddings_url` or alias `network.embed_url`).
48pub const DEFAULT_OPENROUTER_EMBEDDINGS_URL: &str =
49 "https://openrouter.ai/api/v1/embeddings";
50
51/// Fail-fast probe budget for LLM backends before spawning (ms).
52/// Override via XDG `llm.probe_timeout_ms`.
53pub const DEFAULT_LLM_PROBE_TIMEOUT_MS: u64 = 800;
54
55/// Per-call timeout for query embedding (recall/hybrid Auto chain).
56/// Short budget so dead OAuth falls back to FTS quickly (GAP-E2E-06).
57/// Override via XDG `llm.query_embed_timeout_secs`.
58pub const DEFAULT_QUERY_EMBED_TIMEOUT_SECS: u64 = 3;
59
60/// Accepted range for any embedding dimensionality, override or recorded.
61///
62/// Declared once because the bound is checked on the CLI/XDG override, on the
63/// value adopted from `schema_meta`, and in the warning text. Three separate
64/// literals would be three chances to drift.
65pub const EMBEDDING_DIM_RANGE: std::ops::RangeInclusive<usize> = 8..=4096;
66
67/// Active embedding dimensionality for this process. `0` means unresolved.
68static ACTIVE_EMBEDDING_DIM: std::sync::atomic::AtomicUsize =
69 std::sync::atomic::AtomicUsize::new(0);
70
71/// Resolves the active embedding dimensionality (single source of truth).
72///
73/// Precedence (G-T-XDG-04):
74/// 1. CLI `--embedding-dim` / XDG `embedding.dim` via [`crate::runtime_config`];
75/// 2. the value recorded via [`set_active_embedding_dim`] — from `schema_meta`;
76/// 3. [`DEFAULT_EMBEDDING_DIM`].
77pub fn embedding_dim() -> usize {
78 if let Some(dim) = embedding_dim_from_runtime() {
79 return dim;
80 }
81 let active = ACTIVE_EMBEDDING_DIM.load(std::sync::atomic::Ordering::Acquire);
82 if active != 0 {
83 return active;
84 }
85 DEFAULT_EMBEDDING_DIM
86}
87
88/// Reads the CLI `--embedding-dim` flag or the XDG key `embedding.dim`.
89///
90/// Values outside [`EMBEDDING_DIM_RANGE`] are rejected with a warning rather
91/// than clamped: a clamped width would still mismatch the stored vectors, and
92/// `cosine_similarity` reports a dimension mismatch as `0.0` with no error, so
93/// the search would go quiet instead of failing.
94pub fn embedding_dim_from_runtime() -> Option<usize> {
95 let n = crate::runtime_config::embedding_dim_override()? as usize;
96 if EMBEDDING_DIM_RANGE.contains(&n) {
97 Some(n)
98 } else {
99 tracing::warn!(
100 value = n,
101 min = *EMBEDDING_DIM_RANGE.start(),
102 max = *EMBEDDING_DIM_RANGE.end(),
103 "embedding.dim override out of range; ignoring"
104 );
105 None
106 }
107}
108
109/// Records the dimensionality found in the opened database (`schema_meta.dim`).
110///
111/// Out-of-range values are ignored. A CLI flag or XDG override still wins over
112/// this value — see the precedence documented on [`embedding_dim`].
113pub fn set_active_embedding_dim(dim: usize) {
114 if EMBEDDING_DIM_RANGE.contains(&dim) {
115 ACTIVE_EMBEDDING_DIM.store(dim, std::sync::atomic::Ordering::Release);
116 }
117}
118
119// G46: FASTEMBED_MODEL_DEFAULT removed — the fastembed model was deleted in
120// v1.0.76 (LLM-only build); `schema_meta.model` now records the CLI version.
121
122/// Default worker count for the global Rayon pool.
123///
124/// Each worker holds one batch-embedding call in flight, so a wider pool buys
125/// no throughput on the LLM-only path and risks RSS oversubscription on a
126/// 4-8 GiB host. Override via XDG `parallelism.rayon_threads`.
127pub const DEFAULT_RAYON_THREADS: usize = 2;
128
129/// Batch size for `fastembed` encoding calls.
130pub const FASTEMBED_BATCH_SIZE: usize = 32;
131
132/// Maximum byte length for a memory `name` field in kebab-case.
133pub const MAX_MEMORY_NAME_LEN: usize = 80;
134
135/// Maximum byte length for an `ingest`-derived kebab-case name.
136///
137/// Stricter than `MAX_MEMORY_NAME_LEN` (80) to leave headroom for collision
138/// suffixes (`-2`, `-10`, ...) when multiple files derive to the same base.
139/// Used exclusively by `src/commands/ingest.rs`.
140pub const DERIVED_NAME_MAX_LEN: usize = 60;
141
142/// Maximum character length for a memory `description` field.
143pub const MAX_MEMORY_DESCRIPTION_LEN: usize = 500;
144
145/// Hard upper bound on memory `body` length in bytes.
146pub const MAX_MEMORY_BODY_LEN: usize = 512_000;
147
148/// Body character count above which the body is split into chunks.
149pub const MAX_BODY_CHARS_BEFORE_CHUNK: usize = 8_000;
150
151/// Maximum attempts when a statement returns `SQLITE_BUSY`.
152pub const MAX_SQLITE_BUSY_RETRIES: u32 = 5;
153
154/// Base delay in milliseconds for the first SQLITE_BUSY retry.
155///
156/// Each subsequent attempt doubles the delay (exponential backoff):
157/// 300 ms → 600 ms → 1200 ms → 2400 ms → 4800 ms (≈ 9.3 s total).
158pub const SQLITE_BUSY_BASE_DELAY_MS: u64 = 300;
159
160/// Query timeout applied to statements in milliseconds.
161pub const QUERY_TIMEOUT_MILLIS: u64 = 5_000;
162
163/// Jaccard threshold above which two memories are considered fuzzy duplicates.
164pub const DEDUP_FUZZY_THRESHOLD: f64 = 0.8;
165
166/// Cosine distance threshold below which two memories are semantic duplicates.
167pub const DEDUP_SEMANTIC_THRESHOLD: f32 = 0.1;
168
169/// Maximum number of hops allowed in graph traversals.
170pub const MAX_GRAPH_HOPS: u32 = 2;
171
172/// Minimum relationship weight required for traversal inclusion.
173pub const MIN_RELATION_WEIGHT: f64 = 0.3;
174
175/// Default traversal depth for `related` when `--hops` is omitted.
176pub const DEFAULT_MAX_HOPS: u32 = 2;
177
178/// Default minimum weight filter applied during graph traversal.
179pub const DEFAULT_MIN_WEIGHT: f64 = 0.3;
180
181/// Default weight assigned to newly created relationships.
182pub const DEFAULT_RELATION_WEIGHT: f64 = 0.5;
183
184/// Default `k` used by `recall` when the caller omits `--k`.
185pub const DEFAULT_K_RECALL: usize = 10;
186
187/// Default `k` for memory KNN searches when the caller omits `--k`.
188pub const K_MEMORIES_DEFAULT: usize = 10;
189
190/// Default `k` for entity KNN searches during graph expansion.
191pub const K_ENTITIES_SEARCH: usize = 5;
192
193/// Default upper bound on distinct entities persisted per memory.
194///
195/// Bumped from 30 → 50 in v1.0.43 to reduce semantic loss on rich documents.
196/// Configurable at runtime via XDG / runtime_config (not product env).
197pub const MAX_ENTITIES_PER_MEMORY: usize = 50;
198
199/// Resolves the per-memory entity cap (flag/XDG/`runtime_config`).
200///
201/// v1.0.43: makes the cap (default 50) configurable without product env.
202/// Stress tests showed inputs with 33-46 candidates being truncated at the old cap of 30.
203/// Values outside [1, 1000] fall back to the default.
204pub fn max_entities_per_memory() -> usize {
205 let n = crate::runtime_config::max_entities_per_memory(MAX_ENTITIES_PER_MEMORY);
206 if (1..=1_000).contains(&n) {
207 n
208 } else {
209 MAX_ENTITIES_PER_MEMORY
210 }
211}
212
213/// Upper bound on distinct relationships persisted per memory.
214pub const MAX_RELATIONSHIPS_PER_MEMORY: usize = 50;
215
216/// Resolves the per-memory relationship cap (flag/XDG/`runtime_config`).
217///
218/// v1.0.22: makes the cap (default 50) configurable without product env.
219/// Audit found that rich documents silently hit the cap; users with dense technical corpora
220/// can raise it via XDG. Values outside [1, 10000] fall back to the default.
221pub fn max_relationships_per_memory() -> usize {
222 let n = crate::runtime_config::max_relations_per_memory(MAX_RELATIONSHIPS_PER_MEMORY);
223 if (1..=10_000).contains(&n) {
224 n
225 } else {
226 MAX_RELATIONSHIPS_PER_MEMORY
227 }
228}
229
230/// Character length of the description preview shown in `list` output.
231pub const TEXT_DESCRIPTION_PREVIEW_LEN: usize = 100;
232
233/// `PRAGMA busy_timeout` value applied on every connection.
234pub const BUSY_TIMEOUT_MILLIS: i32 = 5_000;
235
236/// `PRAGMA cache_size` value in kibibytes (negative means KiB).
237pub const CACHE_SIZE_KB: i32 = -64_000;
238
239/// `PRAGMA mmap_size` value in bytes applied to each connection.
240pub const MMAP_SIZE_BYTES: i64 = 268_435_456;
241
242/// `PRAGMA wal_autocheckpoint` threshold in pages.
243pub const WAL_AUTOCHECKPOINT_PAGES: i32 = 1_000;
244
245/// Default `k` constant used by Reciprocal Rank Fusion in `hybrid-search`.
246pub const RRF_K_DEFAULT: u32 = 60;
247
248/// Chunk size expressed in tokens for body splitting.
249pub const CHUNK_SIZE_TOKENS: usize = 400;
250
251/// Token overlap between consecutive chunks.
252pub const CHUNK_OVERLAP_TOKENS: usize = 50;
253
254/// Explicit operational guard for multi-chunk documents in `remember`.
255///
256/// The multi-chunk path uses serial embeddings to avoid ONNX memory amplification.
257/// This limit preserves a clear operational ceiling for agents and scripts.
258pub const REMEMBER_MAX_SAFE_MULTI_CHUNKS: usize = 512;
259
260/// Ceiling on chunks per controlled micro-batch in `remember`.
261///
262/// The `fastembed` runtime uses `BatchLongest` padding, so oversized batches amplify
263/// the cost of the longest chunk. This ceiling keeps batches small even when chunks are short.
264pub const REMEMBER_MAX_CONTROLLED_BATCH_CHUNKS: usize = 4;
265
266/// Maximum padded-token budget per controlled micro-batch in `remember`.
267///
268/// The budget uses `max_tokens_no_batch * batch_size`, approximating the real cost of
269/// `BatchLongest` padding. Values exceeding this fall back to smaller batches or serialisation.
270pub const REMEMBER_MAX_CONTROLLED_BATCH_PADDED_TOKENS: usize = 512;
271
272/// Prefix prepended to bodies before embedding as required by E5 models.
273pub const PASSAGE_PREFIX: &str = "passage: ";
274
275/// Prefix prepended to queries before embedding as required by E5 models.
276pub const QUERY_PREFIX: &str = "query: ";
277
278/// Crate version string sourced from `CARGO_PKG_VERSION` at build time.
279pub const SQLITE_GRAPHRAG_VERSION: &str = env!("CARGO_PKG_VERSION");
280
281/// PRD-canonical regex that validates names and namespaces. Allows 1 char `[a-z0-9]`
282/// OR a 2-80 char string starting with a letter and ending with a letter/digit,
283/// containing only `[a-z0-9-]`. Rejects the `__` prefix (internal reserved).
284pub const NAME_SLUG_REGEX: &str = r"^[a-z][a-z0-9-]{0,78}[a-z0-9]$|^[a-z0-9]$";
285
286static NAME_SLUG_RE: std::sync::OnceLock<regex::Regex> = std::sync::OnceLock::new();
287
288/// Returns a reference to the compiled [`NAME_SLUG_REGEX`] pattern.
289/// Compiled once on first call, cached via `OnceLock`.
290// expect_used (audited v1.0.97): NAME_SLUG_REGEX is a const literal; a parse
291// failure would be a compile-reproducible bug, never a runtime condition.
292#[allow(clippy::expect_used)]
293pub fn name_slug_regex() -> &'static regex::Regex {
294 NAME_SLUG_RE.get_or_init(|| {
295 regex::Regex::new(NAME_SLUG_REGEX).expect("NAME_SLUG_REGEX is a valid pattern")
296 })
297}
298
299/// Default retention period (days) used by `purge` when `--retention-days` is omitted.
300pub const PURGE_RETENTION_DAYS_DEFAULT: u32 = 90;
301
302/// Maximum number of simultaneously active namespaces (deleted_at IS NULL). Exit 5 when exceeded.
303pub const MAX_NAMESPACES_ACTIVE: u32 = 100;
304
305/// Maximum tokens accepted by an embedding input before chunking.
306pub const EMBEDDING_MAX_TOKENS: usize = 512;
307
308/// Maximum token count for a SINGLE embedding request input (GAP-SG-02).
309///
310/// The `qwen/qwen3-embedding-8b` model used by the OpenRouter backend accepts
311/// roughly 32K tokens of context. This ceiling rejects an input above a safe
312/// margin BEFORE the HTTP request, using the conservative cl100k_base proxy in
313/// [`crate::tokenizer::count_tokens`] (which emits at least as many tokens as
314/// Qwen for the same text). Distinct from [`EMBEDDING_MAX_TOKENS`] (512), which
315/// is the per-chunk ceiling that drives chunking.
316pub const EMBEDDING_REQUEST_MAX_TOKENS: usize = 30_000;
317
318/// Initial `max_tokens` budget sent on an `enrich` chat-completion request
319/// (GAP-SG-70/71).
320///
321/// Chosen well below [`ENRICH_MAX_TOKENS_CEILING`] so a well-formed response
322/// completes in one attempt for the common case; only bodies that need more
323/// room trigger the growth loop below.
324pub const ENRICH_INITIAL_MAX_TOKENS: u32 = 4_096;
325
326/// Multiplier applied to `max_tokens` each time OpenRouter reports
327/// `finish_reason: "length"` on an `enrich` chat-completion (GAP-SG-70/71).
328pub const ENRICH_MAX_TOKENS_GROWTH_FACTOR: u32 = 2;
329
330/// Upper bound on `max_tokens` growth for an `enrich` chat-completion
331/// (GAP-SG-70/71).
332///
333/// Kept with margin under the ~32K-token context ceiling of
334/// `deepseek/deepseek-v4-flash:nitro` (see [`EMBEDDING_REQUEST_MAX_TOKENS`]
335/// for the equivalent embedding-side ceiling) so growth never requests a
336/// budget the model cannot honour.
337pub const ENRICH_MAX_TOKENS_CEILING: u32 = 16_384;
338
339/// Maximum number of `max_tokens`-growth re-attempts after a truncated
340/// (`finish_reason: "length"`) `enrich` chat-completion, before giving up and
341/// returning the truncation as an error (GAP-SG-70/71).
342pub const ENRICH_MAX_LENGTH_RETRIES: u32 = 2;
343
344/// Byte budget for one auto-split partition (sub-memory) in `ingest`
345/// (GAP-SG-04/07).
346///
347/// Chosen below the 127 KB body margin so each partition also stays under
348/// [`REMEMBER_MAX_SAFE_MULTI_CHUNKS`] chunks and [`EMBEDDING_REQUEST_MAX_TOKENS`]
349/// tokens, even for multibyte/CJK text (~1 cl100k token per UTF-8 char, so
350/// 80 KiB / 3 bytes-per-char yields about 27K tokens, below the 30K ceiling).
351pub const AUTOSPLIT_PARTITION_MAX_BYTES: usize = 80 * 1024;
352
353/// Maximum result count from the recursive graph CTE in `recall`.
354pub const K_GRAPH_MATCHES_LIMIT: usize = 20;
355
356/// Default `--limit` for `list` when omitted.
357pub const K_LIST_DEFAULT_LIMIT: usize = 100;
358
359/// Default `--limit` for `graph entities` when omitted.
360pub const K_GRAPH_ENTITIES_DEFAULT_LIMIT: usize = 50;
361
362/// Default `--limit` for `related` when omitted.
363pub const K_RELATED_DEFAULT_LIMIT: usize = 10;
364
365/// Default `--limit` for `history` when omitted.
366pub const K_HISTORY_DEFAULT_LIMIT: usize = 20;
367
368/// Default weight for the vector contribution in the `hybrid-search` RRF formula.
369pub const WEIGHT_VEC_DEFAULT: f64 = 1.0;
370
371/// Default weight for the BM25 text contribution in the `hybrid-search` RRF formula.
372pub const WEIGHT_FTS_DEFAULT: f64 = 1.0;
373
374/// Character size of the body preview emitted in text/markdown formats.
375pub const TEXT_BODY_PREVIEW_LEN: usize = 200;
376
377/// Default value injected into ORT_NUM_THREADS when not set by the user.
378pub const ORT_NUM_THREADS_DEFAULT: &str = "1";
379
380/// Default value injected into ORT_INTRA_OP_NUM_THREADS when not set.
381pub const ORT_INTRA_OP_NUM_THREADS_DEFAULT: &str = "1";
382
383/// Default value injected into OMP_NUM_THREADS when not set by the user.
384pub const OMP_NUM_THREADS_DEFAULT: &str = "1";
385
386/// Exit code for partial batch failure (PRD line 1822). Conflicts with DbBusy in v1.x;
387/// in v2.0.0 DbBusy migrates to 15 and this code takes 13 per PRD.
388pub const BATCH_PARTIAL_FAILURE_EXIT_CODE: i32 = 13;
389
390/// Exit code for DbBusy in v2.0.0 (migrated from 13 to free 13 for batch failure).
391pub const DB_BUSY_EXIT_CODE: i32 = 15;
392
393/// Polling interval in milliseconds used by `--wait-lock` between `try_lock_exclusive` attempts.
394pub const CLI_LOCK_POLL_INTERVAL_MS: u64 = 500;
395
396/// Process exit code returned when the lock is busy and no wait was requested (EX_TEMPFAIL).
397pub const CLI_LOCK_EXIT_CODE: i32 = 75;
398
399/// Maximum number of CLI instances running simultaneously.
400///
401/// Limits the counting
402/// semaphore in [`crate::lock`] to prevent memory overload when multiple parallel
403/// v1.0.75 (G18 solution): removed the rigid 4-slot ceiling. The adaptive
404/// `calculate_safe_concurrency` function in [`crate::lock`]` now reports
405/// the dynamic limit. This constant is preserved as a *legacy fallback*
406/// when the dynamic calculation cannot be performed (e.g. when `sysinfo`
407/// cannot read `/proc/meminfo`).
408///
409/// Operators should prefer passing `--max-concurrency` explicitly OR
410/// letting the runtime compute the limit. The default ceiling is intentionally
411/// higher (16) so the legacy 4-slot hard cap does not silently reappear.
412pub const MAX_CONCURRENT_CLI_INSTANCES: usize = 16;
413
414/// G28-B (v1.0.68): polling interval in milliseconds used by
415/// `acquire_job_singleton` between retry attempts when another invocation
416/// already holds the singleton for `(job_type, namespace)`.
417pub const JOB_SINGLETON_POLL_INTERVAL_MS: u64 = 1000;
418
419/// Minimum available memory in MiB required before starting model loading.
420///
421/// If `sysinfo::System::available_memory() / 1_048_576` falls below this value,
422/// the invocation is aborted with [`crate::errors::AppError::LowMemory`]
423/// (exit code [`LOW_MEMORY_EXIT_CODE`]).
424pub const MIN_AVAILABLE_MEMORY_MB: u64 = 2_048;
425
426/// Maximum process RSS in MiB before aborting embedding operations.
427/// Users can override via `--max-rss-mb`. Set to 8 GiB by default.
428pub const DEFAULT_MAX_RSS_MB: u64 = 8_192;
429
430/// Maximum time in seconds an instance waits to acquire a concurrency slot.
431///
432/// Passed as the default for `--max-wait-secs` in the CLI. After exhausting this limit,
433/// the invocation returns [`crate::errors::AppError::AllSlotsFull`] with exit code
434/// [`CLI_LOCK_EXIT_CODE`] (75).
435pub const CLI_LOCK_DEFAULT_WAIT_SECS: u64 = 300;
436
437/// v1.0.75 (G18 + G23): expected RSS in MiB for an LLM-only worker that
438/// spawns a `claude -p` or `codex exec` subprocess. Much lower than the
439/// embedding cost because the ONNX model is not loaded per-worker.
440pub const LLM_WORKER_RSS_MB: u64 = 350;
441
442/// Process exit code returned when available memory is below [`MIN_AVAILABLE_MEMORY_MB`].
443///
444/// Value `77` is `EX_NOPERM` in glibc sysexits, reused here to indicate
445/// "insufficient system resource to proceed".
446pub const LOW_MEMORY_EXIT_CODE: i32 = 77;
447
448/// Process exit code returned when a duplicate memory or entity is detected (exit 9).
449///
450/// Moved from `2` to `9` in v1.0.52 to free exit code `2` for future use and align
451/// with the PRD exit code contract. Shell callers and LLM agents must use `9` from
452/// this version onwards.
453pub const DUPLICATE_EXIT_CODE: i32 = 9;
454
455/// Process exit code returned when shutdown is requested via SIGINT/SIGTERM/SIGHUP
456/// (v1.0.82, GAP-002 final).
457///
458/// The shell sees this code INSTEAD of the legacy `128 + signal` (130/143/129) so
459/// that LLM agents and orchestrators can branch on a single deterministic value
460/// when the operation was cancelled by the user. The signal name is preserved in
461/// the JSON envelope emitted before exit (`{"code":19,"signal":"SIGINT",...}`).
462pub const SHUTDOWN_EXIT_CODE: i32 = 19;
463
464/// Canonical value of `PRAGMA user_version` written after migrations.
465///
466/// **Why 50 instead of `CURRENT_SCHEMA_VERSION` (15)?**
467/// `user_version` is a 32-bit integer that SQLite reserves for application use.
468/// We deliberately set it to a project-specific marker (50 = decimal) so external
469/// inspection tools (`sqlite3 db.sqlite "PRAGMA user_version"`, the `file` command,
470/// SQLite browser GUIs) can distinguish a sqlite-graphrag database from a generic
471/// SQLite file at a glance. The application-level schema version (15, matching
472/// `CURRENT_SCHEMA_VERSION`) is stored in the `schema_meta` table and exposed via
473/// `health --json`/`stats --json`. Bumping migrations does NOT change this constant.
474/// Refinery uses its own `refinery_schema_history` table for migration bookkeeping.
475pub const SCHEMA_USER_VERSION: i64 = 50;
476
477/// Current schema version, equal to the highest migration number in `migrations/Vnnn__*.sql`.
478///
479/// Added in v1.0.27 as a runtime and test sanity check.
480/// Must be bumped in sync with new Refinery migrations; the unit test
481/// `schema_version_matches_migrations_count` validates this automatically.
482pub const CURRENT_SCHEMA_VERSION: u32 = 16;
483
484#[cfg(test)]
485mod tests_schema_version {
486 use super::CURRENT_SCHEMA_VERSION;
487
488 #[test]
489 fn schema_version_matches_migrations_count() {
490 let manifest_dir = env!("CARGO_MANIFEST_DIR");
491 let migrations_dir = std::path::Path::new(manifest_dir).join("migrations");
492 let count = std::fs::read_dir(&migrations_dir)
493 .expect("migrations directory must exist")
494 .filter_map(|entry| entry.ok())
495 .filter(|entry| entry.file_name().to_string_lossy().starts_with('V'))
496 .count() as u32;
497 assert_eq!(
498 CURRENT_SCHEMA_VERSION, count,
499 "CURRENT_SCHEMA_VERSION ({CURRENT_SCHEMA_VERSION}) must equal the number of V*.sql migrations ({count})"
500 );
501 }
502}