Skip to main content

lightstreamer_rs/subscription/
item_update.rs

1//! What the caller receives for every real-time update: [`ItemUpdate`].
2//!
3//! A TLCP subscription is a table. Its **items** are the rows the server was
4//! asked for and its **fields** are the columns of the schema; both are ordered
5//! by the Metadata Adapter and both are numbered from 1 on the wire
6//! [`docs/spec/04-notifications.md` §2.1]. Every `U` notification reports new
7//! values for one item, and [`ItemUpdate`] is that notification after the
8//! second-level value syntax has been decoded against the item's previous
9//! state [`docs/spec/04-notifications.md` §2.3].
10//!
11//! # Null is not empty
12//!
13//! This is the single most important thing to get right about a field value.
14//! TLCP has two distinct markers and they mean different things
15//! [`docs/spec/04-notifications.md` §2.2]:
16//!
17//! | Wire token | Meaning | This crate |
18//! |---|---|---|
19//! | `#` | the field is **null** — it has no value | [`FieldValue::Null`] |
20//! | `$` | the field is the **empty string** | [`FieldValue::Text`] wrapping `""` |
21//! | *(empty token)* | the field is **unchanged** since the last update | the previous [`FieldValue`], and the field is absent from [`ItemUpdate::changed_fields`] |
22//!
23//! A field value is therefore [`FieldValue`], never a bare `&str` and never an
24//! `Option<&str>` that a caller might flatten by accident. `FieldValue::Null`
25//! and `FieldValue::Text("")` compare unequal, print differently, and are
26//! produced by different server intentions. Code that treats a missing quote as
27//! an empty quote is wrong in a way no test of its own will catch, so this
28//! crate refuses to make the two spellable as one.
29//!
30//! # Values are text
31//!
32//! A field value is exactly the UTF-8 string the server sent, after
33//! percent-decoding and after any diff was applied
34//! [`docs/spec/04-notifications.md` §2.2, §2.3]. This crate never parses one
35//! into a number, a date or a boolean: what a value means is the Metadata
36//! Adapter's business and the caller's, not the protocol's.
37//!
38//! # Snapshot or real time
39//!
40//! [`ItemUpdate::kind`] answers it, computed from the specification's own
41//! decision table [`docs/spec/04-notifications.md` §2.4]; the table is
42//! implemented row for row by `classify_update` in
43//! `src/subscription/manager.rs`.
44
45use std::fmt;
46use std::ops::Range;
47use std::sync::Arc;
48
49// ---------------------------------------------------------------------------
50// Field values
51// ---------------------------------------------------------------------------
52
53/// The value of one field of one item.
54///
55/// TLCP distinguishes **null** from the **empty string**, and so does this
56/// type: `#` on the wire produces [`FieldValue::Null`] and `$` produces
57/// [`FieldValue::Text`] wrapping `""` [`docs/spec/04-notifications.md` §2.2].
58/// They are not interchangeable. A quote adapter that has no bid to report
59/// sends `#`; one whose status line is deliberately blank sends `$`.
60///
61/// # Examples
62///
63/// An [`ItemUpdate`] is obtained from a live subscription, so the example is
64/// written as the function you would pass one to — which compiles, and so
65/// keeps saying something true:
66///
67/// ```
68/// use lightstreamer_rs::{FieldValue, ItemUpdate};
69///
70/// fn report_bid(update: &ItemUpdate) {
71///     match update.field_by_name("bid") {
72///         Some(FieldValue::Text(bid)) => println!("bid is {bid}"),
73///         Some(FieldValue::Null) => println!("there is no bid"),
74///         None => println!("this subscription has no `bid` field"),
75///     }
76/// }
77/// ```
78///
79/// The three arms are the three answers, and they are not
80/// interchangeable: `Text("")` is a fourth thing again — a value that happens
81/// to be empty. Use [`is_empty_text`](FieldValue::is_empty_text) to ask.
82#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
83pub enum FieldValue<'a> {
84    /// The field has no value — the `#` marker
85    /// [`docs/spec/04-notifications.md` §2.2].
86    ///
87    /// Also what a field reads as before any update has ever set it, which is
88    /// only observable between the subscription being activated and its first
89    /// `U`: the first update of a subscription carries a token for every field
90    /// [`docs/spec/04-notifications.md` §2.3].
91    Null,
92
93    /// The field holds this text, which may be the **empty string** — the `$`
94    /// marker sets it to exactly that
95    /// [`docs/spec/04-notifications.md` §2.2].
96    Text(&'a str),
97}
98
99impl<'a> FieldValue<'a> {
100    /// Whether the field is null, i.e. has no value at all.
101    ///
102    /// A field holding the empty string is **not** null.
103    #[must_use]
104    #[inline]
105    pub const fn is_null(self) -> bool {
106        matches!(self, Self::Null)
107    }
108
109    /// Whether the field holds the empty string.
110    ///
111    /// A null field is **not** empty text: it has no text.
112    #[must_use]
113    #[inline]
114    pub fn is_empty_text(self) -> bool {
115        matches!(self, Self::Text(text) if text.is_empty())
116    }
117
118    /// The text, or [`None`] if the field is null.
119    ///
120    /// Collapsing the distinction is a deliberate act here, which is the point:
121    /// `text()` reads as "I am choosing to treat null as absent", whereas an
122    /// accessor that returned `&str` directly would make the same choice
123    /// silently.
124    #[must_use]
125    #[inline]
126    pub const fn text(self) -> Option<&'a str> {
127        match self {
128            Self::Null => None,
129            Self::Text(text) => Some(text),
130        }
131    }
132
133    /// The text, substituting `default` if the field is null.
134    ///
135    /// This is how a field value is rendered for a human: `Display` is
136    /// deliberately **not** implemented, because every rendering of a null has
137    /// to choose a spelling and no choice is safe to make on the caller's
138    /// behalf — the empty string collapses null into `$`, and any placeholder
139    /// is a string a field could legitimately hold. Naming the substitute at
140    /// the call site is a one-word cost and it keeps
141    /// `println!("{}", value.text_or("(null)"))` honest.
142    #[must_use]
143    #[inline]
144    pub fn text_or(self, default: &'a str) -> &'a str {
145        self.text().unwrap_or(default)
146    }
147}
148
149// ---------------------------------------------------------------------------
150// COMMAND-mode command field
151// ---------------------------------------------------------------------------
152
153/// What a `COMMAND`-mode update did to the row named by its key.
154///
155/// In `COMMAND` mode the subscription is a dynamic table: "each real-time
156/// update may add a new row, delete an existing row or change the content of a
157/// specific row" [`docs/spec/04-notifications.md` §3.3]. Which of the three
158/// happened is carried in the field whose position `SUBCMD` reported
159/// [`docs/spec/04-notifications.md` §3.2], and the row it applies to is named
160/// by the value of the key field.
161///
162/// SPEC-AMBIGUITY (command-literals): the specification names the commands
163/// `ADD`, `DELETE` and `UPDATE` but "nowhere states the literal wire values
164/// carried in the command field of a `U` notification, nor their casing"
165/// [`docs/spec/04-notifications.md` §3.3]. This crate matches the three names
166/// **case-insensitively** — the most permissive reading that cannot admit a
167/// fourth command by accident — and preserves anything else as
168/// [`ItemCommand::Unrecognized`] rather than rejecting the update, so a server
169/// that grows a command cannot break a client that does not know it.
170#[derive(Debug, Clone, PartialEq, Eq, Hash)]
171pub enum ItemCommand {
172    /// A row entered the table. Its fields are the ones this update carries.
173    Add,
174
175    /// An existing row changed. The fields this update did not carry keep the
176    /// values they already had.
177    Update,
178
179    /// A row left the table. The caller should drop its own representation of
180    /// the row named by [`ItemUpdate::key`].
181    ///
182    /// Note what this does **not** mean for the state this crate keeps: see
183    /// the `DELETE` discussion on [`ItemUpdate::key`].
184    Delete,
185
186    /// The command field held something else. The value is preserved verbatim.
187    Unrecognized(
188        /// The command exactly as the server sent it, after decoding.
189        String,
190    ),
191}
192
193impl ItemCommand {
194    /// Classifies a decoded command-field value.
195    #[must_use]
196    fn from_wire(value: &str) -> Self {
197        // SPEC-AMBIGUITY (command-literals): casing is unspecified
198        // [`docs/spec/04-notifications.md` §3.3], so the comparison ignores it.
199        if value.eq_ignore_ascii_case("ADD") {
200            Self::Add
201        } else if value.eq_ignore_ascii_case("UPDATE") {
202            Self::Update
203        } else if value.eq_ignore_ascii_case("DELETE") {
204            Self::Delete
205        } else {
206            Self::Unrecognized(value.to_owned())
207        }
208    }
209
210    /// The command as an uppercase name, or the preserved literal for
211    /// [`ItemCommand::Unrecognized`].
212    #[must_use]
213    #[inline]
214    pub fn as_str(&self) -> &str {
215        match self {
216            Self::Add => "ADD",
217            Self::Update => "UPDATE",
218            Self::Delete => "DELETE",
219            Self::Unrecognized(literal) => literal,
220        }
221    }
222}
223
224impl fmt::Display for ItemCommand {
225    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226        f.write_str(self.as_str())
227    }
228}
229
230// ---------------------------------------------------------------------------
231// Snapshot classification
232// ---------------------------------------------------------------------------
233
234/// Whether an update carries part of an item's initial state or a live change.
235///
236/// `U` carries both: "the notification is also used to send the item's
237/// snapshot" [`docs/spec/04-notifications.md` §2.1], and telling them apart is
238/// a function of four inputs, tabulated in
239/// [`docs/spec/04-notifications.md` §2.4].
240#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
241pub enum UpdateKind {
242    /// Part of the item's **initial state**, delivered because the
243    /// subscription asked for a snapshot.
244    ///
245    /// A snapshot may be one `U` (`MERGE`) or many (`DISTINCT`, `COMMAND`),
246    /// and in the latter case its end is announced by an end-of-snapshot event
247    /// [`docs/spec/04-notifications.md` §3.5]. `RAW` has no snapshot at all.
248    Snapshot,
249
250    /// A **live change** to the item, produced after its initial state.
251    RealTime,
252}
253
254impl UpdateKind {
255    /// Whether this is snapshot content.
256    #[must_use]
257    #[inline]
258    pub const fn is_snapshot(self) -> bool {
259        matches!(self, Self::Snapshot)
260    }
261}
262
263// ---------------------------------------------------------------------------
264// Schema
265// ---------------------------------------------------------------------------
266
267/// One item or field name, remembering whether the caller declared it.
268///
269/// The protocol never transmits item or field names: `SUBOK` reports only how
270/// many of each there are, and their order "is determined by the Metadata
271/// Adapter" [`docs/spec/04-notifications.md` §3.1]. Names therefore come from
272/// the caller, who knows them whenever it spelled out an item list and a field
273/// list rather than naming a server-side group and schema. When a name was not
274/// declared, this holds the position rendered in decimal so that
275/// [`ItemUpdate::item_name`] can always return something printable.
276#[derive(Debug, Clone, PartialEq, Eq)]
277struct SchemaName {
278    text: String,
279    declared: bool,
280}
281
282impl SchemaName {
283    /// A name the caller supplied.
284    #[must_use]
285    fn declared(text: impl Into<String>) -> Self {
286        Self {
287            text: text.into(),
288            declared: true,
289        }
290    }
291
292    /// A stand-in for a name the caller did not supply: the 1-based position.
293    #[must_use]
294    fn positional(position: usize) -> Self {
295        Self {
296            text: position.to_string(),
297            declared: false,
298        }
299    }
300
301    /// The printable name.
302    #[must_use]
303    #[inline]
304    fn as_str(&self) -> &str {
305        &self.text
306    }
307
308    /// The name, only if the caller declared it.
309    #[must_use]
310    #[inline]
311    fn declared_str(&self) -> Option<&str> {
312        if self.declared {
313            Some(&self.text)
314        } else {
315            None
316        }
317    }
318}
319
320/// The shape of one activated subscription: how many items and fields it has,
321/// what they are called, and — in `COMMAND` mode — where the key and command
322/// fields sit.
323///
324/// Shared by [`Arc`] across every [`ItemUpdate`] of the subscription, because
325/// it is fixed from the moment `SUBOK`/`SUBCMD` arrives
326/// [`docs/spec/04-notifications.md` §3.1, §3.2] and copying it per update would
327/// be pure waste.
328#[derive(Debug, Clone, PartialEq, Eq)]
329pub(crate) struct SubscriptionSchema {
330    items: Vec<SchemaName>,
331    fields: Vec<SchemaName>,
332    key_field: Option<usize>,
333    command_field: Option<usize>,
334}
335
336impl SubscriptionSchema {
337    /// Builds the schema announced by `SUBOK` or `SUBCMD`.
338    ///
339    /// `declared_items` and `declared_fields` are the caller's own names, taken
340    /// positionally. They may be shorter than the server's counts (or empty, if
341    /// the caller subscribed to a server-side group and schema); every position
342    /// they do not cover falls back to its 1-based index
343    /// [`docs/spec/04-notifications.md` §3.1].
344    ///
345    /// `command_fields` carries the 0-based key and command positions of a
346    /// `COMMAND`-mode subscription [`docs/spec/04-notifications.md` §3.2].
347    #[must_use]
348    pub(crate) fn new(
349        item_count: usize,
350        field_count: usize,
351        declared_items: &[String],
352        declared_fields: &[String],
353        command_fields: Option<(usize, usize)>,
354    ) -> Self {
355        let (key_field, command_field) = match command_fields {
356            Some((key, command)) => (Some(key), Some(command)),
357            None => (None, None),
358        };
359        Self {
360            items: Self::name_positions(item_count, declared_items),
361            fields: Self::name_positions(field_count, declared_fields),
362            key_field,
363            command_field,
364        }
365    }
366
367    /// Builds a schema from a name per position, [`None`] meaning the caller
368    /// declared none for that position.
369    ///
370    /// [`new`](Self::new) can only take declared names as a *prefix*, which is
371    /// what a subscription's item and field lists actually are. The
372    /// `test-util` builder ([`crate::test_util`]) needs to name one item at an
373    /// arbitrary position, so it supplies the names slot by slot instead.
374    #[cfg(feature = "test-util")]
375    #[must_use]
376    pub(crate) fn from_optional_names(
377        items: Vec<Option<String>>,
378        fields: Vec<Option<String>>,
379        command_fields: Option<(usize, usize)>,
380    ) -> Self {
381        /// Maps each slot to a declared name or to its 1-based position.
382        fn named(slots: Vec<Option<String>>) -> Vec<SchemaName> {
383            slots
384                .into_iter()
385                .enumerate()
386                .map(|(index, name)| match name {
387                    Some(text) => SchemaName::declared(text),
388                    // Cannot overflow: `index` is an index into a `Vec`.
389                    None => SchemaName::positional(index + 1),
390                })
391                .collect()
392        }
393
394        let (key_field, command_field) = match command_fields {
395            Some((key, command)) => (Some(key), Some(command)),
396            None => (None, None),
397        };
398        Self {
399            items: named(items),
400            fields: named(fields),
401            key_field,
402            command_field,
403        }
404    }
405
406    /// Zips `count` positions against the names the caller declared.
407    #[must_use]
408    fn name_positions(count: usize, declared: &[String]) -> Vec<SchemaName> {
409        let mut names = Vec::with_capacity(count);
410        for index in 0..count {
411            match declared.get(index) {
412                Some(name) => names.push(SchemaName::declared(name.clone())),
413                // Cannot overflow: `index < count <= usize::MAX`.
414                None => names.push(SchemaName::positional(index + 1)),
415            }
416        }
417        names
418    }
419
420    /// How many items the subscription resolved to.
421    // No non-test caller: the item count reaches the caller through
422    // `SubscriptionEvent::Activated`, and this is the inspection surface the
423    // tests assert against.
424    #[allow(dead_code)]
425    #[must_use]
426    #[inline]
427    pub(crate) fn item_count(&self) -> usize {
428        self.items.len()
429    }
430
431    /// How many fields the schema resolved to — the width every `U` value list
432    /// is decoded against [`docs/spec/04-notifications.md` §3.1].
433    #[must_use]
434    #[inline]
435    pub(crate) fn field_count(&self) -> usize {
436        self.fields.len()
437    }
438
439    /// Whether this subscription carries a key and a command field, i.e. was
440    /// activated by `SUBCMD` [`docs/spec/04-notifications.md` §3.2].
441    #[must_use]
442    #[inline]
443    pub(crate) fn is_command_mode(&self) -> bool {
444        self.key_field.is_some() && self.command_field.is_some()
445    }
446}
447
448// ---------------------------------------------------------------------------
449// ItemUpdate
450// ---------------------------------------------------------------------------
451
452/// One real-time update, decoded: the new state of one item of one
453/// subscription, plus which of its fields this update actually changed.
454///
455/// This is what a caller consumes from the subscription's event stream
456/// (`docs/adr/0003-typed-event-stream-as-delivery-surface.md`) — but not
457/// *directly*: [`Updates`](crate::Updates) yields a
458/// [`SubscriptionEvent`](crate::SubscriptionEvent), of which an update is one
459/// variant among the things that can happen to a subscription. Matching it out
460/// is the loop:
461///
462/// ```
463/// use futures_util::StreamExt;
464/// use lightstreamer_rs::{SubscriptionEvent, Updates};
465///
466/// async fn print_changes(mut updates: Updates) {
467///     while let Some(event) = updates.next().await {
468///         if let SubscriptionEvent::Update(update) = event {
469///             println!("{}: {:?}", update.item_name(), update.changed_fields());
470///         }
471///     }
472/// }
473/// ```
474///
475/// # Every field is readable, not just the changed ones
476///
477/// An update on the wire is a delta: a field the server did not mention is
478/// "unchanged compared to the previous update of the same field"
479/// [`docs/spec/04-notifications.md` §2.2]. This type carries the item's
480/// **complete** state after the delta was applied, so
481/// [`field`](Self::field) and [`field_by_name`](Self::field_by_name) always
482/// answer, whether or not this particular update touched the field. Use
483/// [`changed_fields`](Self::changed_fields) or
484/// [`is_field_changed`](Self::is_field_changed) to ask what moved.
485///
486/// # Positions are 1-based
487///
488/// Item and field positions match the specification's own numbering — items
489/// and fields are numbered from 1 [`docs/spec/04-notifications.md` §2.1,
490/// §3.2] — so a position printed in a log lines up with a position in a
491/// packet capture.
492#[derive(Debug, Clone, PartialEq, Eq)]
493pub struct ItemUpdate {
494    schema: Arc<SubscriptionSchema>,
495    /// 0-based index of the item within the subscription.
496    item: usize,
497    kind: UpdateKind,
498    /// The complete state of the item after this update, one entry per field
499    /// of the schema. `None` is null; `Some(text)` is text, possibly empty.
500    values: Vec<Option<String>>,
501    /// 0-based positions of the fields this update carried a token for,
502    /// ascending.
503    changed: Vec<usize>,
504}
505
506impl ItemUpdate {
507    /// Assembles an update from the decoded state of an item.
508    ///
509    /// `item` and `changed` are 0-based; `values` has one entry per field of
510    /// `schema`, `None` meaning null.
511    #[must_use]
512    pub(crate) fn new(
513        schema: Arc<SubscriptionSchema>,
514        item: usize,
515        kind: UpdateKind,
516        values: Vec<Option<String>>,
517        changed: Vec<usize>,
518    ) -> Self {
519        Self {
520            schema,
521            item,
522            kind,
523            values,
524            changed,
525        }
526    }
527
528    // -- The item -----------------------------------------------------------
529
530    /// The item's **1-based** position within the subscription.
531    ///
532    /// This is the `<item>` argument of the `U` notification verbatim; the
533    /// order of items "is determined by the Metadata Adapter"
534    /// [`docs/spec/04-notifications.md` §2.1].
535    #[must_use]
536    #[inline]
537    pub const fn item_index(&self) -> usize {
538        // Cannot overflow: `item` is an index into a `Vec`.
539        self.item + 1
540    }
541
542    /// The item's name.
543    ///
544    /// TLCP never sends item names — `SUBOK` reports only a count
545    /// [`docs/spec/04-notifications.md` §3.1] — so this is the name the caller
546    /// declared for this position when it subscribed. If the caller subscribed
547    /// to a server-side group and did not declare a name for this position,
548    /// this returns the item's 1-based index rendered in decimal, so that it is
549    /// always printable. Use [`declared_item_name`](Self::declared_item_name)
550    /// when the difference matters.
551    #[must_use]
552    #[inline]
553    pub fn item_name(&self) -> &str {
554        self.schema
555            .items
556            .get(self.item)
557            .map_or("", SchemaName::as_str)
558    }
559
560    /// The item's name, or [`None`] if the caller never declared one.
561    ///
562    /// Unlike [`item_name`](Self::item_name) this never invents a stand-in.
563    #[must_use]
564    #[inline]
565    pub fn declared_item_name(&self) -> Option<&str> {
566        self.schema
567            .items
568            .get(self.item)
569            .and_then(SchemaName::declared_str)
570    }
571
572    // -- Snapshot or real time ----------------------------------------------
573
574    /// Whether this update is snapshot content or a live change
575    /// [`docs/spec/04-notifications.md` §2.4].
576    #[must_use]
577    #[inline]
578    pub const fn kind(&self) -> UpdateKind {
579        self.kind
580    }
581
582    /// Shorthand for `self.kind().is_snapshot()`.
583    #[must_use]
584    #[inline]
585    pub const fn is_snapshot(&self) -> bool {
586        self.kind.is_snapshot()
587    }
588
589    // -- Fields --------------------------------------------------------------
590
591    /// How many fields the subscription's schema has
592    /// [`docs/spec/04-notifications.md` §3.1].
593    #[must_use]
594    #[inline]
595    pub fn field_count(&self) -> usize {
596        self.schema.field_count()
597    }
598
599    /// The name of the field at `position` (**1-based**), or [`None`] if the
600    /// schema has no such field.
601    ///
602    /// As with [`item_name`](Self::item_name), the protocol does not transmit
603    /// field names; an undeclared position renders as its 1-based index.
604    #[must_use]
605    #[inline]
606    pub fn field_name(&self, position: usize) -> Option<&str> {
607        self.schema
608            .fields
609            .get(position.checked_sub(1)?)
610            .map(SchemaName::as_str)
611    }
612
613    /// The value of the field at `position` (**1-based**), or [`None`] if the
614    /// schema has no such field.
615    ///
616    /// Note the two levels: [`None`] means *there is no such field*, whereas
617    /// `Some(`[`FieldValue::Null`]`)` means *the field exists and is null*.
618    /// See the module documentation on null versus empty.
619    #[must_use]
620    pub fn field(&self, position: usize) -> Option<FieldValue<'_>> {
621        let index = position.checked_sub(1)?;
622        Some(match self.values.get(index)? {
623            None => FieldValue::Null,
624            Some(text) => FieldValue::Text(text),
625        })
626    }
627
628    /// The value of the field the caller declared as `name`, or [`None`] if no
629    /// declared field has that name.
630    ///
631    /// Only names the caller declared are matched: the decimal stand-ins that
632    /// [`field_name`](Self::field_name) returns for undeclared positions are
633    /// **not** lookup keys, so `field_by_name("3")` cannot accidentally resolve
634    /// to the third field of a subscription whose fields were never named.
635    /// Names are compared exactly, and if the caller declared the same name
636    /// twice the first position wins.
637    #[must_use]
638    pub fn field_by_name(&self, name: &str) -> Option<FieldValue<'_>> {
639        self.field(self.field_position(name)?)
640    }
641
642    /// The **1-based** position of the field the caller declared as `name`, or
643    /// [`None`] if no declared field has that name.
644    #[must_use]
645    pub fn field_position(&self, name: &str) -> Option<usize> {
646        self.schema
647            .fields
648            .iter()
649            .position(|field| field.declared_str() == Some(name))
650            // Cannot overflow: `position` is an index into a `Vec`.
651            .map(|index| index + 1)
652    }
653
654    /// Every field of the schema, in order, changed or not.
655    #[must_use]
656    #[inline]
657    pub fn fields(&self) -> Fields<'_> {
658        Fields {
659            update: self,
660            positions: 0..self.values.len(),
661        }
662    }
663
664    // -- What changed --------------------------------------------------------
665
666    /// The fields this update changed, in ascending position order.
667    ///
668    /// "Changed" means **the update carried an explicit token for the field**,
669    /// which is exactly what the specification's own worked examples mark
670    /// *Changed* [`docs/spec/04-notifications.md` §2.5]. It is not a comparison
671    /// of values: an update that sets a field to the value it already had is
672    /// still a change, and the spec's third example does precisely that to its
673    /// `status` field. Fields left out by an empty token or skipped by a `^N`
674    /// run are absent, since both spellings mean "unchanged"
675    /// [`docs/spec/04-notifications.md` §2.2].
676    #[must_use]
677    #[inline]
678    pub fn changed_fields(&self) -> ChangedFields<'_> {
679        ChangedFields {
680            update: self,
681            positions: self.changed.iter(),
682        }
683    }
684
685    /// How many fields this update changed.
686    #[must_use]
687    #[inline]
688    pub fn changed_count(&self) -> usize {
689        self.changed.len()
690    }
691
692    /// Whether this update carried a token for the field at `position`
693    /// (**1-based**).
694    #[must_use]
695    pub fn is_field_changed(&self, position: usize) -> bool {
696        position
697            .checked_sub(1)
698            .is_some_and(|index| self.changed.contains(&index))
699    }
700
701    /// Whether this update carried a token for the field the caller declared as
702    /// `name`. `false` if there is no such declared field.
703    #[must_use]
704    pub fn is_field_changed_by_name(&self, name: &str) -> bool {
705        self.field_position(name)
706            .is_some_and(|position| self.is_field_changed(position))
707    }
708
709    // -- COMMAND mode --------------------------------------------------------
710
711    /// Whether this update belongs to a `COMMAND`-mode subscription, i.e. one
712    /// activated by `SUBCMD` [`docs/spec/04-notifications.md` §3.2].
713    #[must_use]
714    #[inline]
715    pub fn is_command_mode(&self) -> bool {
716        self.schema.is_command_mode()
717    }
718
719    /// The row this update is about, for a `COMMAND`-mode subscription.
720    ///
721    /// [`None`] if the subscription is not in `COMMAND` mode. Otherwise the
722    /// value of the field `SUBCMD` designated as the key
723    /// [`docs/spec/04-notifications.md` §3.2] — an ordinary field, decoded like
724    /// any other, so it can legitimately be [`FieldValue::Null`] and that is
725    /// reported rather than hidden.
726    ///
727    /// # What a `DELETE` does, and does not, do here
728    ///
729    /// [`ItemCommand::Delete`] tells the caller to drop **its own**
730    /// representation of the row. It does not erase anything inside this crate:
731    /// the field values this crate keeps per item are the baseline that the
732    /// next update's unchanged-field markers are resolved against
733    /// [`docs/spec/04-notifications.md` §2.2], and discarding that baseline
734    /// would make the very next `U` undecodable.
735    ///
736    /// SPEC-AMBIGUITY (command-delta-baseline): the specification "does not
737    /// state ... whether unchanged-field markers in a `U` are resolved against
738    /// the previous update of the same key or of the same item", noting that
739    /// the decoding algorithm "speaks only of 'the previous update of the same
740    /// field', without qualification by key"
741    /// [`docs/spec/04-notifications.md` §3.3]. This crate takes the algorithm
742    /// literally and keeps **one baseline per item**, because that is the only
743    /// reading the spec actually states; a per-key baseline would be an
744    /// invention, and it would have no defined value for a key seen for the
745    /// first time.
746    #[must_use]
747    pub fn key(&self) -> Option<FieldValue<'_>> {
748        let index = self.schema.key_field?;
749        // 1-based for `field`.
750        self.field(index.checked_add(1)?)
751    }
752
753    /// What this update did to the row named by [`key`](Self::key), for a
754    /// `COMMAND`-mode subscription.
755    ///
756    /// [`None`] if the subscription is not in `COMMAND` mode, or if the command
757    /// field is null — a null command names no operation, so this crate reports
758    /// its absence rather than guessing one.
759    ///
760    /// # Check [`is_command_changed`](Self::is_command_changed) before acting
761    ///
762    /// The command is an ordinary field, and an ordinary field that this update
763    /// did not carry keeps the value the previous update gave it
764    /// [`docs/spec/04-notifications.md` §2.2]. So a `U` that leaves the command
765    /// field unchanged makes this method return the **previous** update's
766    /// command, and a caller that deletes a row whenever it sees
767    /// [`ItemCommand::Delete`] would delete on the strength of an operation
768    /// that already happened.
769    ///
770    /// SPEC-AMBIGUITY (command-delta-baseline): the specification does not
771    /// state the client-side `COMMAND` state machine, nor "whether
772    /// unchanged-field markers in a `U` are resolved against the previous
773    /// update **of the same key** or of the same item"
774    /// [`docs/spec/04-notifications.md` §3.3]. This crate does not invent one:
775    /// it reports the field value the decoding algorithm produces, and reports
776    /// separately whether *this* update carried it, so the caller can require
777    /// the stronger evidence before doing something destructive.
778    #[must_use]
779    pub fn command(&self) -> Option<ItemCommand> {
780        let index = self.schema.command_field?;
781        let value = self.field(index.checked_add(1)?)?;
782        value.text().map(ItemCommand::from_wire)
783    }
784
785    /// Whether **this** update carried a token for the command field, as
786    /// opposed to inheriting its value from an earlier update of the same item
787    /// [`docs/spec/04-notifications.md` §2.2, §2.5].
788    ///
789    /// `false` when the subscription is not in `COMMAND` mode.
790    #[must_use]
791    pub fn is_command_changed(&self) -> bool {
792        self.schema
793            .command_field
794            .and_then(|index| index.checked_add(1))
795            .is_some_and(|position| self.is_field_changed(position))
796    }
797
798    /// Whether **this** update carried a token for the key field.
799    ///
800    /// `false` when the subscription is not in `COMMAND` mode. A `false` here
801    /// does not mean the row is unknown: under the per-item baseline this crate
802    /// keeps (see [`key`](Self::key)) it means the key is the one the previous
803    /// update named.
804    #[must_use]
805    pub fn is_key_changed(&self) -> bool {
806        self.schema
807            .key_field
808            .and_then(|index| index.checked_add(1))
809            .is_some_and(|position| self.is_field_changed(position))
810    }
811}
812
813// ---------------------------------------------------------------------------
814// Field views and iterators
815// ---------------------------------------------------------------------------
816
817/// One field of an [`ItemUpdate`]: where it sits, what it is called, what it
818/// holds, and whether this update changed it.
819#[derive(Clone, Copy)]
820pub struct Field<'a> {
821    update: &'a ItemUpdate,
822    /// 0-based.
823    index: usize,
824}
825
826impl<'a> Field<'a> {
827    /// The field's **1-based** position in the schema.
828    #[must_use]
829    #[inline]
830    pub const fn position(&self) -> usize {
831        // Cannot overflow: `index` is an index into a `Vec`.
832        self.index + 1
833    }
834
835    /// The field's name, or its 1-based position in decimal if the caller
836    /// declared no name for it — see [`ItemUpdate::field_name`].
837    #[must_use]
838    #[inline]
839    pub fn name(&self) -> &'a str {
840        self.update
841            .schema
842            .fields
843            .get(self.index)
844            .map_or("", SchemaName::as_str)
845    }
846
847    /// The field's name, or [`None`] if the caller declared none.
848    #[must_use]
849    #[inline]
850    pub fn declared_name(&self) -> Option<&'a str> {
851        self.update
852            .schema
853            .fields
854            .get(self.index)
855            .and_then(SchemaName::declared_str)
856    }
857
858    /// The field's value after this update. Null and empty stay distinct; see
859    /// [`FieldValue`].
860    #[must_use]
861    #[inline]
862    pub fn value(&self) -> FieldValue<'a> {
863        match self.update.values.get(self.index) {
864            Some(Some(text)) => FieldValue::Text(text),
865            Some(None) | None => FieldValue::Null,
866        }
867    }
868
869    /// Whether this update carried a token for the field
870    /// [`docs/spec/04-notifications.md` §2.5].
871    #[must_use]
872    #[inline]
873    pub fn is_changed(&self) -> bool {
874        self.update.changed.contains(&self.index)
875    }
876}
877
878impl fmt::Debug for Field<'_> {
879    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
880        f.debug_struct("Field")
881            .field("position", &self.position())
882            .field("name", &self.name())
883            .field("value", &self.value())
884            .field("changed", &self.is_changed())
885            .finish()
886    }
887}
888
889/// Iterator over every field of an [`ItemUpdate`]; see
890/// [`ItemUpdate::fields`].
891#[derive(Clone)]
892pub struct Fields<'a> {
893    update: &'a ItemUpdate,
894    positions: Range<usize>,
895}
896
897impl<'a> Iterator for Fields<'a> {
898    type Item = Field<'a>;
899
900    fn next(&mut self) -> Option<Self::Item> {
901        let index = self.positions.next()?;
902        Some(Field {
903            update: self.update,
904            index,
905        })
906    }
907
908    fn size_hint(&self) -> (usize, Option<usize>) {
909        self.positions.size_hint()
910    }
911}
912
913impl ExactSizeIterator for Fields<'_> {}
914
915impl fmt::Debug for Fields<'_> {
916    /// Renders as a map from field name to value, which is what makes
917    /// `println!("{:?}", update.fields())` readable.
918    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
919        f.debug_map()
920            .entries(self.clone().map(|field| (field.name(), field.value())))
921            .finish()
922    }
923}
924
925/// Iterator over the fields an [`ItemUpdate`] changed; see
926/// [`ItemUpdate::changed_fields`].
927#[derive(Clone)]
928pub struct ChangedFields<'a> {
929    update: &'a ItemUpdate,
930    positions: std::slice::Iter<'a, usize>,
931}
932
933impl<'a> Iterator for ChangedFields<'a> {
934    type Item = Field<'a>;
935
936    fn next(&mut self) -> Option<Self::Item> {
937        let index = *self.positions.next()?;
938        Some(Field {
939            update: self.update,
940            index,
941        })
942    }
943
944    fn size_hint(&self) -> (usize, Option<usize>) {
945        self.positions.size_hint()
946    }
947}
948
949impl ExactSizeIterator for ChangedFields<'_> {}
950
951impl fmt::Debug for ChangedFields<'_> {
952    /// Renders as a map from field name to value, so the ADR-0003 usage
953    /// `println!("{}: {:?}", update.item_name(), update.changed_fields())`
954    /// prints something a human can read.
955    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
956        f.debug_map()
957            .entries(self.clone().map(|field| (field.name(), field.value())))
958            .finish()
959    }
960}
961
962// ---------------------------------------------------------------------------
963// Tests
964// ---------------------------------------------------------------------------
965
966#[cfg(test)]
967mod tests {
968    use super::*;
969
970    fn names(values: &[&str]) -> Vec<String> {
971        values.iter().map(|value| (*value).to_owned()).collect()
972    }
973
974    fn quote_schema() -> Arc<SubscriptionSchema> {
975        Arc::new(SubscriptionSchema::new(
976            2,
977            3,
978            &names(&["item1", "item2"]),
979            &names(&["last_price", "time", "status"]),
980            None,
981        ))
982    }
983
984    fn update_with(values: Vec<Option<String>>, changed: Vec<usize>) -> ItemUpdate {
985        ItemUpdate::new(quote_schema(), 0, UpdateKind::RealTime, values, changed)
986    }
987
988    // -- Null vs empty -------------------------------------------------------
989
990    #[test]
991    fn test_field_value_null_and_empty_text_are_distinct() {
992        // `#` is null and `$` is the empty string
993        // [`docs/spec/04-notifications.md` §2.2].
994        assert_ne!(FieldValue::Null, FieldValue::Text(""));
995        assert!(FieldValue::Null.is_null());
996        assert!(!FieldValue::Null.is_empty_text());
997        assert!(FieldValue::Text("").is_empty_text());
998        assert!(!FieldValue::Text("").is_null());
999        assert_eq!(FieldValue::Null.text(), None);
1000        assert_eq!(FieldValue::Text("").text(), Some(""));
1001    }
1002
1003    #[test]
1004    fn test_field_value_text_or_substitutes_only_for_null() {
1005        assert_eq!(FieldValue::Null.text_or("-"), "-");
1006        assert_eq!(FieldValue::Text("").text_or("-"), "");
1007    }
1008
1009    #[test]
1010    fn test_null_and_empty_reach_the_caller_distinctly() {
1011        let update = update_with(
1012            vec![None, Some(String::new()), Some("live".to_owned())],
1013            vec![0, 1, 2],
1014        );
1015        assert_eq!(update.field(1), Some(FieldValue::Null));
1016        assert_eq!(update.field(2), Some(FieldValue::Text("")));
1017        assert_eq!(update.field_by_name("last_price"), Some(FieldValue::Null));
1018        assert_eq!(update.field_by_name("time"), Some(FieldValue::Text("")));
1019        assert_ne!(update.field(1), update.field(2));
1020    }
1021
1022    #[test]
1023    fn test_every_rendering_of_a_field_value_keeps_null_distinguishable() {
1024        // There is no `Display`: rendering goes through `text_or`, which makes
1025        // the substitute for a null explicit and therefore visible in a log.
1026        assert_eq!(FieldValue::Null.text_or("(null)"), "(null)");
1027        assert_eq!(FieldValue::Text("").text_or("(null)"), "");
1028        assert_eq!(FieldValue::Text("x").text_or("(null)"), "x");
1029        // `Debug` — what the iterators print — never collapses them either.
1030        assert_eq!(format!("{:?}", FieldValue::Null), "Null");
1031        assert_eq!(format!("{:?}", FieldValue::Text("")), r#"Text("")"#);
1032    }
1033
1034    // -- Positions and names -------------------------------------------------
1035
1036    #[test]
1037    fn test_positions_are_one_based() {
1038        let update = update_with(vec![Some("a".to_owned()), None, None], vec![0]);
1039        // Items and fields are numbered from 1
1040        // [`docs/spec/04-notifications.md` §2.1].
1041        assert_eq!(update.item_index(), 1);
1042        assert_eq!(update.field(1), Some(FieldValue::Text("a")));
1043        assert_eq!(update.field(0), None);
1044        assert_eq!(update.field(4), None);
1045        assert_eq!(update.field_name(1), Some("last_price"));
1046        assert_eq!(update.field_name(0), None);
1047    }
1048
1049    #[test]
1050    fn test_names_come_from_the_caller() {
1051        let update = update_with(vec![None, None, None], vec![]);
1052        assert_eq!(update.item_name(), "item1");
1053        assert_eq!(update.declared_item_name(), Some("item1"));
1054        assert_eq!(update.field_position("status"), Some(3));
1055    }
1056
1057    #[test]
1058    fn test_undeclared_names_fall_back_to_the_position() {
1059        // `SUBOK` reports counts only [`docs/spec/04-notifications.md` §3.1].
1060        let schema = Arc::new(SubscriptionSchema::new(1, 2, &[], &[], None));
1061        let update = ItemUpdate::new(
1062            schema,
1063            0,
1064            UpdateKind::RealTime,
1065            vec![Some("a".to_owned()), None],
1066            vec![0],
1067        );
1068        assert_eq!(update.item_name(), "1");
1069        assert_eq!(update.declared_item_name(), None);
1070        assert_eq!(update.field_name(2), Some("2"));
1071        // The stand-in is not a lookup key.
1072        assert_eq!(update.field_by_name("2"), None);
1073        assert_eq!(update.field_position("1"), None);
1074    }
1075
1076    #[test]
1077    fn test_partially_declared_names_are_taken_positionally() {
1078        let schema = Arc::new(SubscriptionSchema::new(
1079            1,
1080            3,
1081            &names(&["only_item"]),
1082            &names(&["first"]),
1083            None,
1084        ));
1085        let update = ItemUpdate::new(schema, 0, UpdateKind::RealTime, vec![None; 3], vec![]);
1086        assert_eq!(update.field_name(1), Some("first"));
1087        assert_eq!(update.field_name(2), Some("2"));
1088        assert_eq!(update.field_position("first"), Some(1));
1089    }
1090
1091    #[test]
1092    fn test_unknown_field_name_is_none_everywhere() {
1093        let update = update_with(vec![Some("a".to_owned()), None, None], vec![0]);
1094        assert_eq!(update.field_by_name("no_such_field"), None);
1095        assert_eq!(update.field_position("no_such_field"), None);
1096        assert!(!update.is_field_changed_by_name("no_such_field"));
1097    }
1098
1099    #[test]
1100    fn test_duplicate_declared_names_resolve_to_the_first() {
1101        let schema = Arc::new(SubscriptionSchema::new(
1102            1,
1103            2,
1104            &names(&["i"]),
1105            &names(&["dup", "dup"]),
1106            None,
1107        ));
1108        let update = ItemUpdate::new(
1109            schema,
1110            0,
1111            UpdateKind::RealTime,
1112            vec![Some("first".to_owned()), Some("second".to_owned())],
1113            vec![0, 1],
1114        );
1115        assert_eq!(update.field_position("dup"), Some(1));
1116        assert_eq!(update.field_by_name("dup"), Some(FieldValue::Text("first")));
1117    }
1118
1119    // -- Changed fields ------------------------------------------------------
1120
1121    #[test]
1122    fn test_changed_fields_reports_only_carried_tokens() {
1123        let update = update_with(
1124            vec![
1125                Some("3.04".to_owned()),
1126                Some("20:00:33".to_owned()),
1127                Some(String::new()),
1128            ],
1129            vec![0, 2],
1130        );
1131        let changed: Vec<(usize, &str)> = update
1132            .changed_fields()
1133            .map(|field| (field.position(), field.name()))
1134            .collect();
1135        assert_eq!(changed, vec![(1, "last_price"), (3, "status")]);
1136        assert_eq!(update.changed_count(), 2);
1137        assert!(update.is_field_changed(1));
1138        assert!(!update.is_field_changed(2));
1139        assert!(!update.is_field_changed(0));
1140        assert!(update.is_field_changed_by_name("status"));
1141        assert!(!update.is_field_changed_by_name("time"));
1142    }
1143
1144    #[test]
1145    fn test_fields_iterates_the_whole_schema_with_changed_flags() {
1146        let update = update_with(
1147            vec![Some("a".to_owned()), None, Some(String::new())],
1148            vec![0, 2],
1149        );
1150        let seen: Vec<(usize, bool)> = update
1151            .fields()
1152            .map(|field| (field.position(), field.is_changed()))
1153            .collect();
1154        assert_eq!(seen, vec![(1, true), (2, false), (3, true)]);
1155        assert_eq!(update.fields().len(), 3);
1156        assert_eq!(update.changed_fields().len(), 2);
1157    }
1158
1159    #[test]
1160    fn test_changed_fields_debug_is_a_readable_map() {
1161        // The ADR-0003 usage prints this with `{:?}`.
1162        let update = update_with(
1163            vec![Some("3.04".to_owned()), None, Some(String::new())],
1164            vec![0, 1],
1165        );
1166        let rendered = format!("{:?}", update.changed_fields());
1167        assert!(rendered.contains("last_price"), "{rendered}");
1168        assert!(rendered.contains("Text(\"3.04\")"), "{rendered}");
1169        assert!(rendered.contains("Null"), "{rendered}");
1170    }
1171
1172    #[test]
1173    fn test_adr_0003_usage_compiles_and_prints() {
1174        // `docs/adr/0003-typed-event-stream-as-delivery-surface.md` pins this
1175        // exact pair of accessors.
1176        let update = update_with(vec![Some("3.04".to_owned()), None, None], vec![0]);
1177        let line = format!("{}: {:?}", update.item_name(), update.changed_fields());
1178        assert!(line.starts_with("item1: "), "{line}");
1179    }
1180
1181    // -- COMMAND mode --------------------------------------------------------
1182
1183    fn command_update(key: &str, command: &str) -> ItemUpdate {
1184        // `SUBCMD,3,1,3,1,2` places the key first and the command second
1185        // [`docs/spec/04-notifications.md` §3.2].
1186        let schema = Arc::new(SubscriptionSchema::new(
1187            1,
1188            3,
1189            &names(&["portfolio"]),
1190            &names(&["key", "command", "qty"]),
1191            Some((0, 1)),
1192        ));
1193        ItemUpdate::new(
1194            schema,
1195            0,
1196            UpdateKind::RealTime,
1197            vec![
1198                Some(key.to_owned()),
1199                Some(command.to_owned()),
1200                Some("10".to_owned()),
1201            ],
1202            vec![0, 1, 2],
1203        )
1204    }
1205
1206    #[test]
1207    fn test_command_mode_exposes_key_and_command() {
1208        let update = command_update("AAPL", "ADD");
1209        assert!(update.is_command_mode());
1210        assert_eq!(update.key(), Some(FieldValue::Text("AAPL")));
1211        assert_eq!(update.command(), Some(ItemCommand::Add));
1212    }
1213
1214    #[test]
1215    fn test_command_literals_are_case_insensitive() {
1216        // SPEC-AMBIGUITY (command-literals): casing is unspecified
1217        // [`docs/spec/04-notifications.md` §3.3].
1218        assert_eq!(command_update("k", "add").command(), Some(ItemCommand::Add));
1219        assert_eq!(
1220            command_update("k", "Update").command(),
1221            Some(ItemCommand::Update)
1222        );
1223        assert_eq!(
1224            command_update("k", "DELETE").command(),
1225            Some(ItemCommand::Delete)
1226        );
1227    }
1228
1229    #[test]
1230    fn test_unrecognized_command_is_preserved_not_rejected() {
1231        let update = command_update("k", "MERGE_ROW");
1232        assert_eq!(
1233            update.command(),
1234            Some(ItemCommand::Unrecognized("MERGE_ROW".to_owned()))
1235        );
1236        assert_eq!(
1237            update.command().as_ref().map(ItemCommand::as_str),
1238            Some("MERGE_ROW")
1239        );
1240    }
1241
1242    #[test]
1243    fn test_command_names_render_uppercase() {
1244        assert_eq!(ItemCommand::Add.to_string(), "ADD");
1245        assert_eq!(ItemCommand::Update.as_str(), "UPDATE");
1246        assert_eq!(ItemCommand::Delete.as_str(), "DELETE");
1247    }
1248
1249    #[test]
1250    fn test_non_command_subscription_has_no_key_or_command() {
1251        let update = update_with(vec![Some("a".to_owned()), None, None], vec![0]);
1252        assert!(!update.is_command_mode());
1253        assert_eq!(update.key(), None);
1254        assert_eq!(update.command(), None);
1255        assert!(!update.is_key_changed());
1256        assert!(!update.is_command_changed());
1257    }
1258
1259    #[test]
1260    fn test_a_retained_command_is_reported_as_not_changed() {
1261        // `SUBCMD,3,1,3,1,2`: key first, command second
1262        // [`docs/spec/04-notifications.md` §3.2]. This update carried only the
1263        // third field, so the key and command values it exposes are the ones an
1264        // earlier update left behind [§2.2].
1265        let schema = Arc::new(SubscriptionSchema::new(
1266            1,
1267            3,
1268            &names(&["portfolio"]),
1269            &names(&["key", "command", "qty"]),
1270            Some((0, 1)),
1271        ));
1272        let update = ItemUpdate::new(
1273            schema,
1274            0,
1275            UpdateKind::RealTime,
1276            vec![
1277                Some("AAPL".to_owned()),
1278                Some("DELETE".to_owned()),
1279                Some("10".to_owned()),
1280            ],
1281            vec![2],
1282        );
1283
1284        // The field values are what the decoding algorithm produced...
1285        assert_eq!(update.key(), Some(FieldValue::Text("AAPL")));
1286        assert_eq!(update.command(), Some(ItemCommand::Delete));
1287        // ...and this is the evidence that says they are not this update's
1288        // doing, which is what a caller must check before deleting a row.
1289        assert!(!update.is_key_changed());
1290        assert!(!update.is_command_changed());
1291    }
1292
1293    #[test]
1294    fn test_a_carried_command_is_reported_as_changed() {
1295        let update = command_update("AAPL", "DELETE");
1296        assert!(update.is_key_changed());
1297        assert!(update.is_command_changed());
1298    }
1299
1300    #[test]
1301    fn test_null_key_is_reported_as_null_not_missing() {
1302        let schema = Arc::new(SubscriptionSchema::new(
1303            1,
1304            2,
1305            &[],
1306            &names(&["key", "command"]),
1307            Some((0, 1)),
1308        ));
1309        let update = ItemUpdate::new(
1310            schema,
1311            0,
1312            UpdateKind::RealTime,
1313            vec![None, None],
1314            vec![0, 1],
1315        );
1316        // The key field is an ordinary field and may be null; that is different
1317        // from the subscription having no key field at all.
1318        assert_eq!(update.key(), Some(FieldValue::Null));
1319        // A null command names no operation.
1320        assert_eq!(update.command(), None);
1321    }
1322
1323    // -- Snapshot classification --------------------------------------------
1324
1325    #[test]
1326    fn test_update_kind_accessors() {
1327        let schema = quote_schema();
1328        let snapshot = ItemUpdate::new(
1329            Arc::clone(&schema),
1330            0,
1331            UpdateKind::Snapshot,
1332            vec![None; 3],
1333            vec![],
1334        );
1335        let live = ItemUpdate::new(schema, 1, UpdateKind::RealTime, vec![None; 3], vec![]);
1336        assert!(snapshot.is_snapshot());
1337        assert_eq!(snapshot.kind(), UpdateKind::Snapshot);
1338        assert!(!live.is_snapshot());
1339        assert_eq!(live.item_index(), 2);
1340        assert_eq!(live.item_name(), "item2");
1341    }
1342}