Skip to main content

azure_lite_rs/api/
sql.rs

1//! Azure SQL API client.
2//!
3//! Wraps the ARM management plane operations for Azure SQL: servers,
4//! databases, and firewall rules. All URL construction is in
5//! `ops::sql::SqlOps`. `subscription_id` is auto-injected from the
6//! parent `AzureHttpClient`.
7
8use crate::{
9    AzureHttpClient, Result,
10    ops::sql::SqlOps,
11    types::sql::{
12        Database, DatabaseCreateRequest, DatabaseListResult, EnableServerAuditingRequest,
13        FirewallRule, FirewallRuleCreateRequest, FirewallRuleListResult, Server,
14        ServerBlobAuditingPolicy, ServerCreateRequest, ServerListResult,
15    },
16};
17
18/// Client for the Azure SQL ARM management plane.
19///
20/// Wraps [`SqlOps`] with ergonomic signatures that auto-inject
21/// `subscription_id` from the parent [`AzureHttpClient`].
22pub struct SqlClient<'a> {
23    ops: SqlOps<'a>,
24    client: &'a AzureHttpClient,
25}
26
27impl<'a> SqlClient<'a> {
28    /// Create a new Azure SQL API client.
29    pub(crate) fn new(client: &'a AzureHttpClient) -> Self {
30        Self {
31            ops: SqlOps::new(client),
32            client,
33        }
34    }
35
36    // --- Server operations ---
37
38    /// Gets a list of all servers in the subscription.
39    pub async fn list_servers(&self) -> Result<ServerListResult> {
40        self.ops.list_servers(self.client.subscription_id()).await
41    }
42
43    /// Gets a server.
44    pub async fn get_server(&self, resource_group_name: &str, server_name: &str) -> Result<Server> {
45        self.ops
46            .get_server(
47                self.client.subscription_id(),
48                resource_group_name,
49                server_name,
50            )
51            .await
52    }
53
54    /// Creates or updates a server.
55    pub async fn create_server(
56        &self,
57        resource_group_name: &str,
58        server_name: &str,
59        body: &ServerCreateRequest,
60    ) -> Result<Server> {
61        self.ops
62            .create_server(
63                self.client.subscription_id(),
64                resource_group_name,
65                server_name,
66                body,
67            )
68            .await
69    }
70
71    /// Deletes a server.
72    pub async fn delete_server(&self, resource_group_name: &str, server_name: &str) -> Result<()> {
73        self.ops
74            .delete_server(
75                self.client.subscription_id(),
76                resource_group_name,
77                server_name,
78            )
79            .await
80    }
81
82    // --- Database operations ---
83
84    /// Gets a list of databases in a server.
85    pub async fn list_databases(
86        &self,
87        resource_group_name: &str,
88        server_name: &str,
89    ) -> Result<DatabaseListResult> {
90        self.ops
91            .list_databases(
92                self.client.subscription_id(),
93                resource_group_name,
94                server_name,
95            )
96            .await
97    }
98
99    /// Gets a database.
100    pub async fn get_database(
101        &self,
102        resource_group_name: &str,
103        server_name: &str,
104        database_name: &str,
105    ) -> Result<Database> {
106        self.ops
107            .get_database(
108                self.client.subscription_id(),
109                resource_group_name,
110                server_name,
111                database_name,
112            )
113            .await
114    }
115
116    /// Creates or updates a database.
117    pub async fn create_database(
118        &self,
119        resource_group_name: &str,
120        server_name: &str,
121        database_name: &str,
122        body: &DatabaseCreateRequest,
123    ) -> Result<Database> {
124        self.ops
125            .create_database(
126                self.client.subscription_id(),
127                resource_group_name,
128                server_name,
129                database_name,
130                body,
131            )
132            .await
133    }
134
135    /// Deletes a database.
136    pub async fn delete_database(
137        &self,
138        resource_group_name: &str,
139        server_name: &str,
140        database_name: &str,
141    ) -> Result<()> {
142        self.ops
143            .delete_database(
144                self.client.subscription_id(),
145                resource_group_name,
146                server_name,
147                database_name,
148            )
149            .await
150    }
151
152    // --- Auditing operations ---
153
154    /// Gets the server-level blob auditing policy (`auditingSettings/default`).
155    pub async fn get_server_audit_policy(
156        &self,
157        resource_group_name: &str,
158        server_name: &str,
159    ) -> Result<ServerBlobAuditingPolicy> {
160        self.ops
161            .get_server_audit_policy(
162                self.client.subscription_id(),
163                resource_group_name,
164                server_name,
165            )
166            .await
167    }
168
169    /// Enables server-level auditing (sets `state: "Enabled"` on `auditingSettings/default`).
170    pub async fn enable_server_auditing(
171        &self,
172        resource_group_name: &str,
173        server_name: &str,
174        body: &EnableServerAuditingRequest,
175    ) -> Result<ServerBlobAuditingPolicy> {
176        self.ops
177            .enable_server_auditing(
178                self.client.subscription_id(),
179                resource_group_name,
180                server_name,
181                body,
182            )
183            .await
184    }
185
186    // --- Firewall rule operations ---
187
188    /// Gets a list of firewall rules for a server.
189    pub async fn list_firewall_rules(
190        &self,
191        resource_group_name: &str,
192        server_name: &str,
193    ) -> Result<FirewallRuleListResult> {
194        self.ops
195            .list_firewall_rules(
196                self.client.subscription_id(),
197                resource_group_name,
198                server_name,
199            )
200            .await
201    }
202
203    /// Creates or updates a firewall rule.
204    pub async fn create_firewall_rule(
205        &self,
206        resource_group_name: &str,
207        server_name: &str,
208        firewall_rule_name: &str,
209        body: &FirewallRuleCreateRequest,
210    ) -> Result<FirewallRule> {
211        self.ops
212            .create_firewall_rule(
213                self.client.subscription_id(),
214                resource_group_name,
215                server_name,
216                firewall_rule_name,
217                body,
218            )
219            .await
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::*;
226    use crate::{
227        MockClient,
228        types::sql::{DatabaseSku, FirewallRuleProperties, ServerCreateOrUpdateProperties},
229    };
230
231    const SUB_ID: &str = "test-subscription-id";
232    const RG: &str = "test-rg";
233    const SERVER: &str = "cloud-lite-test-sql-srv";
234    const DB: &str = "cloud-lite-test-db";
235    const FW_RULE: &str = "cloud-lite-test-fw-rule";
236
237    fn make_client(mock: MockClient) -> AzureHttpClient {
238        AzureHttpClient::from_mock(mock)
239    }
240
241    fn server_json() -> serde_json::Value {
242        serde_json::json!({
243            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}"),
244            "name": SERVER,
245            "type": "Microsoft.Sql/servers",
246            "location": "eastus",
247            "properties": {
248                "administratorLogin": "cloudliteadmin",
249                "fullyQualifiedDomainName": format!("{SERVER}.database.windows.net"),
250                "state": "Ready",
251                "version": "12.0"
252            }
253        })
254    }
255
256    fn database_json() -> serde_json::Value {
257        serde_json::json!({
258            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/databases/{DB}"),
259            "name": DB,
260            "type": "Microsoft.Sql/servers/databases",
261            "location": "eastus",
262            "sku": { "name": "Basic", "tier": "Basic" },
263            "properties": {
264                "status": "Online",
265                "collation": "SQL_Latin1_General_CP1_CI_AS",
266                "requestedServiceObjectiveName": "Basic",
267                "currentServiceObjectiveName": "Basic"
268            }
269        })
270    }
271
272    fn firewall_rule_json() -> serde_json::Value {
273        serde_json::json!({
274            "id": format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/firewallRules/{FW_RULE}"),
275            "name": FW_RULE,
276            "type": "Microsoft.Sql/servers/firewallRules",
277            "properties": {
278                "startIpAddress": "0.0.0.0",
279                "endIpAddress": "0.0.0.0"
280            }
281        })
282    }
283
284    #[tokio::test]
285    async fn list_servers_returns_list() {
286        let mut mock = MockClient::new();
287        mock.expect_get(&format!(
288            "/subscriptions/{SUB_ID}/providers/Microsoft.Sql/servers"
289        ))
290        .returning_json(serde_json::json!({ "value": [server_json()] }));
291        let client = make_client(mock);
292        let result = client
293            .sql()
294            .list_servers()
295            .await
296            .expect("list_servers failed");
297        assert_eq!(result.value.len(), 1);
298        let s = &result.value[0];
299        assert_eq!(s.name.as_deref(), Some(SERVER));
300        let props = s.properties.as_ref().unwrap();
301        assert_eq!(props.state.as_deref(), Some("Ready"));
302    }
303
304    #[tokio::test]
305    async fn get_server_deserializes_properties() {
306        let mut mock = MockClient::new();
307        mock.expect_get(&format!(
308            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}"
309        ))
310        .returning_json(server_json());
311        let client = make_client(mock);
312        let s = client
313            .sql()
314            .get_server(RG, SERVER)
315            .await
316            .expect("get_server failed");
317        assert_eq!(s.name.as_deref(), Some(SERVER));
318        let props = s.properties.as_ref().unwrap();
319        assert_eq!(props.administrator_login.as_deref(), Some("cloudliteadmin"));
320        assert_eq!(
321            props.fully_qualified_domain_name.as_deref(),
322            Some(&format!("{SERVER}.database.windows.net") as &str)
323        );
324        assert_eq!(props.version.as_deref(), Some("12.0"));
325    }
326
327    #[tokio::test]
328    async fn create_server_sends_body() {
329        let mut mock = MockClient::new();
330        mock.expect_put(&format!(
331            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}"
332        ))
333        .returning_json(server_json());
334        let client = make_client(mock);
335        let body = ServerCreateRequest {
336            location: "eastus".into(),
337            properties: Some(ServerCreateOrUpdateProperties {
338                administrator_login: "cloudliteadmin".into(),
339                administrator_login_password: Some("Password123!".into()),
340                version: Some("12.0".into()),
341            }),
342            ..Default::default()
343        };
344        let s = client
345            .sql()
346            .create_server(RG, SERVER, &body)
347            .await
348            .expect("create_server failed");
349        assert_eq!(s.name.as_deref(), Some(SERVER));
350    }
351
352    #[tokio::test]
353    async fn delete_server_succeeds() {
354        let mut mock = MockClient::new();
355        mock.expect_delete(&format!(
356            "/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}"
357        ))
358        .returning_json(serde_json::json!({}));
359        let client = make_client(mock);
360        client
361            .sql()
362            .delete_server(RG, SERVER)
363            .await
364            .expect("delete_server failed");
365    }
366
367    #[tokio::test]
368    async fn list_databases_returns_list() {
369        let mut mock = MockClient::new();
370        mock.expect_get(
371            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/databases"),
372        )
373        .returning_json(serde_json::json!({ "value": [database_json()] }));
374        let client = make_client(mock);
375        let result = client
376            .sql()
377            .list_databases(RG, SERVER)
378            .await
379            .expect("list_databases failed");
380        assert_eq!(result.value.len(), 1);
381        let db = &result.value[0];
382        let props = db.properties.as_ref().unwrap();
383        assert_eq!(props.status.as_deref(), Some("Online"));
384    }
385
386    #[tokio::test]
387    async fn get_database_deserializes_properties() {
388        let mut mock = MockClient::new();
389        mock.expect_get(
390            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/databases/{DB}"),
391        )
392        .returning_json(database_json());
393        let client = make_client(mock);
394        let db = client
395            .sql()
396            .get_database(RG, SERVER, DB)
397            .await
398            .expect("get_database failed");
399        assert_eq!(db.name.as_deref(), Some(DB));
400        let props = db.properties.as_ref().unwrap();
401        assert_eq!(
402            props.current_service_objective_name.as_deref(),
403            Some("Basic")
404        );
405        assert_eq!(
406            props.collation.as_deref(),
407            Some("SQL_Latin1_General_CP1_CI_AS")
408        );
409        let sku = db.sku.as_ref().unwrap();
410        assert_eq!(sku.name, "Basic");
411    }
412
413    #[tokio::test]
414    async fn create_database_sends_body() {
415        let mut mock = MockClient::new();
416        mock.expect_put(
417            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/databases/{DB}"),
418        )
419        .returning_json(database_json());
420        let client = make_client(mock);
421        let body = DatabaseCreateRequest {
422            location: "eastus".into(),
423            sku: Some(DatabaseSku {
424                name: "Basic".into(),
425                ..Default::default()
426            }),
427            ..Default::default()
428        };
429        let db = client
430            .sql()
431            .create_database(RG, SERVER, DB, &body)
432            .await
433            .expect("create_database failed");
434        assert_eq!(db.name.as_deref(), Some(DB));
435    }
436
437    #[tokio::test]
438    async fn delete_database_succeeds() {
439        let mut mock = MockClient::new();
440        mock.expect_delete(
441            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/databases/{DB}"),
442        )
443        .returning_json(serde_json::json!({}));
444        let client = make_client(mock);
445        client
446            .sql()
447            .delete_database(RG, SERVER, DB)
448            .await
449            .expect("delete_database failed");
450    }
451
452    #[tokio::test]
453    async fn list_firewall_rules_returns_list() {
454        let mut mock = MockClient::new();
455        mock.expect_get(
456            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/firewallRules"),
457        )
458        .returning_json(serde_json::json!({ "value": [firewall_rule_json()] }));
459        let client = make_client(mock);
460        let result = client
461            .sql()
462            .list_firewall_rules(RG, SERVER)
463            .await
464            .expect("list_firewall_rules failed");
465        assert_eq!(result.value.len(), 1);
466        assert_eq!(result.value[0].name.as_deref(), Some(FW_RULE));
467    }
468
469    #[tokio::test]
470    async fn create_firewall_rule_sends_body() {
471        let mut mock = MockClient::new();
472        mock.expect_put(
473            &format!("/subscriptions/{SUB_ID}/resourceGroups/{RG}/providers/Microsoft.Sql/servers/{SERVER}/firewallRules/{FW_RULE}"),
474        )
475        .returning_json(firewall_rule_json());
476        let client = make_client(mock);
477        let body = FirewallRuleCreateRequest {
478            properties: Some(FirewallRuleProperties {
479                start_ip_address: "0.0.0.0".into(),
480                end_ip_address: "0.0.0.0".into(),
481            }),
482        };
483        let fw = client
484            .sql()
485            .create_firewall_rule(RG, SERVER, FW_RULE, &body)
486            .await
487            .expect("create_firewall_rule failed");
488        assert_eq!(fw.name.as_deref(), Some(FW_RULE));
489        let props = fw.properties.as_ref().unwrap();
490        assert_eq!(props.start_ip_address, "0.0.0.0");
491    }
492}