Skip to main content

ignite_client/protocol/
types.rs

1use bigdecimal::BigDecimal;
2use std::fmt;
3use uuid::Uuid;
4
5use crate::protocol::binary::value::BinaryObject;
6
7// ─── Type codes ──────────────────────────────────────────────────────────────
8
9pub mod type_code {
10    #![allow(dead_code)]
11    pub const BYTE: u8 = 1;
12    pub const SHORT: u8 = 2;
13    pub const INT: u8 = 3;
14    pub const LONG: u8 = 4;
15    pub const FLOAT: u8 = 5;
16    pub const DOUBLE: u8 = 6;
17    pub const CHAR: u8 = 7;
18    pub const BOOL: u8 = 8;
19    pub const STRING: u8 = 9;
20    pub const UUID: u8 = 10;
21    pub const DATE: u8 = 11;
22    pub const BYTE_ARRAY: u8 = 12;
23    pub const SHORT_ARRAY: u8 = 13;
24    pub const INT_ARRAY: u8 = 14;
25    pub const LONG_ARRAY: u8 = 15;
26    pub const FLOAT_ARRAY: u8 = 16;
27    pub const DOUBLE_ARRAY: u8 = 17;
28    pub const CHAR_ARRAY: u8 = 18;
29    pub const BOOL_ARRAY: u8 = 19;
30    pub const STRING_ARRAY: u8 = 20;
31    pub const UUID_ARRAY: u8 = 21;
32    pub const DATE_ARRAY: u8 = 22;
33    pub const DECIMAL: u8 = 30;
34    pub const DECIMAL_ARRAY: u8 = 31;
35    pub const TIMESTAMP: u8 = 33;
36    pub const TIMESTAMP_ARRAY: u8 = 34;
37    pub const TIME: u8 = 36;
38    pub const TIME_ARRAY: u8 = 37;
39    pub const ENUM: u8 = 28;
40    pub const ENUM_ARRAY: u8 = 29;
41    pub const BINARY_OBJECT: u8 = 27;
42    pub const COMPLEX_OBJECT: u8 = 103;
43    pub const NULL: u8 = 101;
44    pub const HANDLE: u8 = 104;
45    pub const OBJECT_ARRAY: u8 = 23;
46    pub const COLLECTION: u8 = 24;
47    pub const PROTO_VER: u8 = 1;
48    pub const MAP: u8 = 25;
49    /// "Optimised marshaller" — Ignite uses this type code when a value is
50    /// serialised with Java's `ObjectOutputStream` rather than the Ignite
51    /// binary codec.  Used for `java.sql.Date`, `java.sql.Time`, and
52    /// `java.sql.Timestamp` when returned by `OP_QUERY_SQL_FIELDS`.
53    pub const OPTM_MARSH: u8 = 0xFE;
54}
55
56// ─── Op codes ─────────────────────────────────────────────────────────────────
57
58pub mod op_code {
59    #![allow(dead_code)]
60    pub const RESOURCE_CLOSE: i16 = 0;
61    pub const CACHE_GET: i16 = 1000;
62    pub const CACHE_PUT: i16 = 1001;
63    pub const CACHE_PUT_IF_ABSENT: i16 = 1002;
64    pub const CACHE_GET_ALL: i16 = 1003;
65    pub const CACHE_PUT_ALL: i16 = 1004;
66    pub const CACHE_GET_AND_PUT: i16 = 1005;
67    pub const CACHE_GET_AND_REMOVE: i16 = 1007;
68    pub const CACHE_GET_AND_REPLACE: i16 = 1006;
69    pub const CACHE_REPLACE: i16 = 1009;
70    pub const CACHE_CONTAINS_KEY: i16 = 1011;
71    pub const CACHE_REMOVE_KEY: i16 = 1019;
72    pub const CACHE_REMOVE_KEYS: i16 = 1021;
73    pub const CACHE_REMOVE_ALL: i16 = 1022;
74    pub const CACHE_GET_NAMES: i16 = 1050;
75    pub const CACHE_GET_OR_CREATE_WITH_NAME: i16 = 1052;
76    pub const CACHE_CREATE_WITH_CONFIGURATION: i16 = 1051;
77    pub const CACHE_GET_OR_CREATE_WITH_CONFIGURATION: i16 = 1053;
78    pub const CACHE_DESTROY: i16 = 1056;
79    pub const CACHE_GET_SIZE: i16 = 1020;
80    pub const CACHE_PARTITIONS: i16 = 1101;
81    pub const CLUSTER_GROUP_GET_NODE_ENDPOINTS: i16 = 5102;
82    pub const QUERY_SQL: i16 = 2002; // deprecated
83    pub const QUERY_SQL_FIELDS: i16 = 2004;
84    pub const QUERY_SQL_FIELDS_CURSOR_GET_PAGE: i16 = 2005;
85    pub const TX_START: i16 = 4000;
86    pub const TX_END: i16 = 4001;
87    pub const BINARY_TYPE_GET: i16 = 3002;
88    pub const BINARY_TYPE_PUT: i16 = 3003;
89}
90
91// ─── Expiry / TTL ─────────────────────────────────────────────────────────────
92
93/// Lifetime applied to a cache entry by an [`ExpiryPolicy`] for one event
94/// (creation, update, or access).
95///
96/// Maps to the Ignite thin-client duration encoding: `Unchanged` = -2 (leave the
97/// entry's current expiry untouched), `Eternal` = -1 (never expires),
98/// `Immediate` = 0 (expire at once), `Millis(n)` = a positive time-to-live in
99/// milliseconds.
100#[derive(Debug, Clone, Copy, PartialEq, Eq)]
101pub enum ExpiryDuration {
102    /// Leave the entry's current expiry unchanged for this event.
103    Unchanged,
104    /// The entry never expires.
105    Eternal,
106    /// The entry expires immediately.
107    Immediate,
108    /// The entry expires after this many milliseconds.
109    Millis(u64),
110}
111
112impl ExpiryDuration {
113    /// Build from a [`std::time::Duration`]; a zero duration becomes
114    /// [`ExpiryDuration::Immediate`].
115    pub fn from_duration(d: std::time::Duration) -> Self {
116        let ms = d.as_millis();
117        if ms == 0 {
118            ExpiryDuration::Immediate
119        } else {
120            ExpiryDuration::Millis(ms.min(i64::MAX as u128) as u64)
121        }
122    }
123
124    /// Encode to the thin-client wire value (`-2`, `-1`, `0`, or `>0` ms).
125    pub(crate) fn to_wire(self) -> i64 {
126        match self {
127            ExpiryDuration::Unchanged => -2,
128            ExpiryDuration::Eternal => -1,
129            ExpiryDuration::Immediate => 0,
130            ExpiryDuration::Millis(n) => n.min(i64::MAX as u64) as i64,
131        }
132    }
133}
134
135/// Per-entry lifetime policy: the time-to-live applied when an entry is created,
136/// updated, and accessed.  Attach it to a cache handle with
137/// [`crate::IgniteCache::with_expiry_policy`].
138#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139pub struct ExpiryPolicy {
140    /// TTL set when a new entry is inserted.
141    pub create: ExpiryDuration,
142    /// TTL reset when an existing entry is overwritten.
143    pub update: ExpiryDuration,
144    /// TTL reset when an entry is read.
145    pub access: ExpiryDuration,
146}
147
148impl ExpiryPolicy {
149    /// Full control over all three durations.
150    pub fn new(create: ExpiryDuration, update: ExpiryDuration, access: ExpiryDuration) -> Self {
151        Self {
152            create,
153            update,
154            access,
155        }
156    }
157}
158
159// ─── Transaction enums ────────────────────────────────────────────────────────
160
161#[derive(Debug, Clone, Copy, PartialEq, Eq)]
162#[repr(i32)]
163pub enum TxConcurrency {
164    Optimistic = 0,
165    Pessimistic = 1,
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169#[repr(i32)]
170pub enum TxIsolation {
171    ReadCommitted = 0,
172    RepeatableRead = 1,
173    Serializable = 2,
174}
175
176// ─── Statement types ─────────────────────────────────────────────────────────
177
178#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179#[repr(i8)]
180pub enum StatementType {
181    Any = 0,
182    Select = 1,
183    Update = 2,
184}
185
186// ─── Value enum ───────────────────────────────────────────────────────────────
187
188/// Rust representation of a typed Ignite binary value.
189#[derive(Debug, Clone, PartialEq)]
190#[non_exhaustive]
191pub enum IgniteValue {
192    /// SQL NULL — absence of a value for any type.
193    Null,
194    /// SQL BOOLEAN.
195    Bool(bool),
196    /// Signed 8-bit integer (TINYINT).
197    Byte(i8),
198    /// Signed 16-bit integer (SMALLINT).
199    Short(i16),
200    /// Signed 32-bit integer (INT).
201    Int(i32),
202    /// Signed 64-bit integer (BIGINT).
203    Long(i64),
204    /// Single-precision IEEE 754 float (REAL / FLOAT).
205    Float(f32),
206    /// Double-precision IEEE 754 float (DOUBLE).
207    Double(f64),
208    /// Unicode code point in the Basic Multilingual Plane (CHAR).
209    Char(u16),
210    /// UTF-8 string (VARCHAR / LONGVARCHAR).
211    String(String),
212    /// 128-bit universally unique identifier (UUID / CHAR(36)).
213    Uuid(Uuid),
214    /// Milliseconds from the Unix epoch (DATE).
215    Date(i64),
216    /// `(milliseconds_from_epoch, nanosecond_fraction)` (TIMESTAMP).
217    Timestamp(i64, i32),
218    /// Nanoseconds from midnight (TIME).
219    Time(i64),
220    /// Arbitrary-precision decimal number (DECIMAL / NUMERIC).
221    Decimal(BigDecimal),
222    /// Raw byte array (BINARY / VARBINARY).
223    ByteArray(Vec<u8>),
224    /// Payload bytes of an Ignite `BINARY_OBJECT` (type code 27), **without** the
225    /// outer type-code wrapper.  When encoded, the codec writes the full
226    /// `[u8: 27][i32: len][bytes][i32: offset=0]` frame automatically.
227    /// When decoded, only the inner payload bytes (between the length prefix and
228    /// the trailing offset) are stored here.
229    RawObject(Vec<u8>),
230    /// A fully-encoded nested binary (complex) object.  Encoding writes its
231    /// frame bytes verbatim (the frame already begins with the `COMPLEX_OBJECT`
232    /// (103) type code).  Decoded both for a top-level `CACHE_GET` binary
233    /// object and for a `COMPLEX_OBJECT` nested inside another one's field
234    /// data (see the `COMPLEX_OBJECT` arm in `codec::decode_value_with_code`);
235    /// see [`crate::binary`] for the ergonomic derive-based layer built on
236    /// top of this variant.
237    Object(BinaryObject),
238    /// Signed 32-bit integer array (INT_ARRAY).
239    IntArray(Vec<i32>),
240    /// Nullable UTF-8 string array (STRING_ARRAY); each element is `None` for
241    /// a null entry.
242    StringArray(Vec<Option<String>>),
243    /// A Java collection (`ARR_LIST` = 2, `LINKED_LIST` = 3 in some Ignite
244    /// versions, `HASH_SET` = 3, `USER_COL` = 0, etc. — the raw collection-type
245    /// byte is preserved as-is) of type-coded elements.
246    Collection(u8, Vec<IgniteValue>),
247    /// A Java map (`HASH_MAP` = 1, `LINKED_HASH_MAP` = 2, etc. — the raw
248    /// map-type byte is preserved as-is) of type-coded key/value pairs.
249    Map(u8, Vec<(IgniteValue, IgniteValue)>),
250    /// A Java enum constant: the binary type id of the enum type and the
251    /// ordinal of the constant.
252    Enum { type_id: i32, ordinal: i32 },
253}
254
255// ─── Column type ─────────────────────────────────────────────────────────────
256
257/// The SQL type of a value returned by Ignite, inferred from the 1-byte
258/// wire type-tag that accompanies each encoded value.
259///
260/// `Unknown` is returned for `NULL` values, which carry no type information.
261#[derive(Debug, Clone, Copy, PartialEq, Eq)]
262#[non_exhaustive]
263pub enum ColumnType {
264    Boolean,
265    Byte,
266    Short,
267    Int,
268    Long,
269    Float,
270    Double,
271    Char,
272    String,
273    Uuid,
274    Date,
275    Timestamp,
276    Time,
277    Decimal,
278    Binary,
279    /// Could not be determined (all rows were NULL, or no rows returned).
280    Unknown,
281}
282
283impl ColumnType {
284    /// Return the canonical SQL type name for this column type.
285    pub fn as_str(&self) -> &'static str {
286        match self {
287            ColumnType::Boolean => "BOOLEAN",
288            ColumnType::Byte => "TINYINT",
289            ColumnType::Short => "SMALLINT",
290            ColumnType::Int => "INT",
291            ColumnType::Long => "BIGINT",
292            ColumnType::Float => "FLOAT",
293            ColumnType::Double => "DOUBLE",
294            ColumnType::Char => "CHAR",
295            ColumnType::String => "VARCHAR",
296            ColumnType::Uuid => "UUID",
297            ColumnType::Date => "DATE",
298            ColumnType::Timestamp => "TIMESTAMP",
299            ColumnType::Time => "TIME",
300            ColumnType::Decimal => "DECIMAL",
301            ColumnType::Binary => "BINARY",
302            ColumnType::Unknown => "UNKNOWN",
303        }
304    }
305}
306
307impl fmt::Display for ColumnType {
308    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
309        f.write_str(self.as_str())
310    }
311}
312
313impl From<&IgniteValue> for ColumnType {
314    fn from(v: &IgniteValue) -> Self {
315        match v {
316            IgniteValue::Null => ColumnType::Unknown,
317            IgniteValue::Bool(_) => ColumnType::Boolean,
318            IgniteValue::Byte(_) => ColumnType::Byte,
319            IgniteValue::Short(_) => ColumnType::Short,
320            IgniteValue::Int(_) => ColumnType::Int,
321            IgniteValue::Long(_) => ColumnType::Long,
322            IgniteValue::Float(_) => ColumnType::Float,
323            IgniteValue::Double(_) => ColumnType::Double,
324            IgniteValue::Char(_) => ColumnType::Char,
325            IgniteValue::String(_) => ColumnType::String,
326            IgniteValue::Uuid(_) => ColumnType::Uuid,
327            IgniteValue::Date(_) => ColumnType::Date,
328            IgniteValue::Timestamp(_, _) => ColumnType::Timestamp,
329            IgniteValue::Time(_) => ColumnType::Time,
330            IgniteValue::Decimal(_) => ColumnType::Decimal,
331            IgniteValue::ByteArray(_) | IgniteValue::RawObject(_) => ColumnType::Binary,
332            IgniteValue::Object(_)
333            | IgniteValue::IntArray(_)
334            | IgniteValue::StringArray(_)
335            | IgniteValue::Collection(_, _)
336            | IgniteValue::Map(_, _)
337            | IgniteValue::Enum { .. } => ColumnType::Binary,
338        }
339    }
340}
341
342impl IgniteValue {
343    /// Returns the [`ColumnType`] for this value, derived directly from its
344    /// 1-byte wire type-tag.
345    ///
346    /// This is the **reliable** way to determine a column's SQL type: because
347    /// `OP_QUERY_SQL_FIELDS` embeds a type code with every encoded value, the
348    /// type is always determinable at the value level regardless of schema
349    /// metadata.  The only case that returns [`ColumnType::Unknown`] is
350    /// [`IgniteValue::Null`], which carries no type information by definition.
351    pub fn column_type(&self) -> ColumnType {
352        ColumnType::from(self)
353    }
354}
355
356// ─── Java hash ────────────────────────────────────────────────────────────────
357
358/// Replicates Java's `String.hashCode()`.  Used to compute type IDs and field IDs
359/// in the Binary Object format.
360///
361/// ```
362/// use ignite_client::java_hash;
363/// assert_eq!(java_hash("abc"), 96354);
364/// assert_eq!(java_hash(""), 0);
365/// ```
366pub fn java_hash(s: &str) -> i32 {
367    s.chars()
368        .fold(0i32, |h, c| h.wrapping_mul(31).wrapping_add(c as i32))
369}
370
371/// Cache ID is the Java hash of the exact cache name, case-sensitive —
372/// mirrors Ignite server-side `CU.cacheId(String)`, which is literally
373/// `cacheName.hashCode()` with no case transformation.
374///
375/// (Previously this upper-cased the name first, which happened to be
376/// invisible as long as every cache name used in this crate's own tests was
377/// already all-uppercase, but produces the wrong id — a lookup miss against
378/// the real server — for any mixed/lower-case name, including one created by
379/// a non-Rust peer such as the Java thin client.)
380pub fn cache_id(name: &str) -> i32 {
381    java_hash(name)
382}
383
384#[cfg(test)]
385mod tests {
386    use super::*;
387
388    #[test]
389    fn java_hash_empty() {
390        assert_eq!(java_hash(""), 0);
391    }
392
393    #[test]
394    fn java_hash_known() {
395        // Verified against Ignite Java source / manual calculation
396        assert_eq!(java_hash("abc"), 96354);
397        assert_eq!(java_hash("Hello"), 69609650);
398        // "PUBLIC" wraps past i32::MAX → negative
399        assert_eq!(java_hash("PUBLIC"), -1924094359_i32);
400    }
401
402    #[test]
403    fn cache_id_is_case_sensitive() {
404        // Mirrors real Ignite (`CU.cacheId` == `cacheName.hashCode()`, no case
405        // transform): two differently-cased names must NOT collide, since a
406        // peer (e.g. the Java thin client) can create a cache under either
407        // exact spelling and this client must compute the same id it did.
408        assert_ne!(cache_id("myCache"), cache_id("MYCACHE"));
409        assert_eq!(cache_id("MYCACHE"), java_hash("MYCACHE"));
410    }
411
412    #[test]
413    fn cache_partitions_opcode_matches_java() {
414        // Java org.apache.ignite.internal.client.thin.ClientOperation
415        // defines CACHE_PARTITIONS(1101).  A wrong opcode is a silent failure.
416        assert_eq!(op_code::CACHE_PARTITIONS, 1101);
417    }
418
419    #[test]
420    fn node_endpoints_opcode_matches_java() {
421        // ClientOperation.CLUSTER_GROUP_GET_NODE_ENDPOINTS(5102).
422        assert_eq!(op_code::CLUSTER_GROUP_GET_NODE_ENDPOINTS, 5102);
423    }
424
425    #[test]
426    fn binary_type_opcodes_match_java() {
427        // ClientOperation.GET_BINARY_TYPE(3002) / PUT_BINARY_TYPE(3003).
428        assert_eq!(op_code::BINARY_TYPE_GET, 3002);
429        assert_eq!(op_code::BINARY_TYPE_PUT, 3003);
430    }
431
432    #[test]
433    fn expiry_duration_to_wire_sentinels() {
434        // Java PlatformExpiryPolicy: UNCHANGED=-2, ETERNAL=-1, ZERO=0, ms>0.
435        assert_eq!(ExpiryDuration::Unchanged.to_wire(), -2);
436        assert_eq!(ExpiryDuration::Eternal.to_wire(), -1);
437        assert_eq!(ExpiryDuration::Immediate.to_wire(), 0);
438        assert_eq!(ExpiryDuration::Millis(60_000).to_wire(), 60_000);
439    }
440
441    #[test]
442    fn expiry_duration_from_std_duration() {
443        use std::time::Duration;
444        assert_eq!(
445            ExpiryDuration::from_duration(Duration::from_secs(1)),
446            ExpiryDuration::Millis(1000)
447        );
448        assert_eq!(
449            ExpiryDuration::from_duration(Duration::ZERO),
450            ExpiryDuration::Immediate
451        );
452    }
453}