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