prax_query/mem_optimize/
lazy.rs

1//! Lazy schema parsing for on-demand introspection.
2//!
3//! This module provides lazy-loading wrappers for schema introspection that
4//! defer parsing until fields are actually accessed, reducing memory usage
5//! for large schemas where not all information is needed.
6//!
7//! # Memory Savings
8//!
9//! For a database with 100 tables, each with 20 columns:
10//! - **Eager parsing**: Parses all 2000 columns upfront (~40-50% of introspection time)
11//! - **Lazy parsing**: Only parses columns when accessed (0% until needed)
12//!
13//! # Example
14//!
15//! ```rust,ignore
16//! use prax_query::mem_optimize::lazy::LazySchema;
17//!
18//! // Create lazy schema from raw introspection data
19//! let schema = LazySchema::from_json(raw_json)?;
20//!
21//! // Table names available immediately (minimal parsing)
22//! for name in schema.table_names() {
23//!     println!("Table: {}", name);
24//! }
25//!
26//! // Columns only parsed when accessed
27//! if let Some(users) = schema.get_table("users") {
28//!     // Column parsing happens here
29//!     for col in users.columns() {
30//!         println!("  Column: {} ({})", col.name(), col.db_type());
31//!     }
32//! }
33//! ```
34
35use parking_lot::RwLock;
36use serde::{Deserialize, Serialize};
37use std::collections::HashMap;
38use std::sync::Arc;
39
40// ============================================================================
41// Lazy Schema
42// ============================================================================
43
44/// Lazy-loaded database schema.
45///
46/// Table metadata is parsed on first access, reducing memory usage
47/// when only a subset of tables are needed.
48pub struct LazySchema {
49    /// Database name.
50    name: String,
51    /// Schema/namespace.
52    schema: Option<String>,
53    /// Table entries (lazy-loaded).
54    tables: RwLock<HashMap<String, LazyTableEntry>>,
55    /// Table names (for fast iteration without loading).
56    table_names: Vec<String>,
57    /// Enum definitions (usually small, loaded eagerly).
58    enums: Vec<LazyEnum>,
59}
60
61/// Entry for a table - either raw data or parsed.
62enum LazyTableEntry {
63    /// Raw JSON data, not yet parsed.
64    Raw(serde_json::Value),
65    /// Parsed table information.
66    Parsed(LazyTable),
67}
68
69impl LazySchema {
70    /// Create from raw JSON introspection data.
71    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
72        let raw: RawSchema = serde_json::from_str(json)?;
73        Ok(Self::from_raw(raw))
74    }
75
76    /// Create from parsed raw schema.
77    pub fn from_raw(raw: RawSchema) -> Self {
78        let table_names: Vec<String> = raw.tables.iter().map(|t| t.name.clone()).collect();
79
80        let mut tables = HashMap::with_capacity(raw.tables.len());
81        for table in raw.tables {
82            let name = table.name.clone();
83            tables.insert(name, LazyTableEntry::Raw(serde_json::to_value(table).unwrap()));
84        }
85
86        Self {
87            name: raw.name,
88            schema: raw.schema,
89            tables: RwLock::new(tables),
90            table_names,
91            enums: raw.enums.into_iter().map(LazyEnum::from).collect(),
92        }
93    }
94
95    /// Get database name.
96    pub fn name(&self) -> &str {
97        &self.name
98    }
99
100    /// Get schema/namespace.
101    pub fn schema(&self) -> Option<&str> {
102        self.schema.as_deref()
103    }
104
105    /// Get table names without parsing table details.
106    pub fn table_names(&self) -> &[String] {
107        &self.table_names
108    }
109
110    /// Get number of tables.
111    pub fn table_count(&self) -> usize {
112        self.table_names.len()
113    }
114
115    /// Get a table by name, parsing on first access.
116    pub fn get_table(&self, name: &str) -> Option<LazyTable> {
117        let tables = self.tables.read();
118
119        // Check if we have this table
120        if let Some(entry) = tables.get(name) {
121            match entry {
122                LazyTableEntry::Parsed(table) => return Some(table.clone()),
123                LazyTableEntry::Raw(_) => {
124                    // Need to parse - drop read lock and acquire write lock
125                }
126            }
127        } else {
128            return None;
129        }
130
131        drop(tables);
132
133        // Acquire write lock and parse
134        let mut tables = self.tables.write();
135
136        // Double-check (another thread may have parsed)
137        if let Some(entry) = tables.get(name) {
138            if let LazyTableEntry::Parsed(table) = entry {
139                return Some(table.clone());
140            }
141        }
142
143        // Parse the raw data
144        if let Some(entry) = tables.remove(name) {
145            if let LazyTableEntry::Raw(raw) = entry {
146                match serde_json::from_value::<RawTable>(raw) {
147                    Ok(raw_table) => {
148                        let table = LazyTable::from_raw(raw_table);
149                        tables.insert(name.to_string(), LazyTableEntry::Parsed(table.clone()));
150                        return Some(table);
151                    }
152                    Err(_) => return None,
153                }
154            }
155        }
156
157        None
158    }
159
160    /// Check if a table exists.
161    pub fn has_table(&self, name: &str) -> bool {
162        self.table_names.iter().any(|n| n == name)
163    }
164
165    /// Get all enums.
166    pub fn enums(&self) -> &[LazyEnum] {
167        &self.enums
168    }
169
170    /// Get enum by name.
171    pub fn get_enum(&self, name: &str) -> Option<&LazyEnum> {
172        self.enums.iter().find(|e| e.name == name)
173    }
174
175    /// Get memory statistics.
176    pub fn memory_stats(&self) -> LazySchemaStats {
177        let tables = self.tables.read();
178        let parsed = tables
179            .values()
180            .filter(|e| matches!(e, LazyTableEntry::Parsed(_)))
181            .count();
182        let raw = tables.len() - parsed;
183
184        LazySchemaStats {
185            total_tables: self.table_names.len(),
186            parsed_tables: parsed,
187            unparsed_tables: raw,
188            enum_count: self.enums.len(),
189        }
190    }
191}
192
193// ============================================================================
194// Lazy Table
195// ============================================================================
196
197/// Lazy-loaded table information.
198#[derive(Clone)]
199pub struct LazyTable {
200    inner: Arc<LazyTableInner>,
201}
202
203struct LazyTableInner {
204    /// Table name.
205    name: String,
206    /// Schema.
207    schema: Option<String>,
208    /// Comment.
209    comment: Option<String>,
210    /// Primary key columns.
211    primary_key: Vec<String>,
212    /// Columns (lazy-loaded).
213    columns: RwLock<LazyColumns>,
214    /// Foreign keys (lazy-loaded).
215    foreign_keys: RwLock<LazyForeignKeys>,
216    /// Indexes (lazy-loaded).
217    indexes: RwLock<LazyIndexes>,
218}
219
220enum LazyColumns {
221    Raw(Vec<serde_json::Value>),
222    Parsed(Vec<LazyColumn>),
223}
224
225enum LazyForeignKeys {
226    Raw(Vec<serde_json::Value>),
227    Parsed(Vec<LazyForeignKey>),
228}
229
230enum LazyIndexes {
231    Raw(Vec<serde_json::Value>),
232    Parsed(Vec<LazyIndex>),
233}
234
235impl LazyTable {
236    fn from_raw(raw: RawTable) -> Self {
237        Self {
238            inner: Arc::new(LazyTableInner {
239                name: raw.name,
240                schema: raw.schema,
241                comment: raw.comment,
242                primary_key: raw.primary_key,
243                columns: RwLock::new(LazyColumns::Raw(
244                    raw.columns
245                        .into_iter()
246                        .map(|c| serde_json::to_value(c).unwrap())
247                        .collect(),
248                )),
249                foreign_keys: RwLock::new(LazyForeignKeys::Raw(
250                    raw.foreign_keys
251                        .into_iter()
252                        .map(|f| serde_json::to_value(f).unwrap())
253                        .collect(),
254                )),
255                indexes: RwLock::new(LazyIndexes::Raw(
256                    raw.indexes
257                        .into_iter()
258                        .map(|i| serde_json::to_value(i).unwrap())
259                        .collect(),
260                )),
261            }),
262        }
263    }
264
265    /// Get table name.
266    pub fn name(&self) -> &str {
267        &self.inner.name
268    }
269
270    /// Get schema.
271    pub fn schema(&self) -> Option<&str> {
272        self.inner.schema.as_deref()
273    }
274
275    /// Get comment.
276    pub fn comment(&self) -> Option<&str> {
277        self.inner.comment.as_deref()
278    }
279
280    /// Get primary key columns.
281    pub fn primary_key(&self) -> &[String] {
282        &self.inner.primary_key
283    }
284
285    /// Get columns, parsing on first access.
286    pub fn columns(&self) -> Vec<LazyColumn> {
287        // Fast path: already parsed
288        {
289            let columns = self.inner.columns.read();
290            if let LazyColumns::Parsed(cols) = &*columns {
291                return cols.clone();
292            }
293        }
294
295        // Slow path: parse
296        let mut columns = self.inner.columns.write();
297
298        // Double-check
299        if let LazyColumns::Parsed(cols) = &*columns {
300            return cols.clone();
301        }
302
303        // Parse
304        if let LazyColumns::Raw(raw) = &*columns {
305            let parsed: Vec<LazyColumn> = raw
306                .iter()
307                .filter_map(|v| serde_json::from_value::<RawColumn>(v.clone()).ok())
308                .map(LazyColumn::from)
309                .collect();
310            let result = parsed.clone();
311            *columns = LazyColumns::Parsed(parsed);
312            return result;
313        }
314
315        vec![]
316    }
317
318    /// Get column by name.
319    pub fn get_column(&self, name: &str) -> Option<LazyColumn> {
320        self.columns().into_iter().find(|c| c.name() == name)
321    }
322
323    /// Get column count without parsing.
324    pub fn column_count(&self) -> usize {
325        let columns = self.inner.columns.read();
326        match &*columns {
327            LazyColumns::Raw(raw) => raw.len(),
328            LazyColumns::Parsed(parsed) => parsed.len(),
329        }
330    }
331
332    /// Get foreign keys, parsing on first access.
333    pub fn foreign_keys(&self) -> Vec<LazyForeignKey> {
334        // Fast path
335        {
336            let fks = self.inner.foreign_keys.read();
337            if let LazyForeignKeys::Parsed(fks) = &*fks {
338                return fks.clone();
339            }
340        }
341
342        // Slow path
343        let mut fks = self.inner.foreign_keys.write();
344
345        if let LazyForeignKeys::Parsed(fks) = &*fks {
346            return fks.clone();
347        }
348
349        if let LazyForeignKeys::Raw(raw) = &*fks {
350            let parsed: Vec<LazyForeignKey> = raw
351                .iter()
352                .filter_map(|v| serde_json::from_value::<RawForeignKey>(v.clone()).ok())
353                .map(LazyForeignKey::from)
354                .collect();
355            let result = parsed.clone();
356            *fks = LazyForeignKeys::Parsed(parsed);
357            return result;
358        }
359
360        vec![]
361    }
362
363    /// Get indexes, parsing on first access.
364    pub fn indexes(&self) -> Vec<LazyIndex> {
365        // Fast path
366        {
367            let idxs = self.inner.indexes.read();
368            if let LazyIndexes::Parsed(idxs) = &*idxs {
369                return idxs.clone();
370            }
371        }
372
373        // Slow path
374        let mut idxs = self.inner.indexes.write();
375
376        if let LazyIndexes::Parsed(idxs) = &*idxs {
377            return idxs.clone();
378        }
379
380        if let LazyIndexes::Raw(raw) = &*idxs {
381            let parsed: Vec<LazyIndex> = raw
382                .iter()
383                .filter_map(|v| serde_json::from_value::<RawIndex>(v.clone()).ok())
384                .map(LazyIndex::from)
385                .collect();
386            let result = parsed.clone();
387            *idxs = LazyIndexes::Parsed(parsed);
388            return result;
389        }
390
391        vec![]
392    }
393}
394
395// ============================================================================
396// Lazy Column
397// ============================================================================
398
399/// Lazy-loaded column information.
400#[derive(Clone)]
401pub struct LazyColumn {
402    name: String,
403    db_type: String,
404    nullable: bool,
405    default: Option<String>,
406    auto_increment: bool,
407    is_primary_key: bool,
408    is_unique: bool,
409    comment: Option<String>,
410}
411
412impl LazyColumn {
413    /// Get column name.
414    pub fn name(&self) -> &str {
415        &self.name
416    }
417
418    /// Get database type.
419    pub fn db_type(&self) -> &str {
420        &self.db_type
421    }
422
423    /// Check if nullable.
424    pub fn is_nullable(&self) -> bool {
425        self.nullable
426    }
427
428    /// Get default value.
429    pub fn default(&self) -> Option<&str> {
430        self.default.as_deref()
431    }
432
433    /// Check if auto-increment.
434    pub fn is_auto_increment(&self) -> bool {
435        self.auto_increment
436    }
437
438    /// Check if primary key.
439    pub fn is_primary_key(&self) -> bool {
440        self.is_primary_key
441    }
442
443    /// Check if unique.
444    pub fn is_unique(&self) -> bool {
445        self.is_unique
446    }
447
448    /// Get comment.
449    pub fn comment(&self) -> Option<&str> {
450        self.comment.as_deref()
451    }
452}
453
454impl From<RawColumn> for LazyColumn {
455    fn from(raw: RawColumn) -> Self {
456        Self {
457            name: raw.name,
458            db_type: raw.db_type,
459            nullable: raw.nullable,
460            default: raw.default,
461            auto_increment: raw.auto_increment,
462            is_primary_key: raw.is_primary_key,
463            is_unique: raw.is_unique,
464            comment: raw.comment,
465        }
466    }
467}
468
469// ============================================================================
470// Lazy Foreign Key
471// ============================================================================
472
473/// Lazy-loaded foreign key information.
474#[derive(Clone)]
475pub struct LazyForeignKey {
476    name: String,
477    columns: Vec<String>,
478    referenced_table: String,
479    referenced_columns: Vec<String>,
480    on_delete: String,
481    on_update: String,
482}
483
484impl LazyForeignKey {
485    /// Get constraint name.
486    pub fn name(&self) -> &str {
487        &self.name
488    }
489
490    /// Get local columns.
491    pub fn columns(&self) -> &[String] {
492        &self.columns
493    }
494
495    /// Get referenced table.
496    pub fn referenced_table(&self) -> &str {
497        &self.referenced_table
498    }
499
500    /// Get referenced columns.
501    pub fn referenced_columns(&self) -> &[String] {
502        &self.referenced_columns
503    }
504
505    /// Get ON DELETE action.
506    pub fn on_delete(&self) -> &str {
507        &self.on_delete
508    }
509
510    /// Get ON UPDATE action.
511    pub fn on_update(&self) -> &str {
512        &self.on_update
513    }
514}
515
516impl From<RawForeignKey> for LazyForeignKey {
517    fn from(raw: RawForeignKey) -> Self {
518        Self {
519            name: raw.name,
520            columns: raw.columns,
521            referenced_table: raw.referenced_table,
522            referenced_columns: raw.referenced_columns,
523            on_delete: raw.on_delete,
524            on_update: raw.on_update,
525        }
526    }
527}
528
529// ============================================================================
530// Lazy Index
531// ============================================================================
532
533/// Lazy-loaded index information.
534#[derive(Clone)]
535pub struct LazyIndex {
536    name: String,
537    columns: Vec<String>,
538    is_unique: bool,
539    is_primary: bool,
540    index_type: Option<String>,
541}
542
543impl LazyIndex {
544    /// Get index name.
545    pub fn name(&self) -> &str {
546        &self.name
547    }
548
549    /// Get indexed columns.
550    pub fn columns(&self) -> &[String] {
551        &self.columns
552    }
553
554    /// Check if unique.
555    pub fn is_unique(&self) -> bool {
556        self.is_unique
557    }
558
559    /// Check if primary key index.
560    pub fn is_primary(&self) -> bool {
561        self.is_primary
562    }
563
564    /// Get index type.
565    pub fn index_type(&self) -> Option<&str> {
566        self.index_type.as_deref()
567    }
568}
569
570impl From<RawIndex> for LazyIndex {
571    fn from(raw: RawIndex) -> Self {
572        Self {
573            name: raw.name,
574            columns: raw.columns,
575            is_unique: raw.is_unique,
576            is_primary: raw.is_primary,
577            index_type: raw.index_type,
578        }
579    }
580}
581
582// ============================================================================
583// Lazy Enum
584// ============================================================================
585
586/// Enum type definition.
587#[derive(Clone)]
588pub struct LazyEnum {
589    name: String,
590    schema: Option<String>,
591    values: Vec<String>,
592}
593
594impl LazyEnum {
595    /// Get enum name.
596    pub fn name(&self) -> &str {
597        &self.name
598    }
599
600    /// Get schema.
601    pub fn schema(&self) -> Option<&str> {
602        self.schema.as_deref()
603    }
604
605    /// Get enum values.
606    pub fn values(&self) -> &[String] {
607        &self.values
608    }
609}
610
611impl From<RawEnum> for LazyEnum {
612    fn from(raw: RawEnum) -> Self {
613        Self {
614            name: raw.name,
615            schema: raw.schema,
616            values: raw.values,
617        }
618    }
619}
620
621// ============================================================================
622// Statistics
623// ============================================================================
624
625/// Statistics for lazy schema loading.
626#[derive(Debug, Clone, Default)]
627pub struct LazySchemaStats {
628    /// Total table count.
629    pub total_tables: usize,
630    /// Tables that have been parsed.
631    pub parsed_tables: usize,
632    /// Tables still in raw form.
633    pub unparsed_tables: usize,
634    /// Enum count.
635    pub enum_count: usize,
636}
637
638impl LazySchemaStats {
639    /// Get parse ratio (0.0 to 1.0).
640    pub fn parse_ratio(&self) -> f64 {
641        if self.total_tables == 0 {
642            0.0
643        } else {
644            self.parsed_tables as f64 / self.total_tables as f64
645        }
646    }
647}
648
649// ============================================================================
650// Parse On Demand Trait
651// ============================================================================
652
653/// Trait for types that support lazy/on-demand parsing.
654pub trait ParseOnDemand {
655    /// The parsed output type.
656    type Output;
657
658    /// Check if already parsed.
659    fn is_parsed(&self) -> bool;
660
661    /// Force parsing (if not already done).
662    fn parse(&self) -> Self::Output;
663}
664
665// ============================================================================
666// Raw Schema Types (for deserialization)
667// ============================================================================
668
669#[derive(Debug, Deserialize, Serialize)]
670struct RawSchema {
671    #[serde(default)]
672    name: String,
673    schema: Option<String>,
674    #[serde(default)]
675    tables: Vec<RawTable>,
676    #[serde(default)]
677    enums: Vec<RawEnum>,
678}
679
680#[derive(Debug, Deserialize, Serialize)]
681struct RawTable {
682    name: String,
683    schema: Option<String>,
684    comment: Option<String>,
685    #[serde(default)]
686    columns: Vec<RawColumn>,
687    #[serde(default)]
688    primary_key: Vec<String>,
689    #[serde(default)]
690    foreign_keys: Vec<RawForeignKey>,
691    #[serde(default)]
692    indexes: Vec<RawIndex>,
693}
694
695#[derive(Debug, Deserialize, Serialize)]
696struct RawColumn {
697    name: String,
698    db_type: String,
699    #[serde(default)]
700    nullable: bool,
701    default: Option<String>,
702    #[serde(default)]
703    auto_increment: bool,
704    #[serde(default)]
705    is_primary_key: bool,
706    #[serde(default)]
707    is_unique: bool,
708    comment: Option<String>,
709}
710
711#[derive(Debug, Deserialize, Serialize)]
712struct RawForeignKey {
713    name: String,
714    #[serde(default)]
715    columns: Vec<String>,
716    referenced_table: String,
717    #[serde(default)]
718    referenced_columns: Vec<String>,
719    #[serde(default = "default_action")]
720    on_delete: String,
721    #[serde(default = "default_action")]
722    on_update: String,
723}
724
725fn default_action() -> String {
726    "NO ACTION".to_string()
727}
728
729#[derive(Debug, Deserialize, Serialize)]
730struct RawIndex {
731    name: String,
732    #[serde(default)]
733    columns: Vec<String>,
734    #[serde(default)]
735    is_unique: bool,
736    #[serde(default)]
737    is_primary: bool,
738    index_type: Option<String>,
739}
740
741#[derive(Debug, Deserialize, Serialize)]
742struct RawEnum {
743    name: String,
744    schema: Option<String>,
745    #[serde(default)]
746    values: Vec<String>,
747}
748
749#[cfg(test)]
750mod tests {
751    use super::*;
752
753    fn sample_schema_json() -> &'static str {
754        r#"{
755            "name": "test_db",
756            "schema": "public",
757            "tables": [
758                {
759                    "name": "users",
760                    "columns": [
761                        {"name": "id", "db_type": "integer", "is_primary_key": true},
762                        {"name": "email", "db_type": "varchar(255)", "nullable": false},
763                        {"name": "name", "db_type": "varchar(100)", "nullable": true}
764                    ],
765                    "primary_key": ["id"],
766                    "indexes": [
767                        {"name": "users_email_idx", "columns": ["email"], "is_unique": true}
768                    ]
769                },
770                {
771                    "name": "posts",
772                    "columns": [
773                        {"name": "id", "db_type": "integer", "is_primary_key": true},
774                        {"name": "user_id", "db_type": "integer"},
775                        {"name": "title", "db_type": "varchar(255)"}
776                    ],
777                    "primary_key": ["id"],
778                    "foreign_keys": [
779                        {
780                            "name": "posts_user_fk",
781                            "columns": ["user_id"],
782                            "referenced_table": "users",
783                            "referenced_columns": ["id"]
784                        }
785                    ]
786                }
787            ],
788            "enums": [
789                {"name": "status", "values": ["pending", "active", "archived"]}
790            ]
791        }"#
792    }
793
794    #[test]
795    fn test_lazy_schema_from_json() {
796        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
797
798        assert_eq!(schema.name(), "test_db");
799        assert_eq!(schema.table_count(), 2);
800        assert!(schema.has_table("users"));
801        assert!(schema.has_table("posts"));
802    }
803
804    #[test]
805    fn test_lazy_table_names_no_parse() {
806        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
807
808        // Getting table names should not parse tables
809        let names = schema.table_names();
810        assert_eq!(names.len(), 2);
811
812        // Check stats - nothing should be parsed yet
813        let stats = schema.memory_stats();
814        assert_eq!(stats.parsed_tables, 0);
815        assert_eq!(stats.unparsed_tables, 2);
816    }
817
818    #[test]
819    fn test_lazy_table_parsing() {
820        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
821
822        // Access one table
823        let users = schema.get_table("users").unwrap();
824        assert_eq!(users.name(), "users");
825
826        // Check stats - one table parsed
827        let stats = schema.memory_stats();
828        assert_eq!(stats.parsed_tables, 1);
829        assert_eq!(stats.unparsed_tables, 1);
830    }
831
832    #[test]
833    fn test_lazy_columns() {
834        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
835        let users = schema.get_table("users").unwrap();
836
837        // Columns should be lazy
838        assert_eq!(users.column_count(), 3);
839
840        // Access columns
841        let columns = users.columns();
842        assert_eq!(columns.len(), 3);
843
844        // Find specific column
845        let email = users.get_column("email").unwrap();
846        assert_eq!(email.db_type(), "varchar(255)");
847        assert!(!email.is_nullable());
848    }
849
850    #[test]
851    fn test_lazy_foreign_keys() {
852        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
853        let posts = schema.get_table("posts").unwrap();
854
855        let fks = posts.foreign_keys();
856        assert_eq!(fks.len(), 1);
857
858        let fk = &fks[0];
859        assert_eq!(fk.name(), "posts_user_fk");
860        assert_eq!(fk.referenced_table(), "users");
861    }
862
863    #[test]
864    fn test_lazy_indexes() {
865        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
866        let users = schema.get_table("users").unwrap();
867
868        let indexes = users.indexes();
869        assert_eq!(indexes.len(), 1);
870
871        let idx = &indexes[0];
872        assert_eq!(idx.name(), "users_email_idx");
873        assert!(idx.is_unique());
874    }
875
876    #[test]
877    fn test_lazy_enums() {
878        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
879
880        let enums = schema.enums();
881        assert_eq!(enums.len(), 1);
882
883        let status = schema.get_enum("status").unwrap();
884        assert_eq!(status.values(), &["pending", "active", "archived"]);
885    }
886
887    #[test]
888    fn test_cached_access() {
889        let schema = LazySchema::from_json(sample_schema_json()).unwrap();
890
891        // Access same table multiple times
892        let users1 = schema.get_table("users").unwrap();
893        let users2 = schema.get_table("users").unwrap();
894
895        // Should get same data
896        assert_eq!(users1.name(), users2.name());
897        assert_eq!(users1.column_count(), users2.column_count());
898    }
899}
900