1use crate::checkpoint::{
2 CheckpointMutationResult, CheckpointReadConsistency, CheckpointRequestEnvelope,
3 DestinationCheckpointStatus, PreparedAttemptSummary, RetentionGap,
4};
5use crate::destination::{
6 DestinationId, DestinationOperationId, MaterializationDestination, QueryRoute,
7};
8use crate::fork::{ForkError, ForkKind};
9use crate::graph::SourceRef;
10use crate::hello::{BackendDescriptor, OpVersions};
11use crate::kv::KvError;
12use crate::query::{Consistency, QueryError, QueryExecutionId, QueryExecutionStatus, QueryResult};
13use crate::result::ResultCode;
14use crate::schema::{Digest32, LogicalSchema, TypedValue, UuidValue};
15use crate::topology::WireTopology;
16use serde::{Deserialize, Serialize};
17use std::collections::BTreeMap;
18
19pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
21pub const QUERY_PATH: &str = "/agdx/query";
23pub const DESTINATIONS_PATH: &str = "/agdx/destinations";
25pub const QUERY_ROUTES_PATH: &str = "/agdx/query-routes";
27pub const PROJECTIONS_PATH: &str = "/agdx/projections";
29pub const BINDINGS_PATH: &str = "/agdx/bindings";
31pub const SCHEMAS_PATH: &str = "/agdx/schemas";
33pub const KV_PATH: &str = "/agdx/kv";
35pub const FORKS_PATH: &str = "/agdx/forks";
37pub const GRAPHS_PATH: &str = "/agdx/graphs";
39pub const CLIENTS_PATH: &str = "/agdx/clients";
43pub const RUNS_PATH: &str = "/agdx/runs";
47pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
49pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
51
52pub fn destination_path(id: DestinationId) -> String {
53 format!("{DESTINATIONS_PATH}/{id}")
54}
55
56pub fn destination_enable_path(id: DestinationId) -> String {
57 format!("{}/{id}/enable", DESTINATIONS_PATH)
58}
59
60pub fn destination_disable_path(id: DestinationId) -> String {
61 format!("{}/{id}/disable", DESTINATIONS_PATH)
62}
63
64pub fn destination_status_path(id: DestinationId) -> String {
65 format!("{}/{id}/status", DESTINATIONS_PATH)
66}
67
68pub fn destination_checkpoint_path(id: DestinationId) -> String {
69 format!("{}/{id}/checkpoint", DESTINATIONS_PATH)
70}
71
72pub fn destination_retention_gap_path(id: DestinationId) -> String {
73 format!("{}/{id}/retention-gap", DESTINATIONS_PATH)
74}
75
76pub fn destination_prepared_attempt_path(id: DestinationId) -> String {
77 format!("{}/{id}/prepared-attempt", DESTINATIONS_PATH)
78}
79
80pub fn destination_table_path(id: DestinationId) -> String {
81 format!("{}/{id}/table", DESTINATIONS_PATH)
82}
83
84pub fn destination_table_schema_path(id: DestinationId) -> String {
85 format!("{}/{id}/table/schema", DESTINATIONS_PATH)
86}
87
88pub fn destination_snapshots_path(id: DestinationId) -> String {
89 format!("{}/{id}/table/snapshots", DESTINATIONS_PATH)
90}
91
92pub fn destination_current_snapshot_path(id: DestinationId) -> String {
93 format!("{}/{id}/table/current-snapshot", DESTINATIONS_PATH)
94}
95
96pub fn destination_snapshot_path(id: DestinationId, snapshot_id: i64) -> String {
97 format!("{}/{id}/table/snapshots/{snapshot_id}", DESTINATIONS_PATH)
98}
99
100pub fn destination_files_path(id: DestinationId) -> String {
101 format!("{}/{id}/table/files", DESTINATIONS_PATH)
102}
103
104pub fn destination_metrics_path(id: DestinationId) -> String {
105 format!("{}/{id}/table/metrics", DESTINATIONS_PATH)
106}
107
108pub fn query_execution_path(id: QueryExecutionId) -> String {
109 format!("{QUERY_PATH}/{id}")
110}
111
112pub fn query_page_path(id: QueryExecutionId) -> String {
113 format!("{QUERY_PATH}/{id}/pages")
114}
115
116pub fn query_cancel_path(id: QueryExecutionId) -> String {
117 format!("{QUERY_PATH}/{id}/cancel")
118}
119
120pub fn destination_operation_path(id: DestinationOperationId) -> String {
121 format!("{DESTINATIONS_PATH}/operations/{id}")
122}
123
124#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
125pub struct CheckpointReadQuery {
126 pub consistency: CheckpointReadConsistency,
127}
128
129impl CheckpointReadQuery {
130 pub const fn new(consistency: CheckpointReadConsistency) -> Self {
131 Self { consistency }
132 }
133}
134
135#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
136pub struct DestinationListQuery {
137 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub source_stream: Option<String>,
139 #[serde(default, skip_serializing_if = "Option::is_none")]
140 pub source_topic: Option<String>,
141 #[serde(default, skip_serializing_if = "Option::is_none")]
142 pub name_contains: Option<String>,
143 #[serde(default, skip_serializing_if = "Option::is_none")]
144 pub after: Option<String>,
145 #[serde(default, skip_serializing_if = "Option::is_none")]
146 pub limit: Option<usize>,
147 pub consistency: CheckpointReadConsistency,
148}
149
150#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
151pub struct DestinationPageView {
152 pub destinations: Vec<DestinationView>,
153 #[serde(default, skip_serializing_if = "Option::is_none")]
154 pub next_cursor: Option<String>,
155 pub global_state_revision: u64,
156 pub consistency: crate::checkpoint::CheckpointReadConsistency,
157}
158
159#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
160pub struct DestinationView {
161 pub destination: MaterializationDestination,
162 pub status: DestinationCheckpointStatus,
163}
164
165#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
166pub struct DestinationMutationBody {
167 pub checkpoint: CheckpointRequestEnvelope,
168}
169
170#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
171pub struct AcceptedOperationView {
172 pub operation_id: DestinationOperationId,
173 pub request_id: crate::checkpoint::CheckpointRequestId,
174 pub state: OperationState,
175 pub submitted_at_micros: u64,
176 #[serde(default, skip_serializing_if = "Option::is_none")]
177 pub completed_at_micros: Option<u64>,
178 #[serde(default, skip_serializing_if = "Option::is_none")]
179 pub error: Option<OperationErrorView>,
180 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub result: Option<CheckpointMutationResult>,
182}
183
184#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
185pub struct OperationErrorView {
186 pub code: ResultCode,
187 pub message: String,
188}
189
190#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192#[non_exhaustive]
193pub enum OperationState {
194 Accepted,
195 Running,
196 Succeeded,
197 Failed,
198 Cancelled,
199}
200
201#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
202pub struct QueryRouteListQuery {
203 #[serde(default, skip_serializing_if = "Option::is_none")]
204 pub name_contains: Option<String>,
205 #[serde(default, skip_serializing_if = "Option::is_none")]
206 pub after: Option<String>,
207 #[serde(default, skip_serializing_if = "Option::is_none")]
208 pub limit: Option<usize>,
209 pub consistency: CheckpointReadConsistency,
210}
211
212#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
213pub struct QueryRoutePageView {
214 pub routes: Vec<QueryRoute>,
215 #[serde(default, skip_serializing_if = "Option::is_none")]
216 pub next_cursor: Option<String>,
217 pub definition_revision: u64,
218 pub global_state_revision: u64,
219 pub consistency: CheckpointReadConsistency,
220}
221
222#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
223pub struct TableView {
224 pub table_uuid: UuidValue,
225 pub destination_id: DestinationId,
226 pub destination_generation: u64,
227 pub namespace: Vec<String>,
228 pub table: String,
229 pub current_snapshot_id: i64,
230 pub current_schema_id: i32,
231 pub current_partition_spec_id: i32,
232 pub metadata_identity: String,
233 pub properties: BTreeMap<String, String>,
234}
235
236#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
237pub struct TableSchemaView {
238 pub table_uuid: UuidValue,
239 pub iceberg_schema_id: i32,
240 pub logical_schema: LogicalSchema,
241}
242
243#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
244pub struct SnapshotListQuery {
245 #[serde(default, skip_serializing_if = "Option::is_none")]
246 pub before_snapshot_id: Option<i64>,
247 #[serde(default, skip_serializing_if = "Option::is_none")]
248 pub limit: Option<usize>,
249}
250
251#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
252pub struct SnapshotPageView {
253 pub snapshots: Vec<TableSnapshotView>,
254 #[serde(default, skip_serializing_if = "Option::is_none")]
255 pub next_before_snapshot_id: Option<i64>,
256}
257
258#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
259pub struct TableSnapshotView {
260 pub snapshot_id: i64,
261 #[serde(default, skip_serializing_if = "Option::is_none")]
262 pub parent_snapshot_id: Option<i64>,
263 pub sequence_number: i64,
264 pub committed_at_micros: u64,
265 pub schema_id: i32,
266 pub partition_spec_id: i32,
267 pub materialization_boundary_digest: Digest32,
268 pub checkpoint_revision: u64,
269 pub summary: BTreeMap<String, String>,
270}
271
272#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
273pub struct TableFileListQuery {
274 #[serde(default, skip_serializing_if = "Option::is_none")]
275 pub snapshot_id: Option<i64>,
276 #[serde(default, skip_serializing_if = "Option::is_none")]
277 pub after: Option<String>,
278 #[serde(default, skip_serializing_if = "Option::is_none")]
279 pub limit: Option<usize>,
280}
281
282#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
283pub struct TableFilePageView {
284 pub files: Vec<TableFileView>,
285 #[serde(default, skip_serializing_if = "Option::is_none")]
286 pub next_cursor: Option<String>,
287}
288
289#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
290pub struct TableFileView {
291 pub object_identity: String,
292 pub file_size_bytes: u64,
293 pub row_count: u64,
294 pub partition: BTreeMap<String, TypedValue>,
295 pub lower_bounds: BTreeMap<u32, TypedValue>,
296 pub upper_bounds: BTreeMap<u32, TypedValue>,
297 pub null_value_counts: BTreeMap<u32, u64>,
298}
299
300#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
301pub struct TableMetricsView {
302 pub snapshot_id: i64,
303 pub data_file_count: u64,
304 pub delete_file_count: u64,
305 pub total_rows: u64,
306 pub total_bytes: u64,
307 pub partition_count: u64,
308}
309
310#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
311pub struct QueryPageBody {
312 pub cursor: String,
313 pub deadline_micros: u64,
314}
315
316#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
317pub struct QueryExecutionView {
318 pub status: QueryExecutionStatus,
319 #[serde(default, skip_serializing_if = "Option::is_none")]
320 pub result: Option<QueryResult>,
321}
322
323#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
324pub struct DestinationIssueView {
325 #[serde(default, skip_serializing_if = "Option::is_none")]
326 pub retention_gap: Option<RetentionGap>,
327 #[serde(default, skip_serializing_if = "Option::is_none")]
328 pub prepared_attempt: Option<PreparedAttemptSummary>,
329 #[serde(default, skip_serializing_if = "Option::is_none")]
330 pub block: Option<crate::checkpoint::DestinationBlock>,
331 pub global_state_revision: u64,
332 pub checkpoint_revision: u64,
333 pub consistency: CheckpointReadConsistency,
334}
335
336#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
338#[serde(rename_all = "snake_case")]
339#[non_exhaustive]
340pub enum DestinationHttpOperation {
341 ListDestinations,
342 GetDestination,
343 CreateDestination,
344 EnableDestination,
345 DisableDestination,
346 GetOperation,
347 GetStatus,
348 GetCheckpoint,
349 GetRetentionGap,
350 GetPreparedAttempt,
351 ListQueryRoutes,
352 GetTable,
353 GetTableSchema,
354 GetCurrentSnapshot,
355 ListSnapshots,
356 GetSnapshot,
357 ListFiles,
358 GetMetrics,
359}
360
361#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
367pub struct DestinationHttpRequest {
368 pub v: u32,
369 pub operation: DestinationHttpOperation,
370 pub parameters: Vec<String>,
371 pub query: Vec<u8>,
372 pub body: Vec<u8>,
373}
374
375#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
377pub struct DestinationHttpReply {
378 pub status: u16,
379 pub body: Vec<u8>,
380}
381
382pub fn authz_role_path(name: &str) -> String {
384 format!("{AUTHZ_ROLES_PATH}/{name}")
385}
386
387pub fn authz_user_roles_path(user_id: u32) -> String {
389 format!("/agdx/authz/users/{user_id}/roles")
390}
391
392pub fn graph_path(id: &str) -> String {
394 format!("{GRAPHS_PATH}/{id}")
395}
396
397pub fn graph_query_path(name: &str) -> String {
399 format!("/agdx/graph/{name}/query")
400}
401
402pub fn graph_neighbors_path(name: &str, node: &str) -> String {
404 format!("/agdx/graph/{name}/neighbors/{node}")
405}
406
407pub fn projection_path(id: &str) -> String {
409 format!("{PROJECTIONS_PATH}/{id}")
410}
411
412pub fn schema_path(id: u32) -> String {
414 format!("{SCHEMAS_PATH}/{id}")
415}
416
417pub fn schema_decode_path(id: u32) -> String {
419 format!("{SCHEMAS_PATH}/{id}/decode")
420}
421
422pub fn kv_namespace_path(namespace: &str) -> String {
424 format!("{KV_PATH}/{namespace}")
425}
426
427pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
431 format!("{KV_PATH}/{namespace}/{key_b64}")
432}
433
434pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
441 format!("{KV_PATH}/{namespace}/{key_b64}/cas")
442}
443
444pub fn fork_path(id: &str) -> String {
446 format!("{FORKS_PATH}/{id}")
447}
448
449pub fn fork_promote_path(id: &str) -> String {
451 format!("{FORKS_PATH}/{id}/promote")
452}
453
454pub fn fork_rows_path(id: &str) -> String {
456 format!("{FORKS_PATH}/{id}/rows")
457}
458
459pub fn run_path(id: &str) -> String {
461 format!("{RUNS_PATH}/{id}")
462}
463
464pub fn run_cancel_path(id: &str) -> String {
466 format!("{RUNS_PATH}/{id}/cancel")
467}
468
469#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
476#[non_exhaustive]
477pub struct Capabilities {
478 pub managed: bool,
482 pub query: QueryCapsView,
485 pub destinations: DestinationCapsView,
487 pub kv: KvCapsView,
489 #[serde(default)]
493 pub graph: bool,
494 pub fork: bool,
496 #[serde(default)]
499 pub agent_workflow: bool,
500 #[serde(default)]
504 pub watch: bool,
505 #[serde(default)]
508 pub authz: bool,
509 pub versions: OpVersions,
510 #[serde(default, skip_serializing_if = "Vec::is_empty")]
515 pub backends: Vec<BackendDescriptor>,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
522 pub topology: Option<WireTopology>,
523}
524
525#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
528pub struct QueryCapsView {
529 pub available: bool,
531 pub projections: bool,
533 pub schemas: bool,
535 #[serde(default)]
539 pub consistency: Consistency,
540 #[serde(default)]
544 pub keyword: bool,
545 #[serde(default)]
547 pub cursor_paging: bool,
548 #[serde(default)]
550 pub cancellation: bool,
551 #[serde(default)]
553 pub execution_status: bool,
554}
555
556#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
557pub struct DestinationCapsView {
558 pub available: bool,
559 #[serde(default)]
560 pub lifecycle: bool,
561 #[serde(default)]
562 pub checkpoint_status: bool,
563 #[serde(default)]
564 pub query_routes: bool,
565 #[serde(default)]
566 pub table_schema: bool,
567 #[serde(default)]
568 pub snapshots: bool,
569 #[serde(default)]
570 pub files: bool,
571 #[serde(default)]
572 pub metrics: bool,
573 #[serde(default)]
574 pub strongest_consistency: crate::checkpoint::CheckpointReadConsistency,
575}
576
577#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
579pub struct KvCapsView {
580 pub available: bool,
582 #[serde(default)]
586 pub cas: bool,
587 #[serde(default)]
591 pub cas_fenced: bool,
592 #[serde(default)]
595 pub fenced_leases: bool,
596}
597
598impl Capabilities {
599 pub fn new(enabled: bool, versions: OpVersions) -> Self {
606 Self {
607 managed: enabled,
608 query: QueryCapsView {
609 available: enabled,
610 projections: enabled,
611 schemas: enabled,
612 consistency: Consistency::Eventual,
613 keyword: false,
614 cursor_paging: false,
615 cancellation: false,
616 execution_status: false,
617 },
618 destinations: DestinationCapsView {
619 available: false,
620 lifecycle: false,
621 checkpoint_status: false,
622 query_routes: false,
623 table_schema: false,
624 snapshots: false,
625 files: false,
626 metrics: false,
627 strongest_consistency:
628 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
629 },
630 kv: KvCapsView {
631 available: enabled,
632 cas: false,
633 cas_fenced: false,
634 fenced_leases: false,
635 },
636 graph: false,
637 fork: enabled,
638 agent_workflow: false,
639 watch: false,
640 authz: false,
641 versions,
642 backends: Vec::new(),
643 topology: None,
644 }
645 }
646
647 #[must_use]
650 pub fn with_graph(mut self, value: bool) -> Self {
651 self.graph = value;
652 self
653 }
654
655 #[must_use]
658 pub fn with_agent_workflow(mut self, value: bool) -> Self {
659 self.agent_workflow = value;
660 self
661 }
662
663 #[must_use]
665 pub fn with_query_keyword(mut self, value: bool) -> Self {
666 self.query.keyword = value;
667 self
668 }
669
670 #[must_use]
671 pub fn with_query_execution(mut self, paging: bool, cancellation: bool, status: bool) -> Self {
672 self.query.cursor_paging = paging;
673 self.query.cancellation = cancellation;
674 self.query.execution_status = status;
675 self
676 }
677
678 #[must_use]
679 pub fn with_destinations(mut self, destinations: DestinationCapsView) -> Self {
680 self.destinations = destinations;
681 self
682 }
683
684 #[must_use]
687 pub fn with_watch(mut self, value: bool) -> Self {
688 self.watch = value;
689 self
690 }
691
692 #[must_use]
694 pub fn with_authz(mut self, value: bool) -> Self {
695 self.authz = value;
696 self
697 }
698
699 #[must_use]
702 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
703 self.backends = backends;
704 self
705 }
706
707 #[must_use]
710 pub fn with_topology(mut self, topology: WireTopology) -> Self {
711 self.topology = Some(topology);
712 self
713 }
714
715 #[must_use]
718 pub fn with_kv_cas(mut self, on: bool) -> Self {
719 self.kv.cas = on;
720 self
721 }
722
723 #[must_use]
726 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
727 self.kv.cas_fenced = on;
728 self
729 }
730
731 #[must_use]
734 pub fn with_kv_fenced_leases(mut self, on: bool) -> Self {
735 self.kv.fenced_leases = on;
736 self.kv.cas_fenced |= on;
737 self
738 }
739
740 #[must_use]
742 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
743 self.query.consistency = level;
744 self
745 }
746
747 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
753 use crate::hello::feature;
754 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
755 Consistency::Strong
756 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
757 Consistency::ReadYourWrites
758 } else {
759 Consistency::Eventual
760 };
761 let destinations_available =
762 enabled && versions.checkpoint > 0 && versions.has_feature(feature::DESTINATIONS);
763 Self::new(enabled, versions)
764 .with_kv_cas(versions.has_feature(feature::KV_CAS))
765 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
766 .with_kv_fenced_leases(versions.has_feature(feature::KV_FENCED_LEASES))
767 .with_agent_workflow(versions.has_feature(feature::AGENT_WORKFLOW))
768 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
769 .with_watch(versions.has_feature(feature::WATCH))
770 .with_authz(versions.has_feature(feature::AUTHZ))
771 .with_query_consistency(consistency)
772 .with_destinations(DestinationCapsView {
773 available: destinations_available,
774 lifecycle: destinations_available,
775 checkpoint_status: destinations_available,
776 query_routes: destinations_available,
777 table_schema: false,
778 snapshots: false,
779 files: false,
780 metrics: false,
781 strongest_consistency:
782 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
783 })
784 .with_graph(enabled && versions.graph > 0)
787 }
788}
789
790#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
794pub struct KvEntryView {
795 pub key: String,
796 pub value: String,
797 pub expires_at_micros: Option<u64>,
798 #[serde(default, skip_serializing_if = "Option::is_none")]
803 pub scope: Option<crate::kv::MemoryRowScope>,
804 #[serde(default, skip_serializing_if = "Option::is_none")]
809 pub source: Option<crate::graph::SourceRef>,
810}
811
812#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
814pub struct KvPageView {
815 pub entries: Vec<KvEntryView>,
816 pub cursor: Option<String>,
817}
818
819#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
824pub struct RunPageView {
825 pub runs: Vec<crate::agent_workflow::AgentRunInfo>,
826 pub cursor: Option<String>,
827}
828
829#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
831pub struct DeletedManyView {
832 pub deleted: usize,
833}
834
835#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
840pub struct ClientMetadataView {
841 pub client_id: u32,
842 pub user_id: Option<u32>,
843 pub transport: u8,
844 pub address: String,
845 pub consumer_groups_count: u32,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
847 pub metadata: Option<String>,
848}
849
850#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
853pub struct ClientMetadataListView {
854 pub clients: Vec<ClientMetadataView>,
855 #[serde(default, skip_serializing_if = "Option::is_none")]
856 pub next_cursor: Option<u32>,
857}
858
859#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
863pub struct ClientsQuery {
864 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
866 pub with_metadata_only: bool,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
869 pub user_id: Option<u32>,
870 #[serde(default, skip_serializing_if = "Option::is_none")]
872 pub after: Option<u32>,
873 #[serde(default, skip_serializing_if = "Option::is_none")]
875 pub limit: Option<u32>,
876}
877
878#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
880pub struct PromotedView {
881 pub rows: usize,
882}
883
884#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
888pub struct GraphNodeView {
889 pub id: String,
890 #[serde(default, skip_serializing_if = "Vec::is_empty")]
891 pub labels: Vec<String>,
892 #[serde(default, skip_serializing_if = "Vec::is_empty")]
893 pub attrs: Vec<(String, String)>,
894 #[serde(default, skip_serializing_if = "Option::is_none")]
896 pub source: Option<SourceRef>,
897}
898
899#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
902pub struct GraphEdgeView {
903 pub id: String,
904 pub from: String,
905 pub to: String,
906 pub edge_type: String,
907 pub weight: f32,
908 #[serde(default, skip_serializing_if = "Option::is_none")]
910 pub valid_from: Option<u64>,
911 #[serde(default, skip_serializing_if = "Option::is_none")]
912 pub valid_to: Option<u64>,
913 #[serde(default, skip_serializing_if = "Option::is_none")]
916 pub source: Option<SourceRef>,
917}
918
919#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
922pub struct GraphResultView {
923 #[serde(default, skip_serializing_if = "Vec::is_empty")]
924 pub nodes: Vec<GraphNodeView>,
925 #[serde(default, skip_serializing_if = "Vec::is_empty")]
926 pub edges: Vec<GraphEdgeView>,
927 #[serde(default, skip_serializing_if = "Vec::is_empty")]
928 pub paths: Vec<PathView>,
929}
930
931#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
934pub struct PathView {
935 pub nodes: Vec<String>,
936 pub edges: Vec<String>,
937}
938
939#[derive(Clone, Debug, Serialize, Deserialize)]
943pub struct RegisterSchemaBody {
944 pub source: crate::control::SchemaSource,
945 #[serde(default, skip_serializing_if = "Option::is_none")]
946 pub name: Option<String>,
947 #[serde(default, skip_serializing_if = "Option::is_none")]
948 pub version: Option<u32>,
949}
950
951#[derive(Clone, Debug, Serialize, Deserialize)]
954pub struct DecodeRecordBody {
955 pub payload: String,
956}
957
958#[derive(Clone, Debug, Serialize, Deserialize)]
960pub struct ForkCreateBody {
961 pub fork_id: String,
962 #[serde(default, skip_serializing_if = "Option::is_none")]
963 pub parent: Option<String>,
964 #[serde(default)]
965 pub kind: ForkKind,
966 #[serde(default, skip_serializing_if = "Vec::is_empty")]
967 pub tables: Vec<String>,
968}
969
970#[derive(Clone, Debug, Serialize, Deserialize)]
973pub struct ForkPutBody {
974 pub table: String,
975 pub partition_id: u32,
976 pub offset: u64,
977 #[serde(default)]
978 pub projection_id: String,
979 #[serde(default)]
980 pub projection_version: u32,
981 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
982 pub fields: BTreeMap<String, String>,
983 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
984 pub metadata: BTreeMap<String, String>,
985 #[serde(default, skip_serializing_if = "Option::is_none")]
986 pub payload_b64: Option<String>,
987 #[serde(default, skip_serializing_if = "Option::is_none")]
988 pub embedding: Option<String>,
989 #[serde(default)]
990 pub tombstone: bool,
991}
992
993#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
998pub struct RemoveBindingBody {
999 pub stream: String,
1000 pub topic: String,
1001 #[serde(default, skip_serializing_if = "Option::is_none")]
1002 pub projection_ref: Option<String>,
1003}
1004
1005#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1014pub struct ErrorBody {
1015 pub code: ResultCode,
1016 pub message: String,
1017 #[serde(default, skip_serializing_if = "Option::is_none")]
1018 pub detail: Option<serde_json::Value>,
1019}
1020
1021impl ErrorBody {
1022 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
1024 Self {
1025 code,
1026 message: message.into(),
1027 detail: None,
1028 }
1029 }
1030
1031 #[must_use]
1033 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
1034 self.detail = Some(detail);
1035 self
1036 }
1037
1038 pub fn http_status(&self) -> u16 {
1041 self.code.http_status()
1042 }
1043}
1044
1045impl From<&QueryError> for ErrorBody {
1046 fn from(error: &QueryError) -> Self {
1047 Self::new(ResultCode::from(error), error.to_string())
1048 }
1049}
1050
1051impl From<&KvError> for ErrorBody {
1052 fn from(error: &KvError) -> Self {
1053 Self::new(ResultCode::from(error), error.to_string())
1054 }
1055}
1056
1057impl From<&ForkError> for ErrorBody {
1058 fn from(error: &ForkError) -> Self {
1059 Self::new(ResultCode::from(error), error.to_string())
1060 }
1061}
1062
1063impl From<&crate::agent_workflow::AgentError> for ErrorBody {
1064 fn from(error: &crate::agent_workflow::AgentError) -> Self {
1065 Self::new(ResultCode::from(error), error.to_string())
1066 }
1067}
1068
1069pub const PARAM_TOPIC: &str = "topic";
1071pub const PARAM_NAME_CONTAINS: &str = "name_contains";
1073pub const PARAM_ID_PREFIX: &str = "id_prefix";
1075pub const PARAM_SEARCH: &str = "search";
1080pub const PARAM_PREFIX: &str = "prefix";
1082pub const PARAM_START: &str = "start";
1084pub const PARAM_END: &str = "end";
1086pub const PARAM_KEY_CONTAINS: &str = "key_contains";
1088pub const PARAM_LIMIT: &str = "limit";
1090pub const PARAM_CURSOR: &str = "cursor";
1092pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
1094pub const PARAM_EXPECT_VERSION: &str = "expect_version";
1097pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
1100
1101pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
1107
1108#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1113pub struct ProjectionListQuery {
1114 #[serde(default, skip_serializing_if = "Option::is_none")]
1115 pub topic: Option<String>,
1116 #[serde(default, skip_serializing_if = "Option::is_none")]
1117 pub name_contains: Option<String>,
1118 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub id_prefix: Option<String>,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub search: Option<String>,
1124}
1125
1126#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1130pub struct SchemaListQuery {
1131 #[serde(default, skip_serializing_if = "Option::is_none")]
1132 pub name_contains: Option<String>,
1133}
1134
1135#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1139pub struct KvScanQuery {
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub prefix: Option<String>,
1142 #[serde(default, skip_serializing_if = "Option::is_none")]
1143 pub start: Option<String>,
1144 #[serde(default, skip_serializing_if = "Option::is_none")]
1145 pub end: Option<String>,
1146 #[serde(default, skip_serializing_if = "Option::is_none")]
1147 pub key_contains: Option<String>,
1148 #[serde(default, skip_serializing_if = "Option::is_none")]
1153 pub conversation: Option<String>,
1154 #[serde(default, skip_serializing_if = "Option::is_none")]
1155 pub limit: Option<usize>,
1156 #[serde(default, skip_serializing_if = "Option::is_none")]
1157 pub cursor: Option<String>,
1158}
1159
1160#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1162pub struct KvPutQuery {
1163 #[serde(default, skip_serializing_if = "Option::is_none")]
1164 pub expires_at_micros: Option<u64>,
1165}
1166
1167#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1173pub struct GraphNeighborsQuery {
1174 #[serde(default, skip_serializing_if = "Option::is_none")]
1175 pub dir: Option<String>,
1176 #[serde(default, skip_serializing_if = "Option::is_none")]
1177 pub edge_type: Option<String>,
1178 #[serde(default, skip_serializing_if = "Option::is_none")]
1179 pub depth: Option<u32>,
1180 #[serde(default, skip_serializing_if = "Option::is_none")]
1181 pub limit: Option<usize>,
1182 #[serde(default, skip_serializing_if = "Option::is_none")]
1184 pub as_of: Option<u64>,
1185 #[serde(default, skip_serializing_if = "Option::is_none")]
1189 pub conversation: Option<String>,
1190}
1191
1192#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1199pub struct RunsQuery {
1200 #[serde(default, skip_serializing_if = "Option::is_none")]
1201 pub agent_id: Option<String>,
1202 #[serde(default, skip_serializing_if = "Option::is_none")]
1203 pub state: Option<String>,
1204 #[serde(default, skip_serializing_if = "Option::is_none")]
1205 pub limit: Option<u32>,
1206 #[serde(default, skip_serializing_if = "Option::is_none")]
1207 pub cursor: Option<String>,
1208}
1209
1210#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1215pub struct KvCasQuery {
1216 #[serde(default, skip_serializing_if = "Option::is_none")]
1217 pub expect_version: Option<u64>,
1218 #[serde(default, skip_serializing_if = "Option::is_none")]
1219 pub expect_absent: Option<bool>,
1220 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub expires_at_micros: Option<u64>,
1222}
1223
1224#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1227pub struct CasCommittedView {
1228 pub version: u64,
1229}
1230
1231#[cfg(test)]
1232mod tests {
1233 use super::*;
1234
1235 #[test]
1236 fn given_path_builders_when_rendered_then_should_match_the_router() {
1237 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1238 assert_eq!(projection_path("order.v1"), "/agdx/projections/order.v1");
1239 assert_eq!(schema_path(7), "/agdx/schemas/7");
1240 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1241 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1242 assert_eq!(
1243 kv_entry_path("sessions", "dXNlcjo0Mg"),
1244 "/agdx/kv/sessions/dXNlcjo0Mg"
1245 );
1246 assert_eq!(
1247 kv_cas_path("sessions", "dXNlcjo0Mg"),
1248 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1249 );
1250 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1251 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1252 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1253 }
1254
1255 #[test]
1256 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1257 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1258 assert!(
1259 caps.query.available && caps.kv.available && caps.fork,
1260 "core surfaces track enabled"
1261 );
1262 assert!(
1263 !caps.kv.cas
1264 && !caps.kv.cas_fenced
1265 && !caps.kv.fenced_leases
1266 && caps.query.consistency == Consistency::Eventual,
1267 "sub-features must be opt-in, never on by default"
1268 );
1269 let opted = caps
1270 .with_kv_cas(true)
1271 .with_query_consistency(Consistency::ReadYourWrites);
1272 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1273 }
1274
1275 #[test]
1276 fn given_the_fenced_lease_bit_when_building_http_capabilities_then_should_advertise_it() {
1277 let versions =
1278 OpVersions::new(1, 1, 1, 1).with_features(crate::hello::feature::KV_FENCED_LEASES);
1279 let caps = Capabilities::from_versions(true, versions);
1280 assert!(caps.kv.fenced_leases);
1281 assert!(
1282 caps.kv.cas_fenced,
1283 "the full fenced-lease contract subsumes fenced CAS"
1284 );
1285 }
1286
1287 #[test]
1288 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1289 use crate::codes::CHECKPOINT_OP_VERSION;
1290 use crate::hello::feature;
1291
1292 let versions = OpVersions::new(2, 2, 1, 1)
1293 .with_checkpoint(CHECKPOINT_OP_VERSION)
1294 .with_features(feature::DESTINATIONS);
1295 let caps = Capabilities::from_versions(true, versions);
1296
1297 assert!(caps.destinations.available);
1298 assert!(caps.destinations.lifecycle);
1299 assert!(caps.destinations.checkpoint_status);
1300 assert!(caps.destinations.query_routes);
1301 assert!(!caps.destinations.table_schema);
1302 }
1303
1304 #[test]
1305 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1306 use crate::destination::BackendResourceId;
1307 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1308 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1309 BackendDescriptor::new(
1310 BackendResourceId::from_u128(1),
1311 BackendMode::Operational,
1312 "Embedded",
1313 BackendImplementation {
1314 kind: "embedded".to_owned(),
1315 version: "1.0.0".to_owned(),
1316 },
1317 1,
1318 1,
1319 ),
1320 BackendDescriptor::new(
1321 BackendResourceId::from_u128(2),
1322 BackendMode::Lakehouse,
1323 "Warehouse",
1324 BackendImplementation {
1325 kind: "columnar".to_owned(),
1326 version: "1.0.0".to_owned(),
1327 },
1328 1,
1329 1,
1330 ),
1331 ]);
1332 let json = serde_json::to_string(&caps).expect("serializes");
1333 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1334 assert_eq!(back.backends.len(), 2);
1335 assert_eq!(
1336 back.backends[1].resource_id,
1337 BackendResourceId::from_u128(2)
1338 );
1339 assert_eq!(back.backends[1].implementation.kind, "columnar");
1340
1341 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1344 let json = serde_json::to_string(&plain).expect("json");
1345 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1346 }
1347
1348 #[test]
1349 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1350 let custom = WireTopology {
1351 ops_stream: "custom-ops".to_owned(),
1352 ..WireTopology::default()
1353 };
1354 let caps =
1355 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1356 let json = serde_json::to_string(&caps).expect("serializes");
1357 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1358 assert_eq!(back.topology, Some(custom));
1359
1360 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1363 let json = serde_json::to_string(&plain).expect("json");
1364 assert!(
1365 !json.contains("topology"),
1366 "absent topology omitted: {json}"
1367 );
1368 }
1369
1370 #[test]
1371 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1372 let body = ErrorBody::from(&QueryError::IndexNotFound("orders".to_owned()));
1373 assert_eq!(body.code, ResultCode::NotFound);
1374 assert_eq!(body.http_status(), 404);
1375 assert!(body.message.contains("orders"));
1376 let json = serde_json::to_string(&body).expect("serializes");
1378 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1379 assert_eq!(back, body);
1380 }
1381
1382 #[test]
1383 #[cfg(feature = "http-client")]
1384 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1385 let query = KvScanQuery {
1386 prefix: Some("dXNlcjo".to_owned()),
1387 limit: Some(50),
1388 ..Default::default()
1389 };
1390 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1391 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1392 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1394 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1395 }
1396
1397 #[test]
1398 #[cfg(feature = "http-client")]
1399 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1400 let projections = ProjectionListQuery {
1401 name_contains: Some("order".to_owned()),
1402 id_prefix: Some("order.".to_owned()),
1403 ..Default::default()
1404 };
1405 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1406 assert_eq!(encoded, "name_contains=order&id_prefix=order.");
1407 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1408 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1409
1410 let schemas = SchemaListQuery {
1411 name_contains: Some("Order".to_owned()),
1412 };
1413 assert_eq!(
1414 serde_urlencoded::to_string(&schemas).expect("encodes"),
1415 "name_contains=Order"
1416 );
1417 }
1418}