surrealdb-core 3.2.0

A scalable, distributed, collaborative, document-graph database, for the realtime web
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
pub(crate) mod dynamic;

use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
use std::str::FromStr;
use std::sync::LazyLock;
use std::time::Duration;

use crate::iam::file::extract_allowed_paths;
use crate::str::ParseBytes;

/// The publicly visible name of the server
pub const SERVER_NAME: &str = "SurrealDB";

/// The characters which are supported in server record IDs
pub const ID_CHARS: [char; 36] = [
	'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i',
	'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
];

/// Specifies the names of parameters which can not be specified in a query
pub const PROTECTED_PARAM_NAMES: &[&str] = &["access", "auth", "token", "session"];

/// Default capacity for the bounded channel used to deliver live-query
/// notifications from the datastore to subscribers.
pub const NOTIFICATIONS_CHANNEL_SIZE: usize = 15_000;

/// A map with a set of configuration values stored as pairs of strings.
#[derive(Clone, Debug)]
pub struct ConfigMap {
	values: HashMap<String, String>,
}

impl Default for ConfigMap {
	fn default() -> Self {
		Self::empty()
	}
}

impl ConfigMap {
	/// Returns an empty map.
	pub fn empty() -> Self {
		ConfigMap {
			values: HashMap::new(),
		}
	}

	/// Adds a new key and value into the config map, will overwrite existing values.
	pub fn with_key_value<K, V>(mut self, key: K, value: V) -> Self
	where
		String: From<K>,
		String: From<V>,
	{
		self.values.insert(key.into(), value.into());
		self
	}

	/// Creates a config map from all the environment variables prefixed with `SURREAL_`
	pub fn from_env() -> Self {
		Self::from_env_prefix("SURREAL_")
	}

	/// Creates a config map from all the environment variables prefixed with the specific prefix.
	pub fn from_env_prefix(prefix: &str) -> Self {
		let mut values = HashMap::new();
		for (k, v) in std::env::vars() {
			let Some(x) = k.strip_prefix(prefix) else {
				continue;
			};

			let key_name = x.to_lowercase();
			values.insert(key_name, v);
		}
		ConfigMap {
			values,
		}
	}

	/// Map all the keys in the config map with the given closure.
	pub fn map_keys<F: FnMut(String) -> String>(self, mut f: F) -> Self {
		Self {
			values: self.values.into_iter().map(|(k, v)| (f(k), v)).collect(),
		}
	}

	/// Creates a config map from all the environment variables
	pub fn from_config_string(s: &str) -> Self {
		let values = s
			.split('&')
			.filter_map(|x| {
				let (k, v) = x.split_once('=')?;
				Some((k.to_lowercase(), v.to_string()))
			})
			.collect();

		ConfigMap {
			values,
		}
	}

	/// Join two config maps together prefering the values not in self.
	pub fn join(mut self, other: ConfigMap) -> Self {
		for (k, v) in other.values {
			self.values.insert(k, v);
		}
		self
	}

	/// Load a config type from the map.
	pub fn load<C: Config>(&self) -> C {
		let mut def = C::default();
		def.parse(self);
		def
	}

	/// Parse a value out of the map if it exists.
	///
	/// If either the key does not exist or the parsing values the value is unaltered.
	pub fn parse_key<S: FromStr>(&self, key: &str, value: &mut S) -> &Self {
		self.parse_key_with(key, value, |x| S::from_str(x).ok())
	}

	pub fn parse_key_option<S: FromStr>(&self, key: &str, value: &mut Option<S>) -> &Self {
		self.parse_key_with(key, value, |x| S::from_str(x).ok().map(Some))
	}

	/// Parse a boolean out of the map if it exists.
	///
	/// If either the key does not exist or the parsing values the value is unaltered.
	pub fn parse_key_bool(&self, key: &str, value: &mut bool) -> &Self {
		self.parse_key_with(key, value, |x| {
			if x.eq_ignore_ascii_case("true") || x == "1" {
				Some(true)
			} else if x.eq_ignore_ascii_case("false") || x == "0" {
				Some(false)
			} else {
				None
			}
		})
	}

	/// Parse a value out of the map if it exists.
	/// Takes a closure which can be used to define how to parse the string
	///
	/// If either the key does not exist or the parsing closure returns `None` the value is
	/// unaltered.
	pub fn parse_key_with<R, F: FnOnce(&str) -> Option<R>>(
		&self,
		key: &str,
		value: &mut R,
		f: F,
	) -> &Self {
		let Some(v) = self.values.get(key) else {
			return self;
		};

		let Some(v) = f(v) else {
			warn!("Could not parse configuration value for key `{}`", key.to_uppercase());
			return self;
		};

		*value = v;
		self
	}

	pub fn has_key(&self, key: &str) -> bool {
		self.values.contains_key(key)
	}
}

/// Trait for types which contain configureation information.
pub trait Config: Default {
	fn parse(&mut self, map: &ConfigMap);
}

/// Selects which live-query execution engine the datastore uses.
///
/// See [`crate::lq`] for the architecture. Defaults to [`LiveQueryEngine::Inline`]
/// (the historical behaviour) so the new pipeline can be rolled out behind this
/// flag without changing existing deployments.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub enum LiveQueryEngine {
	/// Legacy engine: per-subscriber matching, permission checks, and projection
	/// run inline on the mutator's transaction path before commit, so write cost
	/// scales with the number of live subscribers on the written table.
	#[default]
	Inline,
	/// Inverted engine: the mutator only persists before/after values, and a
	/// per-node router performs per-subscriber matching off the write path, so
	/// write throughput is independent of the subscriber count.
	Router,
}

impl fmt::Display for LiveQueryEngine {
	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::Inline => f.write_str("inline"),
			Self::Router => f.write_str("router"),
		}
	}
}

impl FromStr for LiveQueryEngine {
	type Err = String;

	fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
		match s.to_lowercase().as_str() {
			"inline" => Ok(Self::Inline),
			"router" => Ok(Self::Router),
			v => Err(format!("Invalid live query engine: '{v}'. Expected 'inline' or 'router'")),
		}
	}
}

#[derive(Debug)]
pub struct CommonConfig {
	pub memory_threshold: usize,
	/// Specifies how many concurrent jobs can be buffered in the worker channel
	pub max_concurrent_tasks: usize,
	/// Specifies how deep recursive computation will go before erroring (default:
	/// 120)
	pub max_computation_depth: u32,
	/// Specifies how deep the parser will parse nested objects and arrays (default:
	/// 100)
	pub max_object_parsing_depth: u32,
	/// Specifies how deep the parser will parse recursive queries (default: 20)
	pub max_query_parsing_depth: u32,
	/// Specifies how deep the parser will build an expression operator tree
	/// before erroring. Bounds left-associative operator spines (e.g.
	/// `1 + 1 + 1 + ...`) and prefix/postfix chains, which are otherwise
	/// unbounded and overflow the call stack when the resulting tree is later
	/// walked recursively (e.g. dropped, formatted, or lowered to `expr::Expr`).
	/// Kept low enough that even those recursive walks stay well within a
	/// conservative worker-thread stack (default: 128)
	pub max_expression_parsing_depth: u32,
	/// The maximum recursive idiom path depth allowed (default: 256)
	pub idiom_recursion_limit: u32,
	/// The maximum size of a compiled regular expression (default: 10 MiB)
	pub regex_size_limit: usize,
	/// Specifies the number of computed regexes which can be cached in the engine
	/// (default: 1000)
	pub regex_cache_size: usize,
	/// Specifies the number of items which can be cached within a single
	/// transaction (default: 512)
	pub transaction_cache_size: usize,
	/// Specifies the number of definitions which can be cached across transactions
	/// (default: 1,000)
	pub datastore_cache_size: usize,
	/// The maximum number of keys that should be scanned at once for export queries
	/// (default: 1000)
	pub export_batch_size: u32,
	/// Batch size used when allocating sequence-based document IDs for the
	/// concurrent full-text index. Larger batches reduce coordination on the
	/// distributed sequence at the cost of larger gaps when a node is lost
	/// before exhausting its current batch. (default: 1000)
	pub fts_doc_ids_batch_size: u32,
	/// The number of batches each operator buffers ahead of downstream demand.
	/// Set to 0 to disable operator-level pipeline buffering.
	/// (default: 2)
	pub operator_buffer_size: usize,
	/// Default batch size for scan operators that collect records before
	/// yielding downstream (table/index/record-id/graph/reference scans).
	/// Memory-constrained deployments can reduce this to lower per-pipeline
	/// in-flight memory at the cost of slightly more per-batch dispatch
	/// overhead. Per-batch unit is values, not bytes. (default: 1000)
	pub scan_batch_size: usize,
	/// The maximum size of the priority queue triggering usage of the priority
	/// queue for the result collector.
	pub max_order_limit_priority_queue_size: u32,
	/// Whether eligible `ORDER BY … LIMIT` table scans may skip record decode
	/// for rows that cannot beat the current top-K threshold (default: true)
	pub topk_threshold_pushdown_enabled: bool,
	/// Maximum number of build-side rows a GQL `MATCH` hash join (and the
	/// whole-row `Distinct` dedup that rides the same budget) may hold in memory
	/// before failing the query (default: 1,000,000). Bounds the in-memory
	/// build/seen set for GQL v2 binding-table execution; spill to disk is a
	/// future change (matching the `Aggregate` stance). Errors that trip this
	/// guard name the env knob (`SURREAL_GQL_MAX_JOIN_BUILD_ROWS`).
	pub gql_max_join_build_rows: usize,
	/// Maximum number of paths a single GQL `MATCH` `PathExpand` (variable-length
	/// / quantified edge traversal) may have live on its DFS stack plus already
	/// emitted, per source row, before failing the query (default: 1,000,000).
	/// Bounds the worst-case path explosion of a quantified pattern over a dense
	/// or cyclic graph; edge-uniqueness-within-path guarantees termination but the
	/// number of distinct paths can still be very large. Errors that trip this
	/// guard name the env knob (`SURREAL_GQL_MAX_PATH_ROWS`).
	pub gql_max_path_rows: usize,
	/// Maximum number of rows a single GQL `MATCH` fan-out operator (`HashJoin` —
	/// including the `Cross` cartesian product — and single-hop `Expand`) may
	/// emit, cumulatively across all batches, before failing the query (default:
	/// 1,000,000). Unlike `gql_max_join_build_rows` (which bounds the in-memory
	/// build/seen set), this bounds the *output* product: a cross join of a
	/// bounded build side against a streaming probe side, or a high-fan-out
	/// expand, can emit unboundedly many rows while holding only a small build
	/// set. Errors that trip this guard name the env knob
	/// (`SURREAL_GQL_MAX_OUTPUT_ROWS`).
	pub gql_max_output_rows: usize,
	/// The maximum stack size of the JavaScript function runtime (default: 256 KiB)
	pub scripting_max_stack_size: usize,
	/// The maximum memory limit of the JavaScript function runtime (default: 2 MiB)
	pub scripting_max_memory_limit: usize,
	/// The maximum amount of time that a JavaScript function can run (default: 5
	/// seconds)
	pub scripting_max_time_limit: Duration,
	/// The maximum number of HTTP redirects allowed within http functions (default:
	/// 10)
	pub max_http_redirects: usize,
	/// The maximum number of idle HTTP connections to maintain per host (default: 128)
	pub max_http_idle_connections_per_host: usize,
	/// The maximum number of total idle HTTP connections to maintain (default: 1000)
	pub max_http_idle_connections: usize,
	/// The timeout for idle HTTP connections before closing (default: 90 seconds)
	pub http_idle_timeout_secs: u64,
	/// The timeout for connecting to HTTP endpoints (default: 30 seconds)
	pub http_connect_timeout_secs: u64,
	/// Forward all authentication errors to the client. Do not use in production
	/// (default: false)
	pub insecure_forward_access_errors: bool,
	/// The number of result records which will trigger on-disk sorting (default:
	/// 50,000)
	pub external_sorting_buffer_limit: usize,
	/// Used to limit allocation for builtin functions. Default: 2^20 (1 MiB),
	/// can be as large as 28 (2^28, 256 MiB)
	pub generation_allocation_limit: usize,
	/// The maximum input string length for similarity/distance functions (default:
	/// 16384 bytes)
	pub string_similarity_limit: usize,
	/// Specifies a list of paths in which files can be accessed (default: empty)
	pub file_allowlist: Vec<PathBuf>,
	/// Specify the name of a global bucket for file data (default: None)
	pub global_bucket: Option<String>,
	/// Whether to enforce a global bucket for file data (default: false)
	pub global_bucket_enforced: bool,
	/// Specify the USER-AGENT string used by HTTP requests
	pub surrealdb_user_agent: String,
	/// The maximum total size of the HNSW ANN cache (default: 256 MiB)
	pub hnsw_cache_size: u64,
	/// The maximum total size of the DiskANN ANN cache (default: 256 MiB)
	pub diskann_cache_size: u64,
	/// Specifies the number of surrealism modules which can be cached across transactions
	/// (default: 100)
	pub surrealism_cache_size: usize,
	/// Per-module WASM linear memory ceiling in bytes (default: none / unlimited).
	/// When set, each WASM store is limited via `StoreLimits`. Effective limit is
	/// `min(this, module_config.max_memory_bytes)` when both are set.
	pub surrealism_max_memory: Option<usize>,
	/// Per-invocation execution time ceiling in milliseconds for Surrealism WASM modules
	/// (default: none / unlimited). Combined with module config and query context timeout
	/// via `min()` to produce the effective deadline.
	pub surrealism_max_execution_time: Option<u64>,

	/// Per-module KV store entry count ceiling for Surrealism WASM modules (default: none /
	/// unlimited). Effective limit is `min(this, module_config.max_kv_entries)` when both are
	/// set.
	pub surrealism_max_kv_entries: Option<usize>,
	/// Per-module KV store maximum value size in bytes for Surrealism WASM modules
	/// (default: none / unlimited). Effective limit is
	/// `min(this, module_config.max_kv_value_bytes)` when both are set.
	pub surrealism_max_kv_value_bytes: Option<usize>,
	/// Maximum aggregate size in bytes for attached filesystem entries in `.surli` archives
	/// (default: 100 MiB). Applied when unpacking module archives during `DEFINE MODULE` or
	/// eager loading.
	pub surrealism_max_fs_bytes: u64,
	/// Per-module controller pool size ceiling for Surrealism WASM modules (default: 8).
	/// Each pooled controller holds an instantiated WASM store. Effective pool size is
	/// `min(this, module_config.max_pool_size.unwrap_or(this))`.
	pub surrealism_max_pool_size: usize,
	/// Per-module controller pool size ceiling for Surrealism WASM modules (default: 8).
	/// Each pooled controller holds an instantiated WASM store. Effective pool size is
	/// `min(this, module_config.max_pool_size.unwrap_or(this))`.
	pub surrealism_log_level: String,
	/// Selects the live-query execution engine (default: [`LiveQueryEngine::Inline`]).
	pub live_query_engine: LiveQueryEngine,
	/// Retention window for the dedicated live-query event keyspace (only used when
	/// `live_query_engine` is `Router`). Sized to cover subscriber reconnect and
	/// rolling-upgrade windows during which a subscriber may need to replay missed
	/// events. Independent of any user-defined `CHANGEFEED` retention (default: 1h).
	pub live_query_retention: Duration,
}

impl Default for CommonConfig {
	fn default() -> Self {
		Self {
			memory_threshold: 0,
			#[cfg(not(target_family = "wasm"))]
			max_concurrent_tasks: 64,
			#[cfg(target_family = "wasm")]
			max_concurrent_tasks: 1,
			max_computation_depth: 120,
			max_object_parsing_depth: 100,
			max_query_parsing_depth: 20,
			max_expression_parsing_depth: 128,
			idiom_recursion_limit: 256,
			regex_size_limit: 10 * 1024 * 1024,
			regex_cache_size: 1_000,
			transaction_cache_size: 512,
			datastore_cache_size: 1_000,
			export_batch_size: 1000,
			fts_doc_ids_batch_size: 1000,
			operator_buffer_size: 2,
			scan_batch_size: crate::exec::operators::scan::common::DEFAULT_SCAN_BATCH_SIZE,
			max_order_limit_priority_queue_size: 1000,
			topk_threshold_pushdown_enabled: true,
			gql_max_join_build_rows: 1_000_000,
			gql_max_path_rows: 1_000_000,
			gql_max_output_rows: 1_000_000,
			scripting_max_stack_size: 256 * 1024,
			scripting_max_memory_limit: 2 << 20,
			scripting_max_time_limit: Duration::from_secs(5),
			max_http_redirects: 10,
			max_http_idle_connections_per_host: 128,
			max_http_idle_connections: 1000,
			http_idle_timeout_secs: 90,
			http_connect_timeout_secs: 30,
			insecure_forward_access_errors: false,
			external_sorting_buffer_limit: 50_000,
			generation_allocation_limit: 2 << 20,
			string_similarity_limit: 16384,
			file_allowlist: Vec::new(),
			global_bucket: None,
			global_bucket_enforced: false,
			surrealdb_user_agent: "SurrealDB".to_string(),
			hnsw_cache_size: 256 * 1024 * 1024,
			diskann_cache_size: 256 * 1024 * 1024,
			surrealism_cache_size: 100,
			surrealism_max_memory: None,
			surrealism_max_execution_time: None,
			surrealism_max_kv_entries: None,
			surrealism_max_kv_value_bytes: None,
			surrealism_max_fs_bytes: 100 * 1024 * 1024,
			surrealism_max_pool_size: 8,
			surrealism_log_level: "debug".to_string(),
			live_query_engine: LiveQueryEngine::Inline,
			live_query_retention: Duration::from_secs(3600),
		}
	}
}

impl Config for CommonConfig {
	fn parse(&mut self, map: &ConfigMap) {
		map.parse_key_with("memory_threshold", &mut self.memory_threshold, parse_memory_threshold)
			.parse_key("max_concurrent_tasks", &mut self.max_concurrent_tasks)
			.parse_key("max_computation_depth", &mut self.max_computation_depth)
			.parse_key("max_object_parsing_depth", &mut self.max_object_parsing_depth)
			.parse_key("max_query_parsing_depth", &mut self.max_query_parsing_depth)
			.parse_key("max_expression_parsing_depth", &mut self.max_expression_parsing_depth)
			.parse_key("regex_size_limit", &mut self.regex_size_limit)
			.parse_key("regex_cache_size", &mut self.regex_cache_size)
			.parse_key("transaction_cache_size", &mut self.transaction_cache_size)
			.parse_key("datastore_cache_size", &mut self.datastore_cache_size)
			.parse_key("surrealism_cache_size", &mut self.surrealism_cache_size)
			.parse_key("export_batch_size", &mut self.export_batch_size)
			.parse_key("fts_doc_ids_batch_size", &mut self.fts_doc_ids_batch_size)
			.parse_key("operator_buffer_size", &mut self.operator_buffer_size)
			.parse_key("scan_batch_size", &mut self.scan_batch_size)
			.parse_key(
				"max_order_limit_priority_queue_size",
				&mut self.max_order_limit_priority_queue_size,
			)
			.parse_key("topk_threshold_pushdown_enabled", &mut self.topk_threshold_pushdown_enabled)
			.parse_key("gql_max_join_build_rows", &mut self.gql_max_join_build_rows)
			.parse_key("gql_max_path_rows", &mut self.gql_max_path_rows)
			.parse_key("gql_max_output_rows", &mut self.gql_max_output_rows)
			.parse_key("scripting_max_stack_size", &mut self.scripting_max_stack_size)
			.parse_key("scripting_max_memory_limit", &mut self.scripting_max_memory_limit)
			.parse_key_with("scripting_max_time_limit", &mut self.scripting_max_time_limit, |x| {
				x.parse().map(Duration::from_millis).ok()
			})
			.parse_key("max_http_redirects", &mut self.max_http_redirects)
			.parse_key(
				"max_http_idle_connections_per_host",
				&mut self.max_http_idle_connections_per_host,
			)
			.parse_key("max_http_idle_connections", &mut self.max_http_idle_connections)
			.parse_key("http_idle_timeout_secs", &mut self.http_idle_timeout_secs)
			.parse_key("http_connect_timeout_secs", &mut self.http_connect_timeout_secs)
			.parse_key("insecure_forward_access_errors", &mut self.insecure_forward_access_errors)
			.parse_key("external_sorting_buffer_limit", &mut self.external_sorting_buffer_limit)
			.parse_key_with(
				"generation_allocation_limit",
				&mut self.generation_allocation_limit,
				|x| x.parse::<usize>().ok().map(|x| 2 << x.min(28)),
			)
			.parse_key("string_similarity_limit", &mut self.string_similarity_limit)
			.parse_key("hnsw_cache_size", &mut self.hnsw_cache_size)
			.parse_key("diskann_cache_size", &mut self.diskann_cache_size)
			.parse_key_with("file_allowlist", &mut self.file_allowlist, |x| {
				// FIXME: We really shouldn't be doing random, faillable, IO when reading
				// configuration values. But no way to fix it without restructuring the
				// datastore entirely.
				Some(extract_allowed_paths(x, true, "file"))
			})
			.parse_key("surrealdb_user_agent", &mut self.surrealdb_user_agent)
			.parse_key_option("surrealism_max_memory", &mut self.surrealism_max_memory)
			.parse_key_option(
				"surrealism_max_execution_time",
				&mut self.surrealism_max_execution_time,
			)
			.parse_key_option("surrealism_max_kv_entries", &mut self.surrealism_max_kv_entries)
			.parse_key_option(
				"surrealism_max_kv_value_bytes",
				&mut self.surrealism_max_kv_value_bytes,
			)
			.parse_key("surrealism_max_fs_bytes", &mut self.surrealism_max_fs_bytes)
			.parse_key_with("surrealism_log_level", &mut self.surrealism_log_level, |s| {
				Some(s.to_string())
			})
			.parse_key("live_query_engine", &mut self.live_query_engine)
			.parse_key_with("live_query_retention", &mut self.live_query_retention, |x| {
				crate::kvs::config::parse_duration(x).ok()
			});
	}
}

//FIXME: These configuration values should be removed.
// We advertise that we are embeddable, but configuring solely through environment variables is not
// acceptable for an embeddable database.
// Currently these cannot be removed without a major restructure.

// Used in the memory allocator global, so hard to remove.

/// The memory usage threshold before tasks are forced to exit (default: 0
/// bytes). The default 0 bytes means that there is no memory threshold.
/// Any other user-set memory threshold will default to at least 1 MiB.
pub static MEMORY_THRESHOLD: LazyLock<usize> = LazyLock::new(|| {
	std::env::var("SURREAL_MEMORY_THRESHOLD")
		.ok()
		.and_then(|x| parse_memory_threshold(&x))
		.unwrap_or(0)
});

/// Parse a `SURREAL_MEMORY_THRESHOLD` value into a byte count. Accepts a plain
/// byte count or a human-readable size suffix (`b`/`kb`/`kib`/`mb`/`mib`/
/// `gb`/`gib`, case-insensitive). Returns `None` for unparseable values;
/// `Some(0)` for `"0"` (disables the threshold); otherwise `Some(n)` floored
/// to 1 MiB.
fn parse_memory_threshold(value: &str) -> Option<usize> {
	value.parse_bytes::<usize>().ok().map(|x| match x {
		0 => 0,
		x => x.max(1024 * 1024),
	})
}

/// Optional fixed seed for the HNSW level-assignment RNG.
///
/// Unset (the default) seeds the RNG from entropy, so every index build produces
/// a different graph. Set `SURREAL_HNSW_BUILD_SEED=<u64>` to build a
/// *deterministic* graph (the structure then depends only on insertion order and
/// the vectors), which makes HNSW search benchmarks reproducible across runs — a
/// prerequisite for a clean before/after comparison of search-path changes. It
/// only affects graph construction, never search behaviour, results, or recall.
///
/// Read once at first use, like the other knobs here: the benchmark harness sets
/// the variable out-of-process before launch, so a read-once `LazyLock` is
/// sufficient and avoids any in-process `set_var`.
pub static HNSW_BUILD_SEED: LazyLock<Option<u64>> = LazyLock::new(|| {
	std::env::var("SURREAL_HNSW_BUILD_SEED").ok().and_then(|s| s.parse::<u64>().ok())
});

/// Optional fixed seed for the deterministic data-generation RNG (see
/// `crate::rnd`).
///
/// Unset (the default) leaves `rand::*` and generated record ids drawing from
/// the per-thread RNG, exactly as in production. Set `SURREAL_RAND_SEED=<u64>`
/// to route them through a single seeded RNG so benchmark datasets are identical
/// across runs. TEST AND BENCHMARK USE ONLY — never set it on a shared or
/// multi-tenant deployment, where it makes record ids and `rand::*` values
/// predictable process-wide.
///
/// Read once at first use, like the other knobs here. A value that is set but
/// not a valid `u64` is reported via `tracing::warn!` and ignored, rather than
/// silently falling back to the default.
pub static RAND_SEED: LazyLock<Option<u64>> =
	LazyLock::new(|| match std::env::var("SURREAL_RAND_SEED") {
		Ok(v) => match v.parse::<u64>() {
			Ok(seed) => Some(seed),
			Err(_) => {
				warn!("Ignoring invalid SURREAL_RAND_SEED value `{v}`; expected a u64");
				None
			}
		},
		Err(_) => None,
	});

/// Initial (and minimum) window size for the DiskANN filtered-KNN record
/// prefetch. The committed-graph search prefetches candidate records in
/// distance-ascending windows that grow geometrically (doubling, capped by
/// [`DISKANN_FILTER_PREFETCH_MAX_CHUNK`]); this is the first window's size and
/// the floor. A smaller value bounds the over-fetch tighter when the result
/// builder fills early (non-selective filters); a larger value amortises each
/// window's multi-get over more candidates. Read once at first use.
pub static DISKANN_FILTER_PREFETCH_MIN_CHUNK: LazyLock<usize> = LazyLock::new(|| {
	std::env::var("SURREAL_DISKANN_FILTER_PREFETCH_MIN_CHUNK")
		.ok()
		.and_then(|s| s.parse::<usize>().ok())
		.filter(|n| *n > 0)
		.unwrap_or(64)
});

/// Upper bound on the DiskANN filtered-KNN record-prefetch window (the geometric
/// growth is capped here). Read once at first use.
pub static DISKANN_FILTER_PREFETCH_MAX_CHUNK: LazyLock<usize> = LazyLock::new(|| {
	std::env::var("SURREAL_DISKANN_FILTER_PREFETCH_MAX_CHUNK")
		.ok()
		.and_then(|s| s.parse::<usize>().ok())
		.filter(|n| *n > 0)
		.unwrap_or(4096)
});

// Used in a lot of surrealql functions which randomly access this limit as well as casting
// functions Both of which cannot be changed without massive restructuring.

/// Used to limit allocation for builtin functions. Default: 2^20 (1 MiB),
/// can be as large as 28 (2^28, 256 MiB)
pub static GENERATION_ALLOCATION_LIMIT: LazyLock<usize> = LazyLock::new(|| {
	let n = std::env::var("SURREAL_GENERATION_ALLOCATION_LIMIT")
		.map(|s| s.parse::<u32>().unwrap_or(20))
		.unwrap_or(20);
	2usize.pow(n.min(28))
});

// Used in a lot of surrealql functions which randomly access this limit.
// Which cannot be changed without massive restructuring the planner.

/// The maximum input string length for similarity/distance functions (default:
/// 16384 bytes)
pub static STRING_SIMILARITY_LIMIT: LazyLock<usize> =
	lazy_env_parse!("SURREAL_STRING_SIMILARITY_LIMIT", usize, 16384);

// Used in global regex cache, we would first need to make that cache non-global.

/// The maximum size of a compiled regular expression (default: 10 MiB)
pub static REGEX_SIZE_LIMIT: LazyLock<usize> =
	lazy_env_parse!("SURREAL_REGEX_SIZE_LIMIT", usize, 10 * 1024 * 1024);

/// Specifies the number of computed regexes which can be cached in the engine
/// (default: 1000)
pub static REGEX_CACHE_SIZE: LazyLock<usize> =
	lazy_env_parse!("SURREAL_REGEX_CACHE_SIZE", usize, 1_000);

/// Per-module controller pool size ceiling for Surrealism WASM modules (default: 8).
/// Each pooled controller holds an instantiated WASM store. Effective pool size is
/// `min(this, module_config.max_pool_size.unwrap_or(this))`.
pub static SURREALISM_MAX_POOL_SIZE: LazyLock<usize> =
	lazy_env_parse!("SURREAL_SURREALISM_MAX_POOL_SIZE", usize, 8);

// The GQL v2 MATCH resource limits (`gql_max_join_build_rows`,
// `gql_max_path_rows`, `gql_max_output_rows`) live on `CommonConfig` above, not
// as global statics: every operator that reads them already has the execution
// `CommonConfig` in hand (`ctx.root().ctx.config`), so they are per-datastore
// and settable programmatically (not only via `SURREAL_GQL_*` env vars).

/// Number of worker threads in the shared KVS blocking threadpool
/// (`surrealdb-threadpool`) used by the `kv-mem`, `kv-rocksdb`, and
/// `kv-surrealkv` storage backends to run synchronous storage work off the
/// tokio runtime.
///
/// Default: `num_cpus::get()` on hosts with at least 16 logical cores
/// (matching the legacy `thread_per_core` behaviour with one pinned
/// worker per core), `16` on smaller hosts. Override with
/// `SURREAL_KVS_THREADPOOL_SIZE=<N>` (minimum `4`) to oversubscribe (more
/// concurrent blocking-IO slots, useful when many workers stall on disk
/// reads or fsyncs) or undersubscribe (cap blocking concurrency below
/// core count).
///
/// Explicit overrides drop the per-core CPU pinning that the default
/// applies on >=16-core hosts — pinning only makes sense when the
/// worker count exactly matches the core count.
///
/// **Minimum: 4.** Some kvs operations always run on this pool — read-only
/// `count` with sharded fan-out, `compact`, writable scans — and below ~4
/// workers their throughput collapses (sharded `COUNT(*)` becomes serial,
/// `compact` blocks all other always-pool work). Values below 4, non-numeric
/// values, and an empty string are reported via `tracing::warn!` and the
/// computed default is used instead.
#[cfg(any(feature = "kv-mem", feature = "kv-rocksdb", feature = "kv-surrealkv"))]
#[cfg(not(target_family = "wasm"))]
pub static KVS_THREADPOOL_SIZE: LazyLock<usize> = LazyLock::new(|| {
	let default = || {
		let cores = num_cpus::get();
		if cores >= 16 {
			cores
		} else {
			16
		}
	};
	const MINIMUM_OVERRIDE: usize = 4;
	match std::env::var("SURREAL_KVS_THREADPOOL_SIZE") {
		Err(_) => default(),
		Ok(s) if s.is_empty() => default(),
		Ok(s) => match s.parse::<usize>() {
			Ok(n) if n >= MINIMUM_OVERRIDE => n,
			Ok(n) => {
				tracing::warn!(
					target: "surrealdb::kvs::threadpool",
					"SURREAL_KVS_THREADPOOL_SIZE={n} is below the minimum of {MINIMUM_OVERRIDE}; using default",
				);
				default()
			}
			Err(_) => {
				tracing::warn!(
					target: "surrealdb::kvs::threadpool",
					"SURREAL_KVS_THREADPOOL_SIZE={s:?} is not a valid integer; using default",
				);
				default()
			}
		},
	}
});

#[cfg(test)]
mod tests {
	use super::*;

	/// The TopK threshold pushdown kill switch must default on and parse off
	/// from the config map (`SURREAL_TOPK_THRESHOLD_PUSHDOWN_ENABLED=false`).
	/// Disabling it routes planning through the same code path as "no ORDER
	/// BY opportunity" (TopKPushdownRequest::NotApplicable), which the
	/// topk_pushdown language tests cover.
	#[test]
	fn topk_threshold_pushdown_kill_switch_parses() {
		let mut config = CommonConfig::default();
		assert!(config.topk_threshold_pushdown_enabled, "feature defaults on");
		let map = ConfigMap::empty().with_key_value("topk_threshold_pushdown_enabled", "false");
		config.parse(&map);
		assert!(!config.topk_threshold_pushdown_enabled, "config map disables the feature");
	}

	/// The GQL v2 MATCH resource limits live on `CommonConfig` (not as global
	/// statics): they default to 1M and parse from the config map under the same
	/// keys `ConfigMap::from_env` derives from `SURREAL_GQL_MAX_*`, so the env
	/// vars keep working and embedded callers can set them programmatically.
	#[test]
	fn gql_match_limits_parse_from_config_map() {
		let mut config = CommonConfig::default();
		assert_eq!(config.gql_max_join_build_rows, 1_000_000);
		assert_eq!(config.gql_max_path_rows, 1_000_000);
		assert_eq!(config.gql_max_output_rows, 1_000_000);

		let map = ConfigMap::empty()
			.with_key_value("gql_max_join_build_rows", "5")
			.with_key_value("gql_max_path_rows", "7")
			.with_key_value("gql_max_output_rows", "9");
		config.parse(&map);
		assert_eq!(config.gql_max_join_build_rows, 5);
		assert_eq!(config.gql_max_path_rows, 7);
		assert_eq!(config.gql_max_output_rows, 9);
	}

	/// `memory_threshold` in the config map must accept human-readable byte
	/// suffixes (the config-map counterpart to the `SURREAL_MEMORY_THRESHOLD`
	/// env-var fix in `parse_memory_threshold`).  A previous agent only fixed
	/// the legacy env-var path; this test guards the configmap path.
	#[test]
	fn memory_threshold_configmap_parses_byte_suffixes() {
		let mut config = CommonConfig::default();
		assert_eq!(config.memory_threshold, 0, "default is no threshold");

		// Human-readable suffix is honoured (the previously-regressed case).
		let map = ConfigMap::empty().with_key_value("memory_threshold", "1792mb");
		config.parse(&map);
		assert_eq!(config.memory_threshold, 1792 * 1024 * 1024);

		// Another suffix variant.
		let map = ConfigMap::empty().with_key_value("memory_threshold", "1g");
		config.parse(&map);
		assert_eq!(config.memory_threshold, 1024 * 1024 * 1024);

		// Plain byte count still works.
		let map = ConfigMap::empty().with_key_value("memory_threshold", "1879048192");
		config.parse(&map);
		assert_eq!(config.memory_threshold, 1792 * 1024 * 1024);

		// Any non-zero value is floored to at least 1 MiB.
		let map = ConfigMap::empty().with_key_value("memory_threshold", "10");
		config.parse(&map);
		assert_eq!(config.memory_threshold, 1024 * 1024);

		// An unparseable value leaves the field unchanged rather than panicking.
		config.memory_threshold = 0;
		let map = ConfigMap::empty().with_key_value("memory_threshold", "garbage");
		config.parse(&map);
		assert_eq!(config.memory_threshold, 0, "unparseable value must not change the field");
	}

	/// `SURREAL_MEMORY_THRESHOLD` must accept human-readable byte suffixes
	/// (regression for #6860, which dropped suffix parsing and silently
	/// disabled the guard for values like `1792mb`).
	#[test]
	fn memory_threshold_parses_byte_suffixes() {
		// Human-readable suffix is honoured (the regressed case).
		assert_eq!(parse_memory_threshold("1792mb"), Some(1792 * 1024 * 1024));
		assert_eq!(parse_memory_threshold("1g"), Some(1024 * 1024 * 1024));
		// A plain byte count still works.
		assert_eq!(parse_memory_threshold("1879048192"), Some(1792 * 1024 * 1024));
		// `0` disables the threshold.
		assert_eq!(parse_memory_threshold("0"), Some(0));
		// Any non-zero value is floored to at least 1 MiB.
		assert_eq!(parse_memory_threshold("10"), Some(1024 * 1024));
		// An unparseable value returns None; callers map that to disabled (0).
		assert_eq!(parse_memory_threshold("garbage"), None);
	}
}