Skip to main content

questdb/egress/
column_kind.rs

1/*******************************************************************************
2 *     ___                  _   ____  ____
3 *    / _ \ _   _  ___  ___| |_|  _ \| __ )
4 *   | | | | | | |/ _ \/ __| __| | | |  _ \
5 *   | |_| | |_| |  __/\__ \ |_| |_| | |_) |
6 *    \__\_\\__,_|\___||___/\__|____/|____/
7 *
8 *  Copyright (c) 2014-2019 Appsicle
9 *  Copyright (c) 2019-2025 QuestDB
10 *
11 *  Licensed under the Apache License, Version 2.0 (the "License");
12 *  you may not use this file except in compliance with the License.
13 *  You may obtain a copy of the License at
14 *
15 *  http://www.apache.org/licenses/LICENSE-2.0
16 *
17 *  Unless required by applicable law or agreed to in writing, software
18 *  distributed under the License is distributed on an "AS IS" BASIS,
19 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20 *  See the License for the specific language governing permissions and
21 *  limitations under the License.
22 *
23 ******************************************************************************/
24
25//! QWP column type codes.
26//!
27//! ABI-stable: variants append-only, never reorder. `0x08` is reserved
28//! (formerly `STRING`, removed); senders use [`Varchar`](ColumnKind::Varchar).
29
30use crate::error::{Result, fmt};
31
32/// QWP wire type code.
33///
34/// `#[non_exhaustive]` because the QWP type table is append-only — new
35/// type codes may be added in future protocol revisions, and exhaustive
36/// matches in downstream code shouldn't break when that happens.
37#[repr(u8)]
38#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
39#[non_exhaustive]
40pub enum ColumnKind {
41    Boolean = 0x01,
42    Byte = 0x02,
43    Short = 0x03,
44    Int = 0x04,
45    Long = 0x05,
46    Float = 0x06,
47    Double = 0x07,
48    // 0x08 reserved (formerly STRING)
49    Symbol = 0x09,
50    /// Microsecond-precision timestamp.
51    Timestamp = 0x0A,
52    Date = 0x0B,
53    Uuid = 0x0C,
54    Long256 = 0x0D,
55    Geohash = 0x0E,
56    Varchar = 0x0F,
57    /// Nanosecond-precision timestamp.
58    TimestampNanos = 0x10,
59    DoubleArray = 0x11,
60    LongArray = 0x12,
61    Decimal64 = 0x13,
62    Decimal128 = 0x14,
63    Decimal256 = 0x15,
64    Char = 0x16,
65    Binary = 0x17,
66    Ipv4 = 0x18,
67}
68
69impl ColumnKind {
70    /// Parse a wire byte into a known column kind.
71    pub fn from_u8(byte: u8) -> Result<Self> {
72        Ok(match byte {
73            0x01 => ColumnKind::Boolean,
74            0x02 => ColumnKind::Byte,
75            0x03 => ColumnKind::Short,
76            0x04 => ColumnKind::Int,
77            0x05 => ColumnKind::Long,
78            0x06 => ColumnKind::Float,
79            0x07 => ColumnKind::Double,
80            0x09 => ColumnKind::Symbol,
81            0x0A => ColumnKind::Timestamp,
82            0x0B => ColumnKind::Date,
83            0x0C => ColumnKind::Uuid,
84            0x0D => ColumnKind::Long256,
85            0x0E => ColumnKind::Geohash,
86            0x0F => ColumnKind::Varchar,
87            0x10 => ColumnKind::TimestampNanos,
88            0x11 => ColumnKind::DoubleArray,
89            0x12 => ColumnKind::LongArray,
90            0x13 => ColumnKind::Decimal64,
91            0x14 => ColumnKind::Decimal128,
92            0x15 => ColumnKind::Decimal256,
93            0x16 => ColumnKind::Char,
94            0x17 => ColumnKind::Binary,
95            0x18 => ColumnKind::Ipv4,
96            0x08 => {
97                return Err(fmt!(
98                    ProtocolError,
99                    "type code 0x08 is reserved (was STRING)"
100                ));
101            }
102            other => {
103                return Err(fmt!(
104                    ProtocolError,
105                    "unknown column type code 0x{:02X}",
106                    other
107                ));
108            }
109        })
110    }
111
112    pub fn as_u8(self) -> u8 {
113        self as u8
114    }
115
116    /// Stable, lower-case name for diagnostics.
117    pub fn name(self) -> &'static str {
118        match self {
119            ColumnKind::Boolean => "boolean",
120            ColumnKind::Byte => "byte",
121            ColumnKind::Short => "short",
122            ColumnKind::Int => "int",
123            ColumnKind::Long => "long",
124            ColumnKind::Float => "float",
125            ColumnKind::Double => "double",
126            ColumnKind::Symbol => "symbol",
127            ColumnKind::Timestamp => "timestamp",
128            ColumnKind::Date => "date",
129            ColumnKind::Uuid => "uuid",
130            ColumnKind::Long256 => "long256",
131            ColumnKind::Geohash => "geohash",
132            ColumnKind::Varchar => "varchar",
133            ColumnKind::TimestampNanos => "timestamp_nanos",
134            ColumnKind::DoubleArray => "double_array",
135            ColumnKind::LongArray => "long_array",
136            ColumnKind::Decimal64 => "decimal64",
137            ColumnKind::Decimal128 => "decimal128",
138            ColumnKind::Decimal256 => "decimal256",
139            ColumnKind::Char => "char",
140            ColumnKind::Binary => "binary",
141            ColumnKind::Ipv4 => "ipv4",
142        }
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149
150    const ALL: &[ColumnKind] = &[
151        ColumnKind::Boolean,
152        ColumnKind::Byte,
153        ColumnKind::Short,
154        ColumnKind::Int,
155        ColumnKind::Long,
156        ColumnKind::Float,
157        ColumnKind::Double,
158        ColumnKind::Symbol,
159        ColumnKind::Timestamp,
160        ColumnKind::Date,
161        ColumnKind::Uuid,
162        ColumnKind::Long256,
163        ColumnKind::Geohash,
164        ColumnKind::Varchar,
165        ColumnKind::TimestampNanos,
166        ColumnKind::DoubleArray,
167        ColumnKind::LongArray,
168        ColumnKind::Decimal64,
169        ColumnKind::Decimal128,
170        ColumnKind::Decimal256,
171        ColumnKind::Char,
172        ColumnKind::Binary,
173        ColumnKind::Ipv4,
174    ];
175
176    #[test]
177    fn roundtrip_all_known_codes() {
178        for &k in ALL {
179            assert_eq!(ColumnKind::from_u8(k.as_u8()).unwrap(), k, "{}", k.name());
180        }
181    }
182
183    #[test]
184    fn reserved_string_code_rejected() {
185        assert!(ColumnKind::from_u8(0x08).is_err());
186    }
187
188    #[test]
189    fn unknown_codes_rejected() {
190        assert!(ColumnKind::from_u8(0x00).is_err());
191        assert!(ColumnKind::from_u8(0x19).is_err());
192        assert!(ColumnKind::from_u8(0xFF).is_err());
193    }
194
195    #[test]
196    fn names_unique() {
197        let names: Vec<_> = ALL.iter().map(|k| k.name()).collect();
198        let mut sorted = names.clone();
199        sorted.sort_unstable();
200        sorted.dedup();
201        assert_eq!(names.len(), sorted.len());
202    }
203}