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
1321pub const ON_BEHALF_OF_HEADER: &str = "agdx-on-behalf-of";
1325
1326#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1331pub struct ProjectionListQuery {
1332 #[serde(default, skip_serializing_if = "Option::is_none")]
1333 pub topic: Option<String>,
1334 #[serde(default, skip_serializing_if = "Option::is_none")]
1335 pub name_contains: Option<String>,
1336 #[serde(default, skip_serializing_if = "Option::is_none")]
1337 pub id_prefix: Option<String>,
1338 #[serde(default, skip_serializing_if = "Option::is_none")]
1341 pub search: Option<String>,
1342}
1343
1344#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1348pub struct SchemaListQuery {
1349 #[serde(default, skip_serializing_if = "Option::is_none")]
1350 pub name_contains: Option<String>,
1351 #[serde(default, skip_serializing_if = "Option::is_none")]
1354 pub stream: Option<String>,
1355}
1356
1357#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1361pub struct SchemaQuery {
1362 #[serde(default, skip_serializing_if = "Option::is_none")]
1363 pub stream: Option<String>,
1364}
1365
1366#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1372pub struct OperationQuery {
1373 #[serde(default, skip_serializing_if = "Option::is_none")]
1374 pub operation_id: Option<String>,
1375}
1376
1377impl OperationQuery {
1378 pub fn operation_id(&self) -> Result<Option<NonZeroU128>, InvalidOperationId> {
1380 self.operation_id
1381 .as_deref()
1382 .map(|text| {
1383 if !text.bytes().all(|byte| byte.is_ascii_digit()) {
1385 return Err(InvalidOperationId);
1386 }
1387 text.parse::<NonZeroU128>().map_err(|_| InvalidOperationId)
1388 })
1389 .transpose()
1390 }
1391}
1392
1393impl From<NonZeroU128> for OperationQuery {
1394 fn from(operation_id: NonZeroU128) -> Self {
1395 Self {
1396 operation_id: Some(operation_id.to_string()),
1397 }
1398 }
1399}
1400
1401#[derive(Clone, Copy, Debug, PartialEq, Eq, thiserror::Error)]
1403#[error("operation_id must be a non-zero decimal u128")]
1404pub struct InvalidOperationId;
1405
1406#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1408pub struct FilterListQuery {
1409 #[serde(default, skip_serializing_if = "Option::is_none")]
1410 pub name_contains: Option<String>,
1411 #[serde(default, skip_serializing_if = "Option::is_none")]
1412 pub state: Option<crate::filter::FilterState>,
1413 #[serde(default, skip_serializing_if = "Option::is_none")]
1415 pub before_id: Option<u32>,
1416 #[serde(default, skip_serializing_if = "Option::is_none")]
1418 pub owner_stream_id: Option<u32>,
1419 #[serde(default, skip_serializing_if = "Option::is_none")]
1420 pub owner_stream_created_at_micros: Option<u64>,
1421 #[serde(default, skip_serializing_if = "Option::is_none")]
1422 pub owner_topic_id: Option<u32>,
1423 #[serde(default, skip_serializing_if = "Option::is_none")]
1424 pub owner_topic_created_at_micros: Option<u64>,
1425 #[serde(default, skip_serializing_if = "Option::is_none")]
1426 pub owner_group_id: Option<u64>,
1427 #[serde(default, skip_serializing_if = "Option::is_none")]
1428 pub page: Option<u32>,
1429 #[serde(default, skip_serializing_if = "Option::is_none")]
1430 pub page_size: Option<u32>,
1431}
1432
1433impl FilterListQuery {
1434 pub fn owner(&self) -> Result<Option<FilterGroupIdentity>, InvalidError> {
1436 match (
1437 self.owner_stream_id,
1438 self.owner_stream_created_at_micros,
1439 self.owner_topic_id,
1440 self.owner_topic_created_at_micros,
1441 self.owner_group_id,
1442 ) {
1443 (None, None, None, None, None) => Ok(None),
1444 (
1445 Some(stream_id),
1446 Some(stream_created_at_micros),
1447 Some(topic_id),
1448 Some(topic_created_at_micros),
1449 Some(group_id),
1450 ) => Ok(Some(FilterGroupIdentity {
1451 stream_id,
1452 stream_created_at_micros,
1453 topic_id,
1454 topic_created_at_micros,
1455 group_id,
1456 })),
1457 _ => Err(InvalidError::new(
1458 "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",
1459 )),
1460 }
1461 }
1462}
1463
1464impl From<FilterGroupIdentity> for FilterListQuery {
1465 fn from(identity: FilterGroupIdentity) -> Self {
1466 Self {
1467 owner_stream_id: Some(identity.stream_id),
1468 owner_stream_created_at_micros: Some(identity.stream_created_at_micros),
1469 owner_topic_id: Some(identity.topic_id),
1470 owner_topic_created_at_micros: Some(identity.topic_created_at_micros),
1471 owner_group_id: Some(identity.group_id),
1472 ..Self::default()
1473 }
1474 }
1475}
1476
1477#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1479pub struct FilterRevisionListQuery {
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1481 pub page: Option<u32>,
1482 #[serde(default, skip_serializing_if = "Option::is_none")]
1483 pub page_size: Option<u32>,
1484}
1485
1486#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1488pub struct FilterBindingListQuery {
1489 #[serde(default, skip_serializing_if = "Option::is_none")]
1490 pub filter_id: Option<u32>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1492 pub stream: Option<String>,
1493 #[serde(default, skip_serializing_if = "Option::is_none")]
1494 pub topic: Option<String>,
1495 #[serde(default, skip_serializing_if = "Option::is_none")]
1496 pub page: Option<u32>,
1497 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub page_size: Option<u32>,
1499}
1500
1501#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
1504pub struct FilterTestBody {
1505 pub filter: crate::filter::FilterRef,
1506 #[serde(default, skip_serializing_if = "Option::is_none")]
1507 pub payload_base64: Option<String>,
1508 #[serde(default, skip_serializing_if = "Option::is_none")]
1509 pub payload_text: Option<String>,
1510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
1511 pub headers: Vec<crate::filter::FilterHeader>,
1512}
1513
1514#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1518pub struct KvScanQuery {
1519 #[serde(default, skip_serializing_if = "Option::is_none")]
1520 pub prefix: Option<String>,
1521 #[serde(default, skip_serializing_if = "Option::is_none")]
1522 pub start: Option<String>,
1523 #[serde(default, skip_serializing_if = "Option::is_none")]
1524 pub end: Option<String>,
1525 #[serde(default, skip_serializing_if = "Option::is_none")]
1526 pub key_contains: Option<String>,
1527 #[serde(default, skip_serializing_if = "Option::is_none")]
1532 pub conversation: Option<String>,
1533 #[serde(default, skip_serializing_if = "Option::is_none")]
1534 pub limit: Option<usize>,
1535 #[serde(default, skip_serializing_if = "Option::is_none")]
1536 pub cursor: Option<String>,
1537}
1538
1539#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1541pub struct KvPutQuery {
1542 #[serde(default, skip_serializing_if = "Option::is_none")]
1543 pub expires_at_micros: Option<u64>,
1544}
1545
1546#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1552pub struct GraphNeighborsQuery {
1553 #[serde(default, skip_serializing_if = "Option::is_none")]
1554 pub dir: Option<String>,
1555 #[serde(default, skip_serializing_if = "Option::is_none")]
1556 pub edge_type: Option<String>,
1557 #[serde(default, skip_serializing_if = "Option::is_none")]
1558 pub depth: Option<u32>,
1559 #[serde(default, skip_serializing_if = "Option::is_none")]
1560 pub limit: Option<usize>,
1561 #[serde(default, skip_serializing_if = "Option::is_none")]
1563 pub as_of: Option<u64>,
1564 #[serde(default, skip_serializing_if = "Option::is_none")]
1568 pub conversation: Option<String>,
1569}
1570
1571#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1574pub struct SessionsQuery {
1575 #[serde(default, skip_serializing_if = "Option::is_none")]
1576 pub status: Option<String>,
1577 #[serde(default, skip_serializing_if = "Option::is_none")]
1578 pub root: Option<String>,
1579 #[serde(default, skip_serializing_if = "Option::is_none")]
1580 pub label_prefix: Option<String>,
1581 #[serde(default, skip_serializing_if = "Option::is_none")]
1582 pub agent: Option<String>,
1583 #[serde(default, skip_serializing_if = "Option::is_none")]
1584 pub text: Option<String>,
1585 #[serde(default, skip_serializing_if = "Option::is_none")]
1586 pub cursor: Option<String>,
1587 #[serde(default, skip_serializing_if = "Option::is_none")]
1588 pub limit: Option<u32>,
1589 #[serde(default, rename = "total")]
1590 pub want_total: bool,
1591}
1592
1593#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1596pub struct SessionEventsQuery {
1597 #[serde(default, skip_serializing_if = "Option::is_none")]
1598 pub cursor: Option<String>,
1599 #[serde(default, skip_serializing_if = "Option::is_none")]
1600 pub limit: Option<u32>,
1601 #[serde(default)]
1602 pub fixed_frontier: bool,
1603}
1604
1605#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1608pub struct SessionStateQuery {
1609 #[serde(default, skip_serializing_if = "Option::is_none")]
1610 pub history_limit: Option<u32>,
1611}
1612
1613#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1615pub struct SessionLinksQuery {
1616 #[serde(default, skip_serializing_if = "Option::is_none")]
1617 pub surface: Option<String>,
1618}
1619
1620#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1623pub struct SessionChangesQuery {
1624 #[serde(default)]
1625 pub after: u64,
1626 #[serde(default, skip_serializing_if = "Option::is_none")]
1627 pub limit: Option<u32>,
1628 #[serde(default, skip_serializing_if = "Option::is_none")]
1629 pub series: Option<u64>,
1630}
1631
1632#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1637pub struct KvCasQuery {
1638 #[serde(default, skip_serializing_if = "Option::is_none")]
1639 pub expect_version: Option<u64>,
1640 #[serde(default, skip_serializing_if = "Option::is_none")]
1641 pub expect_absent: Option<bool>,
1642 #[serde(default, skip_serializing_if = "Option::is_none")]
1643 pub expires_at_micros: Option<u64>,
1644}
1645
1646#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1649pub struct CasCommittedView {
1650 pub version: u64,
1651}
1652
1653#[cfg(test)]
1654mod tests {
1655 use super::*;
1656
1657 #[test]
1658 fn given_a_filter_list_query_when_the_owner_is_complete_or_partial_then_should_resolve_or_refuse()
1659 {
1660 let identity = FilterGroupIdentity {
1661 stream_id: 1,
1662 stream_created_at_micros: 10,
1663 topic_id: 2,
1664 topic_created_at_micros: 20,
1665 group_id: 3,
1666 };
1667 let query = FilterListQuery::from(identity);
1668 assert_eq!(query.owner().expect("complete owner"), Some(identity));
1669 assert_eq!(FilterListQuery::default().owner().expect("no owner"), None);
1670 let partial = FilterListQuery {
1671 owner_group_id: None,
1672 ..query
1673 };
1674 assert!(partial.owner().is_err());
1675 }
1676
1677 #[test]
1678 fn given_path_builders_when_rendered_then_should_match_the_router() {
1679 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1680 assert_eq!(
1681 projection_path("reading.v1"),
1682 "/agdx/projections/reading.v1"
1683 );
1684 assert_eq!(schema_path(7), "/agdx/schemas/7");
1685 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1686 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1687 assert_eq!(
1688 kv_entry_path("sessions", "dXNlcjo0Mg"),
1689 "/agdx/kv/sessions/dXNlcjo0Mg"
1690 );
1691 assert_eq!(
1692 kv_cas_path("sessions", "dXNlcjo0Mg"),
1693 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1694 );
1695 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1696 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1697 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1698 }
1699
1700 #[test]
1701 fn given_an_operation_id_when_rendered_and_parsed_then_should_round_trip() {
1702 let operation_id = NonZeroU128::new(u128::MAX).expect("non-zero");
1703 let query = OperationQuery::from(operation_id);
1704 assert_eq!(
1705 query.operation_id.as_deref(),
1706 Some("340282366920938463463374607431768211455")
1707 );
1708 assert_eq!(query.operation_id(), Ok(Some(operation_id)));
1709 assert_eq!(OperationQuery::default().operation_id(), Ok(None));
1710 }
1711
1712 #[test]
1713 fn given_a_malformed_operation_id_when_parsed_then_should_refuse_it() {
1714 for text in [
1715 "",
1716 "0",
1717 "+7",
1718 "-7",
1719 " 7",
1720 "0x7",
1721 "7.0",
1722 "340282366920938463463374607431768211456",
1723 ] {
1724 let query = OperationQuery {
1725 operation_id: Some(text.to_owned()),
1726 };
1727 assert_eq!(query.operation_id(), Err(InvalidOperationId), "{text:?}");
1728 }
1729 }
1730
1731 #[test]
1732 fn given_stream_scoped_names_when_rendered_then_should_encode_each_as_one_segment() {
1733 assert_eq!(
1734 kv_namespace_path("stream:acme/support"),
1735 "/agdx/kv/stream%3Aacme%2Fsupport"
1736 );
1737 assert_eq!(
1738 kv_cas_path("stream:acme/support", "dXNlcjo0Mg"),
1739 "/agdx/kv/stream%3Aacme%2Fsupport/dXNlcjo0Mg/cas"
1740 );
1741 assert_eq!(
1742 graph_neighbors_path("stream:acme/kg", "a/b"),
1743 "/agdx/graph/stream%3Aacme%2Fkg/neighbors/a%2Fb"
1744 );
1745 assert_eq!(
1746 graph_query_path("stream:acme/kg"),
1747 "/agdx/graph/stream%3Aacme%2Fkg/query"
1748 );
1749 assert_eq!(
1750 graph_path("stream:acme/kg"),
1751 "/agdx/graphs/stream%3Aacme%2Fkg"
1752 );
1753 assert_eq!(
1754 projection_path("stream:acme/readings"),
1755 "/agdx/projections/stream%3Aacme%2Freadings"
1756 );
1757 }
1758
1759 #[test]
1760 fn given_the_sessions_feature_when_built_from_versions_then_should_advertise_sessions() {
1761 let mut versions = OpVersions::new(1, 1, 1, 1);
1762 assert!(!Capabilities::from_versions(true, versions).sessions);
1763 versions.features |= crate::hello::feature::SESSIONS;
1764 assert!(Capabilities::from_versions(true, versions).sessions);
1765 assert!(!Capabilities::from_versions(false, versions).sessions);
1766 }
1767
1768 #[test]
1769 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1770 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1771 assert!(
1772 caps.query.available && caps.kv.available && caps.fork,
1773 "core surfaces track enabled"
1774 );
1775 assert!(
1776 !caps.kv.cas
1777 && !caps.kv.cas_fenced
1778 && !caps.kv.fenced_leases
1779 && caps.query.consistency == Consistency::Eventual,
1780 "sub-features must be opt-in, never on by default"
1781 );
1782 let opted = caps
1783 .with_kv_cas(true)
1784 .with_query_consistency(Consistency::ReadYourWrites);
1785 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1786 }
1787
1788 #[test]
1789 fn given_the_fenced_lease_bit_when_building_http_capabilities_then_should_advertise_it() {
1790 let versions =
1791 OpVersions::new(1, 1, 1, 1).with_features(crate::hello::feature::KV_FENCED_LEASES);
1792 let caps = Capabilities::from_versions(true, versions);
1793 assert!(caps.kv.fenced_leases);
1794 assert!(
1795 caps.kv.cas_fenced,
1796 "the full fenced-lease contract subsumes fenced CAS"
1797 );
1798 }
1799
1800 #[test]
1801 fn given_group_reads_without_evaluation_when_building_http_capabilities_then_should_keep_catalog_available()
1802 {
1803 let versions = OpVersions::new(1, 1, 1, 1)
1804 .with_filter(crate::codes::FILTER_OP_VERSION)
1805 .with_features(crate::hello::feature::GROUP_POLICY_READS);
1806 let caps = Capabilities::from_versions(true, versions);
1807 assert!(caps.filters.group_policy_reads);
1808 assert!(caps.filters.catalog && caps.filters.managed_groups);
1809 assert!(!caps.filters.native && !caps.filters.preview);
1810 assert_eq!(caps.filters.evaluator_version, 0);
1811 assert!(caps.filters.codecs.is_empty());
1812 let absent = Capabilities::from_versions(false, OpVersions::new(1, 1, 1, 1));
1813 assert!(!absent.filters.group_policy_reads && !absent.filters.catalog);
1814 }
1815
1816 #[test]
1817 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1818 use crate::codes::CHECKPOINT_OP_VERSION;
1819 use crate::hello::feature;
1820
1821 let versions = OpVersions::new(2, 2, 1, 1)
1822 .with_checkpoint(CHECKPOINT_OP_VERSION)
1823 .with_features(feature::DESTINATIONS);
1824 let caps = Capabilities::from_versions(true, versions);
1825
1826 assert!(caps.destinations.available);
1827 assert!(caps.destinations.lifecycle);
1828 assert!(caps.destinations.checkpoint_status);
1829 assert!(caps.destinations.query_routes);
1830 assert!(!caps.destinations.table_schema);
1831 }
1832
1833 #[test]
1834 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1835 use crate::destination::BackendResourceId;
1836 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1837 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1838 BackendDescriptor::new(
1839 BackendResourceId::from_u128(1),
1840 BackendMode::Operational,
1841 "Embedded",
1842 BackendImplementation {
1843 kind: "embedded".to_owned(),
1844 version: "1.0.0".to_owned(),
1845 },
1846 1,
1847 1,
1848 ),
1849 BackendDescriptor::new(
1850 BackendResourceId::from_u128(2),
1851 BackendMode::Lakehouse,
1852 "Warehouse",
1853 BackendImplementation {
1854 kind: "columnar".to_owned(),
1855 version: "1.0.0".to_owned(),
1856 },
1857 1,
1858 1,
1859 ),
1860 ]);
1861 let json = serde_json::to_string(&caps).expect("serializes");
1862 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1863 assert_eq!(back.backends.len(), 2);
1864 assert_eq!(
1865 back.backends[1].resource_id,
1866 BackendResourceId::from_u128(2)
1867 );
1868 assert_eq!(back.backends[1].implementation.kind, "columnar");
1869
1870 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1873 let json = serde_json::to_string(&plain).expect("json");
1874 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1875 }
1876
1877 #[test]
1878 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1879 let custom = WireTopology {
1880 ops_stream: "custom-ops".to_owned(),
1881 ..WireTopology::default()
1882 };
1883 let caps =
1884 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1885 let json = serde_json::to_string(&caps).expect("serializes");
1886 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1887 assert_eq!(back.topology, Some(custom));
1888
1889 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1892 let json = serde_json::to_string(&plain).expect("json");
1893 assert!(
1894 !json.contains("topology"),
1895 "absent topology omitted: {json}"
1896 );
1897 }
1898
1899 #[test]
1900 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1901 let body = ErrorBody::from(&QueryError::IndexNotFound("readings".to_owned()));
1902 assert_eq!(body.code, ResultCode::NotFound);
1903 assert_eq!(body.http_status(), 404);
1904 assert!(body.message.contains("readings"));
1905 let json = serde_json::to_string(&body).expect("serializes");
1907 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1908 assert_eq!(back, body);
1909 }
1910
1911 #[test]
1912 #[cfg(feature = "http-client")]
1913 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1914 let query = KvScanQuery {
1915 prefix: Some("dXNlcjo".to_owned()),
1916 limit: Some(50),
1917 ..Default::default()
1918 };
1919 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1920 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1921 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1923 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1924 }
1925
1926 #[test]
1927 #[cfg(feature = "http-client")]
1928 fn given_names_with_reserved_characters_when_building_a_binding_path_then_should_encode_each_segment()
1929 {
1930 assert_eq!(
1931 filter_binding_path("orbit", "fleet/changes", "desk?x#1"),
1932 "/agdx/filter-bindings/orbit/fleet%2Fchanges/desk%3Fx%231"
1933 );
1934 }
1935
1936 #[cfg(feature = "http-client")]
1937 #[test]
1938 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1939 let projections = ProjectionListQuery {
1940 name_contains: Some("reading".to_owned()),
1941 id_prefix: Some("reading.".to_owned()),
1942 ..Default::default()
1943 };
1944 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1945 assert_eq!(encoded, "name_contains=reading&id_prefix=reading.");
1946 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1947 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1948
1949 let schemas = SchemaListQuery {
1950 name_contains: Some("Reading".to_owned()),
1951 stream: None,
1952 };
1953 assert_eq!(
1954 serde_urlencoded::to_string(&schemas).expect("encodes"),
1955 "name_contains=Reading"
1956 );
1957 let scoped = SchemaListQuery {
1958 name_contains: None,
1959 stream: Some("acme".to_owned()),
1960 };
1961 assert_eq!(
1962 serde_urlencoded::to_string(&scoped).expect("encodes"),
1963 format!("{PARAM_STREAM}=acme")
1964 );
1965 assert_eq!(
1966 serde_urlencoded::to_string(SchemaQuery::default()).expect("encodes"),
1967 ""
1968 );
1969 assert_eq!(
1970 serde_urlencoded::to_string(SchemaQuery {
1971 stream: Some("acme".to_owned()),
1972 })
1973 .expect("encodes"),
1974 format!("{PARAM_STREAM}=acme")
1975 );
1976 }
1977
1978 #[test]
1979 fn given_a_register_schema_body_without_a_stream_when_encoded_then_should_omit_it() {
1980 let body = RegisterSchemaBody {
1981 source: crate::control::SchemaSource::JsonSchema {
1982 schema: "{}".to_owned(),
1983 },
1984 name: None,
1985 version: None,
1986 stream: None,
1987 };
1988 let json = serde_json::to_string(&body).expect("encodes");
1989 assert!(
1990 !json.contains("stream"),
1991 "an unset stream is omitted: {json}"
1992 );
1993 let scoped: RegisterSchemaBody = serde_json::from_str(
1994 r#"{"source":{"kind":"json_schema","schema":"{}"},"stream":"acme"}"#,
1995 )
1996 .expect("decodes");
1997 assert_eq!(scoped.stream.as_deref(), Some("acme"));
1998 }
1999}