Skip to main content

sochdb_query/
namespace.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2// SochDB - LLM-Optimized Embedded Database
3// Copyright (C) 2026 Sushanth Reddy Vanagala (https://github.com/sushanthpy)
4//
5// This program is free software: you can redistribute it and/or modify
6// it under the terms of the GNU Affero General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// This program is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU Affero General Public License for more details.
14//
15// You should have received a copy of the GNU Affero General Public License
16// along with this program. If not, see <https://www.gnu.org/licenses/>.
17
18//! Namespace-Scoped Query API (Task 2)
19//!
20//! This module enforces **mandatory namespace scoping** at the type level,
21//! making cross-workspace data leakage impossible by construction.
22//!
23//! ## The Problem
24//!
25//! When namespace/tenant scoping is treated as an optional filter parameter,
26//! developers can accidentally:
27//! - Query across workspaces by forgetting to add the namespace filter
28//! - Reuse a handle across workspaces in local-first scenarios
29//! - Mix data from different tenants in multi-tenant deployments
30//!
31//! ## The Solution
32//!
33//! Make `namespace` a **required part of the query identity**, not an
34//! optional filter. The type system enforces:
35//!
36//! 1. `Namespace` is required in every query request
37//! 2. `Namespace` must be validated against the capability token
38//! 3. "No namespace" is not a valid state
39//!
40//! ## Multi-Namespace Queries
41//!
42//! For legitimate multi-namespace queries, use `NamespaceScope::Multiple`
43//! which requires explicit authorization for each namespace.
44//!
45//! ## Example
46//!
47//! ```ignore
48//! // This compiles - namespace is required
49//! let query = ScopedQuery::new(
50//!     Namespace::new("production"),
51//!     QueryOp::VectorSearch { ... }
52//! );
53//!
54//! // This won't compile - no namespace
55//! let query = ScopedQuery::new(QueryOp::VectorSearch { ... });  // ERROR!
56//! ```
57
58use std::fmt;
59use std::sync::Arc;
60
61use serde::{Deserialize, Serialize};
62
63use crate::filter_ir::{AuthScope, FilterIR, FilterBuilder};
64
65// ============================================================================
66// Namespace - Opaque, Validated Identifier
67// ============================================================================
68
69/// A validated namespace identifier
70///
71/// This is an opaque type that can only be constructed via validation,
72/// preventing accidental use of invalid namespace strings.
73#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
74pub struct Namespace(String);
75
76impl Namespace {
77    /// Maximum length for a namespace identifier
78    pub const MAX_LENGTH: usize = 256;
79    
80    /// Create a new namespace (validates format)
81    ///
82    /// # Validation Rules
83    /// - Non-empty
84    /// - Max 256 characters
85    /// - Alphanumeric, underscores, hyphens, and periods only
86    /// - Cannot start with a period or hyphen
87    pub fn new(name: impl Into<String>) -> Result<Self, NamespaceError> {
88        let name = name.into();
89        Self::validate(&name)?;
90        Ok(Self(name))
91    }
92    
93    /// Create without validation (for internal use only)
94    ///
95    /// # Safety
96    /// Caller must ensure the name is valid.
97    #[allow(dead_code)]
98    pub(crate) fn new_unchecked(name: impl Into<String>) -> Self {
99        Self(name.into())
100    }
101    
102    /// Validate a namespace string
103    fn validate(name: &str) -> Result<(), NamespaceError> {
104        Self::validate_name(name)
105    }
106
107    /// Validate a name string (public, reusable for database/table names too)
108    pub fn validate_name(name: &str) -> Result<(), NamespaceError> {
109        if name.is_empty() {
110            return Err(NamespaceError::Empty);
111        }
112        
113        if name.len() > Self::MAX_LENGTH {
114            return Err(NamespaceError::TooLong {
115                length: name.len(),
116                max: Self::MAX_LENGTH,
117            });
118        }
119        
120        // Check first character
121        let first = name.chars().next().unwrap();
122        if first == '.' || first == '-' {
123            return Err(NamespaceError::InvalidStart(first));
124        }
125        
126        // Check all characters
127        for (i, ch) in name.chars().enumerate() {
128            if !ch.is_alphanumeric() && ch != '_' && ch != '-' && ch != '.' {
129                return Err(NamespaceError::InvalidChar { ch, position: i });
130            }
131        }
132        
133        Ok(())
134    }
135    
136    /// Get the namespace as a string slice
137    pub fn as_str(&self) -> &str {
138        &self.0
139    }
140    
141    /// Convert to owned string
142    pub fn into_string(self) -> String {
143        self.0
144    }
145}
146
147impl fmt::Display for Namespace {
148    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
149        write!(f, "{}", self.0)
150    }
151}
152
153impl AsRef<str> for Namespace {
154    fn as_ref(&self) -> &str {
155        &self.0
156    }
157}
158
159/// Errors that can occur when creating a namespace
160#[derive(Debug, Clone, thiserror::Error)]
161pub enum NamespaceError {
162    #[error("namespace cannot be empty")]
163    Empty,
164    
165    #[error("namespace too long: {length} > {max}")]
166    TooLong { length: usize, max: usize },
167    
168    #[error("namespace cannot start with '{0}'")]
169    InvalidStart(char),
170    
171    #[error("invalid character '{ch}' at position {position}")]
172    InvalidChar { ch: char, position: usize },
173}
174
175// ============================================================================
176// Namespace Scope - Single or Multiple
177// ============================================================================
178
179/// Scope for a query - either single namespace or explicitly multiple
180#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181pub enum NamespaceScope {
182    /// Query within a single namespace (most common)
183    Single(Namespace),
184    
185    /// Query across multiple namespaces (requires explicit authorization)
186    Multiple(Vec<Namespace>),
187}
188
189impl NamespaceScope {
190    /// Create a single-namespace scope
191    pub fn single(ns: Namespace) -> Self {
192        Self::Single(ns)
193    }
194    
195    /// Create a multi-namespace scope
196    pub fn multiple(namespaces: Vec<Namespace>) -> Result<Self, NamespaceError> {
197        if namespaces.is_empty() {
198            return Err(NamespaceError::Empty);
199        }
200        Ok(Self::Multiple(namespaces))
201    }
202    
203    /// Get all namespaces in this scope
204    pub fn namespaces(&self) -> Vec<&Namespace> {
205        match self {
206            Self::Single(ns) => vec![ns],
207            Self::Multiple(nss) => nss.iter().collect(),
208        }
209    }
210    
211    /// Check if a namespace is in this scope
212    pub fn contains(&self, ns: &Namespace) -> bool {
213        match self {
214            Self::Single(single) => single == ns,
215            Self::Multiple(multiple) => multiple.contains(ns),
216        }
217    }
218    
219    /// Validate against an auth scope
220    pub fn validate_against(&self, auth: &AuthScope) -> Result<(), ScopeError> {
221        for ns in self.namespaces() {
222            if !auth.is_namespace_allowed(ns.as_str()) {
223                return Err(ScopeError::NamespaceNotAllowed(ns.clone()));
224            }
225        }
226        Ok(())
227    }
228    
229    /// Convert to filter IR clauses
230    pub fn to_filter_ir(&self) -> FilterIR {
231        match self {
232            Self::Single(ns) => FilterBuilder::new()
233                .namespace(ns.as_str())
234                .build(),
235            Self::Multiple(nss) => {
236                use crate::filter_ir::{FilterAtom, FilterValue};
237                FilterIR::from_atom(FilterAtom::in_set(
238                    "namespace",
239                    nss.iter()
240                        .map(|ns| FilterValue::String(ns.as_str().to_string()))
241                        .collect(),
242                ))
243            }
244        }
245    }
246}
247
248impl fmt::Display for NamespaceScope {
249    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
250        match self {
251            Self::Single(ns) => write!(f, "{}", ns),
252            Self::Multiple(nss) => {
253                let names: Vec<_> = nss.iter().map(|ns| ns.as_str()).collect();
254                write!(f, "[{}]", names.join(", "))
255            }
256        }
257    }
258}
259
260/// Errors related to namespace scope
261#[derive(Debug, Clone, thiserror::Error)]
262pub enum ScopeError {
263    #[error("namespace not allowed: {0}")]
264    NamespaceNotAllowed(Namespace),
265    
266    #[error("auth scope expired")]
267    AuthExpired,
268    
269    #[error("insufficient capabilities for this operation")]
270    InsufficientCapabilities,
271}
272
273// ============================================================================
274// Scoped Query - Query with Mandatory Namespace
275// ============================================================================
276
277/// A query that is always scoped to a namespace
278///
279/// This type makes cross-workspace queries impossible by construction.
280/// Every query MUST specify a namespace scope.
281#[derive(Debug, Clone)]
282pub struct ScopedQuery<Q> {
283    /// The namespace scope (mandatory)
284    scope: NamespaceScope,
285    
286    /// The underlying query operation
287    query: Q,
288    
289    /// User-provided filters (in addition to namespace)
290    filters: FilterIR,
291}
292
293impl<Q> ScopedQuery<Q> {
294    /// Create a new scoped query
295    ///
296    /// The namespace scope is required - this is the key invariant.
297    pub fn new(scope: NamespaceScope, query: Q) -> Self {
298        Self {
299            scope,
300            query,
301            filters: FilterIR::all(),
302        }
303    }
304    
305    /// Create a single-namespace query (convenience)
306    pub fn in_namespace(namespace: Namespace, query: Q) -> Self {
307        Self::new(NamespaceScope::Single(namespace), query)
308    }
309    
310    /// Add user filters
311    pub fn with_filters(mut self, filters: FilterIR) -> Self {
312        self.filters = filters;
313        self
314    }
315    
316    /// Get the namespace scope
317    pub fn scope(&self) -> &NamespaceScope {
318        &self.scope
319    }
320    
321    /// Get the underlying query
322    pub fn query(&self) -> &Q {
323        &self.query
324    }
325    
326    /// Get user filters
327    pub fn filters(&self) -> &FilterIR {
328        &self.filters
329    }
330    
331    /// Compute the effective filter (namespace + user filters)
332    ///
333    /// This is the filter that will be passed to executors.
334    pub fn effective_filter(&self) -> FilterIR {
335        self.scope.to_filter_ir().and(self.filters.clone())
336    }
337    
338    /// Validate this query against an auth scope
339    pub fn validate(&self, auth: &AuthScope) -> Result<(), ScopeError> {
340        // Check auth expiry
341        if auth.is_expired() {
342            return Err(ScopeError::AuthExpired);
343        }
344        
345        // Check namespace access
346        self.scope.validate_against(auth)?;
347        
348        Ok(())
349    }
350    
351    /// Extract the query, consuming self
352    pub fn into_query(self) -> Q {
353        self.query
354    }
355}
356
357// ============================================================================
358// Query Request - Complete Request with Auth
359// ============================================================================
360
361/// A complete query request with authentication
362///
363/// This is the type that crosses API boundaries. It bundles:
364/// - The scoped query (with mandatory namespace)
365/// - The auth scope (with capability token)
366///
367/// This makes it impossible to execute a query without proper auth.
368#[derive(Debug, Clone)]
369pub struct QueryRequest<Q> {
370    /// The scoped query
371    query: ScopedQuery<Q>,
372    
373    /// The auth scope (from capability token)
374    auth: Arc<AuthScope>,
375}
376
377impl<Q> QueryRequest<Q> {
378    /// Create a new query request
379    ///
380    /// # Validation
381    /// This validates the query scope against the auth scope at construction time.
382    pub fn new(query: ScopedQuery<Q>, auth: Arc<AuthScope>) -> Result<Self, ScopeError> {
383        query.validate(&auth)?;
384        Ok(Self { query, auth })
385    }
386    
387    /// Get the scoped query
388    pub fn query(&self) -> &ScopedQuery<Q> {
389        &self.query
390    }
391    
392    /// Get the auth scope
393    pub fn auth(&self) -> &AuthScope {
394        &self.auth
395    }
396    
397    /// Compute the complete effective filter
398    ///
399    /// This combines:
400    /// 1. Auth scope constraints (mandatory)
401    /// 2. Namespace scope constraints (mandatory)  
402    /// 3. User-provided filters (optional)
403    pub fn effective_filter(&self) -> FilterIR {
404        self.auth.to_filter_ir()
405            .and(self.query.effective_filter())
406    }
407    
408    /// Get the namespace scope
409    pub fn namespace_scope(&self) -> &NamespaceScope {
410        self.query.scope()
411    }
412}
413
414// ============================================================================
415// Convenience Constructors
416// ============================================================================
417
418/// Create a namespace (shorthand)
419pub fn ns(name: &str) -> Result<Namespace, NamespaceError> {
420    Namespace::new(name)
421}
422
423/// Create a single-namespace scope (shorthand)
424pub fn scope(name: &str) -> Result<NamespaceScope, NamespaceError> {
425    Ok(NamespaceScope::Single(Namespace::new(name)?))
426}
427
428// ============================================================================
429// Database Tier — namespace > database > table (P3.1)
430// ============================================================================
431
432/// A database within a namespace.
433///
434/// Mirrors SurrealDB's three-tier hierarchy: `namespace > database > table`.
435/// Each namespace can contain multiple databases, providing logical grouping
436/// and isolation of tables within the same namespace.
437///
438/// ## Example
439///
440/// ```text
441/// namespace "production"
442///   ├─ database "app"
443///   │   ├─ table "users"
444///   │   └─ table "posts"
445///   └─ database "analytics"
446///       ├─ table "events"
447///       └─ table "sessions"
448/// ```
449#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
450pub struct DatabaseId {
451    /// Parent namespace
452    pub namespace: String,
453    /// Database name within the namespace
454    pub name: String,
455}
456
457impl DatabaseId {
458    /// Maximum length for a database identifier
459    pub const MAX_LENGTH: usize = 256;
460
461    /// Create a new database identifier.
462    pub fn new(namespace: impl Into<String>, name: impl Into<String>) -> Result<Self, NamespaceError> {
463        let namespace = namespace.into();
464        let name = name.into();
465        Namespace::validate_name(&name)?;
466        Ok(Self { namespace, name })
467    }
468
469    /// Return the fully qualified name: `namespace/database`
470    pub fn qualified_name(&self) -> String {
471        format!("{}/{}", self.namespace, self.name)
472    }
473}
474
475impl fmt::Display for DatabaseId {
476    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
477        write!(f, "{}/{}", self.namespace, self.name)
478    }
479}
480
481/// A fully qualified table path: `namespace/database/table`.
482///
483/// Used to address tables unambiguously across the entire hierarchy.
484#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
485pub struct QualifiedTable {
486    pub namespace: String,
487    pub database: String,
488    pub table: String,
489}
490
491impl QualifiedTable {
492    /// Create a new qualified table path.
493    pub fn new(
494        namespace: impl Into<String>,
495        database: impl Into<String>,
496        table: impl Into<String>,
497    ) -> Self {
498        Self {
499            namespace: namespace.into(),
500            database: database.into(),
501            table: table.into(),
502        }
503    }
504
505    /// Return the fully qualified name: `namespace/database/table`
506    pub fn qualified_name(&self) -> String {
507        format!("{}/{}/{}", self.namespace, self.database, self.table)
508    }
509
510    /// Return the storage key prefix for this table.
511    /// All row keys under this table are prefixed with this string.
512    pub fn storage_prefix(&self) -> String {
513        format!("{}:{}:{}", self.namespace, self.database, self.table)
514    }
515}
516
517impl fmt::Display for QualifiedTable {
518    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
519        write!(f, "{}/{}/{}", self.namespace, self.database, self.table)
520    }
521}
522
523/// Registry tracking the namespace → database → table hierarchy.
524///
525/// Provides O(1) lookups and enforces naming constraints.
526#[derive(Debug, Clone, Default)]
527pub struct NamespaceRegistry {
528    /// namespace_name → set of database names
529    databases: std::collections::HashMap<String, std::collections::HashSet<String>>,
530    /// (namespace, database) → set of table names
531    tables: std::collections::HashMap<(String, String), std::collections::HashSet<String>>,
532}
533
534impl NamespaceRegistry {
535    /// Create a new empty registry.
536    pub fn new() -> Self {
537        Self::default()
538    }
539
540    /// Register a namespace (idempotent).
541    pub fn create_namespace(&mut self, namespace: &str) -> Result<(), NamespaceError> {
542        Namespace::validate_name(namespace)?;
543        self.databases.entry(namespace.to_string()).or_default();
544        Ok(())
545    }
546
547    /// Create a database within a namespace.
548    pub fn create_database(&mut self, namespace: &str, database: &str) -> Result<(), NamespaceError> {
549        Namespace::validate_name(database)?;
550        let dbs = self.databases.entry(namespace.to_string()).or_default();
551        dbs.insert(database.to_string());
552        self.tables.entry((namespace.to_string(), database.to_string())).or_default();
553        Ok(())
554    }
555
556    /// Register a table within a namespace/database.
557    pub fn create_table(&mut self, namespace: &str, database: &str, table: &str) -> Result<(), NamespaceError> {
558        Namespace::validate_name(table)?;
559        // Ensure parent database exists
560        let dbs = self.databases.entry(namespace.to_string()).or_default();
561        dbs.insert(database.to_string());
562        let tables = self.tables.entry((namespace.to_string(), database.to_string())).or_default();
563        tables.insert(table.to_string());
564        Ok(())
565    }
566
567    /// List databases in a namespace.
568    pub fn list_databases(&self, namespace: &str) -> Vec<&str> {
569        self.databases.get(namespace)
570            .map(|dbs| dbs.iter().map(|s| s.as_str()).collect())
571            .unwrap_or_default()
572    }
573
574    /// List tables in a database.
575    pub fn list_tables(&self, namespace: &str, database: &str) -> Vec<&str> {
576        self.tables.get(&(namespace.to_string(), database.to_string()))
577            .map(|tables| tables.iter().map(|s| s.as_str()).collect())
578            .unwrap_or_default()
579    }
580
581    /// Check if a namespace exists.
582    pub fn namespace_exists(&self, namespace: &str) -> bool {
583        self.databases.contains_key(namespace)
584    }
585
586    /// Check if a database exists within a namespace.
587    pub fn database_exists(&self, namespace: &str, database: &str) -> bool {
588        self.databases.get(namespace)
589            .map(|dbs| dbs.contains(database))
590            .unwrap_or(false)
591    }
592
593    /// Check if a table exists within a namespace/database.
594    pub fn table_exists(&self, namespace: &str, database: &str, table: &str) -> bool {
595        self.tables.get(&(namespace.to_string(), database.to_string()))
596            .map(|tables| tables.contains(table))
597            .unwrap_or(false)
598    }
599
600    /// Drop a database and all its tables.
601    pub fn drop_database(&mut self, namespace: &str, database: &str) -> bool {
602        self.tables.remove(&(namespace.to_string(), database.to_string()));
603        self.databases.get_mut(namespace)
604            .map(|dbs| dbs.remove(database))
605            .unwrap_or(false)
606    }
607
608    /// Drop a table from a database.
609    pub fn drop_table(&mut self, namespace: &str, database: &str, table: &str) -> bool {
610        self.tables.get_mut(&(namespace.to_string(), database.to_string()))
611            .map(|tables| tables.remove(table))
612            .unwrap_or(false)
613    }
614
615    /// Drop a namespace and all its databases/tables.
616    pub fn drop_namespace(&mut self, namespace: &str) -> bool {
617        if !self.databases.contains_key(namespace) {
618            return false;
619        }
620        // Remove all tables under this namespace
621        let db_names: Vec<String> = self.databases.get(namespace)
622            .map(|dbs| dbs.iter().cloned().collect())
623            .unwrap_or_default();
624        for db in &db_names {
625            self.tables.remove(&(namespace.to_string(), db.clone()));
626        }
627        self.databases.remove(namespace);
628        true
629    }
630
631    /// Resolve a qualified table path to check it exists.
632    pub fn resolve_table(&self, qualified: &QualifiedTable) -> bool {
633        self.table_exists(&qualified.namespace, &qualified.database, &qualified.table)
634    }
635}
636
637// ============================================================================
638// Tests
639// ============================================================================
640
641#[cfg(test)]
642mod tests {
643    use super::*;
644    
645    #[test]
646    fn test_namespace_validation() {
647        // Valid
648        assert!(Namespace::new("production").is_ok());
649        assert!(Namespace::new("my_namespace").is_ok());
650        assert!(Namespace::new("project-123").is_ok());
651        assert!(Namespace::new("v1.0.0").is_ok());
652        
653        // Invalid
654        assert!(Namespace::new("").is_err()); // Empty
655        assert!(Namespace::new("-starts-with-dash").is_err());
656        assert!(Namespace::new(".starts-with-dot").is_err());
657        assert!(Namespace::new("has spaces").is_err());
658        assert!(Namespace::new("has@symbol").is_err());
659    }
660    
661    #[test]
662    fn test_namespace_scope_single() {
663        let ns = Namespace::new("production").unwrap();
664        let scope = NamespaceScope::single(ns.clone());
665        
666        assert!(scope.contains(&ns));
667        assert!(!scope.contains(&Namespace::new("staging").unwrap()));
668    }
669    
670    #[test]
671    fn test_namespace_scope_multiple() {
672        let ns1 = Namespace::new("prod").unwrap();
673        let ns2 = Namespace::new("staging").unwrap();
674        let scope = NamespaceScope::multiple(vec![ns1.clone(), ns2.clone()]).unwrap();
675        
676        assert!(scope.contains(&ns1));
677        assert!(scope.contains(&ns2));
678        assert!(!scope.contains(&Namespace::new("dev").unwrap()));
679    }
680    
681    #[test]
682    fn test_scope_to_filter_ir() {
683        let scope = NamespaceScope::single(Namespace::new("production").unwrap());
684        let filter = scope.to_filter_ir();
685        
686        assert!(filter.constrains_field("namespace"));
687        assert_eq!(filter.clauses.len(), 1);
688    }
689    
690    #[test]
691    fn test_scoped_query_effective_filter() {
692        let ns = Namespace::new("production").unwrap();
693        let user_filter = FilterBuilder::new()
694            .eq("source", "documents")
695            .build();
696        
697        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
698            .with_filters(user_filter);
699        
700        let effective = query.effective_filter();
701        assert!(effective.constrains_field("namespace"));
702        assert!(effective.constrains_field("source"));
703    }
704    
705    #[test]
706    fn test_query_request_validation() {
707        let ns = Namespace::new("production").unwrap();
708        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ());
709        
710        // Auth allows production
711        let auth = Arc::new(AuthScope::for_namespace("production"));
712        assert!(QueryRequest::new(query.clone(), auth).is_ok());
713        
714        // Auth only allows staging
715        let auth2 = Arc::new(AuthScope::for_namespace("staging"));
716        assert!(QueryRequest::new(query, auth2).is_err());
717    }
718    
719    #[test]
720    fn test_query_request_effective_filter() {
721        let ns = Namespace::new("production").unwrap();
722        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
723            .with_filters(FilterBuilder::new().eq("type", "article").build());
724        
725        let auth = Arc::new(
726            AuthScope::for_namespace("production")
727                .with_tenant("acme")
728        );
729        
730        let request = QueryRequest::new(query, auth).unwrap();
731        let effective = request.effective_filter();
732        
733        // Should have: namespace (from scope) + tenant_id (from auth) + type (from user)
734        assert!(effective.constrains_field("namespace"));
735        assert!(effective.constrains_field("tenant_id"));
736        assert!(effective.constrains_field("type"));
737    }
738
739    // ======== Database Tier Tests (P3.1) ========
740
741    #[test]
742    fn test_database_id_creation() {
743        let db = DatabaseId::new("production", "app").unwrap();
744        assert_eq!(db.namespace, "production");
745        assert_eq!(db.name, "app");
746        assert_eq!(db.qualified_name(), "production/app");
747    }
748
749    #[test]
750    fn test_qualified_table() {
751        let qt = QualifiedTable::new("production", "app", "users");
752        assert_eq!(qt.qualified_name(), "production/app/users");
753        assert_eq!(qt.storage_prefix(), "production:app:users");
754    }
755
756    #[test]
757    fn test_namespace_registry_basic() {
758        let mut reg = NamespaceRegistry::new();
759        reg.create_namespace("prod").unwrap();
760        assert!(reg.namespace_exists("prod"));
761        assert!(!reg.namespace_exists("staging"));
762    }
763
764    #[test]
765    fn test_namespace_registry_databases() {
766        let mut reg = NamespaceRegistry::new();
767        reg.create_namespace("prod").unwrap();
768        reg.create_database("prod", "app").unwrap();
769        reg.create_database("prod", "analytics").unwrap();
770        assert!(reg.database_exists("prod", "app"));
771        assert!(reg.database_exists("prod", "analytics"));
772        assert!(!reg.database_exists("prod", "logs"));
773        let dbs = reg.list_databases("prod");
774        assert_eq!(dbs.len(), 2);
775    }
776
777    #[test]
778    fn test_namespace_registry_tables() {
779        let mut reg = NamespaceRegistry::new();
780        reg.create_table("prod", "app", "users").unwrap();
781        reg.create_table("prod", "app", "posts").unwrap();
782        assert!(reg.table_exists("prod", "app", "users"));
783        assert!(reg.table_exists("prod", "app", "posts"));
784        assert!(!reg.table_exists("prod", "app", "comments"));
785        // database was auto-created
786        assert!(reg.database_exists("prod", "app"));
787    }
788
789    #[test]
790    fn test_namespace_registry_drop() {
791        let mut reg = NamespaceRegistry::new();
792        reg.create_table("prod", "app", "users").unwrap();
793        reg.create_table("prod", "app", "posts").unwrap();
794        reg.create_table("prod", "analytics", "events").unwrap();
795
796        // Drop one table
797        assert!(reg.drop_table("prod", "app", "users"));
798        assert!(!reg.table_exists("prod", "app", "users"));
799        assert!(reg.table_exists("prod", "app", "posts"));
800
801        // Drop a database
802        assert!(reg.drop_database("prod", "app"));
803        assert!(!reg.database_exists("prod", "app"));
804        assert!(!reg.table_exists("prod", "app", "posts"));
805
806        // analytics still exists
807        assert!(reg.table_exists("prod", "analytics", "events"));
808
809        // Drop entire namespace
810        assert!(reg.drop_namespace("prod"));
811        assert!(!reg.namespace_exists("prod"));
812        assert!(!reg.table_exists("prod", "analytics", "events"));
813    }
814
815    #[test]
816    fn test_qualified_table_resolve() {
817        let mut reg = NamespaceRegistry::new();
818        reg.create_table("prod", "app", "users").unwrap();
819        let qt = QualifiedTable::new("prod", "app", "users");
820        assert!(reg.resolve_table(&qt));
821        let missing = QualifiedTable::new("prod", "app", "absent");
822        assert!(!reg.resolve_table(&missing));
823    }
824}