Skip to main content

hatidata_cli/
sync.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4/// Response from the control plane sync API.
5#[derive(Debug, Serialize, Deserialize)]
6pub struct SyncResponse {
7    pub success: bool,
8    pub message: String,
9    pub rows_synced: Option<u64>,
10}
11
12/// Remote table schema returned by the control plane.
13#[derive(Debug, Serialize, Deserialize)]
14pub struct TableSchema {
15    pub name: String,
16    pub columns: Vec<ColumnSchema>,
17}
18
19/// Column schema information.
20#[derive(Debug, Serialize, Deserialize)]
21pub struct ColumnSchema {
22    pub name: String,
23    pub data_type: String,
24    pub nullable: bool,
25}
26
27/// Client for syncing data between local DuckDB and the HatiData control plane.
28pub struct SyncClient {
29    _client: reqwest::Client,
30    _endpoint: String,
31    _api_key: String,
32}
33
34impl SyncClient {
35    /// Create a new sync client.
36    pub fn new(endpoint: &str, api_key: &str) -> Self {
37        Self {
38            _client: reqwest::Client::new(),
39            _endpoint: endpoint.trim_end_matches('/').to_string(),
40            _api_key: api_key.to_string(),
41        }
42    }
43
44    /// Push a table's Parquet data to the remote control plane.
45    ///
46    /// Calls `POST /v1/sync/push` with multipart form data.
47    #[allow(unused_variables)]
48    pub async fn push_table(
49        &self,
50        table_name: &str,
51        parquet_data: Vec<u8>,
52    ) -> Result<SyncResponse> {
53        // TODO: Implement actual HTTP upload to control plane
54        // The request should be:
55        //   POST {endpoint}/v1/sync/push
56        //   Authorization: Bearer {api_key}
57        //   Content-Type: multipart/form-data
58        //   Body: table_name + parquet file
59        //
60        // let form = reqwest::multipart::Form::new()
61        //     .text("table_name", table_name.to_string())
62        //     .part("data", reqwest::multipart::Part::bytes(parquet_data)
63        //         .file_name(format!("{table_name}.parquet"))
64        //         .mime_str("application/octet-stream")?);
65        //
66        // let response = self._client
67        //     .post(format!("{}/v1/sync/push", self._endpoint))
68        //     .bearer_auth(&self._api_key)
69        //     .multipart(form)
70        //     .send()
71        //     .await?
72        //     .json::<SyncResponse>()
73        //     .await?;
74
75        Ok(SyncResponse {
76            success: false,
77            message: "Push not yet implemented — waiting for control plane /v1/sync/push endpoint"
78                .to_string(),
79            rows_synced: None,
80        })
81    }
82
83    /// Pull the list of table schemas from the remote control plane.
84    #[allow(unused_variables)]
85    pub async fn pull_schema(&self) -> Result<Vec<TableSchema>> {
86        // TODO: Implement actual HTTP call to control plane
87        // GET {endpoint}/v1/sync/schema
88        // Authorization: Bearer {api_key}
89        //
90        // let response = self._client
91        //     .get(format!("{}/v1/sync/schema", self._endpoint))
92        //     .bearer_auth(&self._api_key)
93        //     .send()
94        //     .await?
95        //     .json::<Vec<TableSchema>>()
96        //     .await?;
97
98        Ok(Vec::new())
99    }
100
101    /// Pull a single table's data as Parquet bytes.
102    #[allow(unused_variables)]
103    pub async fn pull_table(&self, table_name: &str) -> Result<Vec<u8>> {
104        // TODO: Implement actual HTTP call to control plane
105        // GET {endpoint}/v1/sync/pull/{table_name}
106        // Authorization: Bearer {api_key}
107        // Accept: application/octet-stream
108        //
109        // let response = self._client
110        //     .get(format!("{}/v1/sync/pull/{table_name}", self._endpoint))
111        //     .bearer_auth(&self._api_key)
112        //     .send()
113        //     .await?
114        //     .bytes()
115        //     .await?;
116
117        Ok(Vec::new())
118    }
119}