Skip to content

Commit 9818924

Browse files
committed
feat(sync): add CollectionSchema wire message for descriptor sync
Introduces an engine-agnostic CollectionDescriptor plus a CollectionSchemaSyncMsg envelope (opcode 0x13) so any sync peer can materialize a collection in its catalog with the correct engine and config. Bumps the wire format version to reflect the new opcode.
1 parent f9e21b3 commit 9818924

4 files changed

Lines changed: 128 additions & 1 deletion

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
//! Standardized collection descriptor + wire envelope for sync.
4+
5+
use serde::{Deserialize, Serialize};
6+
7+
use crate::collection::CollectionType;
8+
use crate::collection_config::{PartitionStrategy, PrimaryEngine, VectorPrimaryConfig};
9+
use crate::hlc::Hlc;
10+
use crate::id::DatabaseId;
11+
12+
/// Standardized, engine-agnostic descriptor of a collection's identity + engine
13+
/// config. This is the single unit that travels over sync so any peer can
14+
/// materialize the collection in its catalog with the correct engine. Reused by
15+
/// emit/receive/conversion paths in later units.
16+
#[derive(
17+
Debug,
18+
Clone,
19+
PartialEq,
20+
Serialize,
21+
Deserialize,
22+
zerompk::ToMessagePack,
23+
zerompk::FromMessagePack,
24+
)]
25+
#[msgpack(map)]
26+
pub struct CollectionDescriptor {
27+
/// Numeric tenant ID the collection belongs to.
28+
pub tenant_id: u64,
29+
/// Database the collection lives in.
30+
pub database_id: DatabaseId,
31+
/// Collection name.
32+
pub name: String,
33+
/// Storage engine + engine-specific configuration.
34+
pub collection_type: CollectionType,
35+
/// Whether the collection tracks system-time + valid-time versions.
36+
#[msgpack(default)]
37+
pub bitemporal: bool,
38+
/// Lightweight field type hints, e.g. `[("email", "string")]`.
39+
#[msgpack(default)]
40+
pub fields: Vec<(String, String)>,
41+
/// Which engine serves as the primary access path for this collection.
42+
pub primary: PrimaryEngine,
43+
/// Vector-primary configuration, present when `primary == PrimaryEngine::Vector`.
44+
#[msgpack(default)]
45+
pub vector_primary: Option<VectorPrimaryConfig>,
46+
/// How rows are distributed across vShards.
47+
pub partition_strategy: PartitionStrategy,
48+
/// Explicitly declared primary key field, if any.
49+
#[msgpack(default)]
50+
pub declared_primary_key: Option<String>,
51+
/// Monotonic version of this descriptor, bumped on schema-affecting change.
52+
#[msgpack(default)]
53+
pub descriptor_version: u64,
54+
}
55+
56+
/// Wire envelope announcing a collection's descriptor to a sync peer (opcode 0x13).
57+
#[derive(
58+
Debug,
59+
Clone,
60+
PartialEq,
61+
Serialize,
62+
Deserialize,
63+
zerompk::ToMessagePack,
64+
zerompk::FromMessagePack,
65+
)]
66+
pub struct CollectionSchemaSyncMsg {
67+
/// The collection's engine-agnostic descriptor.
68+
pub descriptor: CollectionDescriptor,
69+
/// HLC timestamp at which this descriptor was created/announced.
70+
pub creation_hlc: Hlc,
71+
}
72+
73+
#[cfg(test)]
74+
mod tests {
75+
use super::*;
76+
use crate::sync::wire::{SyncFrame, SyncMessageType};
77+
78+
#[test]
79+
fn collection_descriptor_msgpack_roundtrip() {
80+
let descriptor = CollectionDescriptor {
81+
tenant_id: 7,
82+
database_id: DatabaseId::new(1024),
83+
name: "users".into(),
84+
collection_type: CollectionType::document(),
85+
bitemporal: true,
86+
fields: vec![("email".into(), "string".into())],
87+
primary: PrimaryEngine::Document,
88+
vector_primary: None,
89+
partition_strategy: PartitionStrategy::CollectionHomed,
90+
declared_primary_key: Some("id".into()),
91+
descriptor_version: 3,
92+
};
93+
let msg = CollectionSchemaSyncMsg {
94+
descriptor,
95+
creation_hlc: Hlc {
96+
wall_ns: 123,
97+
logical: 1,
98+
},
99+
};
100+
let frame = SyncFrame::new_msgpack(SyncMessageType::CollectionSchema, &msg).unwrap();
101+
let bytes = frame.to_bytes();
102+
let decoded = SyncFrame::from_bytes(&bytes).unwrap();
103+
assert_eq!(decoded.msg_type, SyncMessageType::CollectionSchema);
104+
let decoded_msg: CollectionSchemaSyncMsg = decoded.decode_body().unwrap();
105+
assert_eq!(decoded_msg.descriptor.name, "users");
106+
assert_eq!(decoded_msg.descriptor.tenant_id, 7);
107+
assert!(decoded_msg.descriptor.bitemporal);
108+
assert_eq!(decoded_msg.descriptor.primary, PrimaryEngine::Document);
109+
}
110+
111+
#[test]
112+
fn collection_schema_opcode() {
113+
assert_eq!(
114+
SyncMessageType::from_u8(0x13),
115+
Some(SyncMessageType::CollectionSchema)
116+
);
117+
assert_eq!(SyncMessageType::CollectionSchema as u8, 0x13);
118+
}
119+
}

nodedb-types/src/sync/wire/frame.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,10 @@ pub enum SyncMessageType {
2626
DeltaPush = 0x10,
2727
DeltaAck = 0x11,
2828
DeltaReject = 0x12,
29+
/// Collection schema descriptor announcement (bidirectional, 0x13).
30+
/// Carries a `CollectionDescriptor` so any peer can materialize the
31+
/// collection in its catalog with the correct engine + config.
32+
CollectionSchema = 0x13,
2933
/// Collection purged notification (server → client, 0x14).
3034
/// Sent when an Origin collection is hard-deleted (UNDROP window
3135
/// expired or explicit `DROP COLLECTION ... PURGE`). The client
@@ -115,6 +119,7 @@ impl SyncMessageType {
115119
0x10 => Some(Self::DeltaPush),
116120
0x11 => Some(Self::DeltaAck),
117121
0x12 => Some(Self::DeltaReject),
122+
0x13 => Some(Self::CollectionSchema),
118123
0x14 => Some(Self::CollectionPurged),
119124
0x20 => Some(Self::ShapeSubscribe),
120125
0x21 => Some(Self::ShapeSnapshot),

nodedb-types/src/sync/wire/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
//! - `0x10` DeltaPush (client → server)
1111
//! - `0x11` DeltaAck (server → client)
1212
//! - `0x12` DeltaReject (server → client)
13+
//! - `0x13` CollectionSchema (bidirectional)
1314
//! - `0x14` CollectionPurged (server → client)
1415
//! - `0x20` ShapeSubscribe (client → server)
1516
//! - `0x21` ShapeSnapshot (server → client)
@@ -53,6 +54,7 @@
5354
pub mod ack_result;
5455
pub mod ack_status;
5556
pub mod array;
57+
pub mod collection_schema;
5658
pub mod columnar;
5759
pub mod delta;
5860
pub mod frame;
@@ -76,6 +78,7 @@ pub use array::{
7678
ArrayAckMsg, ArrayCatchupRequestMsg, ArrayDeltaBatchMsg, ArrayDeltaMsg, ArrayRejectMsg,
7779
ArrayRejectReason, ArraySchemaSyncMsg, ArraySnapshotChunkMsg, ArraySnapshotMsg,
7880
};
81+
pub use collection_schema::{CollectionDescriptor, CollectionSchemaSyncMsg};
7982
pub use columnar::{ColumnarInsertAckMsg, ColumnarInsertMsg};
8083
pub use delta::{CollectionPurgedMsg, DeltaAckMsg, DeltaPushMsg, DeltaRejectMsg};
8184
pub use frame::{SyncFrame, SyncMessageType};

nodedb-types/src/wire_version.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
1717
/// Cluster-wide wire format version. Stamped on every `NodeInfo` and
1818
/// returned by `nodedb::version::WIRE_FORMAT_VERSION` (a re-export).
19-
pub const WIRE_FORMAT_VERSION: u16 = 6;
19+
pub const WIRE_FORMAT_VERSION: u16 = 7;
2020

2121
/// Minimum wire format version this build can read. Equal to
2222
/// `WIRE_FORMAT_VERSION`: floor == ceiling, no backward compat window.

0 commit comments

Comments
 (0)