1use std::path::Path;
13
14#[derive(Debug)]
16pub enum ConfigError {
17 NotFound(String),
19
20 Read(std::io::Error),
22
23 Parse(toml::de::Error),
25
26 Invalid(String),
28
29 MissingEnvVar(String),
31}
32
33impl std::fmt::Display for ConfigError {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 match self {
36 Self::NotFound(path) => write!(f, "Config file not found: {path}"),
37 Self::Read(err) => write!(f, "Failed to read config: {err}"),
38 Self::Parse(err) => write!(f, "Failed to parse TOML: {err}"),
39 Self::Invalid(msg) => write!(f, "Invalid qail.toml: {msg}"),
40 Self::MissingEnvVar(var) => {
41 write!(f, "Missing required environment variable: {var}")
42 }
43 }
44 }
45}
46
47impl std::error::Error for ConfigError {
48 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
49 match self {
50 Self::Read(err) => Some(err),
51 Self::Parse(err) => Some(err),
52 _ => None,
53 }
54 }
55}
56
57impl From<std::io::Error> for ConfigError {
58 fn from(value: std::io::Error) -> Self {
59 Self::Read(value)
60 }
61}
62
63impl From<toml::de::Error> for ConfigError {
64 fn from(value: toml::de::Error) -> Self {
65 Self::Parse(value)
66 }
67}
68
69pub type ConfigResult<T> = Result<T, ConfigError>;
71
72#[derive(Debug, Clone, Default)]
80pub struct QailConfig {
81 pub project: ProjectConfig,
83
84 pub postgres: PostgresConfig,
86
87 pub qdrant: Option<QdrantConfig>,
89
90 pub gateway: Option<GatewayConfig>,
92
93 pub sync: Vec<SyncRule>,
95}
96
97#[derive(Debug, Clone)]
103pub struct ProjectConfig {
104 pub name: String,
106
107 pub mode: String,
109
110 pub schema: Option<String>,
112
113 pub migrations_dir: Option<String>,
115
116 pub schema_strict_manifest: Option<bool>,
121}
122
123impl Default for ProjectConfig {
124 fn default() -> Self {
125 Self {
126 name: default_project_name(),
127 mode: default_mode(),
128 schema: None,
129 migrations_dir: None,
130 schema_strict_manifest: None,
131 }
132 }
133}
134
135fn default_project_name() -> String {
136 "qail-app".to_string()
137}
138
139fn default_mode() -> String {
140 "postgres".to_string()
141}
142
143#[derive(Debug, Clone)]
145pub struct PostgresConfig {
146 pub url: String,
148
149 pub max_connections: usize,
151
152 pub min_connections: usize,
154
155 pub idle_timeout_secs: u64,
157
158 pub acquire_timeout_secs: u64,
160
161 pub connect_timeout_secs: u64,
163
164 pub test_on_acquire: bool,
166
167 pub rls: Option<RlsConfig>,
169
170 pub ssh: Option<String>,
172}
173
174impl Default for PostgresConfig {
175 fn default() -> Self {
176 Self {
177 url: default_pg_url(),
178 max_connections: default_max_connections(),
179 min_connections: default_min_connections(),
180 idle_timeout_secs: default_idle_timeout(),
181 acquire_timeout_secs: default_acquire_timeout(),
182 connect_timeout_secs: default_connect_timeout(),
183 test_on_acquire: false,
184 rls: None,
185 ssh: None,
186 }
187 }
188}
189
190fn default_pg_url() -> String {
191 "postgres://postgres@localhost:5432/postgres".to_string()
192}
193
194fn default_max_connections() -> usize {
195 10
196}
197
198fn default_min_connections() -> usize {
199 1
200}
201
202fn default_idle_timeout() -> u64 {
203 600
204}
205
206fn default_acquire_timeout() -> u64 {
207 30
208}
209
210fn default_connect_timeout() -> u64 {
211 10
212}
213
214#[derive(Debug, Clone, Default)]
216pub struct RlsConfig {
217 pub default_role: Option<String>,
219
220 pub super_admin_role: Option<String>,
222}
223
224#[derive(Debug, Clone)]
226pub struct QdrantConfig {
227 pub url: String,
229
230 pub grpc: Option<String>,
232
233 pub max_connections: usize,
235
236 pub tls: Option<bool>,
241}
242
243impl Default for QdrantConfig {
244 fn default() -> Self {
245 Self {
246 url: default_qdrant_url(),
247 grpc: None,
248 max_connections: default_max_connections(),
249 tls: None,
250 }
251 }
252}
253
254fn default_qdrant_url() -> String {
255 "http://localhost:6333".to_string()
256}
257
258#[derive(Debug, Clone)]
260pub struct GatewayConfig {
261 pub bind: String,
263
264 pub cors: bool,
266
267 pub cors_allowed_origins: Option<Vec<String>>,
269
270 pub policy: Option<String>,
272
273 pub cache: Option<CacheConfig>,
275
276 pub max_expand_depth: usize,
279
280 pub blocked_tables: Option<Vec<String>>,
285
286 pub allowed_tables: Option<Vec<String>>,
291}
292
293impl Default for GatewayConfig {
294 fn default() -> Self {
295 Self {
296 bind: default_bind(),
297 cors: default_true(),
298 cors_allowed_origins: None,
299 policy: None,
300 cache: None,
301 max_expand_depth: default_max_expand_depth(),
302 blocked_tables: None,
303 allowed_tables: None,
304 }
305 }
306}
307
308fn default_bind() -> String {
309 "0.0.0.0:8080".to_string()
310}
311
312fn default_true() -> bool {
313 true
314}
315
316fn default_max_expand_depth() -> usize {
317 4
318}
319
320#[derive(Debug, Clone)]
322pub struct CacheConfig {
323 pub enabled: bool,
325
326 pub max_entries: usize,
328
329 pub ttl_secs: u64,
331}
332
333impl Default for CacheConfig {
334 fn default() -> Self {
335 Self {
336 enabled: default_true(),
337 max_entries: default_cache_max(),
338 ttl_secs: default_cache_ttl(),
339 }
340 }
341}
342
343fn default_cache_max() -> usize {
344 1000
345}
346
347fn default_cache_ttl() -> u64 {
348 60
349}
350
351#[derive(Debug, Clone)]
353pub struct SyncRule {
354 pub source_table: String,
356 pub target_collection: String,
358
359 pub trigger_column: Option<String>,
361
362 pub embedding_model: Option<String>,
364}
365
366impl QailConfig {
371 pub fn load() -> ConfigResult<Self> {
373 Self::load_from("qail.toml")
374 }
375
376 pub fn load_from(path: impl AsRef<Path>) -> ConfigResult<Self> {
378 let path = path.as_ref();
379
380 if !path.exists() {
381 return Err(ConfigError::NotFound(path.display().to_string()));
382 }
383
384 let raw = std::fs::read_to_string(path)?;
385
386 let expanded = expand_env(&raw)?;
388
389 let mut config = Self::from_toml_str(&expanded)?;
391
392 config.apply_env_overrides();
394
395 Ok(config)
396 }
397
398 pub fn postgres_url(&self) -> &str {
400 &self.postgres.url
401 }
402
403 fn apply_env_overrides(&mut self) {
405 if let Ok(url) = std::env::var("DATABASE_URL") {
407 self.postgres.url = url;
408 }
409
410 if let (Ok(url), Some(ref mut q)) = (std::env::var("QDRANT_URL"), self.qdrant.as_mut()) {
412 q.url = url;
413 }
414
415 if let (Ok(bind), Some(ref mut gw)) = (std::env::var("QAIL_BIND"), self.gateway.as_mut()) {
417 gw.bind = bind;
418 }
419 }
420
421 fn from_toml_str(input: &str) -> ConfigResult<Self> {
422 let value: toml::Value = toml::from_str(input)?;
423 let root = value
424 .as_table()
425 .ok_or_else(|| ConfigError::Invalid("root must be a TOML table".to_string()))?;
426
427 Ok(Self {
428 project: parse_project(root)?,
429 postgres: parse_postgres(root)?,
430 qdrant: parse_qdrant(root)?,
431 gateway: parse_gateway(root)?,
432 sync: parse_sync(root)?,
433 })
434 }
435}
436
437fn parse_project(root: &toml::Table) -> ConfigResult<ProjectConfig> {
438 let mut cfg = ProjectConfig::default();
439 let Some(tbl) = subtable(root, "project")? else {
440 return Ok(cfg);
441 };
442
443 if let Some(v) = opt_string(tbl, "project", "name")? {
444 cfg.name = v;
445 }
446 if let Some(v) = opt_string(tbl, "project", "mode")? {
447 cfg.mode = v;
448 }
449 cfg.schema = opt_string(tbl, "project", "schema")?;
450 cfg.migrations_dir = opt_string(tbl, "project", "migrations_dir")?;
451 cfg.schema_strict_manifest = opt_bool(tbl, "project", "schema_strict_manifest")?;
452
453 Ok(cfg)
454}
455
456fn parse_postgres(root: &toml::Table) -> ConfigResult<PostgresConfig> {
457 let mut cfg = PostgresConfig::default();
458 let Some(tbl) = subtable(root, "postgres")? else {
459 return Ok(cfg);
460 };
461
462 if let Some(v) = opt_string(tbl, "postgres", "url")? {
463 cfg.url = v;
464 }
465 if let Some(v) = opt_usize(tbl, "postgres", "max_connections")? {
466 cfg.max_connections = v;
467 }
468 if let Some(v) = opt_usize(tbl, "postgres", "min_connections")? {
469 cfg.min_connections = v;
470 }
471 if let Some(v) = opt_u64(tbl, "postgres", "idle_timeout_secs")? {
472 cfg.idle_timeout_secs = v;
473 }
474 if let Some(v) = opt_u64(tbl, "postgres", "acquire_timeout_secs")? {
475 cfg.acquire_timeout_secs = v;
476 }
477 if let Some(v) = opt_u64(tbl, "postgres", "connect_timeout_secs")? {
478 cfg.connect_timeout_secs = v;
479 }
480 if let Some(v) = opt_bool(tbl, "postgres", "test_on_acquire")? {
481 cfg.test_on_acquire = v;
482 }
483 cfg.ssh = opt_string(tbl, "postgres", "ssh")?;
484
485 cfg.rls = if let Some(rls_tbl) = nested_table(tbl, "postgres", "rls")? {
486 Some(RlsConfig {
487 default_role: opt_string(rls_tbl, "postgres.rls", "default_role")?,
488 super_admin_role: opt_string(rls_tbl, "postgres.rls", "super_admin_role")?,
489 })
490 } else {
491 None
492 };
493
494 Ok(cfg)
495}
496
497fn parse_qdrant(root: &toml::Table) -> ConfigResult<Option<QdrantConfig>> {
498 let Some(tbl) = subtable(root, "qdrant")? else {
499 return Ok(None);
500 };
501
502 let mut cfg = QdrantConfig::default();
503
504 if let Some(v) = opt_string(tbl, "qdrant", "url")? {
505 cfg.url = v;
506 }
507 cfg.grpc = opt_string(tbl, "qdrant", "grpc")?;
508 if let Some(v) = opt_usize(tbl, "qdrant", "max_connections")? {
509 cfg.max_connections = v;
510 }
511 cfg.tls = opt_bool(tbl, "qdrant", "tls")?;
512
513 Ok(Some(cfg))
514}
515
516fn parse_gateway(root: &toml::Table) -> ConfigResult<Option<GatewayConfig>> {
517 let Some(tbl) = subtable(root, "gateway")? else {
518 return Ok(None);
519 };
520
521 let mut cfg = GatewayConfig::default();
522
523 if let Some(v) = opt_string(tbl, "gateway", "bind")? {
524 cfg.bind = v;
525 }
526 if let Some(v) = opt_bool(tbl, "gateway", "cors")? {
527 cfg.cors = v;
528 }
529 cfg.cors_allowed_origins = opt_string_vec(tbl, "gateway", "cors_allowed_origins")?;
530 cfg.policy = opt_string(tbl, "gateway", "policy")?;
531 if let Some(v) = opt_usize(tbl, "gateway", "max_expand_depth")? {
532 cfg.max_expand_depth = v;
533 }
534 cfg.blocked_tables = opt_string_vec(tbl, "gateway", "blocked_tables")?;
535 cfg.allowed_tables = opt_string_vec(tbl, "gateway", "allowed_tables")?;
536
537 cfg.cache = if let Some(cache_tbl) = nested_table(tbl, "gateway", "cache")? {
538 let mut cache = CacheConfig::default();
539 if let Some(v) = opt_bool(cache_tbl, "gateway.cache", "enabled")? {
540 cache.enabled = v;
541 }
542 if let Some(v) = opt_usize(cache_tbl, "gateway.cache", "max_entries")? {
543 cache.max_entries = v;
544 }
545 if let Some(v) = opt_u64(cache_tbl, "gateway.cache", "ttl_secs")? {
546 cache.ttl_secs = v;
547 }
548 Some(cache)
549 } else {
550 None
551 };
552
553 Ok(Some(cfg))
554}
555
556fn parse_sync(root: &toml::Table) -> ConfigResult<Vec<SyncRule>> {
557 let Some(value) = root.get("sync") else {
558 return Ok(Vec::new());
559 };
560
561 let arr = value
562 .as_array()
563 .ok_or_else(|| ConfigError::Invalid("sync must be an array of tables".to_string()))?;
564
565 let mut out = Vec::with_capacity(arr.len());
566 for (idx, item) in arr.iter().enumerate() {
567 let path = format!("sync[{idx}]");
568 let tbl = item
569 .as_table()
570 .ok_or_else(|| ConfigError::Invalid(format!("{path} must be a table")))?;
571
572 out.push(SyncRule {
573 source_table: required_string(tbl, &path, "source_table")?,
574 target_collection: required_string(tbl, &path, "target_collection")?,
575 trigger_column: opt_string(tbl, &path, "trigger_column")?,
576 embedding_model: opt_string(tbl, &path, "embedding_model")?,
577 });
578 }
579
580 Ok(out)
581}
582
583fn subtable<'a>(root: &'a toml::Table, section: &str) -> ConfigResult<Option<&'a toml::Table>> {
584 match root.get(section) {
585 None => Ok(None),
586 Some(value) => value.as_table().map(Some).ok_or_else(|| {
587 ConfigError::Invalid(format!("{section} must be a table (e.g. [{section}])"))
588 }),
589 }
590}
591
592fn nested_table<'a>(
593 table: &'a toml::Table,
594 parent: &str,
595 key: &str,
596) -> ConfigResult<Option<&'a toml::Table>> {
597 match table.get(key) {
598 None => Ok(None),
599 Some(value) => value
600 .as_table()
601 .map(Some)
602 .ok_or_else(|| ConfigError::Invalid(format!("{parent}.{key} must be a table"))),
603 }
604}
605
606fn required_string(table: &toml::Table, section: &str, key: &str) -> ConfigResult<String> {
607 opt_string(table, section, key)?
608 .ok_or_else(|| ConfigError::Invalid(format!("{section}.{key} is required")))
609}
610
611fn opt_string(table: &toml::Table, section: &str, key: &str) -> ConfigResult<Option<String>> {
612 match table.get(key) {
613 None => Ok(None),
614 Some(value) => value
615 .as_str()
616 .map(|s| Some(s.to_string()))
617 .ok_or_else(|| ConfigError::Invalid(format!("{section}.{key} must be a string"))),
618 }
619}
620
621fn opt_bool(table: &toml::Table, section: &str, key: &str) -> ConfigResult<Option<bool>> {
622 match table.get(key) {
623 None => Ok(None),
624 Some(value) => value
625 .as_bool()
626 .map(Some)
627 .ok_or_else(|| ConfigError::Invalid(format!("{section}.{key} must be a boolean"))),
628 }
629}
630
631fn opt_usize(table: &toml::Table, section: &str, key: &str) -> ConfigResult<Option<usize>> {
632 match table.get(key) {
633 None => Ok(None),
634 Some(value) => {
635 let raw = value.as_integer().ok_or_else(|| {
636 ConfigError::Invalid(format!("{section}.{key} must be a non-negative integer"))
637 })?;
638 let converted = usize::try_from(raw).map_err(|_| {
639 ConfigError::Invalid(format!("{section}.{key} must be a non-negative integer"))
640 })?;
641 Ok(Some(converted))
642 }
643 }
644}
645
646fn opt_u64(table: &toml::Table, section: &str, key: &str) -> ConfigResult<Option<u64>> {
647 match table.get(key) {
648 None => Ok(None),
649 Some(value) => {
650 let raw = value.as_integer().ok_or_else(|| {
651 ConfigError::Invalid(format!("{section}.{key} must be a non-negative integer"))
652 })?;
653 let converted = u64::try_from(raw).map_err(|_| {
654 ConfigError::Invalid(format!("{section}.{key} must be a non-negative integer"))
655 })?;
656 Ok(Some(converted))
657 }
658 }
659}
660
661fn opt_string_vec(
662 table: &toml::Table,
663 section: &str,
664 key: &str,
665) -> ConfigResult<Option<Vec<String>>> {
666 let Some(value) = table.get(key) else {
667 return Ok(None);
668 };
669
670 let arr = value
671 .as_array()
672 .ok_or_else(|| ConfigError::Invalid(format!("{section}.{key} must be an array")))?;
673
674 let mut out = Vec::with_capacity(arr.len());
675 for (idx, item) in arr.iter().enumerate() {
676 let Some(s) = item.as_str() else {
677 return Err(ConfigError::Invalid(format!(
678 "{section}.{key}[{idx}] must be a string"
679 )));
680 };
681 out.push(s.to_string());
682 }
683
684 Ok(Some(out))
685}
686
687pub fn expand_env(input: &str) -> ConfigResult<String> {
697 let mut result = String::with_capacity(input.len());
698 let mut chars = input.chars().peekable();
699
700 while let Some(ch) = chars.next() {
701 if ch == '$' {
702 match chars.peek() {
703 Some('$') => {
704 chars.next();
706 result.push('$');
707 }
708 Some('{') => {
709 chars.next(); let mut var_expr = String::new();
711 let mut depth = 1;
712
713 for c in chars.by_ref() {
714 if c == '{' {
715 depth += 1;
716 } else if c == '}' {
717 depth -= 1;
718 if depth == 0 {
719 break;
720 }
721 }
722 var_expr.push(c);
723 }
724
725 let (var_name, default_val) = if let Some(idx) = var_expr.find(":-") {
727 (&var_expr[..idx], Some(&var_expr[idx + 2..]))
728 } else {
729 (var_expr.as_str(), None)
730 };
731
732 match std::env::var(var_name) {
733 Ok(val) => result.push_str(&val),
734 Err(_) => {
735 if let Some(default) = default_val {
736 result.push_str(default);
737 } else {
738 return Err(ConfigError::MissingEnvVar(var_name.to_string()));
739 }
740 }
741 }
742 }
743 _ => {
744 result.push('$');
746 }
747 }
748 } else {
749 result.push(ch);
750 }
751 }
752
753 Ok(result)
754}
755
756#[cfg(test)]
761mod tests {
762 use super::*;
763
764 unsafe fn set_env(key: &str, val: &str) {
767 unsafe { std::env::set_var(key, val) };
768 }
769
770 unsafe fn unset_env(key: &str) {
771 unsafe { std::env::remove_var(key) };
772 }
773
774 #[test]
775 fn test_expand_env_required_var() {
776 unsafe { set_env("QAIL_TEST_VAR", "hello") };
777 let result = expand_env("prefix_${QAIL_TEST_VAR}_suffix").unwrap();
778 assert_eq!(result, "prefix_hello_suffix");
779 unsafe { unset_env("QAIL_TEST_VAR") };
780 }
781
782 #[test]
783 fn test_expand_env_missing_required() {
784 unsafe { unset_env("QAIL_MISSING_VAR_XYZ") };
785 let result = expand_env("${QAIL_MISSING_VAR_XYZ}");
786 assert!(result.is_err());
787 assert!(
788 matches!(result, Err(ConfigError::MissingEnvVar(ref v)) if v == "QAIL_MISSING_VAR_XYZ")
789 );
790 }
791
792 #[test]
793 fn test_expand_env_default_value() {
794 unsafe { unset_env("QAIL_OPT_VAR") };
795 let result = expand_env("${QAIL_OPT_VAR:-fallback}").unwrap();
796 assert_eq!(result, "fallback");
797 }
798
799 #[test]
800 fn test_expand_env_default_empty() {
801 unsafe { unset_env("QAIL_OPT_EMPTY") };
802 let result = expand_env("${QAIL_OPT_EMPTY:-}").unwrap();
803 assert_eq!(result, "");
804 }
805
806 #[test]
807 fn test_expand_env_set_overrides_default() {
808 unsafe { set_env("QAIL_SET_VAR", "real") };
809 let result = expand_env("${QAIL_SET_VAR:-fallback}").unwrap();
810 assert_eq!(result, "real");
811 unsafe { unset_env("QAIL_SET_VAR") };
812 }
813
814 #[test]
815 fn test_expand_env_escaped_dollar() {
816 let result = expand_env("price: $$100").unwrap();
817 assert_eq!(result, "price: $100");
818 }
819
820 #[test]
821 fn test_expand_env_no_expansion() {
822 let result = expand_env("plain text no vars").unwrap();
823 assert_eq!(result, "plain text no vars");
824 }
825
826 #[test]
827 fn test_expand_env_postgres_url() {
828 unsafe { set_env("QAIL_DB_USER", "admin") };
829 unsafe { set_env("QAIL_DB_PASS", "s3cret") };
830 let result =
831 expand_env("postgres://${QAIL_DB_USER}:${QAIL_DB_PASS}@localhost:5432/mydb").unwrap();
832 assert_eq!(result, "postgres://admin:s3cret@localhost:5432/mydb");
833 unsafe { unset_env("QAIL_DB_USER") };
834 unsafe { unset_env("QAIL_DB_PASS") };
835 }
836
837 #[test]
838 fn test_parse_minimal_toml() {
839 let toml_str = r#"
840[project]
841name = "test"
842mode = "postgres"
843
844[postgres]
845url = "postgres://localhost/test"
846"#;
847 let config = QailConfig::from_toml_str(toml_str).unwrap();
848 assert_eq!(config.project.name, "test");
849 assert_eq!(config.postgres.url, "postgres://localhost/test");
850 assert_eq!(config.postgres.max_connections, 10); assert!(config.qdrant.is_none());
852 assert!(config.gateway.is_none());
853 }
854
855 #[test]
856 fn test_parse_full_toml() {
857 let toml_str = r#"
858[project]
859name = "fulltest"
860mode = "hybrid"
861schema = "schema.qail"
862migrations_dir = "deltas"
863schema_strict_manifest = true
864
865[postgres]
866url = "postgres://localhost/test"
867max_connections = 25
868min_connections = 5
869idle_timeout_secs = 300
870
871[postgres.rls]
872default_role = "app_user"
873super_admin_role = "super_admin"
874
875[qdrant]
876url = "http://qdrant:6333"
877grpc = "qdrant:6334"
878max_connections = 15
879
880[gateway]
881bind = "0.0.0.0:9090"
882cors = false
883policy = "policies.yaml"
884
885[gateway.cache]
886enabled = true
887max_entries = 5000
888ttl_secs = 120
889
890[[sync]]
891source_table = "products"
892target_collection = "products_search"
893trigger_column = "description"
894embedding_model = "candle:bert-base"
895"#;
896 let config = QailConfig::from_toml_str(toml_str).unwrap();
897 assert_eq!(config.project.name, "fulltest");
898 assert_eq!(config.project.schema_strict_manifest, Some(true));
899 assert_eq!(config.postgres.max_connections, 25);
900 assert_eq!(config.postgres.min_connections, 5);
901
902 let rls = config.postgres.rls.unwrap();
903 assert_eq!(rls.default_role.unwrap(), "app_user");
904
905 let qdrant = config.qdrant.unwrap();
906 assert_eq!(qdrant.max_connections, 15);
907
908 let gw = config.gateway.unwrap();
909 assert_eq!(gw.bind, "0.0.0.0:9090");
910 assert!(!gw.cors);
911
912 let cache = gw.cache.unwrap();
913 assert_eq!(cache.max_entries, 5000);
914
915 assert_eq!(config.sync.len(), 1);
916 assert_eq!(config.sync[0].source_table, "products");
917 }
918
919 #[test]
920 fn test_backward_compat_existing_toml() {
921 let toml_str = r#"
923[project]
924name = "legacy"
925mode = "postgres"
926
927[postgres]
928url = "postgres://localhost/legacy"
929"#;
930 let config = QailConfig::from_toml_str(toml_str).unwrap();
931 assert_eq!(config.project.name, "legacy");
932 assert_eq!(config.postgres.url, "postgres://localhost/legacy");
933 assert_eq!(config.postgres.max_connections, 10);
935 assert!(config.postgres.rls.is_none());
936 assert!(config.qdrant.is_none());
937 assert!(config.gateway.is_none());
938 }
939}