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, FilterBuilder, FilterIR};
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().namespace(ns.as_str()).build(),
233            Self::Multiple(nss) => {
234                use crate::filter_ir::{FilterAtom, FilterValue};
235                FilterIR::from_atom(FilterAtom::in_set(
236                    "namespace",
237                    nss.iter()
238                        .map(|ns| FilterValue::String(ns.as_str().to_string()))
239                        .collect(),
240                ))
241            }
242        }
243    }
244}
245
246impl fmt::Display for NamespaceScope {
247    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
248        match self {
249            Self::Single(ns) => write!(f, "{}", ns),
250            Self::Multiple(nss) => {
251                let names: Vec<_> = nss.iter().map(|ns| ns.as_str()).collect();
252                write!(f, "[{}]", names.join(", "))
253            }
254        }
255    }
256}
257
258/// Errors related to namespace scope
259#[derive(Debug, Clone, thiserror::Error)]
260pub enum ScopeError {
261    #[error("namespace not allowed: {0}")]
262    NamespaceNotAllowed(Namespace),
263
264    #[error("auth scope expired")]
265    AuthExpired,
266
267    #[error("insufficient capabilities for this operation")]
268    InsufficientCapabilities,
269}
270
271// ============================================================================
272// Scoped Query - Query with Mandatory Namespace
273// ============================================================================
274
275/// A query that is always scoped to a namespace
276///
277/// This type makes cross-workspace queries impossible by construction.
278/// Every query MUST specify a namespace scope.
279#[derive(Debug, Clone)]
280pub struct ScopedQuery<Q> {
281    /// The namespace scope (mandatory)
282    scope: NamespaceScope,
283
284    /// The underlying query operation
285    query: Q,
286
287    /// User-provided filters (in addition to namespace)
288    filters: FilterIR,
289}
290
291impl<Q> ScopedQuery<Q> {
292    /// Create a new scoped query
293    ///
294    /// The namespace scope is required - this is the key invariant.
295    pub fn new(scope: NamespaceScope, query: Q) -> Self {
296        Self {
297            scope,
298            query,
299            filters: FilterIR::all(),
300        }
301    }
302
303    /// Create a single-namespace query (convenience)
304    pub fn in_namespace(namespace: Namespace, query: Q) -> Self {
305        Self::new(NamespaceScope::Single(namespace), query)
306    }
307
308    /// Add user filters
309    pub fn with_filters(mut self, filters: FilterIR) -> Self {
310        self.filters = filters;
311        self
312    }
313
314    /// Get the namespace scope
315    pub fn scope(&self) -> &NamespaceScope {
316        &self.scope
317    }
318
319    /// Get the underlying query
320    pub fn query(&self) -> &Q {
321        &self.query
322    }
323
324    /// Get user filters
325    pub fn filters(&self) -> &FilterIR {
326        &self.filters
327    }
328
329    /// Compute the effective filter (namespace + user filters)
330    ///
331    /// This is the filter that will be passed to executors.
332    pub fn effective_filter(&self) -> FilterIR {
333        self.scope.to_filter_ir().and(self.filters.clone())
334    }
335
336    /// Validate this query against an auth scope
337    pub fn validate(&self, auth: &AuthScope) -> Result<(), ScopeError> {
338        // Check auth expiry
339        if auth.is_expired() {
340            return Err(ScopeError::AuthExpired);
341        }
342
343        // Check namespace access
344        self.scope.validate_against(auth)?;
345
346        Ok(())
347    }
348
349    /// Extract the query, consuming self
350    pub fn into_query(self) -> Q {
351        self.query
352    }
353}
354
355// ============================================================================
356// Query Request - Complete Request with Auth
357// ============================================================================
358
359/// A complete query request with authentication
360///
361/// This is the type that crosses API boundaries. It bundles:
362/// - The scoped query (with mandatory namespace)
363/// - The auth scope (with capability token)
364///
365/// This makes it impossible to execute a query without proper auth.
366#[derive(Debug, Clone)]
367pub struct QueryRequest<Q> {
368    /// The scoped query
369    query: ScopedQuery<Q>,
370
371    /// The auth scope (from capability token)
372    auth: Arc<AuthScope>,
373}
374
375impl<Q> QueryRequest<Q> {
376    /// Create a new query request
377    ///
378    /// # Validation
379    /// This validates the query scope against the auth scope at construction time.
380    pub fn new(query: ScopedQuery<Q>, auth: Arc<AuthScope>) -> Result<Self, ScopeError> {
381        query.validate(&auth)?;
382        Ok(Self { query, auth })
383    }
384
385    /// Get the scoped query
386    pub fn query(&self) -> &ScopedQuery<Q> {
387        &self.query
388    }
389
390    /// Get the auth scope
391    pub fn auth(&self) -> &AuthScope {
392        &self.auth
393    }
394
395    /// Compute the complete effective filter
396    ///
397    /// This combines:
398    /// 1. Auth scope constraints (mandatory)
399    /// 2. Namespace scope constraints (mandatory)  
400    /// 3. User-provided filters (optional)
401    pub fn effective_filter(&self) -> FilterIR {
402        self.auth.to_filter_ir().and(self.query.effective_filter())
403    }
404
405    /// Get the namespace scope
406    pub fn namespace_scope(&self) -> &NamespaceScope {
407        self.query.scope()
408    }
409}
410
411// ============================================================================
412// Convenience Constructors
413// ============================================================================
414
415/// Create a namespace (shorthand)
416pub fn ns(name: &str) -> Result<Namespace, NamespaceError> {
417    Namespace::new(name)
418}
419
420/// Create a single-namespace scope (shorthand)
421pub fn scope(name: &str) -> Result<NamespaceScope, NamespaceError> {
422    Ok(NamespaceScope::Single(Namespace::new(name)?))
423}
424
425// ============================================================================
426// Database Tier — namespace > database > table (P3.1)
427// ============================================================================
428
429/// A database within a namespace.
430///
431/// Mirrors SurrealDB's three-tier hierarchy: `namespace > database > table`.
432/// Each namespace can contain multiple databases, providing logical grouping
433/// and isolation of tables within the same namespace.
434///
435/// ## Example
436///
437/// ```text
438/// namespace "production"
439///   ├─ database "app"
440///   │   ├─ table "users"
441///   │   └─ table "posts"
442///   └─ database "analytics"
443///       ├─ table "events"
444///       └─ table "sessions"
445/// ```
446#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
447pub struct DatabaseId {
448    /// Parent namespace
449    pub namespace: String,
450    /// Database name within the namespace
451    pub name: String,
452}
453
454impl DatabaseId {
455    /// Maximum length for a database identifier
456    pub const MAX_LENGTH: usize = 256;
457
458    /// Create a new database identifier.
459    pub fn new(
460        namespace: impl Into<String>,
461        name: impl Into<String>,
462    ) -> 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(
549        &mut self,
550        namespace: &str,
551        database: &str,
552    ) -> Result<(), NamespaceError> {
553        Namespace::validate_name(database)?;
554        let dbs = self.databases.entry(namespace.to_string()).or_default();
555        dbs.insert(database.to_string());
556        self.tables
557            .entry((namespace.to_string(), database.to_string()))
558            .or_default();
559        Ok(())
560    }
561
562    /// Register a table within a namespace/database.
563    pub fn create_table(
564        &mut self,
565        namespace: &str,
566        database: &str,
567        table: &str,
568    ) -> Result<(), NamespaceError> {
569        Namespace::validate_name(table)?;
570        // Ensure parent database exists
571        let dbs = self.databases.entry(namespace.to_string()).or_default();
572        dbs.insert(database.to_string());
573        let tables = self
574            .tables
575            .entry((namespace.to_string(), database.to_string()))
576            .or_default();
577        tables.insert(table.to_string());
578        Ok(())
579    }
580
581    /// List databases in a namespace.
582    pub fn list_databases(&self, namespace: &str) -> Vec<&str> {
583        self.databases
584            .get(namespace)
585            .map(|dbs| dbs.iter().map(|s| s.as_str()).collect())
586            .unwrap_or_default()
587    }
588
589    /// List tables in a database.
590    pub fn list_tables(&self, namespace: &str, database: &str) -> Vec<&str> {
591        self.tables
592            .get(&(namespace.to_string(), database.to_string()))
593            .map(|tables| tables.iter().map(|s| s.as_str()).collect())
594            .unwrap_or_default()
595    }
596
597    /// Check if a namespace exists.
598    pub fn namespace_exists(&self, namespace: &str) -> bool {
599        self.databases.contains_key(namespace)
600    }
601
602    /// Check if a database exists within a namespace.
603    pub fn database_exists(&self, namespace: &str, database: &str) -> bool {
604        self.databases
605            .get(namespace)
606            .map(|dbs| dbs.contains(database))
607            .unwrap_or(false)
608    }
609
610    /// Check if a table exists within a namespace/database.
611    pub fn table_exists(&self, namespace: &str, database: &str, table: &str) -> bool {
612        self.tables
613            .get(&(namespace.to_string(), database.to_string()))
614            .map(|tables| tables.contains(table))
615            .unwrap_or(false)
616    }
617
618    /// Drop a database and all its tables.
619    pub fn drop_database(&mut self, namespace: &str, database: &str) -> bool {
620        self.tables
621            .remove(&(namespace.to_string(), database.to_string()));
622        self.databases
623            .get_mut(namespace)
624            .map(|dbs| dbs.remove(database))
625            .unwrap_or(false)
626    }
627
628    /// Drop a table from a database.
629    pub fn drop_table(&mut self, namespace: &str, database: &str, table: &str) -> bool {
630        self.tables
631            .get_mut(&(namespace.to_string(), database.to_string()))
632            .map(|tables| tables.remove(table))
633            .unwrap_or(false)
634    }
635
636    /// Drop a namespace and all its databases/tables.
637    pub fn drop_namespace(&mut self, namespace: &str) -> bool {
638        if !self.databases.contains_key(namespace) {
639            return false;
640        }
641        // Remove all tables under this namespace
642        let db_names: Vec<String> = self
643            .databases
644            .get(namespace)
645            .map(|dbs| dbs.iter().cloned().collect())
646            .unwrap_or_default();
647        for db in &db_names {
648            self.tables.remove(&(namespace.to_string(), db.clone()));
649        }
650        self.databases.remove(namespace);
651        true
652    }
653
654    /// Resolve a qualified table path to check it exists.
655    pub fn resolve_table(&self, qualified: &QualifiedTable) -> bool {
656        self.table_exists(&qualified.namespace, &qualified.database, &qualified.table)
657    }
658}
659
660// ============================================================================
661// Tests
662// ============================================================================
663
664#[cfg(test)]
665mod tests {
666    use super::*;
667
668    #[test]
669    fn test_namespace_validation() {
670        // Valid
671        assert!(Namespace::new("production").is_ok());
672        assert!(Namespace::new("my_namespace").is_ok());
673        assert!(Namespace::new("project-123").is_ok());
674        assert!(Namespace::new("v1.0.0").is_ok());
675
676        // Invalid
677        assert!(Namespace::new("").is_err()); // Empty
678        assert!(Namespace::new("-starts-with-dash").is_err());
679        assert!(Namespace::new(".starts-with-dot").is_err());
680        assert!(Namespace::new("has spaces").is_err());
681        assert!(Namespace::new("has@symbol").is_err());
682    }
683
684    #[test]
685    fn test_namespace_scope_single() {
686        let ns = Namespace::new("production").unwrap();
687        let scope = NamespaceScope::single(ns.clone());
688
689        assert!(scope.contains(&ns));
690        assert!(!scope.contains(&Namespace::new("staging").unwrap()));
691    }
692
693    #[test]
694    fn test_namespace_scope_multiple() {
695        let ns1 = Namespace::new("prod").unwrap();
696        let ns2 = Namespace::new("staging").unwrap();
697        let scope = NamespaceScope::multiple(vec![ns1.clone(), ns2.clone()]).unwrap();
698
699        assert!(scope.contains(&ns1));
700        assert!(scope.contains(&ns2));
701        assert!(!scope.contains(&Namespace::new("dev").unwrap()));
702    }
703
704    #[test]
705    fn test_scope_to_filter_ir() {
706        let scope = NamespaceScope::single(Namespace::new("production").unwrap());
707        let filter = scope.to_filter_ir();
708
709        assert!(filter.constrains_field("namespace"));
710        assert_eq!(filter.clauses.len(), 1);
711    }
712
713    #[test]
714    fn test_scoped_query_effective_filter() {
715        let ns = Namespace::new("production").unwrap();
716        let user_filter = FilterBuilder::new().eq("source", "documents").build();
717
718        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ()).with_filters(user_filter);
719
720        let effective = query.effective_filter();
721        assert!(effective.constrains_field("namespace"));
722        assert!(effective.constrains_field("source"));
723    }
724
725    #[test]
726    fn test_query_request_validation() {
727        let ns = Namespace::new("production").unwrap();
728        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ());
729
730        // Auth allows production
731        let auth = Arc::new(AuthScope::for_namespace("production"));
732        assert!(QueryRequest::new(query.clone(), auth).is_ok());
733
734        // Auth only allows staging
735        let auth2 = Arc::new(AuthScope::for_namespace("staging"));
736        assert!(QueryRequest::new(query, auth2).is_err());
737    }
738
739    #[test]
740    fn test_query_request_effective_filter() {
741        let ns = Namespace::new("production").unwrap();
742        let query: ScopedQuery<()> = ScopedQuery::in_namespace(ns, ())
743            .with_filters(FilterBuilder::new().eq("type", "article").build());
744
745        let auth = Arc::new(AuthScope::for_namespace("production").with_tenant("acme"));
746
747        let request = QueryRequest::new(query, auth).unwrap();
748        let effective = request.effective_filter();
749
750        // Should have: namespace (from scope) + tenant_id (from auth) + type (from user)
751        assert!(effective.constrains_field("namespace"));
752        assert!(effective.constrains_field("tenant_id"));
753        assert!(effective.constrains_field("type"));
754    }
755
756    // ======== Database Tier Tests (P3.1) ========
757
758    #[test]
759    fn test_database_id_creation() {
760        let db = DatabaseId::new("production", "app").unwrap();
761        assert_eq!(db.namespace, "production");
762        assert_eq!(db.name, "app");
763        assert_eq!(db.qualified_name(), "production/app");
764    }
765
766    #[test]
767    fn test_qualified_table() {
768        let qt = QualifiedTable::new("production", "app", "users");
769        assert_eq!(qt.qualified_name(), "production/app/users");
770        assert_eq!(qt.storage_prefix(), "production:app:users");
771    }
772
773    #[test]
774    fn test_namespace_registry_basic() {
775        let mut reg = NamespaceRegistry::new();
776        reg.create_namespace("prod").unwrap();
777        assert!(reg.namespace_exists("prod"));
778        assert!(!reg.namespace_exists("staging"));
779    }
780
781    #[test]
782    fn test_namespace_registry_databases() {
783        let mut reg = NamespaceRegistry::new();
784        reg.create_namespace("prod").unwrap();
785        reg.create_database("prod", "app").unwrap();
786        reg.create_database("prod", "analytics").unwrap();
787        assert!(reg.database_exists("prod", "app"));
788        assert!(reg.database_exists("prod", "analytics"));
789        assert!(!reg.database_exists("prod", "logs"));
790        let dbs = reg.list_databases("prod");
791        assert_eq!(dbs.len(), 2);
792    }
793
794    #[test]
795    fn test_namespace_registry_tables() {
796        let mut reg = NamespaceRegistry::new();
797        reg.create_table("prod", "app", "users").unwrap();
798        reg.create_table("prod", "app", "posts").unwrap();
799        assert!(reg.table_exists("prod", "app", "users"));
800        assert!(reg.table_exists("prod", "app", "posts"));
801        assert!(!reg.table_exists("prod", "app", "comments"));
802        // database was auto-created
803        assert!(reg.database_exists("prod", "app"));
804    }
805
806    #[test]
807    fn test_namespace_registry_drop() {
808        let mut reg = NamespaceRegistry::new();
809        reg.create_table("prod", "app", "users").unwrap();
810        reg.create_table("prod", "app", "posts").unwrap();
811        reg.create_table("prod", "analytics", "events").unwrap();
812
813        // Drop one table
814        assert!(reg.drop_table("prod", "app", "users"));
815        assert!(!reg.table_exists("prod", "app", "users"));
816        assert!(reg.table_exists("prod", "app", "posts"));
817
818        // Drop a database
819        assert!(reg.drop_database("prod", "app"));
820        assert!(!reg.database_exists("prod", "app"));
821        assert!(!reg.table_exists("prod", "app", "posts"));
822
823        // analytics still exists
824        assert!(reg.table_exists("prod", "analytics", "events"));
825
826        // Drop entire namespace
827        assert!(reg.drop_namespace("prod"));
828        assert!(!reg.namespace_exists("prod"));
829        assert!(!reg.table_exists("prod", "analytics", "events"));
830    }
831
832    #[test]
833    fn test_qualified_table_resolve() {
834        let mut reg = NamespaceRegistry::new();
835        reg.create_table("prod", "app", "users").unwrap();
836        let qt = QualifiedTable::new("prod", "app", "users");
837        assert!(reg.resolve_table(&qt));
838        let missing = QualifiedTable::new("prod", "app", "absent");
839        assert!(!reg.resolve_table(&missing));
840    }
841}