1use crate::checkpoint::{
2 CheckpointRequestEnvelope, DestinationCheckpointStatus, PreparedAttemptSummary, RetentionGap,
3};
4use crate::destination::{
5 DestinationId, DestinationOperationId, MaterializationDestination, QueryRoute,
6};
7use crate::fork::{ForkError, ForkKind};
8use crate::graph::SourceRef;
9use crate::hello::{BackendDescriptor, OpVersions};
10use crate::kv::KvError;
11use crate::query::{Consistency, QueryError, QueryExecutionId, QueryExecutionStatus, QueryResult};
12use crate::result::ResultCode;
13use crate::schema::{Digest32, LogicalSchema, TypedValue, UuidValue};
14use crate::topology::WireTopology;
15use serde::{Deserialize, Serialize};
16use std::collections::BTreeMap;
17
18pub const CAPABILITIES_PATH: &str = "/agdx/capabilities";
20pub const QUERY_PATH: &str = "/agdx/query";
22pub const DESTINATIONS_PATH: &str = "/agdx/destinations";
24pub const QUERY_ROUTES_PATH: &str = "/agdx/query-routes";
26pub const PROJECTIONS_PATH: &str = "/agdx/projections";
28pub const BINDINGS_PATH: &str = "/agdx/bindings";
30pub const SCHEMAS_PATH: &str = "/agdx/schemas";
32pub const KV_PATH: &str = "/agdx/kv";
34pub const FORKS_PATH: &str = "/agdx/forks";
36pub const GRAPHS_PATH: &str = "/agdx/graphs";
38pub const CLIENTS_PATH: &str = "/agdx/clients";
42pub const RUNS_PATH: &str = "/agdx/runs";
46pub const AUTHZ_WHOAMI_PATH: &str = "/agdx/authz/whoami";
48pub const AUTHZ_ROLES_PATH: &str = "/agdx/authz/roles";
50
51pub fn destination_path(id: DestinationId) -> String {
52 format!("{DESTINATIONS_PATH}/{id}")
53}
54
55pub fn destination_enable_path(id: DestinationId) -> String {
56 format!("{}/{id}/enable", DESTINATIONS_PATH)
57}
58
59pub fn destination_disable_path(id: DestinationId) -> String {
60 format!("{}/{id}/disable", DESTINATIONS_PATH)
61}
62
63pub fn destination_status_path(id: DestinationId) -> String {
64 format!("{}/{id}/status", DESTINATIONS_PATH)
65}
66
67pub fn destination_checkpoint_path(id: DestinationId) -> String {
68 format!("{}/{id}/checkpoint", DESTINATIONS_PATH)
69}
70
71pub fn destination_retention_gap_path(id: DestinationId) -> String {
72 format!("{}/{id}/retention-gap", DESTINATIONS_PATH)
73}
74
75pub fn destination_prepared_attempt_path(id: DestinationId) -> String {
76 format!("{}/{id}/prepared-attempt", DESTINATIONS_PATH)
77}
78
79pub fn destination_table_path(id: DestinationId) -> String {
80 format!("{}/{id}/table", DESTINATIONS_PATH)
81}
82
83pub fn destination_table_schema_path(id: DestinationId) -> String {
84 format!("{}/{id}/table/schema", DESTINATIONS_PATH)
85}
86
87pub fn destination_snapshots_path(id: DestinationId) -> String {
88 format!("{}/{id}/table/snapshots", DESTINATIONS_PATH)
89}
90
91pub fn destination_current_snapshot_path(id: DestinationId) -> String {
92 format!("{}/{id}/table/current-snapshot", DESTINATIONS_PATH)
93}
94
95pub fn destination_snapshot_path(id: DestinationId, snapshot_id: i64) -> String {
96 format!("{}/{id}/table/snapshots/{snapshot_id}", DESTINATIONS_PATH)
97}
98
99pub fn destination_files_path(id: DestinationId) -> String {
100 format!("{}/{id}/table/files", DESTINATIONS_PATH)
101}
102
103pub fn destination_metrics_path(id: DestinationId) -> String {
104 format!("{}/{id}/table/metrics", DESTINATIONS_PATH)
105}
106
107pub fn query_execution_path(id: QueryExecutionId) -> String {
108 format!("{QUERY_PATH}/{id}")
109}
110
111pub fn query_page_path(id: QueryExecutionId) -> String {
112 format!("{QUERY_PATH}/{id}/pages")
113}
114
115pub fn query_cancel_path(id: QueryExecutionId) -> String {
116 format!("{QUERY_PATH}/{id}/cancel")
117}
118
119pub fn destination_operation_path(id: DestinationOperationId) -> String {
120 format!("{DESTINATIONS_PATH}/operations/{id}")
121}
122
123#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
124pub struct DestinationListQuery {
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub source_stream: Option<String>,
127 #[serde(default, skip_serializing_if = "Option::is_none")]
128 pub source_topic: Option<String>,
129 #[serde(default, skip_serializing_if = "Option::is_none")]
130 pub name_contains: Option<String>,
131 #[serde(default, skip_serializing_if = "Option::is_none")]
132 pub after: Option<String>,
133 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub limit: Option<usize>,
135}
136
137#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
138pub struct DestinationPageView {
139 pub destinations: Vec<DestinationView>,
140 #[serde(default, skip_serializing_if = "Option::is_none")]
141 pub next_cursor: Option<String>,
142 pub global_state_revision: u64,
143 pub consistency: crate::checkpoint::CheckpointReadConsistency,
144}
145
146#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
147pub struct DestinationView {
148 pub destination: MaterializationDestination,
149 pub status: DestinationCheckpointStatus,
150}
151
152#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
153pub struct DestinationMutationBody {
154 pub checkpoint: CheckpointRequestEnvelope,
155}
156
157#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
158pub struct AcceptedOperationView {
159 pub operation_id: DestinationOperationId,
160 pub request_id: crate::checkpoint::CheckpointRequestId,
161 pub state: OperationState,
162 pub submitted_at_micros: u64,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub completed_at_micros: Option<u64>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
166 pub error: Option<OperationErrorView>,
167}
168
169#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
170pub struct OperationErrorView {
171 pub code: ResultCode,
172 pub message: String,
173}
174
175#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
176#[serde(rename_all = "snake_case")]
177#[non_exhaustive]
178pub enum OperationState {
179 Accepted,
180 Running,
181 Succeeded,
182 Failed,
183 Cancelled,
184}
185
186#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
187pub struct QueryRouteListQuery {
188 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub name_contains: Option<String>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
191 pub after: Option<String>,
192 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub limit: Option<usize>,
194}
195
196#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
197pub struct QueryRoutePageView {
198 pub routes: Vec<QueryRoute>,
199 #[serde(default, skip_serializing_if = "Option::is_none")]
200 pub next_cursor: Option<String>,
201 pub definition_revision: u64,
202}
203
204#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
205pub struct TableView {
206 pub table_uuid: UuidValue,
207 pub destination_id: DestinationId,
208 pub destination_generation: u64,
209 pub namespace: Vec<String>,
210 pub table: String,
211 pub current_snapshot_id: i64,
212 pub current_schema_id: i32,
213 pub current_partition_spec_id: i32,
214 pub metadata_identity: String,
215 pub properties: BTreeMap<String, String>,
216}
217
218#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
219pub struct TableSchemaView {
220 pub table_uuid: UuidValue,
221 pub iceberg_schema_id: i32,
222 pub logical_schema: LogicalSchema,
223}
224
225#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
226pub struct SnapshotListQuery {
227 #[serde(default, skip_serializing_if = "Option::is_none")]
228 pub before_snapshot_id: Option<i64>,
229 #[serde(default, skip_serializing_if = "Option::is_none")]
230 pub limit: Option<usize>,
231}
232
233#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
234pub struct SnapshotPageView {
235 pub snapshots: Vec<TableSnapshotView>,
236 #[serde(default, skip_serializing_if = "Option::is_none")]
237 pub next_before_snapshot_id: Option<i64>,
238}
239
240#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
241pub struct TableSnapshotView {
242 pub snapshot_id: i64,
243 #[serde(default, skip_serializing_if = "Option::is_none")]
244 pub parent_snapshot_id: Option<i64>,
245 pub sequence_number: i64,
246 pub committed_at_micros: u64,
247 pub schema_id: i32,
248 pub partition_spec_id: i32,
249 pub materialization_boundary_digest: Digest32,
250 pub checkpoint_revision: u64,
251 pub summary: BTreeMap<String, String>,
252}
253
254#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
255pub struct TableFileListQuery {
256 #[serde(default, skip_serializing_if = "Option::is_none")]
257 pub snapshot_id: Option<i64>,
258 #[serde(default, skip_serializing_if = "Option::is_none")]
259 pub after: Option<String>,
260 #[serde(default, skip_serializing_if = "Option::is_none")]
261 pub limit: Option<usize>,
262}
263
264#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
265pub struct TableFilePageView {
266 pub files: Vec<TableFileView>,
267 #[serde(default, skip_serializing_if = "Option::is_none")]
268 pub next_cursor: Option<String>,
269}
270
271#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
272pub struct TableFileView {
273 pub object_identity: String,
274 pub file_size_bytes: u64,
275 pub row_count: u64,
276 pub partition: BTreeMap<String, TypedValue>,
277 pub lower_bounds: BTreeMap<u32, TypedValue>,
278 pub upper_bounds: BTreeMap<u32, TypedValue>,
279 pub null_value_counts: BTreeMap<u32, u64>,
280}
281
282#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
283pub struct TableMetricsView {
284 pub snapshot_id: i64,
285 pub data_file_count: u64,
286 pub delete_file_count: u64,
287 pub total_rows: u64,
288 pub total_bytes: u64,
289 pub partition_count: u64,
290}
291
292#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
293pub struct QueryPageBody {
294 pub cursor: String,
295 pub deadline_micros: u64,
296}
297
298#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
299pub struct QueryExecutionView {
300 pub status: QueryExecutionStatus,
301 #[serde(default, skip_serializing_if = "Option::is_none")]
302 pub result: Option<QueryResult>,
303}
304
305#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
306pub struct DestinationIssueView {
307 #[serde(default, skip_serializing_if = "Option::is_none")]
308 pub retention_gap: Option<RetentionGap>,
309 #[serde(default, skip_serializing_if = "Option::is_none")]
310 pub prepared_attempt: Option<PreparedAttemptSummary>,
311 #[serde(default, skip_serializing_if = "Option::is_none")]
312 pub block: Option<crate::checkpoint::DestinationBlock>,
313 pub checkpoint_revision: u64,
314 pub consistency: crate::checkpoint::CheckpointReadConsistency,
315}
316
317pub fn authz_role_path(name: &str) -> String {
319 format!("{AUTHZ_ROLES_PATH}/{name}")
320}
321
322pub fn authz_user_roles_path(user_id: u32) -> String {
324 format!("/agdx/authz/users/{user_id}/roles")
325}
326
327pub fn graph_path(id: &str) -> String {
329 format!("{GRAPHS_PATH}/{id}")
330}
331
332pub fn graph_query_path(name: &str) -> String {
334 format!("/agdx/graph/{name}/query")
335}
336
337pub fn graph_neighbors_path(name: &str, node: &str) -> String {
339 format!("/agdx/graph/{name}/neighbors/{node}")
340}
341
342pub fn projection_path(id: &str) -> String {
344 format!("{PROJECTIONS_PATH}/{id}")
345}
346
347pub fn schema_path(id: u32) -> String {
349 format!("{SCHEMAS_PATH}/{id}")
350}
351
352pub fn schema_decode_path(id: u32) -> String {
354 format!("{SCHEMAS_PATH}/{id}/decode")
355}
356
357pub fn kv_namespace_path(namespace: &str) -> String {
359 format!("{KV_PATH}/{namespace}")
360}
361
362pub fn kv_entry_path(namespace: &str, key_b64: &str) -> String {
366 format!("{KV_PATH}/{namespace}/{key_b64}")
367}
368
369pub fn kv_cas_path(namespace: &str, key_b64: &str) -> String {
376 format!("{KV_PATH}/{namespace}/{key_b64}/cas")
377}
378
379pub fn fork_path(id: &str) -> String {
381 format!("{FORKS_PATH}/{id}")
382}
383
384pub fn fork_promote_path(id: &str) -> String {
386 format!("{FORKS_PATH}/{id}/promote")
387}
388
389pub fn fork_rows_path(id: &str) -> String {
391 format!("{FORKS_PATH}/{id}/rows")
392}
393
394pub fn run_path(id: &str) -> String {
396 format!("{RUNS_PATH}/{id}")
397}
398
399pub fn run_cancel_path(id: &str) -> String {
401 format!("{RUNS_PATH}/{id}/cancel")
402}
403
404#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
411#[non_exhaustive]
412pub struct Capabilities {
413 pub managed: bool,
417 pub query: QueryCapsView,
420 pub destinations: DestinationCapsView,
422 pub kv: KvCapsView,
424 #[serde(default)]
428 pub graph: bool,
429 pub fork: bool,
431 #[serde(default)]
434 pub agent_workflow: bool,
435 #[serde(default)]
439 pub watch: bool,
440 #[serde(default)]
443 pub authz: bool,
444 pub versions: OpVersions,
445 #[serde(default, skip_serializing_if = "Vec::is_empty")]
450 pub backends: Vec<BackendDescriptor>,
451 #[serde(default, skip_serializing_if = "Option::is_none")]
457 pub topology: Option<WireTopology>,
458}
459
460#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
463pub struct QueryCapsView {
464 pub available: bool,
466 pub projections: bool,
468 pub schemas: bool,
470 #[serde(default)]
474 pub consistency: Consistency,
475 #[serde(default)]
479 pub keyword: bool,
480 #[serde(default)]
482 pub cursor_paging: bool,
483 #[serde(default)]
485 pub cancellation: bool,
486 #[serde(default)]
488 pub execution_status: bool,
489}
490
491#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
492pub struct DestinationCapsView {
493 pub available: bool,
494 #[serde(default)]
495 pub lifecycle: bool,
496 #[serde(default)]
497 pub checkpoint_status: bool,
498 #[serde(default)]
499 pub query_routes: bool,
500 #[serde(default)]
501 pub table_schema: bool,
502 #[serde(default)]
503 pub snapshots: bool,
504 #[serde(default)]
505 pub files: bool,
506 #[serde(default)]
507 pub metrics: bool,
508 #[serde(default)]
509 pub strongest_consistency: crate::checkpoint::CheckpointReadConsistency,
510}
511
512#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
514pub struct KvCapsView {
515 pub available: bool,
517 #[serde(default)]
521 pub cas: bool,
522 #[serde(default)]
526 pub cas_fenced: bool,
527}
528
529impl Capabilities {
530 pub fn new(enabled: bool, versions: OpVersions) -> Self {
537 Self {
538 managed: enabled,
539 query: QueryCapsView {
540 available: enabled,
541 projections: enabled,
542 schemas: enabled,
543 consistency: Consistency::Eventual,
544 keyword: false,
545 cursor_paging: false,
546 cancellation: false,
547 execution_status: false,
548 },
549 destinations: DestinationCapsView {
550 available: false,
551 lifecycle: false,
552 checkpoint_status: false,
553 query_routes: false,
554 table_schema: false,
555 snapshots: false,
556 files: false,
557 metrics: false,
558 strongest_consistency:
559 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
560 },
561 kv: KvCapsView {
562 available: enabled,
563 cas: false,
564 cas_fenced: false,
565 },
566 graph: false,
567 fork: enabled,
568 agent_workflow: false,
569 watch: false,
570 authz: false,
571 versions,
572 backends: Vec::new(),
573 topology: None,
574 }
575 }
576
577 #[must_use]
580 pub fn with_graph(mut self, value: bool) -> Self {
581 self.graph = value;
582 self
583 }
584
585 #[must_use]
588 pub fn with_agent_workflow(mut self, value: bool) -> Self {
589 self.agent_workflow = value;
590 self
591 }
592
593 #[must_use]
595 pub fn with_query_keyword(mut self, value: bool) -> Self {
596 self.query.keyword = value;
597 self
598 }
599
600 #[must_use]
601 pub fn with_query_execution(mut self, paging: bool, cancellation: bool, status: bool) -> Self {
602 self.query.cursor_paging = paging;
603 self.query.cancellation = cancellation;
604 self.query.execution_status = status;
605 self
606 }
607
608 #[must_use]
609 pub fn with_destinations(mut self, destinations: DestinationCapsView) -> Self {
610 self.destinations = destinations;
611 self
612 }
613
614 #[must_use]
617 pub fn with_watch(mut self, value: bool) -> Self {
618 self.watch = value;
619 self
620 }
621
622 #[must_use]
624 pub fn with_authz(mut self, value: bool) -> Self {
625 self.authz = value;
626 self
627 }
628
629 #[must_use]
632 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
633 self.backends = backends;
634 self
635 }
636
637 #[must_use]
640 pub fn with_topology(mut self, topology: WireTopology) -> Self {
641 self.topology = Some(topology);
642 self
643 }
644
645 #[must_use]
648 pub fn with_kv_cas(mut self, on: bool) -> Self {
649 self.kv.cas = on;
650 self
651 }
652
653 #[must_use]
656 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
657 self.kv.cas_fenced = on;
658 self
659 }
660
661 #[must_use]
663 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
664 self.query.consistency = level;
665 self
666 }
667
668 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
674 use crate::hello::feature;
675 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
676 Consistency::Strong
677 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
678 Consistency::ReadYourWrites
679 } else {
680 Consistency::Eventual
681 };
682 let destinations_available =
683 enabled && versions.checkpoint > 0 && versions.has_feature(feature::DESTINATIONS);
684 Self::new(enabled, versions)
685 .with_kv_cas(versions.has_feature(feature::KV_CAS))
686 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
687 .with_agent_workflow(versions.has_feature(feature::AGENT_WORKFLOW))
688 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
689 .with_watch(versions.has_feature(feature::WATCH))
690 .with_authz(versions.has_feature(feature::AUTHZ))
691 .with_query_consistency(consistency)
692 .with_destinations(DestinationCapsView {
693 available: destinations_available,
694 lifecycle: destinations_available,
695 checkpoint_status: destinations_available,
696 query_routes: destinations_available,
697 table_schema: false,
698 snapshots: false,
699 files: false,
700 metrics: false,
701 strongest_consistency:
702 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
703 })
704 .with_graph(enabled && versions.graph > 0)
707 }
708}
709
710#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
714pub struct KvEntryView {
715 pub key: String,
716 pub value: String,
717 pub expires_at_micros: Option<u64>,
718 #[serde(default, skip_serializing_if = "Option::is_none")]
723 pub scope: Option<crate::kv::MemoryRowScope>,
724 #[serde(default, skip_serializing_if = "Option::is_none")]
729 pub source: Option<crate::graph::SourceRef>,
730}
731
732#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
734pub struct KvPageView {
735 pub entries: Vec<KvEntryView>,
736 pub cursor: Option<String>,
737}
738
739#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
744pub struct RunPageView {
745 pub runs: Vec<crate::agent_workflow::AgentRunInfo>,
746 pub cursor: Option<String>,
747}
748
749#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
751pub struct DeletedManyView {
752 pub deleted: usize,
753}
754
755#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
760pub struct ClientMetadataView {
761 pub client_id: u32,
762 pub user_id: Option<u32>,
763 pub transport: u8,
764 pub address: String,
765 pub consumer_groups_count: u32,
766 #[serde(default, skip_serializing_if = "Option::is_none")]
767 pub metadata: Option<String>,
768}
769
770#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
773pub struct ClientMetadataListView {
774 pub clients: Vec<ClientMetadataView>,
775 #[serde(default, skip_serializing_if = "Option::is_none")]
776 pub next_cursor: Option<u32>,
777}
778
779#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
783pub struct ClientsQuery {
784 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
786 pub with_metadata_only: bool,
787 #[serde(default, skip_serializing_if = "Option::is_none")]
789 pub user_id: Option<u32>,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
792 pub after: Option<u32>,
793 #[serde(default, skip_serializing_if = "Option::is_none")]
795 pub limit: Option<u32>,
796}
797
798#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
800pub struct PromotedView {
801 pub rows: usize,
802}
803
804#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
808pub struct GraphNodeView {
809 pub id: String,
810 #[serde(default, skip_serializing_if = "Vec::is_empty")]
811 pub labels: Vec<String>,
812 #[serde(default, skip_serializing_if = "Vec::is_empty")]
813 pub attrs: Vec<(String, String)>,
814 #[serde(default, skip_serializing_if = "Option::is_none")]
816 pub source: Option<SourceRef>,
817}
818
819#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
822pub struct GraphEdgeView {
823 pub id: String,
824 pub from: String,
825 pub to: String,
826 pub edge_type: String,
827 pub weight: f32,
828 #[serde(default, skip_serializing_if = "Option::is_none")]
830 pub valid_from: Option<u64>,
831 #[serde(default, skip_serializing_if = "Option::is_none")]
832 pub valid_to: Option<u64>,
833 #[serde(default, skip_serializing_if = "Option::is_none")]
836 pub source: Option<SourceRef>,
837}
838
839#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
842pub struct GraphResultView {
843 #[serde(default, skip_serializing_if = "Vec::is_empty")]
844 pub nodes: Vec<GraphNodeView>,
845 #[serde(default, skip_serializing_if = "Vec::is_empty")]
846 pub edges: Vec<GraphEdgeView>,
847 #[serde(default, skip_serializing_if = "Vec::is_empty")]
848 pub paths: Vec<PathView>,
849}
850
851#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
854pub struct PathView {
855 pub nodes: Vec<String>,
856 pub edges: Vec<String>,
857}
858
859#[derive(Clone, Debug, Serialize, Deserialize)]
863pub struct RegisterSchemaBody {
864 pub source: crate::control::SchemaSource,
865 #[serde(default, skip_serializing_if = "Option::is_none")]
866 pub name: Option<String>,
867 #[serde(default, skip_serializing_if = "Option::is_none")]
868 pub version: Option<u32>,
869}
870
871#[derive(Clone, Debug, Serialize, Deserialize)]
874pub struct DecodeRecordBody {
875 pub payload: String,
876}
877
878#[derive(Clone, Debug, Serialize, Deserialize)]
880pub struct ForkCreateBody {
881 pub fork_id: String,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub parent: Option<String>,
884 #[serde(default)]
885 pub kind: ForkKind,
886 #[serde(default, skip_serializing_if = "Vec::is_empty")]
887 pub tables: Vec<String>,
888}
889
890#[derive(Clone, Debug, Serialize, Deserialize)]
893pub struct ForkPutBody {
894 pub table: String,
895 pub partition_id: u32,
896 pub offset: u64,
897 #[serde(default)]
898 pub projection_id: String,
899 #[serde(default)]
900 pub projection_version: u32,
901 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
902 pub fields: BTreeMap<String, String>,
903 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
904 pub metadata: BTreeMap<String, String>,
905 #[serde(default, skip_serializing_if = "Option::is_none")]
906 pub payload_b64: Option<String>,
907 #[serde(default, skip_serializing_if = "Option::is_none")]
908 pub embedding: Option<String>,
909 #[serde(default)]
910 pub tombstone: bool,
911}
912
913#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
918pub struct RemoveBindingBody {
919 pub stream: String,
920 pub topic: String,
921 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub projection_ref: Option<String>,
923}
924
925#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
934pub struct ErrorBody {
935 pub code: ResultCode,
936 pub message: String,
937 #[serde(default, skip_serializing_if = "Option::is_none")]
938 pub detail: Option<serde_json::Value>,
939}
940
941impl ErrorBody {
942 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
944 Self {
945 code,
946 message: message.into(),
947 detail: None,
948 }
949 }
950
951 #[must_use]
953 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
954 self.detail = Some(detail);
955 self
956 }
957
958 pub fn http_status(&self) -> u16 {
961 self.code.http_status()
962 }
963}
964
965impl From<&QueryError> for ErrorBody {
966 fn from(error: &QueryError) -> Self {
967 Self::new(ResultCode::from(error), error.to_string())
968 }
969}
970
971impl From<&KvError> for ErrorBody {
972 fn from(error: &KvError) -> Self {
973 Self::new(ResultCode::from(error), error.to_string())
974 }
975}
976
977impl From<&ForkError> for ErrorBody {
978 fn from(error: &ForkError) -> Self {
979 Self::new(ResultCode::from(error), error.to_string())
980 }
981}
982
983impl From<&crate::agent_workflow::AgentError> for ErrorBody {
984 fn from(error: &crate::agent_workflow::AgentError) -> Self {
985 Self::new(ResultCode::from(error), error.to_string())
986 }
987}
988
989pub const PARAM_TOPIC: &str = "topic";
991pub const PARAM_NAME_CONTAINS: &str = "name_contains";
993pub const PARAM_ID_PREFIX: &str = "id_prefix";
995pub const PARAM_SEARCH: &str = "search";
1000pub const PARAM_PREFIX: &str = "prefix";
1002pub const PARAM_START: &str = "start";
1004pub const PARAM_END: &str = "end";
1006pub const PARAM_KEY_CONTAINS: &str = "key_contains";
1008pub const PARAM_LIMIT: &str = "limit";
1010pub const PARAM_CURSOR: &str = "cursor";
1012pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
1014pub const PARAM_EXPECT_VERSION: &str = "expect_version";
1017pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
1020
1021pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
1027
1028#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1033pub struct ProjectionListQuery {
1034 #[serde(default, skip_serializing_if = "Option::is_none")]
1035 pub topic: Option<String>,
1036 #[serde(default, skip_serializing_if = "Option::is_none")]
1037 pub name_contains: Option<String>,
1038 #[serde(default, skip_serializing_if = "Option::is_none")]
1039 pub id_prefix: Option<String>,
1040 #[serde(default, skip_serializing_if = "Option::is_none")]
1043 pub search: Option<String>,
1044}
1045
1046#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1050pub struct SchemaListQuery {
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1052 pub name_contains: Option<String>,
1053}
1054
1055#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1059pub struct KvScanQuery {
1060 #[serde(default, skip_serializing_if = "Option::is_none")]
1061 pub prefix: Option<String>,
1062 #[serde(default, skip_serializing_if = "Option::is_none")]
1063 pub start: Option<String>,
1064 #[serde(default, skip_serializing_if = "Option::is_none")]
1065 pub end: Option<String>,
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1067 pub key_contains: Option<String>,
1068 #[serde(default, skip_serializing_if = "Option::is_none")]
1073 pub conversation: Option<String>,
1074 #[serde(default, skip_serializing_if = "Option::is_none")]
1075 pub limit: Option<usize>,
1076 #[serde(default, skip_serializing_if = "Option::is_none")]
1077 pub cursor: Option<String>,
1078}
1079
1080#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1082pub struct KvPutQuery {
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1084 pub expires_at_micros: Option<u64>,
1085}
1086
1087#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1093pub struct GraphNeighborsQuery {
1094 #[serde(default, skip_serializing_if = "Option::is_none")]
1095 pub dir: Option<String>,
1096 #[serde(default, skip_serializing_if = "Option::is_none")]
1097 pub edge_type: Option<String>,
1098 #[serde(default, skip_serializing_if = "Option::is_none")]
1099 pub depth: Option<u32>,
1100 #[serde(default, skip_serializing_if = "Option::is_none")]
1101 pub limit: Option<usize>,
1102 #[serde(default, skip_serializing_if = "Option::is_none")]
1104 pub as_of: Option<u64>,
1105 #[serde(default, skip_serializing_if = "Option::is_none")]
1109 pub conversation: Option<String>,
1110}
1111
1112#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1119pub struct RunsQuery {
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1121 pub agent_id: Option<String>,
1122 #[serde(default, skip_serializing_if = "Option::is_none")]
1123 pub state: Option<String>,
1124 #[serde(default, skip_serializing_if = "Option::is_none")]
1125 pub limit: Option<u32>,
1126 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub cursor: Option<String>,
1128}
1129
1130#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1135pub struct KvCasQuery {
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub expect_version: Option<u64>,
1138 #[serde(default, skip_serializing_if = "Option::is_none")]
1139 pub expect_absent: Option<bool>,
1140 #[serde(default, skip_serializing_if = "Option::is_none")]
1141 pub expires_at_micros: Option<u64>,
1142}
1143
1144#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1147pub struct CasCommittedView {
1148 pub version: u64,
1149}
1150
1151#[cfg(test)]
1152mod tests {
1153 use super::*;
1154
1155 #[test]
1156 fn given_path_builders_when_rendered_then_should_match_the_router() {
1157 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1158 assert_eq!(projection_path("order.v1"), "/agdx/projections/order.v1");
1159 assert_eq!(schema_path(7), "/agdx/schemas/7");
1160 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1161 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1162 assert_eq!(
1163 kv_entry_path("sessions", "dXNlcjo0Mg"),
1164 "/agdx/kv/sessions/dXNlcjo0Mg"
1165 );
1166 assert_eq!(
1167 kv_cas_path("sessions", "dXNlcjo0Mg"),
1168 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1169 );
1170 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1171 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1172 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1173 }
1174
1175 #[test]
1176 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1177 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1178 assert!(
1179 caps.query.available && caps.kv.available && caps.fork,
1180 "core surfaces track enabled"
1181 );
1182 assert!(
1183 !caps.kv.cas && caps.query.consistency == Consistency::Eventual,
1184 "sub-features must be opt-in, never on by default"
1185 );
1186 let opted = caps
1187 .with_kv_cas(true)
1188 .with_query_consistency(Consistency::ReadYourWrites);
1189 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1190 }
1191
1192 #[test]
1193 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1194 use crate::codes::CHECKPOINT_OP_VERSION;
1195 use crate::hello::feature;
1196
1197 let versions = OpVersions::new(2, 2, 1, 1)
1198 .with_checkpoint(CHECKPOINT_OP_VERSION)
1199 .with_features(feature::DESTINATIONS);
1200 let caps = Capabilities::from_versions(true, versions);
1201
1202 assert!(caps.destinations.available);
1203 assert!(caps.destinations.lifecycle);
1204 assert!(caps.destinations.checkpoint_status);
1205 assert!(caps.destinations.query_routes);
1206 assert!(!caps.destinations.table_schema);
1207 }
1208
1209 #[test]
1210 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1211 use crate::destination::BackendResourceId;
1212 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1213 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1214 BackendDescriptor::new(
1215 BackendResourceId::from_u128(1),
1216 BackendMode::Operational,
1217 "Embedded",
1218 BackendImplementation {
1219 kind: "embedded".to_owned(),
1220 version: "1.0.0".to_owned(),
1221 },
1222 1,
1223 1,
1224 ),
1225 BackendDescriptor::new(
1226 BackendResourceId::from_u128(2),
1227 BackendMode::Lakehouse,
1228 "Warehouse",
1229 BackendImplementation {
1230 kind: "columnar".to_owned(),
1231 version: "1.0.0".to_owned(),
1232 },
1233 1,
1234 1,
1235 ),
1236 ]);
1237 let json = serde_json::to_string(&caps).expect("serializes");
1238 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1239 assert_eq!(back.backends.len(), 2);
1240 assert_eq!(
1241 back.backends[1].resource_id,
1242 BackendResourceId::from_u128(2)
1243 );
1244 assert_eq!(back.backends[1].implementation.kind, "columnar");
1245
1246 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1249 let json = serde_json::to_string(&plain).expect("json");
1250 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1251 }
1252
1253 #[test]
1254 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1255 let custom = WireTopology {
1256 ops_stream: "custom-ops".to_owned(),
1257 ..WireTopology::default()
1258 };
1259 let caps =
1260 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1261 let json = serde_json::to_string(&caps).expect("serializes");
1262 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1263 assert_eq!(back.topology, Some(custom));
1264
1265 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1268 let json = serde_json::to_string(&plain).expect("json");
1269 assert!(
1270 !json.contains("topology"),
1271 "absent topology omitted: {json}"
1272 );
1273 }
1274
1275 #[test]
1276 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1277 let body = ErrorBody::from(&QueryError::IndexNotFound("orders".to_owned()));
1278 assert_eq!(body.code, ResultCode::NotFound);
1279 assert_eq!(body.http_status(), 404);
1280 assert!(body.message.contains("orders"));
1281 let json = serde_json::to_string(&body).expect("serializes");
1283 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1284 assert_eq!(back, body);
1285 }
1286
1287 #[test]
1288 #[cfg(feature = "http-client")]
1289 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1290 let query = KvScanQuery {
1291 prefix: Some("dXNlcjo".to_owned()),
1292 limit: Some(50),
1293 ..Default::default()
1294 };
1295 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1296 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1297 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1299 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1300 }
1301
1302 #[test]
1303 #[cfg(feature = "http-client")]
1304 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1305 let projections = ProjectionListQuery {
1306 name_contains: Some("order".to_owned()),
1307 id_prefix: Some("order.".to_owned()),
1308 ..Default::default()
1309 };
1310 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1311 assert_eq!(encoded, "name_contains=order&id_prefix=order.");
1312 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1313 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1314
1315 let schemas = SchemaListQuery {
1316 name_contains: Some("Order".to_owned()),
1317 };
1318 assert_eq!(
1319 serde_urlencoded::to_string(&schemas).expect("encodes"),
1320 "name_contains=Order"
1321 );
1322 }
1323}