Skip to main content

oxisql_core/
lib.rs

1#![forbid(unsafe_code)]
2#![warn(missing_docs)]
3
4//! `oxisql-core` — core traits and types for the OxiSQL Pure-Rust SQL facade.
5//!
6//! This crate defines the public API surface (`Connection`, `Transaction`,
7//! `Row`, `Value`, `OxiSqlError`) that every OxiSQL backend must implement.
8//! It has no storage logic of its own; concrete backends live in
9//! `oxisql-embedded`, `oxisql-postgres`, etc.
10//!
11//! # Extended Type System
12//!
13//! Beyond the basic scalar types (`Null`, `Bool`, `I64`, `F64`, `Text`,
14//! `Blob`), `Value` supports rich database types:
15//!
16//! - [`Value::Timestamp`] — microseconds since Unix epoch (UTC)
17//! - [`Value::Date`] — days since Unix epoch
18//! - [`Value::Time`] — microseconds since midnight
19//! - [`Value::Uuid`] — 128-bit UUID stored as `u128`
20//! - [`Value::Json`] — JSON/JSONB stored as a `String`
21//! - [`Value::Decimal`] — exact decimal as a string representation
22//! - [`Value::Array`] — ordered collection of `Value`s (Postgres arrays)
23//!
24//! # Type-Safe Extraction
25//!
26//! Use [`Row::try_get`] with the [`FromValue`] trait for ergonomic,
27//! type-safe value extraction:
28//!
29//! ```rust
30//! # use oxisql_core::{Row, Value};
31//! let row = Row::new(
32//!     vec!["id".into(), "name".into()],
33//!     vec![Value::I64(42), Value::Text("Alice".into())],
34//! );
35//! let id: i64 = row.try_get("id").unwrap();
36//! let name: String = row.try_get("name").unwrap();
37//! ```
38
39mod cursor;
40mod error;
41pub mod middleware;
42mod migrator;
43pub mod params;
44mod pool;
45mod prepare;
46pub mod query;
47pub mod registry;
48mod row;
49pub mod schema;
50mod traits;
51mod value;
52mod warning;
53
54pub use cursor::Cursor;
55pub use error::OxiSqlError;
56#[cfg(feature = "tracing")]
57pub use middleware::TracingConnection;
58pub use middleware::{
59    ConnectionMetrics, LoggingConnection, MetricsConnection, MetricsSnapshot, RetryConnection,
60    RetryPolicy, RetryPredicate,
61};
62pub use migrator::{MigrationInfo, MigrationStatus, Migrator};
63pub use params::{bind_named_params, rewrite_named_params};
64pub use pool::ConnectionPool;
65pub use prepare::PreparedStatement;
66pub use query::{
67    BuiltQuery, DeleteBuilder, InsertBuilder, SelectBuilder, SortDirection, UpdateBuilder,
68};
69pub use registry::{SqlType, TypeRegistry};
70pub use row::{FromValue, Row, RowSet};
71pub use schema::{ColumnInfo, ForeignKeyInfo, IndexInfo, TableInfo, TableType};
72pub use traits::{Connection, ToSqlValue, Transaction};
73pub use value::{ArrayElementType, BorrowedValue, Value};
74pub use warning::{parse_warning_level, SqlWarning, SqlWarningLevel};
75
76// ── Tests ───────────────────────────────────────────────────────────────────
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn value_display() {
84        assert_eq!(format!("{}", Value::Null), "NULL");
85        assert_eq!(format!("{}", Value::Bool(true)), "true");
86        assert_eq!(format!("{}", Value::I64(42)), "42");
87        assert_eq!(format!("{}", Value::F64(2.71)), "2.71");
88        assert_eq!(format!("{}", Value::Text("hello".into())), "hello");
89        assert_eq!(format!("{}", Value::Blob(vec![1, 2, 3])), "<blob:3 bytes>");
90        assert_eq!(format!("{}", Value::Decimal("123.456".into())), "123.456");
91        assert_eq!(
92            format!("{}", Value::Json(r#"{"key":"val"}"#.into())),
93            r#"{"key":"val"}"#
94        );
95        assert_eq!(
96            format!("{}", Value::Array(vec![Value::I64(1), Value::I64(2)])),
97            "[1, 2]"
98        );
99    }
100
101    #[test]
102    fn value_type_name() {
103        assert_eq!(Value::Null.type_name(), "Null");
104        assert_eq!(Value::Bool(true).type_name(), "Bool");
105        assert_eq!(Value::I64(1).type_name(), "I64");
106        assert_eq!(Value::F64(1.0).type_name(), "F64");
107        assert_eq!(Value::Text("x".into()).type_name(), "Text");
108        assert_eq!(Value::Blob(vec![]).type_name(), "Blob");
109        assert_eq!(Value::Timestamp(0).type_name(), "Timestamp");
110        assert_eq!(Value::Date(0).type_name(), "Date");
111        assert_eq!(Value::Time(0).type_name(), "Time");
112        assert_eq!(Value::Uuid(0).type_name(), "Uuid");
113        assert_eq!(Value::Json("{}".into()).type_name(), "Json");
114        assert_eq!(Value::Decimal("0".into()).type_name(), "Decimal");
115        assert_eq!(Value::Array(vec![]).type_name(), "Array");
116    }
117
118    #[test]
119    fn value_from_impls() {
120        assert_eq!(Value::from(true), Value::Bool(true));
121        assert_eq!(Value::from(42i32), Value::I64(42));
122        assert_eq!(Value::from(42i64), Value::I64(42));
123        assert_eq!(Value::from(2.71f64), Value::F64(2.71));
124        assert_eq!(Value::from("hello"), Value::Text("hello".into()));
125        assert_eq!(
126            Value::from("hello".to_string()),
127            Value::Text("hello".into())
128        );
129        assert_eq!(Value::from(vec![1u8, 2, 3]), Value::Blob(vec![1, 2, 3]));
130        assert_eq!(Value::from(None::<i64>), Value::Null);
131        assert_eq!(Value::from(Some(42i64)), Value::I64(42));
132    }
133
134    #[test]
135    fn value_partial_ord() {
136        assert!(Value::Null < Value::I64(0));
137        assert!(Value::I64(1) < Value::I64(2));
138        assert!(Value::Text("a".into()) < Value::Text("b".into()));
139        assert!(Value::Bool(false) < Value::Bool(true));
140        // Cross-type comparison returns None
141        assert_eq!(Value::I64(1).partial_cmp(&Value::Text("1".into())), None);
142    }
143
144    #[test]
145    fn from_value_basic() {
146        assert_eq!(bool::from_value(&Value::Bool(true)).ok(), Some(true));
147        assert_eq!(i64::from_value(&Value::I64(42)).ok(), Some(42i64));
148        assert_eq!(i32::from_value(&Value::I64(42)).ok(), Some(42i32));
149        assert_eq!(f64::from_value(&Value::F64(2.71)).ok(), Some(2.71));
150        assert_eq!(
151            String::from_value(&Value::Text("hi".into())).ok(),
152            Some("hi".to_string())
153        );
154        assert_eq!(
155            Vec::<u8>::from_value(&Value::Blob(vec![1])).ok(),
156            Some(vec![1u8])
157        );
158    }
159
160    #[test]
161    fn from_value_option() {
162        assert_eq!(Option::<i64>::from_value(&Value::Null).ok(), Some(None));
163        assert_eq!(
164            Option::<i64>::from_value(&Value::I64(42)).ok(),
165            Some(Some(42))
166        );
167    }
168
169    #[test]
170    fn from_value_type_mismatch() {
171        assert!(i64::from_value(&Value::Text("x".into())).is_err());
172        assert!(bool::from_value(&Value::I64(1)).is_err());
173        assert!(String::from_value(&Value::I64(1)).is_err());
174    }
175
176    #[test]
177    fn row_try_get() {
178        let row = Row::new(
179            vec!["id".into(), "name".into(), "empty".into()],
180            vec![Value::I64(42), Value::Text("Alice".into()), Value::Null],
181        );
182        assert_eq!(row.try_get::<i64>("id").ok(), Some(42));
183        assert_eq!(
184            row.try_get::<String>("name").ok(),
185            Some("Alice".to_string())
186        );
187        assert_eq!(row.try_get::<Option<i64>>("empty").ok(), Some(None));
188        assert!(row.try_get::<i64>("nonexistent").is_err());
189    }
190
191    #[test]
192    fn row_column_count_and_is_null() {
193        let row = Row::new(
194            vec!["a".into(), "b".into()],
195            vec![Value::I64(1), Value::Null],
196        );
197        assert_eq!(row.column_count(), 2);
198        assert!(!row.is_null("a"));
199        assert!(row.is_null("b"));
200        assert!(!row.is_null("nonexistent"));
201    }
202
203    #[test]
204    fn row_display() {
205        let row = Row::new(
206            vec!["id".into(), "name".into()],
207            vec![Value::I64(1), Value::Text("Alice".into())],
208        );
209        assert_eq!(format!("{row}"), "{id: 1, name: Alice}");
210    }
211
212    #[test]
213    fn row_into_values() {
214        let row = Row::new(vec!["x".into()], vec![Value::I64(99)]);
215        let vals = row.into_values();
216        assert_eq!(vals, vec![Value::I64(99)]);
217    }
218
219    #[test]
220    fn row_o1_lookup() {
221        let cols: Vec<String> = (0..100).map(|i| format!("col{i}")).collect();
222        let vals: Vec<Value> = (0..100).map(Value::I64).collect();
223        let row = Row::new(cols, vals);
224        assert_eq!(row.try_get::<i64>("col0").ok(), Some(0));
225        assert_eq!(row.try_get::<i64>("col99").ok(), Some(99));
226        assert_eq!(row.try_get::<i64>("col50").ok(), Some(50));
227        assert!(row.try_get::<i64>("nonexistent").is_err());
228    }
229
230    #[test]
231    fn rowset_basic() {
232        let rows = vec![
233            Row::new(vec!["a".into()], vec![Value::I64(1)]),
234            Row::new(vec!["a".into()], vec![Value::I64(2)]),
235        ];
236        let rs = RowSet::from_rows(rows);
237        assert_eq!(rs.len(), 2);
238        assert_eq!(rs.column_count(), 1);
239        assert!(!rs.is_empty());
240        assert_eq!(rs.columns(), &["a".to_string()]);
241    }
242
243    #[test]
244    fn rowset_empty() {
245        let rs = RowSet::from_rows(vec![]);
246        assert!(rs.is_empty());
247        assert_eq!(rs.column_count(), 0);
248    }
249
250    #[test]
251    fn error_display() {
252        assert_eq!(
253            format!("{}", OxiSqlError::Parse("bad sql".into())),
254            "SQL parse error: bad sql"
255        );
256        assert_eq!(
257            format!("{}", OxiSqlError::ConstraintViolation("unique key".into())),
258            "constraint violation: unique key"
259        );
260        assert_eq!(
261            format!("{}", OxiSqlError::Timeout("5s".into())),
262            "timeout: 5s"
263        );
264        assert_eq!(
265            format!("{}", OxiSqlError::ConnectionPool("exhausted".into())),
266            "connection pool error: exhausted"
267        );
268        assert_eq!(
269            format!("{}", OxiSqlError::Migration("failed".into())),
270            "migration error: failed"
271        );
272    }
273
274    #[test]
275    fn uuid_display() {
276        // Zero UUID
277        assert_eq!(
278            format!("{}", Value::Uuid(0)),
279            "00000000-0000-0000-0000-000000000000"
280        );
281    }
282
283    #[test]
284    fn time_display() {
285        // 01:02:03
286        let us = (3600 + 2 * 60 + 3) * 1_000_000i64;
287        assert_eq!(format!("{}", Value::Time(us)), "01:02:03");
288        // 01:02:03.000042
289        let us2 = us + 42;
290        assert_eq!(format!("{}", Value::Time(us2)), "01:02:03.000042");
291    }
292
293    #[test]
294    fn f64_from_i64_coercion() {
295        // FromValue for f64 should accept I64 values
296        assert_eq!(f64::from_value(&Value::I64(42)).ok(), Some(42.0));
297    }
298
299    #[test]
300    fn string_from_json_and_decimal() {
301        // FromValue for String should accept Json and Decimal
302        assert_eq!(
303            String::from_value(&Value::Json(r#"{"a":1}"#.into())).ok(),
304            Some(r#"{"a":1}"#.to_string())
305        );
306        assert_eq!(
307            String::from_value(&Value::Decimal("1.23".into())).ok(),
308            Some("1.23".to_string())
309        );
310    }
311
312    // ── ToSqlValue tests ─────────────────────────────────────────────────────
313
314    #[test]
315    fn to_sql_value_i64() {
316        assert_eq!(42i64.to_value(), Value::I64(42));
317        assert_eq!((-1i64).to_value(), Value::I64(-1));
318        assert_eq!(i64::MAX.to_value(), Value::I64(i64::MAX));
319    }
320
321    #[test]
322    fn to_sql_value_i32() {
323        assert_eq!(100i32.to_value(), Value::I64(100));
324        assert_eq!((-5i32).to_value(), Value::I64(-5));
325    }
326
327    #[test]
328    fn to_sql_value_f64() {
329        assert_eq!(1.5f64.to_value(), Value::F64(1.5));
330        assert_eq!(0.0f64.to_value(), Value::F64(0.0));
331    }
332
333    #[test]
334    fn to_sql_value_bool() {
335        assert_eq!(true.to_value(), Value::Bool(true));
336        assert_eq!(false.to_value(), Value::Bool(false));
337    }
338
339    #[test]
340    fn to_sql_value_str() {
341        assert_eq!("hello".to_value(), Value::Text("hello".into()));
342        assert_eq!("".to_value(), Value::Text(String::new()));
343    }
344
345    #[test]
346    fn to_sql_value_string() {
347        assert_eq!("world".to_string().to_value(), Value::Text("world".into()));
348    }
349
350    #[test]
351    fn to_sql_value_bytes() {
352        assert_eq!(vec![1u8, 2, 3].to_value(), Value::Blob(vec![1, 2, 3]));
353        assert_eq!(Vec::<u8>::new().to_value(), Value::Blob(vec![]));
354    }
355
356    #[test]
357    fn to_sql_value_option_some_and_none() {
358        assert_eq!(Some(99i64).to_value(), Value::I64(99));
359        assert_eq!(None::<i64>.to_value(), Value::Null);
360    }
361
362    #[test]
363    fn to_sql_value_ref_passthrough() {
364        // &T also implements ToSqlValue via blanket impl
365        let n: i64 = 7;
366        assert_eq!(n.to_value(), Value::I64(7));
367        assert_eq!((&"txt").to_value(), Value::Text("txt".into()));
368    }
369
370    // ── FromValue edge-case tests ────────────────────────────────────────────
371
372    #[test]
373    fn from_value_i32_range_check() {
374        // i64 within i32 range succeeds
375        assert_eq!(i32::from_value(&Value::I64(100)).ok(), Some(100i32));
376        // i64 out of i32 range fails
377        assert!(i32::from_value(&Value::I64(i64::MAX)).is_err());
378        assert!(i32::from_value(&Value::I64(i64::MIN)).is_err());
379    }
380
381    #[test]
382    fn from_value_uuid_as_string() {
383        // UUID should format as hyphenated string via String::from_value
384        let uuid_val = Value::Uuid(0u128);
385        let s = String::from_value(&uuid_val).expect("uuid as string");
386        assert!(s.contains('-'), "UUID string should contain hyphens: {s}");
387        assert_eq!(s, "00000000-0000-0000-0000-000000000000");
388    }
389
390    #[test]
391    fn from_value_uuid_nonzero_as_string() {
392        // A known non-zero UUID value
393        let uuid_val = Value::Uuid(0x0102_0304_0506_0708_090a_0b0c_0d0e_0f10u128);
394        let s = String::from_value(&uuid_val).expect("uuid as string");
395        assert!(s.contains('-'), "UUID string should contain hyphens: {s}");
396    }
397
398    #[test]
399    fn from_value_null_non_option_fails() {
400        // Non-Option types should fail on Null
401        assert!(i64::from_value(&Value::Null).is_err());
402        assert!(bool::from_value(&Value::Null).is_err());
403        assert!(String::from_value(&Value::Null).is_err());
404        assert!(f64::from_value(&Value::Null).is_err());
405    }
406
407    #[test]
408    fn from_value_u128_from_uuid() {
409        let val = Value::Uuid(12345678u128);
410        assert_eq!(u128::from_value(&val).ok(), Some(12345678u128));
411    }
412
413    #[test]
414    fn from_value_u128_wrong_type_fails() {
415        assert!(u128::from_value(&Value::I64(1)).is_err());
416    }
417
418    #[test]
419    fn from_value_blob_wrong_type_fails() {
420        assert!(Vec::<u8>::from_value(&Value::Text("x".into())).is_err());
421    }
422
423    #[test]
424    fn from_value_f64_from_i64_boundary() {
425        // Exact integer values round-trip through f64 correctly
426        assert_eq!(f64::from_value(&Value::I64(0)).ok(), Some(0.0f64));
427        assert_eq!(f64::from_value(&Value::I64(-1)).ok(), Some(-1.0f64));
428    }
429
430    // ── QueryBuilder additional tests ────────────────────────────────────────
431
432    #[test]
433    fn select_builder_basic() {
434        let q = SelectBuilder::new()
435            .columns(&["id", "name"])
436            .from("users")
437            .build();
438        assert!(q.sql.contains("SELECT"));
439        assert!(q.sql.contains("users"));
440        assert!(q.sql.contains("id"));
441        assert!(q.sql.contains("name"));
442    }
443
444    #[test]
445    fn select_builder_with_where_eq() {
446        let q = SelectBuilder::new()
447            .from("users")
448            .where_eq("id", &42i64)
449            .build();
450        assert!(q.sql.contains("WHERE"));
451        assert!(!q.params.is_empty());
452        assert_eq!(q.params[0], Value::I64(42));
453    }
454
455    #[test]
456    fn insert_builder_basic() {
457        let q = InsertBuilder::new()
458            .into_table("users")
459            .column("name", &"Alice")
460            .column("age", &30i64)
461            .build();
462        assert!(q.sql.to_uppercase().contains("INSERT"));
463        assert!(q.sql.contains("users"));
464        assert_eq!(q.params.len(), 2);
465    }
466
467    #[test]
468    fn update_builder_basic() {
469        let q = UpdateBuilder::new()
470            .table("users")
471            .set("name", &"Bob")
472            .where_eq("id", &1i64)
473            .build();
474        assert!(q.sql.to_uppercase().contains("UPDATE"));
475        assert!(q.sql.contains("users"));
476        assert_eq!(q.params.len(), 2);
477    }
478
479    #[test]
480    fn delete_builder_basic() {
481        let q = DeleteBuilder::new()
482            .from("users")
483            .where_raw("id = 1")
484            .build();
485        assert!(q.sql.to_uppercase().contains("DELETE"));
486        assert!(q.sql.contains("users"));
487    }
488
489    // ── OxiSqlError Display — all variants ──────────────────────────────────
490
491    #[test]
492    fn oxisql_error_display_all_variants() {
493        let cases: Vec<(OxiSqlError, &str)> = vec![
494            (OxiSqlError::NotConnected, "not connected"),
495            (OxiSqlError::Execution("oops".into()), "oops"),
496            (OxiSqlError::Timeout("30s".into()), "30s"),
497            (OxiSqlError::Parse("bad sql".into()), "bad sql"),
498            (
499                OxiSqlError::ConstraintViolation("unique key".into()),
500                "unique key",
501            ),
502            (OxiSqlError::ConnectionPool("exhausted".into()), "exhausted"),
503            (OxiSqlError::Migration("failed".into()), "failed"),
504            (OxiSqlError::Other("something".into()), "something"),
505            (
506                OxiSqlError::TypeMismatch {
507                    expected: "I64",
508                    got: "Text",
509                },
510                "i64",
511            ),
512        ];
513        for (err, fragment) in cases {
514            let s = err.to_string().to_lowercase();
515            assert!(
516                !s.is_empty(),
517                "error should display non-empty string for {err:?}"
518            );
519            assert!(
520                s.contains(fragment),
521                "expected fragment '{fragment}' not found in error display: '{s}'"
522            );
523        }
524    }
525
526    // ── TypeRegistry tests ───────────────────────────────────────────────────
527
528    #[test]
529    fn type_registry_lookup_standard_types() {
530        let reg = TypeRegistry::new();
531        assert!(reg.lookup("INTEGER").is_some());
532        assert!(reg.lookup("TEXT").is_some());
533        assert!(reg.lookup("BOOLEAN").is_some());
534        assert!(reg.lookup("UUID").is_some());
535        assert!(reg.lookup("TIMESTAMP").is_some());
536        assert!(reg.lookup("DATE").is_some());
537        assert!(reg.lookup("TIME").is_some());
538        assert!(reg.lookup("JSON").is_some());
539        assert!(reg.lookup("DECIMAL").is_some());
540        assert!(reg.lookup("NONEXISTENT_TYPE").is_none());
541    }
542
543    #[test]
544    fn type_registry_lookup_case_insensitive() {
545        let reg = TypeRegistry::new();
546        assert!(reg.lookup("integer").is_some());
547        assert!(reg.lookup("Integer").is_some());
548        assert!(reg.lookup("TEXT").is_some());
549        assert!(reg.lookup("text").is_some());
550    }
551
552    #[test]
553    fn type_registry_lookup_aliases() {
554        let reg = TypeRegistry::new();
555        // INT, INT4 → Integer
556        assert_eq!(reg.lookup("INT"), Some(&SqlType::Integer));
557        assert_eq!(reg.lookup("INT4"), Some(&SqlType::Integer));
558        // INT8, BIGINT → BigInt
559        assert_eq!(reg.lookup("BIGINT"), Some(&SqlType::BigInt));
560        assert_eq!(reg.lookup("INT8"), Some(&SqlType::BigInt));
561        // INT2, SMALLINT → SmallInt
562        assert_eq!(reg.lookup("SMALLINT"), Some(&SqlType::SmallInt));
563        assert_eq!(reg.lookup("INT2"), Some(&SqlType::SmallInt));
564        // FLOAT, FLOAT4, REAL → Float
565        assert_eq!(reg.lookup("FLOAT"), Some(&SqlType::Float));
566        assert_eq!(reg.lookup("REAL"), Some(&SqlType::Float));
567        // DOUBLE, FLOAT8 → Double
568        assert_eq!(reg.lookup("DOUBLE"), Some(&SqlType::Double));
569        assert_eq!(reg.lookup("FLOAT8"), Some(&SqlType::Double));
570        // NUMERIC → Decimal
571        assert_eq!(reg.lookup("NUMERIC"), Some(&SqlType::Decimal));
572        // BOOL → Boolean
573        assert_eq!(reg.lookup("BOOL"), Some(&SqlType::Boolean));
574        // JSONB → Json
575        assert_eq!(reg.lookup("JSONB"), Some(&SqlType::Json));
576        // BYTEA, BLOB → Blob
577        assert_eq!(reg.lookup("BYTEA"), Some(&SqlType::Blob));
578        assert_eq!(reg.lookup("BLOB"), Some(&SqlType::Blob));
579        // TIMESTAMPTZ → Timestamp
580        assert_eq!(reg.lookup("TIMESTAMPTZ"), Some(&SqlType::Timestamp));
581        // TIMETZ → Time
582        assert_eq!(reg.lookup("TIMETZ"), Some(&SqlType::Time));
583    }
584
585    #[test]
586    fn type_registry_default_values() {
587        let reg = TypeRegistry::new();
588        assert_eq!(reg.default_value_for("INTEGER"), Value::I64(0));
589        assert_eq!(reg.default_value_for("BIGINT"), Value::I64(0));
590        assert_eq!(reg.default_value_for("SMALLINT"), Value::I64(0));
591        assert_eq!(reg.default_value_for("TEXT"), Value::Text(String::new()));
592        assert_eq!(reg.default_value_for("VARCHAR"), Value::Text(String::new()));
593        assert_eq!(reg.default_value_for("BOOLEAN"), Value::Bool(false));
594        assert_eq!(reg.default_value_for("TIMESTAMP"), Value::Timestamp(0));
595        assert_eq!(reg.default_value_for("DATE"), Value::Date(0));
596        assert_eq!(reg.default_value_for("TIME"), Value::Time(0));
597        assert_eq!(reg.default_value_for("UUID"), Value::Uuid(0));
598        assert_eq!(reg.default_value_for("JSON"), Value::Json("{}".into()));
599        assert_eq!(reg.default_value_for("DECIMAL"), Value::Decimal("0".into()));
600        assert_eq!(reg.default_value_for("BYTEA"), Value::Blob(Vec::new()));
601        // Unknown type → Null
602        assert_eq!(reg.default_value_for("UNKNOWN_TYPE"), Value::Null);
603    }
604
605    #[test]
606    fn type_registry_register_custom() {
607        let mut reg = TypeRegistry::new();
608        reg.register("MY_CUSTOM_TYPE", SqlType::Text);
609        assert!(reg.lookup("MY_CUSTOM_TYPE").is_some());
610        // Case-insensitive
611        assert!(reg.lookup("my_custom_type").is_some());
612    }
613
614    #[test]
615    fn type_registry_register_overrides_existing() {
616        let mut reg = TypeRegistry::new();
617        // Overwrite INTEGER to map to BigInt
618        reg.register("INTEGER", SqlType::BigInt);
619        assert_eq!(reg.lookup("INTEGER"), Some(&SqlType::BigInt));
620    }
621
622    #[test]
623    fn type_registry_default() {
624        // Default impl delegates to new()
625        let reg = TypeRegistry::default();
626        assert!(reg.lookup("TEXT").is_some());
627    }
628
629    #[test]
630    fn value_matches_type_basic() {
631        assert!(TypeRegistry::value_matches_type(
632            &Value::I64(1),
633            &SqlType::Integer
634        ));
635        assert!(TypeRegistry::value_matches_type(
636            &Value::I64(1),
637            &SqlType::BigInt
638        ));
639        assert!(TypeRegistry::value_matches_type(
640            &Value::I64(1),
641            &SqlType::SmallInt
642        ));
643        assert!(TypeRegistry::value_matches_type(
644            &Value::F64(1.0),
645            &SqlType::Float
646        ));
647        assert!(TypeRegistry::value_matches_type(
648            &Value::F64(1.0),
649            &SqlType::Double
650        ));
651        assert!(TypeRegistry::value_matches_type(
652            &Value::Text("x".into()),
653            &SqlType::Text
654        ));
655        assert!(TypeRegistry::value_matches_type(
656            &Value::Text("x".into()),
657            &SqlType::VarChar(Some(255))
658        ));
659        assert!(TypeRegistry::value_matches_type(
660            &Value::Bool(true),
661            &SqlType::Boolean
662        ));
663        assert!(TypeRegistry::value_matches_type(
664            &Value::Uuid(0),
665            &SqlType::Uuid
666        ));
667        assert!(TypeRegistry::value_matches_type(
668            &Value::Json("{}".into()),
669            &SqlType::Json
670        ));
671        assert!(TypeRegistry::value_matches_type(
672            &Value::Decimal("1.5".into()),
673            &SqlType::Decimal
674        ));
675        assert!(TypeRegistry::value_matches_type(
676            &Value::Blob(vec![]),
677            &SqlType::Blob
678        ));
679        assert!(TypeRegistry::value_matches_type(
680            &Value::Timestamp(0),
681            &SqlType::Timestamp
682        ));
683        assert!(TypeRegistry::value_matches_type(
684            &Value::Date(0),
685            &SqlType::Date
686        ));
687        assert!(TypeRegistry::value_matches_type(
688            &Value::Time(0),
689            &SqlType::Time
690        ));
691        assert!(TypeRegistry::value_matches_type(
692            &Value::Array(vec![]),
693            &SqlType::Array(Box::new(SqlType::Integer))
694        ));
695    }
696
697    #[test]
698    fn value_matches_type_mismatches() {
699        assert!(!TypeRegistry::value_matches_type(
700            &Value::Text("x".into()),
701            &SqlType::Integer
702        ));
703        assert!(!TypeRegistry::value_matches_type(
704            &Value::I64(1),
705            &SqlType::Text
706        ));
707        assert!(!TypeRegistry::value_matches_type(
708            &Value::Bool(true),
709            &SqlType::Integer
710        ));
711        assert!(!TypeRegistry::value_matches_type(
712            &Value::F64(1.0),
713            &SqlType::Integer
714        ));
715    }
716
717    #[test]
718    fn value_matches_type_null_valid_for_any() {
719        // NULL is valid for any SQL type
720        assert!(TypeRegistry::value_matches_type(
721            &Value::Null,
722            &SqlType::Integer
723        ));
724        assert!(TypeRegistry::value_matches_type(
725            &Value::Null,
726            &SqlType::Text
727        ));
728        assert!(TypeRegistry::value_matches_type(
729            &Value::Null,
730            &SqlType::Boolean
731        ));
732        assert!(TypeRegistry::value_matches_type(
733            &Value::Null,
734            &SqlType::Uuid
735        ));
736    }
737
738    #[test]
739    fn sql_type_as_sql_name() {
740        assert_eq!(SqlType::Integer.as_sql_name(), "INTEGER");
741        assert_eq!(SqlType::BigInt.as_sql_name(), "BIGINT");
742        assert_eq!(SqlType::SmallInt.as_sql_name(), "SMALLINT");
743        assert_eq!(SqlType::Float.as_sql_name(), "REAL");
744        assert_eq!(SqlType::Double.as_sql_name(), "DOUBLE PRECISION");
745        assert_eq!(SqlType::Decimal.as_sql_name(), "DECIMAL");
746        assert_eq!(SqlType::Text.as_sql_name(), "TEXT");
747        assert_eq!(SqlType::VarChar(None).as_sql_name(), "VARCHAR");
748        assert_eq!(SqlType::VarChar(Some(255)).as_sql_name(), "VARCHAR(255)");
749        assert_eq!(SqlType::Blob.as_sql_name(), "BYTEA");
750        assert_eq!(SqlType::Boolean.as_sql_name(), "BOOLEAN");
751        assert_eq!(SqlType::Timestamp.as_sql_name(), "TIMESTAMP");
752        assert_eq!(SqlType::Date.as_sql_name(), "DATE");
753        assert_eq!(SqlType::Time.as_sql_name(), "TIME");
754        assert_eq!(SqlType::Uuid.as_sql_name(), "UUID");
755        assert_eq!(SqlType::Json.as_sql_name(), "JSON");
756        assert_eq!(
757            SqlType::Array(Box::new(SqlType::Integer)).as_sql_name(),
758            "INTEGER[]"
759        );
760        assert_eq!(SqlType::Unknown("MYTYPE".into()).as_sql_name(), "MYTYPE");
761    }
762
763    #[test]
764    fn sql_type_default_value_array_and_unknown() {
765        assert_eq!(
766            SqlType::Array(Box::new(SqlType::Integer)).default_value(),
767            Value::Array(Vec::new())
768        );
769        assert_eq!(
770            SqlType::Unknown("CUSTOM".into()).default_value(),
771            Value::Null
772        );
773    }
774
775    // ── Cursor tests ─────────────────────────────────────────────────────────
776
777    #[test]
778    fn cursor_basic_traversal() {
779        let rows = vec![
780            Row::new(vec!["id".into()], vec![Value::I64(1)]),
781            Row::new(vec!["id".into()], vec![Value::I64(2)]),
782        ];
783        let mut cursor = Cursor::new(rows);
784        assert_eq!(cursor.len(), 2);
785        assert_eq!(cursor.remaining(), 2);
786        assert!(!cursor.is_empty());
787
788        let r1 = cursor.advance().expect("first row");
789        assert_eq!(r1.try_get::<i64>("id").unwrap(), 1);
790        assert_eq!(cursor.position(), 1);
791        assert_eq!(cursor.remaining(), 1);
792
793        let r2 = cursor.advance().expect("second row");
794        assert_eq!(r2.try_get::<i64>("id").unwrap(), 2);
795        assert_eq!(cursor.position(), 2);
796        assert_eq!(cursor.remaining(), 0);
797
798        assert!(cursor.advance().is_none());
799    }
800
801    #[test]
802    fn cursor_peek_does_not_advance() {
803        let rows = vec![Row::new(vec!["x".into()], vec![Value::I64(42)])];
804        let cursor = Cursor::new(rows);
805        let peeked = cursor.peek().expect("peek first row");
806        assert_eq!(peeked.try_get::<i64>("x").unwrap(), 42);
807        // position must still be 0
808        assert_eq!(cursor.position(), 0);
809    }
810
811    #[test]
812    fn cursor_reset() {
813        let rows = vec![Row::new(vec!["x".into()], vec![Value::I64(1)])];
814        let mut cursor = Cursor::new(rows);
815        cursor.advance();
816        assert_eq!(cursor.position(), 1);
817        cursor.reset();
818        assert_eq!(cursor.position(), 0);
819        assert_eq!(cursor.remaining(), 1);
820        assert!(cursor.advance().is_some());
821    }
822
823    #[test]
824    fn cursor_skip_by() {
825        let rows: Vec<Row> = (0..5)
826            .map(|i| Row::new(vec!["n".into()], vec![Value::I64(i)]))
827            .collect();
828        let mut cursor = Cursor::new(rows);
829        cursor.skip_by(3);
830        assert_eq!(cursor.position(), 3);
831        assert_eq!(cursor.remaining(), 2);
832        // skip_by past end is clamped
833        cursor.skip_by(100);
834        assert_eq!(cursor.position(), 5);
835        assert_eq!(cursor.remaining(), 0);
836        assert!(cursor.advance().is_none());
837    }
838
839    #[test]
840    fn cursor_into_rows_recovers_all() {
841        let rows = vec![
842            Row::new(vec!["v".into()], vec![Value::I64(10)]),
843            Row::new(vec!["v".into()], vec![Value::I64(20)]),
844        ];
845        let mut cursor = Cursor::new(rows.clone());
846        cursor.advance(); // consume one
847        let recovered = cursor.into_rows();
848        // into_rows returns the full underlying vec, not just remaining
849        assert_eq!(recovered.len(), 2);
850    }
851
852    #[test]
853    fn cursor_empty() {
854        let mut cursor = Cursor::new(vec![]);
855        assert!(cursor.is_empty());
856        assert_eq!(cursor.len(), 0);
857        assert_eq!(cursor.remaining(), 0);
858        assert!(cursor.peek().is_none());
859        assert!(cursor.advance().is_none());
860    }
861
862    #[test]
863    fn cursor_iterator_yields_owned_rows() {
864        let rows: Vec<Row> = (1..=3)
865            .map(|i| Row::new(vec!["i".into()], vec![Value::I64(i)]))
866            .collect();
867        let cursor = Cursor::new(rows);
868        let collected: Vec<Row> = cursor.collect();
869        assert_eq!(collected.len(), 3);
870        assert_eq!(collected[0].try_get::<i64>("i").unwrap(), 1);
871        assert_eq!(collected[2].try_get::<i64>("i").unwrap(), 3);
872    }
873
874    #[test]
875    fn cursor_iterator_and_advance_independent() {
876        // Iterator::next and advance() both advance the same position counter.
877        let rows = vec![
878            Row::new(vec!["k".into()], vec![Value::I64(10)]),
879            Row::new(vec!["k".into()], vec![Value::I64(20)]),
880            Row::new(vec!["k".into()], vec![Value::I64(30)]),
881        ];
882        let mut cursor = Cursor::new(rows);
883        // Consume first via Iterator
884        let owned = Iterator::next(&mut cursor).expect("first via Iterator");
885        assert_eq!(owned.try_get::<i64>("k").unwrap(), 10);
886        // Consume second via advance (borrow)
887        let borrowed = cursor.advance().expect("second via advance");
888        assert_eq!(borrowed.try_get::<i64>("k").unwrap(), 20);
889        // Only the third should remain
890        assert_eq!(cursor.remaining(), 1);
891    }
892}