1use crate::checkpoint::{
2 CheckpointMutationResult, CheckpointReadConsistency, CheckpointRequestEnvelope,
3 DestinationCheckpointStatus, PreparedAttemptSummary, RetentionGap,
4};
5use crate::destination::{
6 DestinationId, DestinationOperationId, MaterializationDestination, QueryRoute,
7};
8use crate::fork::{ForkError, ForkKind};
9use crate::graph::SourceRef;
10use crate::hello::{BackendDescriptor, FilterAnnounce, OpVersions};
11use crate::kv::KvError;
12use crate::query::{Consistency, QueryError, QueryExecutionId, QueryExecutionStatus, QueryResult};
13use crate::result::ResultCode;
14use crate::schema::{Digest32, LogicalSchema, TypedValue, UuidValue};
15use crate::topology::WireTopology;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18use std::num::NonZeroU128;
19
20pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
22pub const QUERY_PATH: &str = "/agdx/query";
24pub const DESTINATIONS_PATH: &str = "/agdx/destinations";
26pub const QUERY_ROUTES_PATH: &str = "/agdx/query-routes";
28pub const PROJECTIONS_PATH: &str = "/agdx/projections";
30pub const BINDINGS_PATH: &str = "/agdx/bindings";
32pub const SCHEMAS_PATH: &str = "/agdx/schemas";
34pub const KV_PATH: &str = "/agdx/kv";
36pub const FORKS_PATH: &str = "/agdx/forks";
38pub const GRAPHS_PATH: &str = "/agdx/graphs";
40pub const GRAPH_PATH: &str = "/agdx/graph";
42pub const CLIENTS_PATH: &str = "/agdx/clients";
46pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
48pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
50pub const FILTERS_PATH: &str = "/agdx/filters";
52pub const FILTER_METRICS_PATH: &str = "/agdx/filters/metrics";
53pub const FILTER_MUTATIONS_PATH: &str = "/agdx/filters/mutations";
55pub const FILTER_VALIDATE_PATH: &str = "/agdx/filters/validate";
57pub const FILTER_TEST_PATH: &str = "/agdx/filters/test";
59pub const FILTER_PREVIEW_PATH: &str = "/agdx/filters/preview";
61pub const FILTER_BINDINGS_PATH: &str = "/agdx/filter-bindings";
63pub const GROUP_POLICIES_PATH: &str = "/agdx/group-policies";
65pub const SESSIONS_PATH: &str = "/agdx/sessions";
67
68pub fn sessions_path(stream: &str) -> String {
69 format!("{SESSIONS_PATH}/{}", path_segment(stream))
70}
71
72pub fn session_changes_path(stream: &str) -> String {
73 format!("{}/changes", sessions_path(stream))
74}
75
76pub fn session_path(stream: &str, id: crate::agent::ConversationId) -> String {
77 format!(
78 "{}/{}",
79 sessions_path(stream),
80 path_segment(&id.to_string())
81 )
82}
83
84pub fn session_events_path(stream: &str, id: crate::agent::ConversationId) -> String {
85 format!("{}/events", session_path(stream, id))
86}
87
88pub fn session_state_path(stream: &str, id: crate::agent::ConversationId) -> String {
89 format!("{}/state", session_path(stream, id))
90}
91
92pub fn session_links_path(stream: &str, id: crate::agent::ConversationId) -> String {
93 format!("{}/links", session_path(stream, id))
94}
95
96pub fn session_sources_path(stream: &str, id: crate::agent::ConversationId) -> String {
97 format!("{}/sources", session_path(stream, id))
98}
99
100pub fn filter_path(id: u32) -> String {
102 format!("{FILTERS_PATH}/{id}")
103}
104
105pub fn filter_revisions_path(id: u32) -> String {
107 format!("{FILTERS_PATH}/{id}/revisions")
108}
109
110pub fn filter_operation_path(operation_id: u128) -> String {
112 format!("{FILTERS_PATH}/operations/{operation_id}")
113}
114
115pub fn filter_binding_path(stream: &str, topic: &str, group: &str) -> String {
119 format!(
120 "{FILTER_BINDINGS_PATH}/{}/{}/{}",
121 path_segment(stream),
122 path_segment(topic),
123 path_segment(group)
124 )
125}
126
127pub fn group_policy_path(stream: &str, topic: &str, group: &str) -> String {
128 format!(
129 "{GROUP_POLICIES_PATH}/{}/{}/{}",
130 path_segment(stream),
131 path_segment(topic),
132 path_segment(group)
133 )
134}
135
136pub fn path_segment(text: &str) -> String {
139 let mut out = String::with_capacity(text.len());
140 for byte in text.bytes() {
141 if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~') {
142 out.push(char::from(byte));
143 } else {
144 out.push_str(&format!("%{byte:02X}"));
145 }
146 }
147 out
148}
149
150pub fn destination_path(id: DestinationId) -> String {
151 format!("{DESTINATIONS_PATH}/{id}")
152}
153
154pub fn destination_enable_path(id: DestinationId) -> String {
155 format!("{}/{id}/enable", DESTINATIONS_PATH)
156}
157
158pub fn destination_disable_path(id: DestinationId) -> String {
159 format!("{}/{id}/disable", DESTINATIONS_PATH)
160}
161
162pub fn destination_status_path(id: DestinationId) -> String {
163 format!("{}/{id}/status", DESTINATIONS_PATH)
164}
165
166pub fn destination_checkpoint_path(id: DestinationId) -> String {
167 format!("{}/{id}/checkpoint", DESTINATIONS_PATH)
168}
169
170pub fn destination_retention_gap_path(id: DestinationId) -> String {
171 format!("{}/{id}/retention-gap", DESTINATIONS_PATH)
172}
173
174pub fn destination_prepared_attempt_path(id: DestinationId) -> String {
175 format!("{}/{id}/prepared-attempt", DESTINATIONS_PATH)
176}
177
178pub fn destination_table_path(id: DestinationId) -> String {
179 format!("{}/{id}/table", DESTINATIONS_PATH)
180}
181
182pub fn destination_table_schema_path(id: DestinationId) -> String {
183 format!("{}/{id}/table/schema", DESTINATIONS_PATH)
184}
185
186pub fn destination_snapshots_path(id: DestinationId) -> String {
187 format!("{}/{id}/table/snapshots", DESTINATIONS_PATH)
188}
189
190pub fn destination_current_snapshot_path(id: DestinationId) -> String {
191 format!("{}/{id}/table/current-snapshot", DESTINATIONS_PATH)
192}
193
194pub fn destination_snapshot_path(id: DestinationId, snapshot_id: i64) -> String {
195 format!("{}/{id}/table/snapshots/{snapshot_id}", DESTINATIONS_PATH)
196}
197
198pub fn destination_files_path(id: DestinationId) -> String {
199 format!("{}/{id}/table/files", DESTINATIONS_PATH)
200}
201
202pub fn destination_metrics_path(id: DestinationId) -> String {
203 format!("{}/{id}/table/metrics", DESTINATIONS_PATH)
204}
205
206pub fn query_execution_path(id: QueryExecutionId) -> String {
207 format!("{QUERY_PATH}/{id}")
208}
209
210pub fn query_page_path(id: QueryExecutionId) -> String {
211 format!("{QUERY_PATH}/{id}/pages")
212}
213
214pub fn query_cancel_path(id: QueryExecutionId) -> String {
215 format!("{QUERY_PATH}/{id}/cancel")
216}
217
218pub fn destination_operation_path(id: DestinationOperationId) -> String {
219 format!("{DESTINATIONS_PATH}/operations/{id}")
220}
221
222#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct CheckpointReadQuery {
224 pub consistency: CheckpointReadConsistency,
225}
226
227impl CheckpointReadQuery {
228 pub const fn new(consistency: CheckpointReadConsistency) -> Self {
229 Self { consistency }
230 }
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234pub struct DestinationListQuery {
235 #[serde(default, skip_serializing_if = "Option::is_none")]
236 pub source_stream: Option<String>,
237 #[serde(default, skip_serializing_if = "Option::is_none")]
238 pub source_topic: Option<String>,
239 #[serde(default, skip_serializing_if = "Option::is_none")]
240 pub name_contains: Option<String>,
241 #[serde(default, skip_serializing_if = "Option::is_none")]
242 pub after: Option<String>,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub limit: Option<usize>,
245 pub consistency: CheckpointReadConsistency,
246}
247
248#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
249pub struct DestinationPageView {
250 pub destinations: Vec<DestinationView>,
251 #[serde(default, skip_serializing_if = "Option::is_none")]
252 pub next_cursor: Option<String>,
253 pub global_state_revision: u64,
254 pub consistency: crate::checkpoint::CheckpointReadConsistency,
255}
256
257#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
258pub struct DestinationView {
259 pub destination: MaterializationDestination,
260 pub status: DestinationCheckpointStatus,
261}
262
263#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
264pub struct DestinationMutationBody {
265 pub checkpoint: CheckpointRequestEnvelope,
266}
267
268#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
269pub struct AcceptedOperationView {
270 pub operation_id: DestinationOperationId,
271 pub request_id: crate::checkpoint::CheckpointRequestId,
272 pub state: OperationState,
273 pub submitted_at_micros: u64,
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub completed_at_micros: Option<u64>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub error: Option<OperationErrorView>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub result: Option<CheckpointMutationResult>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct OperationErrorView {
284 pub code: ResultCode,
285 pub message: String,
286}
287
288#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
289#[serde(rename_all = "snake_case")]
290#[non_exhaustive]
291pub enum OperationState {
292 Accepted,
293 Running,
294 Succeeded,
295 Failed,
296 Cancelled,
297}
298
299#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
300pub struct QueryRouteListQuery {
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub name_contains: Option<String>,
303 #[serde(default, skip_serializing_if = "Option::is_none")]
304 pub after: Option<String>,
305 #[serde(default, skip_serializing_if = "Option::is_none")]
306 pub limit: Option<usize>,
307 pub consistency: CheckpointReadConsistency,
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
311pub struct QueryRoutePageView {
312 pub routes: Vec<QueryRoute>,
313 #[serde(default, skip_serializing_if = "Option::is_none")]
314 pub next_cursor: Option<String>,
315 pub definition_revision: u64,
316 pub global_state_revision: u64,
317 pub consistency: CheckpointReadConsistency,
318}
319
320#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
321pub struct TableView {
322 pub table_uuid: UuidValue,
323 pub destination_id: DestinationId,
324 pub destination_generation: u64,
325 pub namespace: Vec<String>,
326 pub table: String,
327 pub current_snapshot_id: i64,
328 pub current_schema_id: i32,
329 pub current_partition_spec_id: i32,
330 pub metadata_identity: String,
331 pub properties: BTreeMap<String, String>,
332}
333
334#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
335pub struct TableSchemaView {
336 pub table_uuid: UuidValue,
337 pub iceberg_schema_id: i32,
338 pub logical_schema: LogicalSchema,
339}
340
341#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
342pub struct SnapshotListQuery {
343 #[serde(default, skip_serializing_if = "Option::is_none")]
344 pub before_snapshot_id: Option<i64>,
345 #[serde(default, skip_serializing_if = "Option::is_none")]
346 pub limit: Option<usize>,
347}
348
349#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
350pub struct SnapshotPageView {
351 pub snapshots: Vec<TableSnapshotView>,
352 #[serde(default, skip_serializing_if = "Option::is_none")]
353 pub next_before_snapshot_id: Option<i64>,
354}
355
356#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
357pub struct TableSnapshotView {
358 pub snapshot_id: i64,
359 #[serde(default, skip_serializing_if = "Option::is_none")]
360 pub parent_snapshot_id: Option<i64>,
361 pub sequence_number: i64,
362 pub committed_at_micros: u64,
363 pub schema_id: i32,
364 pub partition_spec_id: i32,
365 pub materialization_boundary_digest: Digest32,
366 pub checkpoint_revision: u64,
367 pub summary: BTreeMap<String, String>,
368}
369
370#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
371pub struct TableFileListQuery {
372 #[serde(default, skip_serializing_if = "Option::is_none")]
373 pub snapshot_id: Option<i64>,
374 #[serde(default, skip_serializing_if = "Option::is_none")]
375 pub after: Option<String>,
376 #[serde(default, skip_serializing_if = "Option::is_none")]
377 pub limit: Option<usize>,
378}
379
380#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
381pub struct TableFilePageView {
382 pub files: Vec<TableFileView>,
383 #[serde(default, skip_serializing_if = "Option::is_none")]
384 pub next_cursor: Option<String>,
385}
386
387#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
388pub struct TableFileView {
389 pub object_identity: String,
390 pub file_size_bytes: u64,
391 pub row_count: u64,
392 pub partition: BTreeMap<String, TypedValue>,
393 pub lower_bounds: BTreeMap<u32, TypedValue>,
394 pub upper_bounds: BTreeMap<u32, TypedValue>,
395 pub null_value_counts: BTreeMap<u32, u64>,
396}
397
398#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
399pub struct TableMetricsView {
400 pub snapshot_id: i64,
401 pub data_file_count: u64,
402 pub delete_file_count: u64,
403 pub total_rows: u64,
404 pub total_bytes: u64,
405 pub partition_count: u64,
406}
407
408#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
409pub struct QueryPageBody {
410 pub cursor: String,
411 pub deadline_micros: u64,
412}
413
414#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
415pub struct QueryExecutionView {
416 pub status: QueryExecutionStatus,
417 #[serde(default, skip_serializing_if = "Option::is_none")]
418 pub result: Option<QueryResult>,
419}
420
421#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
422pub struct DestinationIssueView {
423 #[serde(default, skip_serializing_if = "Option::is_none")]
424 pub retention_gap: Option<RetentionGap>,
425 #[serde(default, skip_serializing_if = "Option::is_none")]
426 pub prepared_attempt: Option<PreparedAttemptSummary>,
427 #[serde(default, skip_serializing_if = "Option::is_none")]
428 pub block: Option<crate::checkpoint::DestinationBlock>,
429 pub global_state_revision: u64,
430 pub checkpoint_revision: u64,
431 pub consistency: CheckpointReadConsistency,
432}
433
434#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
436#[serde(rename_all = "snake_case")]
437#[non_exhaustive]
438pub enum DestinationHttpOperation {
439 ListDestinations,
440 GetDestination,
441 CreateDestination,
442 EnableDestination,
443 DisableDestination,
444 GetOperation,
445 GetStatus,
446 GetCheckpoint,
447 GetRetentionGap,
448 GetPreparedAttempt,
449 ListQueryRoutes,
450 GetTable,
451 GetTableSchema,
452 GetCurrentSnapshot,
453 ListSnapshots,
454 GetSnapshot,
455 ListFiles,
456 GetMetrics,
457}
458
459#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
465pub struct DestinationHttpRequest {
466 pub v: u32,
467 pub operation: DestinationHttpOperation,
468 pub parameters: Vec<String>,
469 pub query: Vec<u8>,
470 pub body: Vec<u8>,
471}
472
473#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
475pub struct DestinationHttpReply {
476 pub status: u16,
477 pub body: Vec<u8>,
478}
479
480pub fn authz_role_path(name: &str) -> String {
482 format!("{AUTHZ_ROLES_PATH}/{name}")
483}
484
485pub fn authz_user_roles_path(user_id: u32) -> String {
487 format!("/agdx/authz/users/{user_id}/roles")
488}
489
490pub fn graph_path(id: &str) -> String {
494 format!("{GRAPHS_PATH}/{}", path_segment(id))
495}
496
497pub fn graph_query_path(name: &str) -> String {
499 format!("{GRAPH_PATH}/{}/query", path_segment(name))
500}
501
502pub fn graph_neighbors_path(name: &str, node: &str) -> String {
504 format!(
505 "{GRAPH_PATH}/{}/neighbors/{}",
506 path_segment(name),
507 path_segment(node)
508 )
509}
510
511pub fn projection_path(id: &str) -> String {
513 format!("{PROJECTIONS_PATH}/{}", path_segment(id))
514}
515
516pub fn schema_path(id: u32) -> String {
518 format!("{SCHEMAS_PATH}/{id}")
519}
520
521pub fn schema_decode_path(id: u32) -> String {
523 format!("{SCHEMAS_PATH}/{id}/decode")
524}
525
526pub fn kv_namespace_path(namespace: &str) -> String {
528 format!("{KV_PATH}/{}", path_segment(namespace))
529}
530
531pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
535 format!("{}/{}", kv_namespace_path(namespace), path_segment(key_b64))
536}
537
538pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
545 format!("{}/cas", kv_entry_path(namespace, key_b64))
546}
547
548pub fn fork_path(id: &str) -> String {
550 format!("{FORKS_PATH}/{}", path_segment(id))
551}
552
553pub fn fork_promote_path(id: &str) -> String {
555 format!("{}/promote", fork_path(id))
556}
557
558pub fn fork_rows_path(id: &str) -> String {
560 format!("{}/rows", fork_path(id))
561}
562
563#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
570#[non_exhaustive]
571pub struct Capabilities {
572 pub managed: bool,
576 pub query: QueryCapsView,
579 pub destinations: DestinationCapsView,
581 pub kv: KvCapsView,
583 #[serde(default)]
587 pub graph: bool,
588 pub fork: bool,
590 #[serde(default)]
592 pub sessions: bool,
593 #[serde(default)]
597 pub watch: bool,
598 #[serde(default)]
601 pub authz: bool,
602 #[serde(default)]
605 pub filters: FilterCapsView,
606 pub versions: OpVersions,
607 #[serde(default, skip_serializing_if = "Vec::is_empty")]
612 pub backends: Vec<BackendDescriptor>,
613 #[serde(default, skip_serializing_if = "Option::is_none")]
619 pub topology: Option<WireTopology>,
620}
621
622#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
625pub struct QueryCapsView {
626 pub available: bool,
628 pub projections: bool,
630 pub schemas: bool,
632 #[serde(default)]
636 pub consistency: Consistency,
637 #[serde(default)]
641 pub keyword: bool,
642 #[serde(default)]
644 pub cursor_paging: bool,
645 #[serde(default)]
647 pub cancellation: bool,
648 #[serde(default)]
650 pub execution_status: bool,
651}
652
653#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
654pub struct DestinationCapsView {
655 pub available: bool,
656 #[serde(default)]
657 pub lifecycle: bool,
658 #[serde(default)]
659 pub checkpoint_status: bool,
660 #[serde(default)]
661 pub query_routes: bool,
662 #[serde(default)]
663 pub table_schema: bool,
664 #[serde(default)]
665 pub snapshots: bool,
666 #[serde(default)]
667 pub files: bool,
668 #[serde(default)]
669 pub metrics: bool,
670 #[serde(default)]
671 pub strongest_consistency: crate::checkpoint::CheckpointReadConsistency,
672}
673
674#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
678pub struct FilterCapsView {
679 #[serde(default)]
681 pub native: bool,
682 #[serde(default)]
684 pub group_policy_reads: bool,
685 #[serde(default)]
687 pub preview: bool,
688 #[serde(default)]
690 pub primary_routing: bool,
691 #[serde(default)]
693 pub catalog: bool,
694 #[serde(default)]
696 pub managed_groups: bool,
697 #[serde(default)]
699 pub evaluator_version: u32,
700 #[serde(default, skip_serializing_if = "Vec::is_empty")]
702 pub codecs: Vec<crate::filter::FilterCodec>,
703}
704
705impl FilterCapsView {
706 pub fn served(native: bool, catalog: bool) -> Self {
708 let evaluation = native.then(FilterAnnounce::served);
709 Self {
710 native,
711 group_policy_reads: false,
712 preview: native,
713 primary_routing: native,
714 catalog,
715 managed_groups: catalog,
716 evaluator_version: evaluation
717 .as_ref()
718 .map_or(0, |evaluation| evaluation.evaluator_version),
719 codecs: evaluation
720 .map(|evaluation| evaluation.codecs)
721 .unwrap_or_default(),
722 }
723 }
724
725 #[must_use]
726 pub const fn with_group_policy_reads(mut self, served: bool) -> Self {
727 self.group_policy_reads = served;
728 self
729 }
730
731 #[must_use]
734 pub fn with_evaluation(mut self, evaluation: FilterAnnounce) -> Self {
735 if self.native {
736 self.evaluator_version = evaluation.evaluator_version;
737 self.codecs = evaluation.codecs;
738 }
739 self
740 }
741}
742
743#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
745pub struct KvCapsView {
746 pub available: bool,
748 #[serde(default)]
752 pub cas: bool,
753 #[serde(default)]
757 pub cas_fenced: bool,
758 #[serde(default)]
761 pub fenced_leases: bool,
762}
763
764impl Capabilities {
765 pub fn new(enabled: bool, versions: OpVersions) -> Self {
772 Self {
773 managed: enabled,
774 query: QueryCapsView {
775 available: enabled,
776 projections: enabled,
777 schemas: enabled,
778 consistency: Consistency::Eventual,
779 keyword: false,
780 cursor_paging: false,
781 cancellation: false,
782 execution_status: false,
783 },
784 destinations: DestinationCapsView {
785 available: false,
786 lifecycle: false,
787 checkpoint_status: false,
788 query_routes: false,
789 table_schema: false,
790 snapshots: false,
791 files: false,
792 metrics: false,
793 strongest_consistency:
794 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
795 },
796 kv: KvCapsView {
797 available: enabled,
798 cas: false,
799 cas_fenced: false,
800 fenced_leases: false,
801 },
802 graph: false,
803 fork: enabled,
804 sessions: false,
805 watch: false,
806 authz: false,
807 filters: FilterCapsView::default(),
808 versions,
809 backends: Vec::new(),
810 topology: None,
811 }
812 }
813
814 #[must_use]
816 pub fn with_filters(mut self, filters: FilterCapsView) -> Self {
817 self.filters = filters;
818 self
819 }
820
821 #[must_use]
824 pub fn with_graph(mut self, value: bool) -> Self {
825 self.graph = value;
826 self
827 }
828
829 #[must_use]
831 pub fn with_query_keyword(mut self, value: bool) -> Self {
832 self.query.keyword = value;
833 self
834 }
835
836 #[must_use]
837 pub fn with_query_execution(mut self, paging: bool, cancellation: bool, status: bool) -> Self {
838 self.query.cursor_paging = paging;
839 self.query.cancellation = cancellation;
840 self.query.execution_status = status;
841 self
842 }
843
844 #[must_use]
845 pub fn with_destinations(mut self, destinations: DestinationCapsView) -> Self {
846 self.destinations = destinations;
847 self
848 }
849
850 #[must_use]
852 pub fn with_sessions(mut self, value: bool) -> Self {
853 self.sessions = value;
854 self
855 }
856
857 #[must_use]
860 pub fn with_watch(mut self, value: bool) -> Self {
861 self.watch = value;
862 self
863 }
864
865 #[must_use]
867 pub fn with_authz(mut self, value: bool) -> Self {
868 self.authz = value;
869 self
870 }
871
872 #[must_use]
875 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
876 self.backends = backends;
877 self
878 }
879
880 #[must_use]
883 pub fn with_topology(mut self, topology: WireTopology) -> Self {
884 self.topology = Some(topology);
885 self
886 }
887
888 #[must_use]
891 pub fn with_kv_cas(mut self, on: bool) -> Self {
892 self.kv.cas = on;
893 self
894 }
895
896 #[must_use]
899 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
900 self.kv.cas_fenced = on;
901 self
902 }
903
904 #[must_use]
907 pub fn with_kv_fenced_leases(mut self, on: bool) -> Self {
908 self.kv.fenced_leases = on;
909 self.kv.cas_fenced |= on;
910 self
911 }
912
913 #[must_use]
915 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
916 self.query.consistency = level;
917 self
918 }
919
920 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
926 use crate::hello::feature;
927 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
928 Consistency::Strong
929 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
930 Consistency::ReadYourWrites
931 } else {
932 Consistency::Eventual
933 };
934 let destinations_available =
935 enabled && versions.checkpoint > 0 && versions.has_feature(feature::DESTINATIONS);
936 Self::new(enabled, versions)
937 .with_kv_cas(versions.has_feature(feature::KV_CAS))
938 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
939 .with_kv_fenced_leases(versions.has_feature(feature::KV_FENCED_LEASES))
940 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
941 .with_watch(versions.has_feature(feature::WATCH))
942 .with_sessions(enabled && versions.has_feature(feature::SESSIONS))
943 .with_authz(versions.has_feature(feature::AUTHZ))
944 .with_query_consistency(consistency)
945 .with_destinations(DestinationCapsView {
946 available: destinations_available,
947 lifecycle: destinations_available,
948 checkpoint_status: destinations_available,
949 query_routes: destinations_available,
950 table_schema: false,
951 snapshots: false,
952 files: false,
953 metrics: false,
954 strongest_consistency:
955 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
956 })
957 .with_graph(enabled && versions.graph > 0)
960 .with_filters(
961 FilterCapsView::served(
962 versions.has_feature(feature::CONSUMER_FILTERS),
963 enabled && versions.filter > 0,
964 )
965 .with_group_policy_reads(versions.has_feature(feature::GROUP_POLICY_READS)),
966 )
967 }
968}
969
970#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
974pub struct KvEntryView {
975 pub key: String,
976 pub value: String,
977 pub expires_at_micros: Option<u64>,
978 #[serde(default, skip_serializing_if = "Option::is_none")]
983 pub scope: Option<crate::kv::MemoryRowScope>,
984 #[serde(default, skip_serializing_if = "Option::is_none")]
989 pub source: Option<crate::graph::SourceRef>,
990}
991
992#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
994pub struct KvPageView {
995 pub entries: Vec<KvEntryView>,
996 pub cursor: Option<String>,
997}
998
999#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1001pub struct DeletedManyView {
1002 pub deleted: usize,
1003}
1004
1005#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1010pub struct ClientMetadataView {
1011 pub client_id: u32,
1012 pub user_id: Option<u32>,
1013 pub transport: u8,
1014 pub address: String,
1015 pub consumer_groups_count: u32,
1016 #[serde(default, skip_serializing_if = "Option::is_none")]
1017 pub metadata: Option<String>,
1018}
1019
1020#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1023pub struct ClientMetadataListView {
1024 pub clients: Vec<ClientMetadataView>,
1025 #[serde(default, skip_serializing_if = "Option::is_none")]
1026 pub next_cursor: Option<u32>,
1027}
1028
1029#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1033pub struct ClientsQuery {
1034 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
1036 pub with_metadata_only: bool,
1037 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub user_id: Option<u32>,
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1042 pub after: Option<u32>,
1043 #[serde(default, skip_serializing_if = "Option::is_none")]
1045 pub limit: Option<u32>,
1046}
1047
1048#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1050pub struct PromotedView {
1051 pub rows: usize,
1052}
1053
1054#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1058pub struct GraphNodeView {
1059 pub id: String,
1060 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1061 pub labels: Vec<String>,
1062 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1063 pub attrs: Vec<(String, String)>,
1064 #[serde(default, skip_serializing_if = "Option::is_none")]
1066 pub source: Option<SourceRef>,
1067}
1068
1069#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1072pub struct GraphEdgeView {
1073 pub id: String,
1074 pub from: String,
1075 pub to: String,
1076 pub edge_type: String,
1077 pub weight: f32,
1078 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub valid_from: Option<u64>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub valid_to: Option<u64>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1086 pub source: Option<SourceRef>,
1087}
1088
1089#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1092pub struct GraphResultView {
1093 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1094 pub nodes: Vec<GraphNodeView>,
1095 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1096 pub edges: Vec<GraphEdgeView>,
1097 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1098 pub paths: Vec<PathView>,
1099}
1100
1101#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
1104pub struct PathView {
1105 pub nodes: Vec<String>,
1106 pub edges: Vec<String>,
1107}
1108
1109#[derive(Clone, Debug, Serialize, Deserialize)]
1113pub struct RegisterSchemaBody {
1114 pub source: crate::control::SchemaSource,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub name: Option<String>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1118 pub version: Option<u32>,
1119 #[serde(default, skip_serializing_if = "Option::is_none")]
1122 pub stream: Option<String>,
1123}
1124
1125#[derive(Clone, Debug, Serialize, Deserialize)]
1128pub struct DecodeRecordBody {
1129 pub payload: String,
1130}
1131
1132#[derive(Clone, Debug, Serialize, Deserialize)]
1134pub struct ForkCreateBody {
1135 pub fork_id: String,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub parent: Option<String>,
1138 #[serde(default)]
1139 pub kind: ForkKind,
1140 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1141 pub tables: Vec<String>,
1142}
1143
1144#[derive(Clone, Debug, Serialize, Deserialize)]
1147pub struct ForkPutBody {
1148 pub table: String,
1149 pub partition_id: u32,
1150 pub offset: u64,
1151 #[serde(default)]
1152 pub projection_id: String,
1153 #[serde(default)]
1154 pub projection_version: u32,
1155 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1156 pub fields: BTreeMap<String, String>,
1157 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
1158 pub metadata: BTreeMap<String, String>,
1159 #[serde(default, skip_serializing_if = "Option::is_none")]
1160 pub payload_b64: Option<String>,
1161 #[serde(default, skip_serializing_if = "Option::is_none")]
1162 pub embedding: Option<String>,
1163 #[serde(default)]
1164 pub tombstone: bool,
1165}
1166
1167#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1172pub struct RemoveBindingBody {
1173 pub stream: String,
1174 pub topic: String,
1175 #[serde(default, skip_serializing_if = "Option::is_none")]
1176 pub projection_ref: Option<String>,
1177}
1178
1179#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1188pub struct ErrorBody {
1189 pub code: ResultCode,
1190 pub message: String,
1191 #[serde(default, skip_serializing_if = "Option::is_none")]
1192 pub detail: Option<serde_json::Value>,
1193}
1194
1195impl ErrorBody {
1196 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
1198 Self {
1199 code,
1200 message: message.into(),
1201 detail: None,
1202 }
1203 }
1204
1205 #[must_use]
1207 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
1208 self.detail = Some(detail);
1209 self
1210 }
1211
1212 pub fn http_status(&self) -> u16 {
1215 self.code.http_status()
1216 }
1217}
1218
1219impl From<&QueryError> for ErrorBody {
1220 fn from(error: &QueryError) -> Self {
1221 Self::new(ResultCode::from(error), error.to_string())
1222 }
1223}
1224
1225impl From<&crate::filter::FilterError> for ErrorBody {
1226 fn from(error: &crate::filter::FilterError) -> Self {
1227 Self::new(error.code, error.message.clone())
1228 .with_detail(serde_json::json!({ "reason": error.reason }))
1229 }
1230}
1231
1232impl From<&KvError> for ErrorBody {
1233 fn from(error: &KvError) -> Self {
1234 Self::new(ResultCode::from(error), error.to_string())
1235 }
1236}
1237
1238impl From<&ForkError> for ErrorBody {
1239 fn from(error: &ForkError) -> Self {
1240 Self::new(ResultCode::from(error), error.to_string())
1241 }
1242}
1243
1244impl From<&crate::authz::AuthzError> for ErrorBody {
1245 fn from(error: &crate::authz::AuthzError) -> Self {
1246 Self::new(ResultCode::from(error), error.to_string())
1247 }
1248}
1249
1250impl From<&crate::session::SessionError> for ErrorBody {
1251 fn from(error: &crate::session::SessionError) -> Self {
1252 Self::new(ResultCode::from(error), error.to_string())
1253 }
1254}
1255
1256pub const PARAM_TOPIC: &str = "topic";
1258pub const PARAM_NAME_CONTAINS: &str = "name_contains";
1260pub const PARAM_ID_PREFIX: &str = "id_prefix";
1262pub const PARAM_STREAM: &str = "stream";
1265pub const PARAM_SEARCH: &str = "search";
1270pub const PARAM_PREFIX: &str = "prefix";
1272pub const PARAM_START: &str = "start";
1274pub const PARAM_END: &str = "end";
1276pub const PARAM_KEY_CONTAINS: &str = "key_contains";
1278pub const PARAM_LIMIT: &str = "limit";
1280pub const PARAM_CURSOR: &str = "cursor";
1282pub const PARAM_STATUS: &str = "status";
1284pub const PARAM_AGENT: &str = "agent";
1286pub const PARAM_TEXT: &str = "text";
1288pub const PARAM_ROOT: &str = "root";
1290pub const PARAM_LABEL_PREFIX: &str = "label_prefix";
1292pub const PARAM_SURFACE: &str = "surface";
1294pub const PARAM_AFTER: &str = "after";
1296pub const PARAM_TOTAL: &str = "total";
1298pub const PARAM_FIXED_FRONTIER: &str = "fixed_frontier";
1301pub const PARAM_HISTORY_LIMIT: &str = "history_limit";
1303pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
1305pub const PARAM_EXPECT_VERSION: &str = "expect_version";
1308pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
1311
1312pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
1318
1319#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1324pub struct ProjectionListQuery {
1325 #[serde(default, skip_serializing_if = "Option::is_none")]
1326 pub topic: Option<String>,
1327 #[serde(default, skip_serializing_if = "Option::is_none")]
1328 pub name_contains: Option<String>,
1329 #[serde(default, skip_serializing_if = "Option::is_none")]
1330 pub id_prefix: Option<String>,
1331 #[serde(default, skip_serializing_if = "Option::is_none")]
1334 pub search: Option<String>,
1335}
1336
1337#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1341pub struct SchemaListQuery {
1342 #[serde(default, skip_serializing_if = "Option::is_none")]
1343 pub name_contains: Option<String>,
1344 #[serde(default, skip_serializing_if = "Option::is_none")]
1347 pub stream: Option<String>,
1348}
1349
1350#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1354pub struct SchemaQuery {
1355 #[serde(default, skip_serializing_if = "Option::is_none")]
1356 pub stream: Option<String>,
1357}
1358
1359#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1365pub struct OperationQuery {
1366 #[serde(default, skip_serializing_if = "Option::is_none")]
1367 pub operation_id: Option<String>,
1368}
1369
1370impl OperationQuery {
1371 pub fn operation_id(&self) -> Result<Option<NonZeroU128>, InvalidOperationId> {
1373 self.operation_id
1374 .as_deref()
1375 .map(|text| {
1376 if !text.bytes().all(|byte| byte.is_ascii_digit()) {
1378 return Err(InvalidOperationId);
1379 }
1380 text.parse::<NonZeroU128>().map_err(|_| InvalidOperationId)
1381 })
1382 .transpose()
1383 }
1384}
1385
1386impl From<NonZeroU128> for OperationQuery {
1387 fn from(operation_id: NonZeroU128) -> Self {
1388 Self {
1389 operation_id: Some(operation_id.to_string()),
1390 }
1391 }
1392}
1393
1394#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1396#[error("operation_id must be a non-zero decimal u128")]
1397pub struct InvalidOperationId;
1398
1399#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1401pub struct FilterListQuery {
1402 #[serde(default, skip_serializing_if = "Option::is_none")]
1403 pub name_contains: Option<String>,
1404 #[serde(default, skip_serializing_if = "Option::is_none")]
1405 pub state: Option<crate::filter::FilterState>,
1406 #[serde(default, skip_serializing_if = "Option::is_none")]
1408 pub before_id: Option<u32>,
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 pub page: Option<u32>,
1411 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 pub page_size: Option<u32>,
1413}
1414
1415#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1417pub struct FilterRevisionListQuery {
1418 #[serde(default, skip_serializing_if = "Option::is_none")]
1419 pub page: Option<u32>,
1420 #[serde(default, skip_serializing_if = "Option::is_none")]
1421 pub page_size: Option<u32>,
1422}
1423
1424#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1426pub struct FilterBindingListQuery {
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub filter_id: Option<u32>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 pub stream: Option<String>,
1431 #[serde(default, skip_serializing_if = "Option::is_none")]
1432 pub topic: Option<String>,
1433 #[serde(default, skip_serializing_if = "Option::is_none")]
1434 pub page: Option<u32>,
1435 #[serde(default, skip_serializing_if = "Option::is_none")]
1436 pub page_size: Option<u32>,
1437}
1438
1439#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1442pub struct FilterTestBody {
1443 pub filter: crate::filter::FilterRef,
1444 #[serde(default, skip_serializing_if = "Option::is_none")]
1445 pub payload_base64: Option<String>,
1446 #[serde(default, skip_serializing_if = "Option::is_none")]
1447 pub payload_text: Option<String>,
1448 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1449 pub headers: Vec<crate::filter::FilterHeader>,
1450}
1451
1452#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1456pub struct KvScanQuery {
1457 #[serde(default, skip_serializing_if = "Option::is_none")]
1458 pub prefix: Option<String>,
1459 #[serde(default, skip_serializing_if = "Option::is_none")]
1460 pub start: Option<String>,
1461 #[serde(default, skip_serializing_if = "Option::is_none")]
1462 pub end: Option<String>,
1463 #[serde(default, skip_serializing_if = "Option::is_none")]
1464 pub key_contains: Option<String>,
1465 #[serde(default, skip_serializing_if = "Option::is_none")]
1470 pub conversation: Option<String>,
1471 #[serde(default, skip_serializing_if = "Option::is_none")]
1472 pub limit: Option<usize>,
1473 #[serde(default, skip_serializing_if = "Option::is_none")]
1474 pub cursor: Option<String>,
1475}
1476
1477#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1479pub struct KvPutQuery {
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub expires_at_micros: Option<u64>,
1482}
1483
1484#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1490pub struct GraphNeighborsQuery {
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub dir: Option<String>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1494 pub edge_type: Option<String>,
1495 #[serde(default, skip_serializing_if = "Option::is_none")]
1496 pub depth: Option<u32>,
1497 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub limit: Option<usize>,
1499 #[serde(default, skip_serializing_if = "Option::is_none")]
1501 pub as_of: Option<u64>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub conversation: Option<String>,
1507}
1508
1509#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1512pub struct SessionsQuery {
1513 #[serde(default, skip_serializing_if = "Option::is_none")]
1514 pub status: Option<String>,
1515 #[serde(default, skip_serializing_if = "Option::is_none")]
1516 pub root: Option<String>,
1517 #[serde(default, skip_serializing_if = "Option::is_none")]
1518 pub label_prefix: Option<String>,
1519 #[serde(default, skip_serializing_if = "Option::is_none")]
1520 pub agent: Option<String>,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub text: Option<String>,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub cursor: Option<String>,
1525 #[serde(default, skip_serializing_if = "Option::is_none")]
1526 pub limit: Option<u32>,
1527 #[serde(default, rename = "total")]
1528 pub want_total: bool,
1529}
1530
1531#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1534pub struct SessionEventsQuery {
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub cursor: Option<String>,
1537 #[serde(default, skip_serializing_if = "Option::is_none")]
1538 pub limit: Option<u32>,
1539 #[serde(default)]
1540 pub fixed_frontier: bool,
1541}
1542
1543#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1546pub struct SessionStateQuery {
1547 #[serde(default, skip_serializing_if = "Option::is_none")]
1548 pub history_limit: Option<u32>,
1549}
1550
1551#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1553pub struct SessionLinksQuery {
1554 #[serde(default, skip_serializing_if = "Option::is_none")]
1555 pub surface: Option<String>,
1556}
1557
1558#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1561pub struct SessionChangesQuery {
1562 #[serde(default)]
1563 pub after: u64,
1564 #[serde(default, skip_serializing_if = "Option::is_none")]
1565 pub limit: Option<u32>,
1566 #[serde(default, skip_serializing_if = "Option::is_none")]
1567 pub series: Option<u64>,
1568}
1569
1570#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1575pub struct KvCasQuery {
1576 #[serde(default, skip_serializing_if = "Option::is_none")]
1577 pub expect_version: Option<u64>,
1578 #[serde(default, skip_serializing_if = "Option::is_none")]
1579 pub expect_absent: Option<bool>,
1580 #[serde(default, skip_serializing_if = "Option::is_none")]
1581 pub expires_at_micros: Option<u64>,
1582}
1583
1584#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1587pub struct CasCommittedView {
1588 pub version: u64,
1589}
1590
1591#[cfg(test)]
1592mod tests {
1593 use super::*;
1594
1595 #[test]
1596 fn given_path_builders_when_rendered_then_should_match_the_router() {
1597 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1598 assert_eq!(
1599 projection_path("reading.v1"),
1600 "/agdx/projections/reading.v1"
1601 );
1602 assert_eq!(schema_path(7), "/agdx/schemas/7");
1603 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1604 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1605 assert_eq!(
1606 kv_entry_path("sessions", "dXNlcjo0Mg"),
1607 "/agdx/kv/sessions/dXNlcjo0Mg"
1608 );
1609 assert_eq!(
1610 kv_cas_path("sessions", "dXNlcjo0Mg"),
1611 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1612 );
1613 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1614 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1615 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1616 }
1617
1618 #[test]
1619 fn given_an_operation_id_when_rendered_and_parsed_then_should_round_trip() {
1620 let operation_id = NonZeroU128::new(u128::MAX).expect("non-zero");
1621 let query = OperationQuery::from(operation_id);
1622 assert_eq!(
1623 query.operation_id.as_deref(),
1624 Some("340282366920938463463374607431768211455")
1625 );
1626 assert_eq!(query.operation_id(), Ok(Some(operation_id)));
1627 assert_eq!(OperationQuery::default().operation_id(), Ok(None));
1628 }
1629
1630 #[test]
1631 fn given_a_malformed_operation_id_when_parsed_then_should_refuse_it() {
1632 for text in [
1633 "",
1634 "0",
1635 "+7",
1636 "-7",
1637 " 7",
1638 "0x7",
1639 "7.0",
1640 "340282366920938463463374607431768211456",
1641 ] {
1642 let query = OperationQuery {
1643 operation_id: Some(text.to_owned()),
1644 };
1645 assert_eq!(query.operation_id(), Err(InvalidOperationId), "{text:?}");
1646 }
1647 }
1648
1649 #[test]
1650 fn given_stream_scoped_names_when_rendered_then_should_encode_each_as_one_segment() {
1651 assert_eq!(
1652 kv_namespace_path("stream:acme/support"),
1653 "/agdx/kv/stream%3Aacme%2Fsupport"
1654 );
1655 assert_eq!(
1656 kv_cas_path("stream:acme/support", "dXNlcjo0Mg"),
1657 "/agdx/kv/stream%3Aacme%2Fsupport/dXNlcjo0Mg/cas"
1658 );
1659 assert_eq!(
1660 graph_neighbors_path("stream:acme/kg", "a/b"),
1661 "/agdx/graph/stream%3Aacme%2Fkg/neighbors/a%2Fb"
1662 );
1663 assert_eq!(
1664 graph_query_path("stream:acme/kg"),
1665 "/agdx/graph/stream%3Aacme%2Fkg/query"
1666 );
1667 assert_eq!(
1668 graph_path("stream:acme/kg"),
1669 "/agdx/graphs/stream%3Aacme%2Fkg"
1670 );
1671 assert_eq!(
1672 projection_path("stream:acme/readings"),
1673 "/agdx/projections/stream%3Aacme%2Freadings"
1674 );
1675 }
1676
1677 #[test]
1678 fn given_the_sessions_feature_when_built_from_versions_then_should_advertise_sessions() {
1679 let mut versions = OpVersions::new(1, 1, 1, 1);
1680 assert!(!Capabilities::from_versions(true, versions).sessions);
1681 versions.features |= crate::hello::feature::SESSIONS;
1682 assert!(Capabilities::from_versions(true, versions).sessions);
1683 assert!(!Capabilities::from_versions(false, versions).sessions);
1684 }
1685
1686 #[test]
1687 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1688 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1689 assert!(
1690 caps.query.available && caps.kv.available && caps.fork,
1691 "core surfaces track enabled"
1692 );
1693 assert!(
1694 !caps.kv.cas
1695 && !caps.kv.cas_fenced
1696 && !caps.kv.fenced_leases
1697 && caps.query.consistency == Consistency::Eventual,
1698 "sub-features must be opt-in, never on by default"
1699 );
1700 let opted = caps
1701 .with_kv_cas(true)
1702 .with_query_consistency(Consistency::ReadYourWrites);
1703 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1704 }
1705
1706 #[test]
1707 fn given_the_fenced_lease_bit_when_building_http_capabilities_then_should_advertise_it() {
1708 let versions =
1709 OpVersions::new(1, 1, 1, 1).with_features(crate::hello::feature::KV_FENCED_LEASES);
1710 let caps = Capabilities::from_versions(true, versions);
1711 assert!(caps.kv.fenced_leases);
1712 assert!(
1713 caps.kv.cas_fenced,
1714 "the full fenced-lease contract subsumes fenced CAS"
1715 );
1716 }
1717
1718 #[test]
1719 fn given_group_reads_without_evaluation_when_building_http_capabilities_then_should_keep_catalog_available()
1720 {
1721 let versions = OpVersions::new(1, 1, 1, 1)
1722 .with_filter(crate::codes::FILTER_OP_VERSION)
1723 .with_features(crate::hello::feature::GROUP_POLICY_READS);
1724 let caps = Capabilities::from_versions(true, versions);
1725 assert!(caps.filters.group_policy_reads);
1726 assert!(caps.filters.catalog && caps.filters.managed_groups);
1727 assert!(!caps.filters.native && !caps.filters.preview);
1728 assert_eq!(caps.filters.evaluator_version, 0);
1729 assert!(caps.filters.codecs.is_empty());
1730 let absent = Capabilities::from_versions(false, OpVersions::new(1, 1, 1, 1));
1731 assert!(!absent.filters.group_policy_reads && !absent.filters.catalog);
1732 }
1733
1734 #[test]
1735 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1736 use crate::codes::CHECKPOINT_OP_VERSION;
1737 use crate::hello::feature;
1738
1739 let versions = OpVersions::new(2, 2, 1, 1)
1740 .with_checkpoint(CHECKPOINT_OP_VERSION)
1741 .with_features(feature::DESTINATIONS);
1742 let caps = Capabilities::from_versions(true, versions);
1743
1744 assert!(caps.destinations.available);
1745 assert!(caps.destinations.lifecycle);
1746 assert!(caps.destinations.checkpoint_status);
1747 assert!(caps.destinations.query_routes);
1748 assert!(!caps.destinations.table_schema);
1749 }
1750
1751 #[test]
1752 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1753 use crate::destination::BackendResourceId;
1754 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1755 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1756 BackendDescriptor::new(
1757 BackendResourceId::from_u128(1),
1758 BackendMode::Operational,
1759 "Embedded",
1760 BackendImplementation {
1761 kind: "embedded".to_owned(),
1762 version: "1.0.0".to_owned(),
1763 },
1764 1,
1765 1,
1766 ),
1767 BackendDescriptor::new(
1768 BackendResourceId::from_u128(2),
1769 BackendMode::Lakehouse,
1770 "Warehouse",
1771 BackendImplementation {
1772 kind: "columnar".to_owned(),
1773 version: "1.0.0".to_owned(),
1774 },
1775 1,
1776 1,
1777 ),
1778 ]);
1779 let json = serde_json::to_string(&caps).expect("serializes");
1780 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1781 assert_eq!(back.backends.len(), 2);
1782 assert_eq!(
1783 back.backends[1].resource_id,
1784 BackendResourceId::from_u128(2)
1785 );
1786 assert_eq!(back.backends[1].implementation.kind, "columnar");
1787
1788 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1791 let json = serde_json::to_string(&plain).expect("json");
1792 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1793 }
1794
1795 #[test]
1796 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1797 let custom = WireTopology {
1798 ops_stream: "custom-ops".to_owned(),
1799 ..WireTopology::default()
1800 };
1801 let caps =
1802 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1803 let json = serde_json::to_string(&caps).expect("serializes");
1804 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1805 assert_eq!(back.topology, Some(custom));
1806
1807 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1810 let json = serde_json::to_string(&plain).expect("json");
1811 assert!(
1812 !json.contains("topology"),
1813 "absent topology omitted: {json}"
1814 );
1815 }
1816
1817 #[test]
1818 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1819 let body = ErrorBody::from(&QueryError::IndexNotFound("readings".to_owned()));
1820 assert_eq!(body.code, ResultCode::NotFound);
1821 assert_eq!(body.http_status(), 404);
1822 assert!(body.message.contains("readings"));
1823 let json = serde_json::to_string(&body).expect("serializes");
1825 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1826 assert_eq!(back, body);
1827 }
1828
1829 #[test]
1830 #[cfg(feature = "http-client")]
1831 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1832 let query = KvScanQuery {
1833 prefix: Some("dXNlcjo".to_owned()),
1834 limit: Some(50),
1835 ..Default::default()
1836 };
1837 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1838 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1839 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1841 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1842 }
1843
1844 #[test]
1845 #[cfg(feature = "http-client")]
1846 fn given_names_with_reserved_characters_when_building_a_binding_path_then_should_encode_each_segment()
1847 {
1848 assert_eq!(
1849 filter_binding_path("orbit", "fleet/changes", "desk?x#1"),
1850 "/agdx/filter-bindings/orbit/fleet%2Fchanges/desk%3Fx%231"
1851 );
1852 }
1853
1854 #[cfg(feature = "http-client")]
1855 #[test]
1856 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1857 let projections = ProjectionListQuery {
1858 name_contains: Some("reading".to_owned()),
1859 id_prefix: Some("reading.".to_owned()),
1860 ..Default::default()
1861 };
1862 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1863 assert_eq!(encoded, "name_contains=reading&id_prefix=reading.");
1864 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1865 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1866
1867 let schemas = SchemaListQuery {
1868 name_contains: Some("Reading".to_owned()),
1869 stream: None,
1870 };
1871 assert_eq!(
1872 serde_urlencoded::to_string(&schemas).expect("encodes"),
1873 "name_contains=Reading"
1874 );
1875 let scoped = SchemaListQuery {
1876 name_contains: None,
1877 stream: Some("acme".to_owned()),
1878 };
1879 assert_eq!(
1880 serde_urlencoded::to_string(&scoped).expect("encodes"),
1881 format!("{PARAM_STREAM}=acme")
1882 );
1883 assert_eq!(
1884 serde_urlencoded::to_string(SchemaQuery::default()).expect("encodes"),
1885 ""
1886 );
1887 assert_eq!(
1888 serde_urlencoded::to_string(SchemaQuery {
1889 stream: Some("acme".to_owned()),
1890 })
1891 .expect("encodes"),
1892 format!("{PARAM_STREAM}=acme")
1893 );
1894 }
1895
1896 #[test]
1897 fn given_a_register_schema_body_without_a_stream_when_encoded_then_should_omit_it() {
1898 let body = RegisterSchemaBody {
1899 source: crate::control::SchemaSource::JsonSchema {
1900 schema: "{}".to_owned(),
1901 },
1902 name: None,
1903 version: None,
1904 stream: None,
1905 };
1906 let json = serde_json::to_string(&body).expect("encodes");
1907 assert!(
1908 !json.contains("stream"),
1909 "an unset stream is omitted: {json}"
1910 );
1911 let scoped: RegisterSchemaBody = serde_json::from_str(
1912 r#"{"source":{"kind":"json_schema","schema":"{}"},"stream":"acme"}"#,
1913 )
1914 .expect("decodes");
1915 assert_eq!(scoped.stream.as_deref(), Some("acme"));
1916 }
1917}