Skip to main content

nodedb_types/columnar/
column_type.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! ColumnType — the atomic value type for typed schemas.
4
5use serde::{Deserialize, Serialize};
6
7use super::decimal_typmod::DecimalTypmod;
8use crate::InstantKind;
9use crate::value::Value;
10
11/// Typed column definition for strict document and columnar collections.
12///
13/// `#[non_exhaustive]` — this enum grows with each type system expansion.
14/// External exhaustive `match` arms must handle future variants via a typed
15/// error arm rather than `_ => unreachable!()`.
16#[non_exhaustive]
17#[derive(
18    Debug,
19    Clone,
20    Copy,
21    PartialEq,
22    Eq,
23    Hash,
24    Serialize,
25    Deserialize,
26    zerompk::ToMessagePack,
27    zerompk::FromMessagePack,
28)]
29#[serde(tag = "type", content = "params")]
30pub enum ColumnType {
31    Int64,
32    Float64,
33    String,
34    Bool,
35    Bytes,
36    /// Naive (no-timezone) timestamp with microsecond precision. OID 1114.
37    Timestamp,
38    /// UTC (timezone-aware) timestamp with microsecond precision. OID 1184.
39    Timestamptz,
40    /// System-assigned timestamp (bitemporal `system_from_ms`). Same 8-byte
41    /// layout as `Timestamp`, but tagged distinctly so the planner and DDL
42    /// layer can reject user-supplied values — the column is populated by the
43    /// engine from HLC at commit.
44    SystemTimestamp,
45    /// Exact decimal. `Some` carries the declared `DECIMAL(p,s)` typmod, and
46    /// every value is fitted to it. `None` is a plain `DECIMAL`, and its
47    /// values are stored as given.
48    Decimal(Option<DecimalTypmod>),
49    Geometry,
50    /// Fixed-dimension float32 vector.
51    Vector(u32),
52    /// Dimensionless sparse vector (term-id → weight map). Variable-length:
53    /// a value is a quoted string literal like `'{12: 0.5, 88: 0.3}'`, stored
54    /// as inline UTF-8 bytes and parsed at index-build time. The sparse index
55    /// carries no dimension, so unlike `Vector(u32)` this type takes no `(N)`.
56    SparseVector,
57    Uuid,
58    /// Arbitrary nested data stored as inline MessagePack.
59    /// Variable-length. Accepts any Value type.
60    Json,
61    /// ULID: 16-byte Crockford Base32-encoded sortable ID.
62    Ulid,
63    /// Duration: signed microsecond precision (i64 internally).
64    Duration,
65    /// Ordered array of values. Variable-length, inline MessagePack.
66    Array,
67    /// Ordered set (auto-deduplicated). Variable-length, inline MessagePack.
68    Set,
69    /// Compiled regex pattern. Stored as string internally.
70    Regex,
71    /// Bounded range of values. Variable-length, inline MessagePack.
72    Range,
73    /// Typed reference to another record (`table:id`). Variable-length, inline MessagePack.
74    Record,
75}
76
77impl ColumnType {
78    /// Whether this type has a fixed byte size in Binary Tuple layout.
79    pub fn fixed_size(&self) -> Option<usize> {
80        match self {
81            Self::Int64
82            | Self::Float64
83            | Self::Timestamp
84            | Self::Timestamptz
85            | Self::SystemTimestamp
86            | Self::Duration => Some(8),
87            Self::Bool => Some(1),
88            Self::Decimal { .. } | Self::Uuid | Self::Ulid => Some(16),
89            Self::Vector(dim) => Some(*dim as usize * 4),
90            Self::String
91            | Self::Bytes
92            | Self::Geometry
93            | Self::Json
94            | Self::SparseVector
95            | Self::Array
96            | Self::Set
97            | Self::Regex
98            | Self::Range
99            | Self::Record => None,
100        }
101    }
102
103    /// Whether this type is variable-length (requires offset table entry).
104    pub fn is_variable_length(&self) -> bool {
105        self.fixed_size().is_none()
106    }
107
108    /// Whether a column of this type carries an instant.
109    ///
110    /// This is the single answer to that question for both planes: the
111    /// Control Plane types such a column as a timestamp on the wire, and the
112    /// Data Plane scales exactly these columns from the epoch milliseconds
113    /// storage holds to the epoch microseconds a client reads.
114    ///
115    /// `SystemTimestamp` is not an instant. It is engine-assigned from HLC at
116    /// commit and the planner types it as text.
117    pub const fn is_instant(&self) -> bool {
118        self.instant_kind().is_some()
119    }
120
121    /// The instant kind a column of this type carries: `Naive` for
122    /// `Timestamp`, `Utc` for `Timestamptz`, `None` for every other type.
123    ///
124    /// A stored time cell is wrapped as `Value::NaiveDateTime` or
125    /// `Value::DateTime` by this kind, so a reader never has to decide which
126    /// variant a column means. `SystemTimestamp` and `Duration` share the
127    /// eight-byte time storage but are not instants: their cells read back as
128    /// the integer stored.
129    pub const fn instant_kind(&self) -> Option<InstantKind> {
130        match self {
131            Self::Timestamp => Some(InstantKind::Naive),
132            Self::Timestamptz => Some(InstantKind::Utc),
133            Self::Int64
134            | Self::Float64
135            | Self::String
136            | Self::Bool
137            | Self::Bytes
138            | Self::SystemTimestamp
139            | Self::Decimal { .. }
140            | Self::Geometry
141            | Self::Vector(_)
142            | Self::SparseVector
143            | Self::Uuid
144            | Self::Json
145            | Self::Ulid
146            | Self::Duration
147            | Self::Array
148            | Self::Set
149            | Self::Regex
150            | Self::Range
151            | Self::Record => None,
152        }
153    }
154
155    /// Type a stored time cell by this declared type.
156    ///
157    /// An instant column (`Timestamp`, `Timestamptz`) wraps `micros` as its
158    /// `instant_kind()` variant; every other declared type backed by
159    /// eight-byte time storage (`SystemTimestamp`, `Duration`) returns the
160    /// integer stored. The single mapping every segment-cell and memtable-cell
161    /// reader uses, so a row reads identically whether the reader inferred the
162    /// column's physical kind as `Int64` or `Timestamp`.
163    pub fn time_cell(&self, micros: i64) -> Value {
164        match self.instant_kind() {
165            Some(kind) => kind.from_micros(micros),
166            None => Value::Integer(micros),
167        }
168    }
169
170    /// Return the canonical PostgreSQL type OID for this column type.
171    ///
172    /// This is the single authoritative mapping between NodeDB `ColumnType`
173    /// variants and PostgreSQL wire-protocol OIDs. All pgwire code must derive
174    /// OIDs from this method — no local string-matching tables.
175    ///
176    /// Choices for non-native types:
177    /// - `Geometry` → `25` (TEXT): no standard pg geometry OID; PostGIS uses
178    ///   its own extension OID which we cannot claim. TEXT lets clients at least
179    ///   see the WKT/WKB string.
180    /// - `Vector(_)` → `1021` (FLOAT4_ARRAY): closest built-in pg type for a
181    ///   fixed-dimension float32 vector; pgvector uses a custom OID, which we
182    ///   avoid to stay dependency-free.
183    /// - `Array`, `Set`, `Range`, `Record`, `Regex` → `114` (JSON): these are
184    ///   variable-length MessagePack-encoded structures; JSON is the safest
185    ///   generic text OID for clients that need to read the value as a string.
186    pub fn to_pg_oid(&self) -> u32 {
187        match self {
188            Self::Bool => 16,
189            Self::Bytes => 17,
190            Self::Int64 => 20,
191            Self::Float64 => 701,
192            Self::String => 25,
193            Self::Timestamp | Self::SystemTimestamp => 1114,
194            Self::Timestamptz => 1184,
195            Self::Decimal { .. } => 1700,
196            Self::Uuid | Self::Ulid => 2950,
197            Self::Json => 3802,
198            Self::Duration => 1186,
199            // No standard built-in OID for geometry; TEXT lets clients read WKT.
200            Self::Geometry => 25,
201            // FLOAT4_ARRAY (1021) is the closest built-in for fixed float32 vectors.
202            Self::Vector(_) => 1021,
203            // Variable-length structured types: expose as JSONB so clients can
204            // parse the serialized representation. `SparseVector` groups here —
205            // its `'{id: weight}'` literal reads naturally as a JSON object.
206            Self::Array
207            | Self::Set
208            | Self::Range
209            | Self::Record
210            | Self::Regex
211            | Self::SparseVector => 3802,
212        }
213    }
214
215    /// Whether a `Value` is compatible with this column type.
216    ///
217    /// Accepts both native Value types (e.g., `Value::DateTime` for Timestamp)
218    /// AND coercion sources from SQL input (e.g., `Value::String` for Timestamp).
219    /// Null is accepted for any type — nullability is enforced at schema level.
220    pub fn accepts(&self, value: &Value) -> bool {
221        matches!(
222            (self, value),
223            (Self::Int64, Value::Integer(_))
224                | (Self::Float64, Value::Float(_) | Value::Integer(_))
225                | (Self::String, Value::String(_))
226                | (Self::Bool, Value::Bool(_))
227                | (Self::Bytes, Value::Bytes(_))
228                | (
229                    Self::Timestamp,
230                    Value::NaiveDateTime(_) | Value::Integer(_) | Value::String(_)
231                )
232                | (
233                    Self::Timestamptz,
234                    Value::DateTime(_) | Value::Integer(_) | Value::String(_)
235                )
236                | (
237                    Self::SystemTimestamp,
238                    Value::DateTime(_) | Value::Integer(_)
239                )
240                | (
241                    Self::Decimal { .. },
242                    Value::Decimal(_) | Value::String(_) | Value::Float(_) | Value::Integer(_)
243                )
244                | (Self::Geometry, Value::Geometry(_) | Value::String(_))
245                | (Self::Vector(_), Value::Array(_) | Value::Bytes(_))
246                | (Self::Uuid, Value::Uuid(_) | Value::String(_))
247                | (Self::Ulid, Value::Ulid(_) | Value::String(_))
248                | (
249                    Self::Duration,
250                    Value::Duration(_) | Value::Integer(_) | Value::String(_)
251                )
252                | (Self::Array, Value::Array(_))
253                | (Self::Set, Value::Set(_) | Value::Array(_))
254                | (Self::Regex, Value::Regex(_) | Value::String(_))
255                | (Self::Range, Value::Range { .. })
256                | (Self::Record, Value::Record { .. } | Value::String(_))
257                | (Self::SparseVector, Value::String(_) | Value::Bytes(_))
258                | (Self::Json, _)
259                | (_, Value::Null)
260        )
261    }
262}
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267
268    fn nodedb_types_datetime_epoch() -> crate::datetime::NdbDateTime {
269        crate::datetime::NdbDateTime::from_micros(0)
270    }
271
272    fn decimal(precision: i64, scale: i64) -> ColumnType {
273        ColumnType::Decimal(Some(
274            DecimalTypmod::new(precision, scale).expect("test typmod is valid"),
275        ))
276    }
277
278    #[test]
279    fn instant_kind_names_the_variant_a_time_column_reads_back_as() {
280        assert_eq!(
281            ColumnType::Timestamp.instant_kind(),
282            Some(InstantKind::Naive)
283        );
284        assert_eq!(
285            ColumnType::Timestamptz.instant_kind(),
286            Some(InstantKind::Utc)
287        );
288        assert_eq!(ColumnType::SystemTimestamp.instant_kind(), None);
289        assert_eq!(ColumnType::Duration.instant_kind(), None);
290        assert_eq!(ColumnType::Int64.instant_kind(), None);
291        assert!(ColumnType::Timestamp.is_instant());
292        assert!(!ColumnType::SystemTimestamp.is_instant());
293    }
294
295    #[test]
296    fn to_pg_oid_stable() {
297        assert_eq!(ColumnType::Bool.to_pg_oid(), 16);
298        assert_eq!(ColumnType::Bytes.to_pg_oid(), 17);
299        assert_eq!(ColumnType::Int64.to_pg_oid(), 20);
300        assert_eq!(ColumnType::String.to_pg_oid(), 25);
301        assert_eq!(ColumnType::Float64.to_pg_oid(), 701);
302        assert_eq!(ColumnType::Timestamp.to_pg_oid(), 1114);
303        assert_eq!(ColumnType::Timestamptz.to_pg_oid(), 1184);
304        assert_eq!(ColumnType::SystemTimestamp.to_pg_oid(), 1114);
305        assert_eq!(ColumnType::Duration.to_pg_oid(), 1186);
306        assert_eq!(decimal(28, 10).to_pg_oid(), 1700);
307        assert_eq!(ColumnType::Decimal(None).to_pg_oid(), 1700);
308        assert_eq!(ColumnType::Uuid.to_pg_oid(), 2950);
309        assert_eq!(ColumnType::Ulid.to_pg_oid(), 2950);
310        assert_eq!(ColumnType::Json.to_pg_oid(), 3802);
311        assert_eq!(ColumnType::Geometry.to_pg_oid(), 25);
312        assert_eq!(ColumnType::Vector(768).to_pg_oid(), 1021);
313        assert_eq!(ColumnType::Array.to_pg_oid(), 3802);
314        assert_eq!(ColumnType::Set.to_pg_oid(), 3802);
315        assert_eq!(ColumnType::Range.to_pg_oid(), 3802);
316        assert_eq!(ColumnType::Record.to_pg_oid(), 3802);
317        assert_eq!(ColumnType::Regex.to_pg_oid(), 3802);
318    }
319
320    #[test]
321    fn parse_system_timestamp() {
322        assert_eq!(
323            "SYSTEM_TIMESTAMP".parse::<ColumnType>().unwrap(),
324            ColumnType::SystemTimestamp
325        );
326        assert_eq!(
327            "SystemTimestamp".parse::<ColumnType>().unwrap(),
328            ColumnType::SystemTimestamp
329        );
330        assert_eq!(ColumnType::SystemTimestamp.fixed_size(), Some(8));
331        assert!(!ColumnType::SystemTimestamp.is_variable_length());
332        assert_eq!(ColumnType::SystemTimestamp.to_string(), "SYSTEM_TIMESTAMP");
333        assert!(!ColumnType::SystemTimestamp.accepts(&Value::String("2024-01-01".into())));
334        assert!(ColumnType::SystemTimestamp.accepts(&Value::Integer(1_700_000_000)));
335    }
336
337    #[test]
338    fn parse_canonical() {
339        assert_eq!("BIGINT".parse::<ColumnType>().unwrap(), ColumnType::Int64);
340        assert_eq!(
341            "FLOAT64".parse::<ColumnType>().unwrap(),
342            ColumnType::Float64
343        );
344        assert_eq!("TEXT".parse::<ColumnType>().unwrap(), ColumnType::String);
345        assert_eq!("BOOL".parse::<ColumnType>().unwrap(), ColumnType::Bool);
346        assert_eq!(
347            "TIMESTAMP".parse::<ColumnType>().unwrap(),
348            ColumnType::Timestamp
349        );
350        assert_eq!(
351            "TIMESTAMPTZ".parse::<ColumnType>().unwrap(),
352            ColumnType::Timestamptz
353        );
354        assert_eq!(
355            "TIMESTAMP WITH TIME ZONE".parse::<ColumnType>().unwrap(),
356            ColumnType::Timestamptz
357        );
358        assert_eq!(
359            "GEOMETRY".parse::<ColumnType>().unwrap(),
360            ColumnType::Geometry
361        );
362        assert_eq!("UUID".parse::<ColumnType>().unwrap(), ColumnType::Uuid);
363    }
364
365    /// `INT4`/`INT8`/`SMALLINT`/`INT2` are wire-width DDL keywords: strict/kv
366    /// `CREATE COLLECTION` must accept them as valid PostgreSQL integer
367    /// aliases, not reject them as unknown column types. They all
368    /// map to the same `Int64` storage variant as `BIGINT`/`INTEGER`/`INT` —
369    /// nodedb's columnar/strict/kv storage always keeps integers as a full
370    /// i64; only the wire (`DdlColType`) layer narrows the advertised OID.
371    #[test]
372    fn parse_int_width_aliases_map_to_int64() {
373        assert_eq!("INT4".parse::<ColumnType>().unwrap(), ColumnType::Int64);
374        assert_eq!("INT8".parse::<ColumnType>().unwrap(), ColumnType::Int64);
375        assert_eq!("SMALLINT".parse::<ColumnType>().unwrap(), ColumnType::Int64);
376        assert_eq!("INT2".parse::<ColumnType>().unwrap(), ColumnType::Int64);
377    }
378
379    /// The float analogue of [`parse_int_width_aliases_map_to_int64`]:
380    /// `FLOAT4`/`FLOAT8`/`FLOAT32`/`DOUBLE PRECISION` are valid PostgreSQL
381    /// float aliases that were rejected as unknown column types. They all map
382    /// to the same `Float64` storage variant — nodedb always keeps floats as a
383    /// full f64; only the wire (`DdlColType`) layer narrows the advertised OID
384    /// via `FloatWidth`.
385    ///
386    /// Every spelling `FloatWidth::from_declared_type` recognizes must appear
387    /// here, or DDL accepts a width the wire layer honours and vice versa.
388    #[test]
389    fn parse_float_width_aliases_map_to_float64() {
390        for declared in [
391            "FLOAT4",
392            "FLOAT8",
393            "FLOAT32",
394            "FLOAT64",
395            "REAL",
396            "DOUBLE",
397            "DOUBLE PRECISION",
398            "FLOAT",
399        ] {
400            assert_eq!(
401                declared.parse::<ColumnType>().unwrap(),
402                ColumnType::Float64,
403                "{declared} must resolve to the single Float64 storage variant"
404            );
405            assert!(
406                super::super::FloatWidth::from_declared_type(declared).is_some(),
407                "{declared} must also resolve to a declared FloatWidth"
408            );
409        }
410    }
411
412    #[test]
413    fn parse_vector() {
414        assert_eq!(
415            "VECTOR(768)".parse::<ColumnType>().unwrap(),
416            ColumnType::Vector(768)
417        );
418        assert!("VECTOR(0)".parse::<ColumnType>().is_err());
419    }
420
421    #[test]
422    fn sparsevector_is_variable_length_string_type() {
423        // Dimensionless: parses from the bare keyword and round-trips via Display.
424        let parsed = "SPARSEVECTOR".parse::<ColumnType>().unwrap();
425        assert_eq!(parsed, ColumnType::SparseVector);
426        assert_eq!(ColumnType::SparseVector.to_string(), "SPARSEVECTOR");
427        assert_eq!(
428            ColumnType::SparseVector
429                .to_string()
430                .parse::<ColumnType>()
431                .unwrap(),
432            ColumnType::SparseVector
433        );
434
435        // Variable-length (no fixed byte size), JSONB pg oid like the other
436        // variable-length structured types.
437        assert_eq!(ColumnType::SparseVector.fixed_size(), None);
438        assert!(ColumnType::SparseVector.is_variable_length());
439        assert_eq!(ColumnType::SparseVector.to_pg_oid(), 3802);
440
441        // Accepts the quoted string literal (and raw bytes); rejects numerics.
442        assert!(ColumnType::SparseVector.accepts(&Value::String("{3:0.5}".into())));
443        assert!(ColumnType::SparseVector.accepts(&Value::Bytes(vec![1, 2, 3])));
444        assert!(ColumnType::SparseVector.accepts(&Value::Null));
445        assert!(!ColumnType::SparseVector.accepts(&Value::Integer(1)));
446    }
447
448    #[test]
449    fn display_roundtrip() {
450        for ct in [
451            ColumnType::Int64,
452            ColumnType::Float64,
453            ColumnType::String,
454            ColumnType::Timestamp,
455            ColumnType::Timestamptz,
456            ColumnType::Vector(768),
457            decimal(10, 2),
458            decimal(28, 10),
459            ColumnType::Decimal(None),
460        ] {
461            let s = ct.to_string();
462            let parsed: ColumnType = s.parse().unwrap();
463            assert_eq!(parsed, ct);
464        }
465    }
466
467    #[test]
468    fn decimal_parse_with_params() {
469        assert_eq!(
470            "NUMERIC(10,2)".parse::<ColumnType>().unwrap(),
471            decimal(10, 2)
472        );
473        assert_eq!(
474            "DECIMAL(28,10)".parse::<ColumnType>().unwrap(),
475            decimal(28, 10)
476        );
477        assert_eq!("DECIMAL(7)".parse::<ColumnType>().unwrap(), decimal(7, 0));
478        assert_eq!(
479            "NUMERIC".parse::<ColumnType>().unwrap(),
480            ColumnType::Decimal(None)
481        );
482        assert_eq!(
483            "DECIMAL".parse::<ColumnType>().unwrap(),
484            ColumnType::Decimal(None)
485        );
486        assert_eq!(ColumnType::Decimal(None).to_string(), "DECIMAL");
487        assert_eq!(decimal(5, 2).to_string(), "DECIMAL(5,2)");
488    }
489
490    #[test]
491    fn decimal_parse_invalid() {
492        use super::super::column_parse::ColumnTypeParseError;
493        use super::super::decimal_typmod::DecimalTypmodError;
494
495        assert_eq!(
496            "DECIMAL(5,6)".parse::<ColumnType>(),
497            Err(ColumnTypeParseError::InvalidDecimalTypmod(
498                DecimalTypmodError::ScaleOutOfRange {
499                    precision: 5,
500                    scale: 6
501                }
502            ))
503        );
504        assert_eq!(
505            "DECIMAL(0,0)".parse::<ColumnType>(),
506            Err(ColumnTypeParseError::InvalidDecimalTypmod(
507                DecimalTypmodError::PrecisionOutOfRange { precision: 0 }
508            ))
509        );
510        assert_eq!(
511            "DECIMAL(1001,0)".parse::<ColumnType>(),
512            Err(ColumnTypeParseError::InvalidDecimalTypmod(
513                DecimalTypmodError::PrecisionOutOfRange { precision: 1001 }
514            ))
515        );
516        assert_eq!(
517            "DECIMAL(39,0)".parse::<ColumnType>(),
518            Err(ColumnTypeParseError::InvalidDecimalTypmod(
519                DecimalTypmodError::PrecisionUnsupported { precision: 39 }
520            ))
521        );
522        for malformed in ["DECIMAL()", "DECIMAL(a,2)", "DECIMAL(5,2,1)", "NUMERIC(5"] {
523            assert!(
524                matches!(
525                    malformed.parse::<ColumnType>(),
526                    Err(ColumnTypeParseError::InvalidDecimalParams(_))
527                ),
528                "{malformed}"
529            );
530        }
531        assert_eq!(
532            "NUMERIC_MONEY".parse::<ColumnType>(),
533            Err(ColumnTypeParseError::Unknown("NUMERIC_MONEY".into()))
534        );
535    }
536
537    #[test]
538    fn decimal_fixed_size() {
539        assert_eq!(decimal(10, 2).fixed_size(), Some(16));
540        assert_eq!(ColumnType::Decimal(None).fixed_size(), Some(16));
541    }
542
543    #[test]
544    fn decimal_to_pg_oid_is_1700() {
545        assert_eq!(decimal(10, 2).to_pg_oid(), 1700);
546    }
547
548    #[test]
549    fn decimal_wire_forms_roundtrip() {
550        for ct in [decimal(10, 2), ColumnType::Decimal(None)] {
551            let bytes = zerompk::to_msgpack_vec(&ct).expect("encode");
552            let back: ColumnType = zerompk::from_msgpack(&bytes).expect("decode");
553            assert_eq!(back, ct);
554            let json = serde_json::to_string(&ct).expect("encode json");
555            let back: ColumnType = serde_json::from_str(&json).expect("decode json");
556            assert_eq!(back, ct);
557        }
558    }
559
560    #[test]
561    fn accepts_native_values() {
562        assert!(ColumnType::Int64.accepts(&Value::Integer(42)));
563        assert!(ColumnType::Float64.accepts(&Value::Float(42.0)));
564        assert!(ColumnType::Float64.accepts(&Value::Integer(42)));
565        assert!(ColumnType::String.accepts(&Value::String("x".into())));
566        assert!(ColumnType::Bool.accepts(&Value::Bool(true)));
567        assert!(ColumnType::Bytes.accepts(&Value::Bytes(vec![1])));
568        assert!(
569            ColumnType::Uuid.accepts(&Value::Uuid("550e8400-e29b-41d4-a716-446655440000".into()))
570        );
571        assert!(decimal(28, 10).accepts(&Value::Decimal(rust_decimal::Decimal::ZERO)));
572
573        let naive = Value::NaiveDateTime(nodedb_types_datetime_epoch());
574        let tz = Value::DateTime(nodedb_types_datetime_epoch());
575        assert!(ColumnType::Timestamp.accepts(&naive));
576        assert!(!ColumnType::Timestamp.accepts(&tz));
577        assert!(ColumnType::Timestamptz.accepts(&tz));
578        assert!(!ColumnType::Timestamptz.accepts(&naive));
579
580        assert!(ColumnType::Int64.accepts(&Value::Null));
581        assert!(!ColumnType::Int64.accepts(&Value::String("x".into())));
582        assert!(!ColumnType::Bool.accepts(&Value::Integer(1)));
583    }
584
585    #[test]
586    fn accepts_coercion_sources() {
587        assert!(ColumnType::Timestamp.accepts(&Value::String("2024-01-01".into())));
588        assert!(ColumnType::Timestamp.accepts(&Value::Integer(1_700_000_000)));
589        assert!(ColumnType::Timestamptz.accepts(&Value::String("2024-01-01T00:00:00Z".into())));
590        assert!(ColumnType::Timestamptz.accepts(&Value::Integer(1_700_000_000)));
591        assert!(ColumnType::Uuid.accepts(&Value::String(
592            "550e8400-e29b-41d4-a716-446655440000".into()
593        )));
594        assert!(decimal(10, 2).accepts(&Value::String("99.95".into())));
595        assert!(decimal(10, 2).accepts(&Value::Float(99.95)));
596        assert!(ColumnType::Geometry.accepts(&Value::String("POINT(0 0)".into())));
597    }
598
599    #[test]
600    fn column_def_display() {
601        use super::super::column_def::ColumnDef;
602        let col = ColumnDef::required("id", ColumnType::Int64).with_primary_key();
603        assert_eq!(col.to_string(), "id BIGINT NOT NULL PRIMARY KEY");
604    }
605}