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 #[serde(default)]
530 pub fenced_leases: bool,
531}
532
533impl Capabilities {
534 pub fn new(enabled: bool, versions: OpVersions) -> Self {
541 Self {
542 managed: enabled,
543 query: QueryCapsView {
544 available: enabled,
545 projections: enabled,
546 schemas: enabled,
547 consistency: Consistency::Eventual,
548 keyword: false,
549 cursor_paging: false,
550 cancellation: false,
551 execution_status: false,
552 },
553 destinations: DestinationCapsView {
554 available: false,
555 lifecycle: false,
556 checkpoint_status: false,
557 query_routes: false,
558 table_schema: false,
559 snapshots: false,
560 files: false,
561 metrics: false,
562 strongest_consistency:
563 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
564 },
565 kv: KvCapsView {
566 available: enabled,
567 cas: false,
568 cas_fenced: false,
569 fenced_leases: false,
570 },
571 graph: false,
572 fork: enabled,
573 agent_workflow: false,
574 watch: false,
575 authz: false,
576 versions,
577 backends: Vec::new(),
578 topology: None,
579 }
580 }
581
582 #[must_use]
585 pub fn with_graph(mut self, value: bool) -> Self {
586 self.graph = value;
587 self
588 }
589
590 #[must_use]
593 pub fn with_agent_workflow(mut self, value: bool) -> Self {
594 self.agent_workflow = value;
595 self
596 }
597
598 #[must_use]
600 pub fn with_query_keyword(mut self, value: bool) -> Self {
601 self.query.keyword = value;
602 self
603 }
604
605 #[must_use]
606 pub fn with_query_execution(mut self, paging: bool, cancellation: bool, status: bool) -> Self {
607 self.query.cursor_paging = paging;
608 self.query.cancellation = cancellation;
609 self.query.execution_status = status;
610 self
611 }
612
613 #[must_use]
614 pub fn with_destinations(mut self, destinations: DestinationCapsView) -> Self {
615 self.destinations = destinations;
616 self
617 }
618
619 #[must_use]
622 pub fn with_watch(mut self, value: bool) -> Self {
623 self.watch = value;
624 self
625 }
626
627 #[must_use]
629 pub fn with_authz(mut self, value: bool) -> Self {
630 self.authz = value;
631 self
632 }
633
634 #[must_use]
637 pub fn with_backends(mut self, backends: Vec<BackendDescriptor>) -> Self {
638 self.backends = backends;
639 self
640 }
641
642 #[must_use]
645 pub fn with_topology(mut self, topology: WireTopology) -> Self {
646 self.topology = Some(topology);
647 self
648 }
649
650 #[must_use]
653 pub fn with_kv_cas(mut self, on: bool) -> Self {
654 self.kv.cas = on;
655 self
656 }
657
658 #[must_use]
661 pub fn with_kv_cas_fenced(mut self, on: bool) -> Self {
662 self.kv.cas_fenced = on;
663 self
664 }
665
666 #[must_use]
669 pub fn with_kv_fenced_leases(mut self, on: bool) -> Self {
670 self.kv.fenced_leases = on;
671 self.kv.cas_fenced |= on;
672 self
673 }
674
675 #[must_use]
677 pub fn with_query_consistency(mut self, level: Consistency) -> Self {
678 self.query.consistency = level;
679 self
680 }
681
682 pub fn from_versions(enabled: bool, versions: OpVersions) -> Self {
688 use crate::hello::feature;
689 let consistency = if versions.has_feature(feature::STRONG_CONSISTENCY) {
690 Consistency::Strong
691 } else if versions.has_feature(feature::READ_YOUR_WRITES) {
692 Consistency::ReadYourWrites
693 } else {
694 Consistency::Eventual
695 };
696 let destinations_available =
697 enabled && versions.checkpoint > 0 && versions.has_feature(feature::DESTINATIONS);
698 Self::new(enabled, versions)
699 .with_kv_cas(versions.has_feature(feature::KV_CAS))
700 .with_kv_cas_fenced(versions.has_feature(feature::KV_CAS_FENCED))
701 .with_kv_fenced_leases(versions.has_feature(feature::KV_FENCED_LEASES))
702 .with_agent_workflow(versions.has_feature(feature::AGENT_WORKFLOW))
703 .with_query_keyword(versions.has_feature(feature::KEYWORD_SEARCH))
704 .with_watch(versions.has_feature(feature::WATCH))
705 .with_authz(versions.has_feature(feature::AUTHZ))
706 .with_query_consistency(consistency)
707 .with_destinations(DestinationCapsView {
708 available: destinations_available,
709 lifecycle: destinations_available,
710 checkpoint_status: destinations_available,
711 query_routes: destinations_available,
712 table_schema: false,
713 snapshots: false,
714 files: false,
715 metrics: false,
716 strongest_consistency:
717 crate::checkpoint::CheckpointReadConsistency::PotentiallyStale,
718 })
719 .with_graph(enabled && versions.graph > 0)
722 }
723}
724
725#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
729pub struct KvEntryView {
730 pub key: String,
731 pub value: String,
732 pub expires_at_micros: Option<u64>,
733 #[serde(default, skip_serializing_if = "Option::is_none")]
738 pub scope: Option<crate::kv::MemoryRowScope>,
739 #[serde(default, skip_serializing_if = "Option::is_none")]
744 pub source: Option<crate::graph::SourceRef>,
745}
746
747#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
749pub struct KvPageView {
750 pub entries: Vec<KvEntryView>,
751 pub cursor: Option<String>,
752}
753
754#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
759pub struct RunPageView {
760 pub runs: Vec<crate::agent_workflow::AgentRunInfo>,
761 pub cursor: Option<String>,
762}
763
764#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
766pub struct DeletedManyView {
767 pub deleted: usize,
768}
769
770#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
775pub struct ClientMetadataView {
776 pub client_id: u32,
777 pub user_id: Option<u32>,
778 pub transport: u8,
779 pub address: String,
780 pub consumer_groups_count: u32,
781 #[serde(default, skip_serializing_if = "Option::is_none")]
782 pub metadata: Option<String>,
783}
784
785#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
788pub struct ClientMetadataListView {
789 pub clients: Vec<ClientMetadataView>,
790 #[serde(default, skip_serializing_if = "Option::is_none")]
791 pub next_cursor: Option<u32>,
792}
793
794#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
798pub struct ClientsQuery {
799 #[serde(default, skip_serializing_if = "core::ops::Not::not")]
801 pub with_metadata_only: bool,
802 #[serde(default, skip_serializing_if = "Option::is_none")]
804 pub user_id: Option<u32>,
805 #[serde(default, skip_serializing_if = "Option::is_none")]
807 pub after: Option<u32>,
808 #[serde(default, skip_serializing_if = "Option::is_none")]
810 pub limit: Option<u32>,
811}
812
813#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
815pub struct PromotedView {
816 pub rows: usize,
817}
818
819#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
823pub struct GraphNodeView {
824 pub id: String,
825 #[serde(default, skip_serializing_if = "Vec::is_empty")]
826 pub labels: Vec<String>,
827 #[serde(default, skip_serializing_if = "Vec::is_empty")]
828 pub attrs: Vec<(String, String)>,
829 #[serde(default, skip_serializing_if = "Option::is_none")]
831 pub source: Option<SourceRef>,
832}
833
834#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
837pub struct GraphEdgeView {
838 pub id: String,
839 pub from: String,
840 pub to: String,
841 pub edge_type: String,
842 pub weight: f32,
843 #[serde(default, skip_serializing_if = "Option::is_none")]
845 pub valid_from: Option<u64>,
846 #[serde(default, skip_serializing_if = "Option::is_none")]
847 pub valid_to: Option<u64>,
848 #[serde(default, skip_serializing_if = "Option::is_none")]
851 pub source: Option<SourceRef>,
852}
853
854#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
857pub struct GraphResultView {
858 #[serde(default, skip_serializing_if = "Vec::is_empty")]
859 pub nodes: Vec<GraphNodeView>,
860 #[serde(default, skip_serializing_if = "Vec::is_empty")]
861 pub edges: Vec<GraphEdgeView>,
862 #[serde(default, skip_serializing_if = "Vec::is_empty")]
863 pub paths: Vec<PathView>,
864}
865
866#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
869pub struct PathView {
870 pub nodes: Vec<String>,
871 pub edges: Vec<String>,
872}
873
874#[derive(Clone, Debug, Serialize, Deserialize)]
878pub struct RegisterSchemaBody {
879 pub source: crate::control::SchemaSource,
880 #[serde(default, skip_serializing_if = "Option::is_none")]
881 pub name: Option<String>,
882 #[serde(default, skip_serializing_if = "Option::is_none")]
883 pub version: Option<u32>,
884}
885
886#[derive(Clone, Debug, Serialize, Deserialize)]
889pub struct DecodeRecordBody {
890 pub payload: String,
891}
892
893#[derive(Clone, Debug, Serialize, Deserialize)]
895pub struct ForkCreateBody {
896 pub fork_id: String,
897 #[serde(default, skip_serializing_if = "Option::is_none")]
898 pub parent: Option<String>,
899 #[serde(default)]
900 pub kind: ForkKind,
901 #[serde(default, skip_serializing_if = "Vec::is_empty")]
902 pub tables: Vec<String>,
903}
904
905#[derive(Clone, Debug, Serialize, Deserialize)]
908pub struct ForkPutBody {
909 pub table: String,
910 pub partition_id: u32,
911 pub offset: u64,
912 #[serde(default)]
913 pub projection_id: String,
914 #[serde(default)]
915 pub projection_version: u32,
916 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
917 pub fields: BTreeMap<String, String>,
918 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
919 pub metadata: BTreeMap<String, String>,
920 #[serde(default, skip_serializing_if = "Option::is_none")]
921 pub payload_b64: Option<String>,
922 #[serde(default, skip_serializing_if = "Option::is_none")]
923 pub embedding: Option<String>,
924 #[serde(default)]
925 pub tombstone: bool,
926}
927
928#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
933pub struct RemoveBindingBody {
934 pub stream: String,
935 pub topic: String,
936 #[serde(default, skip_serializing_if = "Option::is_none")]
937 pub projection_ref: Option<String>,
938}
939
940#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
949pub struct ErrorBody {
950 pub code: ResultCode,
951 pub message: String,
952 #[serde(default, skip_serializing_if = "Option::is_none")]
953 pub detail: Option<serde_json::Value>,
954}
955
956impl ErrorBody {
957 pub fn new(code: ResultCode, message: impl Into<String>) -> Self {
959 Self {
960 code,
961 message: message.into(),
962 detail: None,
963 }
964 }
965
966 #[must_use]
968 pub fn with_detail(mut self, detail: serde_json::Value) -> Self {
969 self.detail = Some(detail);
970 self
971 }
972
973 pub fn http_status(&self) -> u16 {
976 self.code.http_status()
977 }
978}
979
980impl From<&QueryError> for ErrorBody {
981 fn from(error: &QueryError) -> Self {
982 Self::new(ResultCode::from(error), error.to_string())
983 }
984}
985
986impl From<&KvError> for ErrorBody {
987 fn from(error: &KvError) -> Self {
988 Self::new(ResultCode::from(error), error.to_string())
989 }
990}
991
992impl From<&ForkError> for ErrorBody {
993 fn from(error: &ForkError) -> Self {
994 Self::new(ResultCode::from(error), error.to_string())
995 }
996}
997
998impl From<&crate::agent_workflow::AgentError> for ErrorBody {
999 fn from(error: &crate::agent_workflow::AgentError) -> Self {
1000 Self::new(ResultCode::from(error), error.to_string())
1001 }
1002}
1003
1004pub const PARAM_TOPIC: &str = "topic";
1006pub const PARAM_NAME_CONTAINS: &str = "name_contains";
1008pub const PARAM_ID_PREFIX: &str = "id_prefix";
1010pub const PARAM_SEARCH: &str = "search";
1015pub const PARAM_PREFIX: &str = "prefix";
1017pub const PARAM_START: &str = "start";
1019pub const PARAM_END: &str = "end";
1021pub const PARAM_KEY_CONTAINS: &str = "key_contains";
1023pub const PARAM_LIMIT: &str = "limit";
1025pub const PARAM_CURSOR: &str = "cursor";
1027pub const PARAM_EXPIRES_AT_MICROS: &str = "expires_at_micros";
1029pub const PARAM_EXPECT_VERSION: &str = "expect_version";
1032pub const PARAM_EXPECT_ABSENT: &str = "expect_absent";
1035
1036pub const KV_EXPIRES_AT_MICROS_HEADER: &str = "agdx-expires-at-micros";
1042
1043#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1048pub struct ProjectionListQuery {
1049 #[serde(default, skip_serializing_if = "Option::is_none")]
1050 pub topic: Option<String>,
1051 #[serde(default, skip_serializing_if = "Option::is_none")]
1052 pub name_contains: Option<String>,
1053 #[serde(default, skip_serializing_if = "Option::is_none")]
1054 pub id_prefix: Option<String>,
1055 #[serde(default, skip_serializing_if = "Option::is_none")]
1058 pub search: Option<String>,
1059}
1060
1061#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1065pub struct SchemaListQuery {
1066 #[serde(default, skip_serializing_if = "Option::is_none")]
1067 pub name_contains: Option<String>,
1068}
1069
1070#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1074pub struct KvScanQuery {
1075 #[serde(default, skip_serializing_if = "Option::is_none")]
1076 pub prefix: Option<String>,
1077 #[serde(default, skip_serializing_if = "Option::is_none")]
1078 pub start: Option<String>,
1079 #[serde(default, skip_serializing_if = "Option::is_none")]
1080 pub end: Option<String>,
1081 #[serde(default, skip_serializing_if = "Option::is_none")]
1082 pub key_contains: Option<String>,
1083 #[serde(default, skip_serializing_if = "Option::is_none")]
1088 pub conversation: Option<String>,
1089 #[serde(default, skip_serializing_if = "Option::is_none")]
1090 pub limit: Option<usize>,
1091 #[serde(default, skip_serializing_if = "Option::is_none")]
1092 pub cursor: Option<String>,
1093}
1094
1095#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1097pub struct KvPutQuery {
1098 #[serde(default, skip_serializing_if = "Option::is_none")]
1099 pub expires_at_micros: Option<u64>,
1100}
1101
1102#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1108pub struct GraphNeighborsQuery {
1109 #[serde(default, skip_serializing_if = "Option::is_none")]
1110 pub dir: Option<String>,
1111 #[serde(default, skip_serializing_if = "Option::is_none")]
1112 pub edge_type: Option<String>,
1113 #[serde(default, skip_serializing_if = "Option::is_none")]
1114 pub depth: Option<u32>,
1115 #[serde(default, skip_serializing_if = "Option::is_none")]
1116 pub limit: Option<usize>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1119 pub as_of: Option<u64>,
1120 #[serde(default, skip_serializing_if = "Option::is_none")]
1124 pub conversation: Option<String>,
1125}
1126
1127#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1134pub struct RunsQuery {
1135 #[serde(default, skip_serializing_if = "Option::is_none")]
1136 pub agent_id: Option<String>,
1137 #[serde(default, skip_serializing_if = "Option::is_none")]
1138 pub state: Option<String>,
1139 #[serde(default, skip_serializing_if = "Option::is_none")]
1140 pub limit: Option<u32>,
1141 #[serde(default, skip_serializing_if = "Option::is_none")]
1142 pub cursor: Option<String>,
1143}
1144
1145#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
1150pub struct KvCasQuery {
1151 #[serde(default, skip_serializing_if = "Option::is_none")]
1152 pub expect_version: Option<u64>,
1153 #[serde(default, skip_serializing_if = "Option::is_none")]
1154 pub expect_absent: Option<bool>,
1155 #[serde(default, skip_serializing_if = "Option::is_none")]
1156 pub expires_at_micros: Option<u64>,
1157}
1158
1159#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
1162pub struct CasCommittedView {
1163 pub version: u64,
1164}
1165
1166#[cfg(test)]
1167mod tests {
1168 use super::*;
1169
1170 #[test]
1171 fn given_path_builders_when_rendered_then_should_match_the_router() {
1172 assert_eq!(CAPABILITIES_PATH, "/agdx/capabilities");
1173 assert_eq!(projection_path("order.v1"), "/agdx/projections/order.v1");
1174 assert_eq!(schema_path(7), "/agdx/schemas/7");
1175 assert_eq!(schema_decode_path(7), "/agdx/schemas/7/decode");
1176 assert_eq!(kv_namespace_path("sessions"), "/agdx/kv/sessions");
1177 assert_eq!(
1178 kv_entry_path("sessions", "dXNlcjo0Mg"),
1179 "/agdx/kv/sessions/dXNlcjo0Mg"
1180 );
1181 assert_eq!(
1182 kv_cas_path("sessions", "dXNlcjo0Mg"),
1183 "/agdx/kv/sessions/dXNlcjo0Mg/cas"
1184 );
1185 assert_eq!(fork_path("f1"), "/agdx/forks/f1");
1186 assert_eq!(fork_promote_path("f1"), "/agdx/forks/f1/promote");
1187 assert_eq!(fork_rows_path("f1"), "/agdx/forks/f1/rows");
1188 }
1189
1190 #[test]
1191 fn given_capabilities_when_constructed_then_extended_features_default_off() {
1192 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1193 assert!(
1194 caps.query.available && caps.kv.available && caps.fork,
1195 "core surfaces track enabled"
1196 );
1197 assert!(
1198 !caps.kv.cas
1199 && !caps.kv.cas_fenced
1200 && !caps.kv.fenced_leases
1201 && caps.query.consistency == Consistency::Eventual,
1202 "sub-features must be opt-in, never on by default"
1203 );
1204 let opted = caps
1205 .with_kv_cas(true)
1206 .with_query_consistency(Consistency::ReadYourWrites);
1207 assert!(opted.kv.cas && opted.query.consistency == Consistency::ReadYourWrites);
1208 }
1209
1210 #[test]
1211 fn given_the_fenced_lease_bit_when_building_http_capabilities_then_should_advertise_it() {
1212 let versions =
1213 OpVersions::new(1, 1, 1, 1).with_features(crate::hello::feature::KV_FENCED_LEASES);
1214 let caps = Capabilities::from_versions(true, versions);
1215 assert!(caps.kv.fenced_leases);
1216 assert!(
1217 caps.kv.cas_fenced,
1218 "the full fenced-lease contract subsumes fenced CAS"
1219 );
1220 }
1221
1222 #[test]
1223 fn given_destination_versions_when_building_capabilities_then_the_core_surface_is_available() {
1224 use crate::codes::CHECKPOINT_OP_VERSION;
1225 use crate::hello::feature;
1226
1227 let versions = OpVersions::new(2, 2, 1, 1)
1228 .with_checkpoint(CHECKPOINT_OP_VERSION)
1229 .with_features(feature::DESTINATIONS);
1230 let caps = Capabilities::from_versions(true, versions);
1231
1232 assert!(caps.destinations.available);
1233 assert!(caps.destinations.lifecycle);
1234 assert!(caps.destinations.checkpoint_status);
1235 assert!(caps.destinations.query_routes);
1236 assert!(!caps.destinations.table_schema);
1237 }
1238
1239 #[test]
1240 fn given_capabilities_backends_when_json_round_tripped_then_should_preserve_and_omit_empty() {
1241 use crate::destination::BackendResourceId;
1242 use crate::hello::{BackendDescriptor, BackendImplementation, BackendMode};
1243 let caps = Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_backends(vec![
1244 BackendDescriptor::new(
1245 BackendResourceId::from_u128(1),
1246 BackendMode::Operational,
1247 "Embedded",
1248 BackendImplementation {
1249 kind: "embedded".to_owned(),
1250 version: "1.0.0".to_owned(),
1251 },
1252 1,
1253 1,
1254 ),
1255 BackendDescriptor::new(
1256 BackendResourceId::from_u128(2),
1257 BackendMode::Lakehouse,
1258 "Warehouse",
1259 BackendImplementation {
1260 kind: "columnar".to_owned(),
1261 version: "1.0.0".to_owned(),
1262 },
1263 1,
1264 1,
1265 ),
1266 ]);
1267 let json = serde_json::to_string(&caps).expect("serializes");
1268 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1269 assert_eq!(back.backends.len(), 2);
1270 assert_eq!(
1271 back.backends[1].resource_id,
1272 BackendResourceId::from_u128(2)
1273 );
1274 assert_eq!(back.backends[1].implementation.kind, "columnar");
1275
1276 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1279 let json = serde_json::to_string(&plain).expect("json");
1280 assert!(!json.contains("backends"), "empty backends omitted: {json}");
1281 }
1282
1283 #[test]
1284 fn given_capabilities_topology_when_json_round_tripped_then_should_preserve_and_omit_absent() {
1285 let custom = WireTopology {
1286 ops_stream: "custom-ops".to_owned(),
1287 ..WireTopology::default()
1288 };
1289 let caps =
1290 Capabilities::new(true, OpVersions::new(1, 1, 1, 1)).with_topology(custom.clone());
1291 let json = serde_json::to_string(&caps).expect("serializes");
1292 let back: Capabilities = serde_json::from_str(&json).expect("deserializes");
1293 assert_eq!(back.topology, Some(custom));
1294
1295 let plain = Capabilities::new(true, OpVersions::new(1, 1, 1, 1));
1298 let json = serde_json::to_string(&plain).expect("json");
1299 assert!(
1300 !json.contains("topology"),
1301 "absent topology omitted: {json}"
1302 );
1303 }
1304
1305 #[test]
1306 fn given_a_typed_error_when_made_into_a_body_then_should_carry_code_and_message() {
1307 let body = ErrorBody::from(&QueryError::IndexNotFound("orders".to_owned()));
1308 assert_eq!(body.code, ResultCode::NotFound);
1309 assert_eq!(body.http_status(), 404);
1310 assert!(body.message.contains("orders"));
1311 let json = serde_json::to_string(&body).expect("serializes");
1313 let back: ErrorBody = serde_json::from_str(&json).expect("deserializes");
1314 assert_eq!(back, body);
1315 }
1316
1317 #[test]
1318 #[cfg(feature = "http-client")]
1319 fn given_scan_filters_when_url_encoded_then_should_omit_absent_fields() {
1320 let query = KvScanQuery {
1321 prefix: Some("dXNlcjo".to_owned()),
1322 limit: Some(50),
1323 ..Default::default()
1324 };
1325 let encoded = serde_urlencoded::to_string(&query).expect("encodes");
1326 assert_eq!(encoded, "prefix=dXNlcjo&limit=50");
1327 assert!(encoded.contains(&format!("{PARAM_PREFIX}=")));
1329 assert!(encoded.contains(&format!("{PARAM_LIMIT}=")));
1330 }
1331
1332 #[test]
1333 #[cfg(feature = "http-client")]
1334 fn given_list_filters_when_url_encoded_then_field_names_match_the_param_consts() {
1335 let projections = ProjectionListQuery {
1336 name_contains: Some("order".to_owned()),
1337 id_prefix: Some("order.".to_owned()),
1338 ..Default::default()
1339 };
1340 let encoded = serde_urlencoded::to_string(&projections).expect("encodes");
1341 assert_eq!(encoded, "name_contains=order&id_prefix=order.");
1342 assert!(encoded.contains(&format!("{PARAM_NAME_CONTAINS}=")));
1343 assert!(encoded.contains(&format!("{PARAM_ID_PREFIX}=")));
1344
1345 let schemas = SchemaListQuery {
1346 name_contains: Some("Order".to_owned()),
1347 };
1348 assert_eq!(
1349 serde_urlencoded::to_string(&schemas).expect("encodes"),
1350 "name_contains=Order"
1351 );
1352 }
1353}