1use serde::{Deserialize, Serialize};
6
7use super::decimal_typmod::DecimalTypmod;
8use crate::InstantKind;
9use crate::value::Value;
10
11#[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 Timestamp,
38 Timestamptz,
40 SystemTimestamp,
45 Decimal(Option<DecimalTypmod>),
49 Geometry,
50 Vector(u32),
52 SparseVector,
57 Uuid,
58 Json,
61 Ulid,
63 Duration,
65 Array,
67 Set,
69 Regex,
71 Range,
73 Record,
75}
76
77impl ColumnType {
78 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 pub fn is_variable_length(&self) -> bool {
105 self.fixed_size().is_none()
106 }
107
108 pub const fn is_instant(&self) -> bool {
118 self.instant_kind().is_some()
119 }
120
121 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 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 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 Self::Geometry => 25,
201 Self::Vector(_) => 1021,
203 Self::Array
207 | Self::Set
208 | Self::Range
209 | Self::Record
210 | Self::Regex
211 | Self::SparseVector => 3802,
212 }
213 }
214
215 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 #[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 #[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 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 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 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}