Skip to main content

qail_pg/driver/
access.rs

1//! Native access-policy wrappers for qail-pg execution APIs.
2
3use qail_core::access::{AccessContext, AccessError, AccessPolicy};
4use qail_core::ast::Qail;
5
6use super::{
7    AstPipelineMode, AutoCountPlan, PgDriver, PgError, PgPool, PgResult, PgRow, PooledConnection,
8    PreparedAstQuery, QueryResult, ResultFormat,
9};
10
11fn access_denied_error(err: AccessError) -> PgError {
12    PgError::Query(format!("Access denied by policy: {}", err))
13}
14
15fn check_access(policy: &AccessPolicy, ctx: &AccessContext, cmd: &Qail) -> PgResult<()> {
16    policy.check_command(ctx, cmd).map_err(access_denied_error)
17}
18
19fn check_all_access(policy: &AccessPolicy, ctx: &AccessContext, cmds: &[Qail]) -> PgResult<()> {
20    for cmd in cmds {
21        check_access(policy, ctx, cmd)?;
22    }
23    Ok(())
24}
25
26fn copy_export_table_command(table: &str, columns: &[String]) -> Qail {
27    Qail::export(table).columns(columns.iter().map(String::as_str))
28}
29
30impl PgDriver {
31    /// Check a command against an access policy without executing it.
32    pub fn check_access(
33        &self,
34        cmd: &Qail,
35        access_ctx: &AccessContext,
36        access_policy: &AccessPolicy,
37    ) -> PgResult<()> {
38        check_access(access_policy, access_ctx, cmd)
39    }
40
41    /// Execute a checked QAIL command and fetch all rows using the default text format.
42    pub async fn fetch_all_checked(
43        &mut self,
44        cmd: &Qail,
45        access_ctx: &AccessContext,
46        access_policy: &AccessPolicy,
47    ) -> PgResult<Vec<PgRow>> {
48        check_access(access_policy, access_ctx, cmd)?;
49        self.fetch_all(cmd).await
50    }
51
52    /// Execute a checked QAIL command and fetch all rows using an explicit result format.
53    pub async fn fetch_all_with_format_checked(
54        &mut self,
55        cmd: &Qail,
56        result_format: ResultFormat,
57        access_ctx: &AccessContext,
58        access_policy: &AccessPolicy,
59    ) -> PgResult<Vec<PgRow>> {
60        check_access(access_policy, access_ctx, cmd)?;
61        self.fetch_all_with_format(cmd, result_format).await
62    }
63
64    /// Execute a checked QAIL command using the uncached path.
65    pub async fn fetch_all_uncached_checked(
66        &mut self,
67        cmd: &Qail,
68        access_ctx: &AccessContext,
69        access_policy: &AccessPolicy,
70    ) -> PgResult<Vec<PgRow>> {
71        check_access(access_policy, access_ctx, cmd)?;
72        self.fetch_all_uncached(cmd).await
73    }
74
75    /// Execute a checked QAIL command using the uncached path and explicit result format.
76    pub async fn fetch_all_uncached_with_format_checked(
77        &mut self,
78        cmd: &Qail,
79        result_format: ResultFormat,
80        access_ctx: &AccessContext,
81        access_policy: &AccessPolicy,
82    ) -> PgResult<Vec<PgRow>> {
83        check_access(access_policy, access_ctx, cmd)?;
84        self.fetch_all_uncached_with_format(cmd, result_format)
85            .await
86    }
87
88    /// Execute a checked QAIL command using the fast path.
89    pub async fn fetch_all_fast_checked(
90        &mut self,
91        cmd: &Qail,
92        access_ctx: &AccessContext,
93        access_policy: &AccessPolicy,
94    ) -> PgResult<Vec<PgRow>> {
95        check_access(access_policy, access_ctx, cmd)?;
96        self.fetch_all_fast(cmd).await
97    }
98
99    /// Execute a checked QAIL command using the fast path and explicit result format.
100    pub async fn fetch_all_fast_with_format_checked(
101        &mut self,
102        cmd: &Qail,
103        result_format: ResultFormat,
104        access_ctx: &AccessContext,
105        access_policy: &AccessPolicy,
106    ) -> PgResult<Vec<PgRow>> {
107        check_access(access_policy, access_ctx, cmd)?;
108        self.fetch_all_fast_with_format(cmd, result_format).await
109    }
110
111    /// Execute a checked QAIL command and fetch one row.
112    pub async fn fetch_one_checked(
113        &mut self,
114        cmd: &Qail,
115        access_ctx: &AccessContext,
116        access_policy: &AccessPolicy,
117    ) -> PgResult<PgRow> {
118        check_access(access_policy, access_ctx, cmd)?;
119        self.fetch_one(cmd).await
120    }
121
122    /// Prepare a checked AST query once and return a reusable frozen handle.
123    ///
124    /// Policy is checked at prepare time. Callers that need per-request policy
125    /// changes should prepare per request or execute through the non-prepared
126    /// checked wrappers.
127    pub async fn prepare_ast_query_checked(
128        &mut self,
129        cmd: &Qail,
130        access_ctx: &AccessContext,
131        access_policy: &AccessPolicy,
132    ) -> PgResult<PreparedAstQuery> {
133        check_access(access_policy, access_ctx, cmd)?;
134        self.prepare_ast_query(cmd).await
135    }
136
137    /// Execute a checked QAIL command and decode rows into typed structs.
138    pub async fn fetch_typed_checked<T: super::row::QailRow>(
139        &mut self,
140        cmd: &Qail,
141        access_ctx: &AccessContext,
142        access_policy: &AccessPolicy,
143    ) -> PgResult<Vec<T>> {
144        check_access(access_policy, access_ctx, cmd)?;
145        self.fetch_typed(cmd).await
146    }
147
148    /// Execute a checked QAIL command and decode typed rows using an explicit result format.
149    pub async fn fetch_typed_with_format_checked<T: super::row::QailRow>(
150        &mut self,
151        cmd: &Qail,
152        result_format: ResultFormat,
153        access_ctx: &AccessContext,
154        access_policy: &AccessPolicy,
155    ) -> PgResult<Vec<T>> {
156        check_access(access_policy, access_ctx, cmd)?;
157        self.fetch_typed_with_format(cmd, result_format).await
158    }
159
160    /// Execute a checked QAIL command and decode one typed row.
161    pub async fn fetch_one_typed_checked<T: super::row::QailRow>(
162        &mut self,
163        cmd: &Qail,
164        access_ctx: &AccessContext,
165        access_policy: &AccessPolicy,
166    ) -> PgResult<Option<T>> {
167        check_access(access_policy, access_ctx, cmd)?;
168        self.fetch_one_typed(cmd).await
169    }
170
171    /// Execute a checked QAIL command and decode one typed row using an explicit format.
172    pub async fn fetch_one_typed_with_format_checked<T: super::row::QailRow>(
173        &mut self,
174        cmd: &Qail,
175        result_format: ResultFormat,
176        access_ctx: &AccessContext,
177        access_policy: &AccessPolicy,
178    ) -> PgResult<Option<T>> {
179        check_access(access_policy, access_ctx, cmd)?;
180        self.fetch_one_typed_with_format(cmd, result_format).await
181    }
182
183    /// Execute a checked mutation command.
184    pub async fn execute_checked(
185        &mut self,
186        cmd: &Qail,
187        access_ctx: &AccessContext,
188        access_policy: &AccessPolicy,
189    ) -> PgResult<u64> {
190        check_access(access_policy, access_ctx, cmd)?;
191        self.execute(cmd).await
192    }
193
194    /// Bulk insert checked AST rows using PostgreSQL COPY.
195    pub async fn copy_bulk_checked(
196        &mut self,
197        cmd: &Qail,
198        rows: &[Vec<qail_core::ast::Value>],
199        access_ctx: &AccessContext,
200        access_policy: &AccessPolicy,
201    ) -> PgResult<u64> {
202        check_access(access_policy, access_ctx, cmd)?;
203        self.copy_bulk(cmd, rows).await
204    }
205
206    /// Bulk insert checked pre-encoded COPY bytes.
207    pub async fn copy_bulk_bytes_checked(
208        &mut self,
209        cmd: &Qail,
210        data: &[u8],
211        access_ctx: &AccessContext,
212        access_policy: &AccessPolicy,
213    ) -> PgResult<u64> {
214        check_access(access_policy, access_ctx, cmd)?;
215        self.copy_bulk_bytes(cmd, data).await
216    }
217
218    /// Export a checked table/column selection using COPY TO STDOUT.
219    pub async fn copy_export_table_checked(
220        &mut self,
221        table: &str,
222        columns: &[String],
223        access_ctx: &AccessContext,
224        access_policy: &AccessPolicy,
225    ) -> PgResult<Vec<u8>> {
226        check_access(
227            access_policy,
228            access_ctx,
229            &copy_export_table_command(table, columns),
230        )?;
231        self.copy_export_table(table, columns).await
232    }
233
234    /// Stream a checked table/column selection using COPY TO STDOUT.
235    pub async fn copy_export_table_stream_checked<F, Fut>(
236        &mut self,
237        table: &str,
238        columns: &[String],
239        on_chunk: F,
240        access_ctx: &AccessContext,
241        access_policy: &AccessPolicy,
242    ) -> PgResult<()>
243    where
244        F: FnMut(Vec<u8>) -> Fut,
245        Fut: std::future::Future<Output = PgResult<()>>,
246    {
247        check_access(
248            access_policy,
249            access_ctx,
250            &copy_export_table_command(table, columns),
251        )?;
252        self.copy_export_table_stream(table, columns, on_chunk)
253            .await
254    }
255
256    /// Stream a checked AST-native export command as raw COPY chunks.
257    pub async fn copy_export_cmd_stream_checked<F, Fut>(
258        &mut self,
259        cmd: &Qail,
260        on_chunk: F,
261        access_ctx: &AccessContext,
262        access_policy: &AccessPolicy,
263    ) -> PgResult<()>
264    where
265        F: FnMut(Vec<u8>) -> Fut,
266        Fut: std::future::Future<Output = PgResult<()>>,
267    {
268        check_access(access_policy, access_ctx, cmd)?;
269        self.copy_export_cmd_stream(cmd, on_chunk).await
270    }
271
272    /// Stream a checked AST-native export command as parsed rows.
273    pub async fn copy_export_cmd_stream_rows_checked<F>(
274        &mut self,
275        cmd: &Qail,
276        on_row: F,
277        access_ctx: &AccessContext,
278        access_policy: &AccessPolicy,
279    ) -> PgResult<()>
280    where
281        F: FnMut(Vec<String>) -> PgResult<()>,
282    {
283        check_access(access_policy, access_ctx, cmd)?;
284        self.copy_export_cmd_stream_rows(cmd, on_row).await
285    }
286
287    /// Stream checked cursor batches for a QAIL command.
288    pub async fn stream_cmd_checked(
289        &mut self,
290        cmd: &Qail,
291        batch_size: usize,
292        access_ctx: &AccessContext,
293        access_policy: &AccessPolicy,
294    ) -> PgResult<Vec<Vec<PgRow>>> {
295        check_access(access_policy, access_ctx, cmd)?;
296        self.stream_cmd(cmd, batch_size).await
297    }
298
299    /// Execute a checked query and return a structured query result.
300    pub async fn query_ast_checked(
301        &mut self,
302        cmd: &Qail,
303        access_ctx: &AccessContext,
304        access_policy: &AccessPolicy,
305    ) -> PgResult<QueryResult> {
306        check_access(access_policy, access_ctx, cmd)?;
307        self.query_ast(cmd).await
308    }
309
310    /// Execute a checked query and return a structured query result using an explicit format.
311    pub async fn query_ast_with_format_checked(
312        &mut self,
313        cmd: &Qail,
314        result_format: ResultFormat,
315        access_ctx: &AccessContext,
316        access_policy: &AccessPolicy,
317    ) -> PgResult<QueryResult> {
318        check_access(access_policy, access_ctx, cmd)?;
319        self.query_ast_with_format(cmd, result_format).await
320    }
321
322    /// Execute checked commands in one transaction.
323    ///
324    /// All commands are checked before `BEGIN`, so a denied later command cannot
325    /// partially execute earlier commands.
326    pub async fn execute_batch_checked(
327        &mut self,
328        cmds: &[Qail],
329        access_ctx: &AccessContext,
330        access_policy: &AccessPolicy,
331    ) -> PgResult<Vec<u64>> {
332        check_all_access(access_policy, access_ctx, cmds)?;
333        self.execute_batch(cmds).await
334    }
335
336    /// Execute checked commands with runtime auto strategy and return both count and plan.
337    pub async fn execute_count_auto_with_plan_checked(
338        &mut self,
339        cmds: &[Qail],
340        access_ctx: &AccessContext,
341        access_policy: &AccessPolicy,
342    ) -> PgResult<(usize, AutoCountPlan)> {
343        check_all_access(access_policy, access_ctx, cmds)?;
344        self.execute_count_auto_with_plan(cmds).await
345    }
346
347    /// Execute checked commands with runtime auto strategy.
348    pub async fn execute_count_auto_checked(
349        &mut self,
350        cmds: &[Qail],
351        access_ctx: &AccessContext,
352        access_policy: &AccessPolicy,
353    ) -> PgResult<usize> {
354        check_all_access(access_policy, access_ctx, cmds)?;
355        self.execute_count_auto(cmds).await
356    }
357
358    /// Execute checked commands with an explicit pipeline strategy.
359    pub async fn pipeline_execute_count_with_mode_checked(
360        &mut self,
361        cmds: &[Qail],
362        mode: AstPipelineMode,
363        access_ctx: &AccessContext,
364        access_policy: &AccessPolicy,
365    ) -> PgResult<usize> {
366        check_all_access(access_policy, access_ctx, cmds)?;
367        self.pipeline_execute_count_with_mode(cmds, mode).await
368    }
369
370    /// Execute checked commands with the default pipeline strategy.
371    pub async fn pipeline_execute_count_checked(
372        &mut self,
373        cmds: &[Qail],
374        access_ctx: &AccessContext,
375        access_policy: &AccessPolicy,
376    ) -> PgResult<usize> {
377        check_all_access(access_policy, access_ctx, cmds)?;
378        self.pipeline_execute_count(cmds).await
379    }
380
381    /// Execute checked commands and return full row data.
382    pub async fn pipeline_execute_rows_checked(
383        &mut self,
384        cmds: &[Qail],
385        access_ctx: &AccessContext,
386        access_policy: &AccessPolicy,
387    ) -> PgResult<Vec<Vec<PgRow>>> {
388        check_all_access(access_policy, access_ctx, cmds)?;
389        self.pipeline_execute_rows(cmds).await
390    }
391}
392
393impl PooledConnection {
394    /// Check a command against an access policy without executing it.
395    pub fn check_access(
396        &self,
397        cmd: &Qail,
398        access_ctx: &AccessContext,
399        access_policy: &AccessPolicy,
400    ) -> PgResult<()> {
401        check_access(access_policy, access_ctx, cmd)
402    }
403
404    /// Execute a checked QAIL mutation command and return the affected row count.
405    pub async fn execute_checked(
406        &mut self,
407        cmd: &Qail,
408        access_ctx: &AccessContext,
409        access_policy: &AccessPolicy,
410    ) -> PgResult<u64> {
411        check_access(access_policy, access_ctx, cmd)?;
412        self.execute(cmd).await
413    }
414
415    /// Execute a checked QAIL command using the default cached pooled path.
416    pub async fn fetch_all_cached_checked(
417        &mut self,
418        cmd: &Qail,
419        access_ctx: &AccessContext,
420        access_policy: &AccessPolicy,
421    ) -> PgResult<Vec<PgRow>> {
422        check_access(access_policy, access_ctx, cmd)?;
423        self.fetch_all_cached(cmd).await
424    }
425
426    /// Execute a checked QAIL command using the cached pooled path with explicit format.
427    pub async fn fetch_all_cached_with_format_checked(
428        &mut self,
429        cmd: &Qail,
430        result_format: ResultFormat,
431        access_ctx: &AccessContext,
432        access_policy: &AccessPolicy,
433    ) -> PgResult<Vec<PgRow>> {
434        check_access(access_policy, access_ctx, cmd)?;
435        self.fetch_all_cached_with_format(cmd, result_format).await
436    }
437
438    /// Execute a checked QAIL command using the uncached pooled path.
439    pub async fn fetch_all_uncached_checked(
440        &mut self,
441        cmd: &Qail,
442        access_ctx: &AccessContext,
443        access_policy: &AccessPolicy,
444    ) -> PgResult<Vec<PgRow>> {
445        check_access(access_policy, access_ctx, cmd)?;
446        self.fetch_all_uncached(cmd).await
447    }
448
449    /// Execute a checked QAIL command using the uncached pooled path with explicit format.
450    pub async fn fetch_all_uncached_with_format_checked(
451        &mut self,
452        cmd: &Qail,
453        result_format: ResultFormat,
454        access_ctx: &AccessContext,
455        access_policy: &AccessPolicy,
456    ) -> PgResult<Vec<PgRow>> {
457        check_access(access_policy, access_ctx, cmd)?;
458        self.fetch_all_uncached_with_format(cmd, result_format)
459            .await
460    }
461
462    /// Execute a checked QAIL command using the fast pooled path.
463    pub async fn fetch_all_fast_checked(
464        &mut self,
465        cmd: &Qail,
466        access_ctx: &AccessContext,
467        access_policy: &AccessPolicy,
468    ) -> PgResult<Vec<PgRow>> {
469        check_access(access_policy, access_ctx, cmd)?;
470        self.fetch_all_fast(cmd).await
471    }
472
473    /// Execute a checked QAIL command using the fast pooled path with explicit format.
474    pub async fn fetch_all_fast_with_format_checked(
475        &mut self,
476        cmd: &Qail,
477        result_format: ResultFormat,
478        access_ctx: &AccessContext,
479        access_policy: &AccessPolicy,
480    ) -> PgResult<Vec<PgRow>> {
481        check_access(access_policy, access_ctx, cmd)?;
482        self.fetch_all_fast_with_format(cmd, result_format).await
483    }
484
485    /// Execute a checked QAIL command under an already prepared RLS setup string.
486    pub async fn fetch_all_with_rls_checked(
487        &mut self,
488        cmd: &Qail,
489        rls_sql: &str,
490        access_ctx: &AccessContext,
491        access_policy: &AccessPolicy,
492    ) -> PgResult<Vec<PgRow>> {
493        check_access(access_policy, access_ctx, cmd)?;
494        self.fetch_all_with_rls(cmd, rls_sql).await
495    }
496
497    /// Execute a checked QAIL command under RLS with an explicit result format.
498    pub async fn fetch_all_with_rls_with_format_checked(
499        &mut self,
500        cmd: &Qail,
501        rls_sql: &str,
502        result_format: ResultFormat,
503        access_ctx: &AccessContext,
504        access_policy: &AccessPolicy,
505    ) -> PgResult<Vec<PgRow>> {
506        check_access(access_policy, access_ctx, cmd)?;
507        self.fetch_all_with_rls_with_format(cmd, rls_sql, result_format)
508            .await
509    }
510
511    /// Export checked data using AST-native COPY TO STDOUT.
512    pub async fn copy_export_checked(
513        &mut self,
514        cmd: &Qail,
515        access_ctx: &AccessContext,
516        access_policy: &AccessPolicy,
517    ) -> PgResult<Vec<Vec<String>>> {
518        check_access(access_policy, access_ctx, cmd)?;
519        self.copy_export(cmd).await
520    }
521
522    /// Stream a checked AST-native COPY export as raw chunks.
523    pub async fn copy_export_stream_raw_checked<F, Fut>(
524        &mut self,
525        cmd: &Qail,
526        on_chunk: F,
527        access_ctx: &AccessContext,
528        access_policy: &AccessPolicy,
529    ) -> PgResult<()>
530    where
531        F: FnMut(Vec<u8>) -> Fut,
532        Fut: std::future::Future<Output = PgResult<()>>,
533    {
534        check_access(access_policy, access_ctx, cmd)?;
535        self.copy_export_stream_raw(cmd, on_chunk).await
536    }
537
538    /// Stream a checked AST-native COPY export as parsed rows.
539    pub async fn copy_export_stream_rows_checked<F>(
540        &mut self,
541        cmd: &Qail,
542        on_row: F,
543        access_ctx: &AccessContext,
544        access_policy: &AccessPolicy,
545    ) -> PgResult<()>
546    where
547        F: FnMut(Vec<String>) -> PgResult<()>,
548    {
549        check_access(access_policy, access_ctx, cmd)?;
550        self.copy_export_stream_rows(cmd, on_row).await
551    }
552
553    /// Export a checked table/column selection using COPY TO STDOUT.
554    pub async fn copy_export_table_checked(
555        &mut self,
556        table: &str,
557        columns: &[String],
558        access_ctx: &AccessContext,
559        access_policy: &AccessPolicy,
560    ) -> PgResult<Vec<u8>> {
561        check_access(
562            access_policy,
563            access_ctx,
564            &copy_export_table_command(table, columns),
565        )?;
566        self.copy_export_table(table, columns).await
567    }
568
569    /// Stream a checked table/column selection using COPY TO STDOUT.
570    pub async fn copy_export_table_stream_checked<F, Fut>(
571        &mut self,
572        table: &str,
573        columns: &[String],
574        on_chunk: F,
575        access_ctx: &AccessContext,
576        access_policy: &AccessPolicy,
577    ) -> PgResult<()>
578    where
579        F: FnMut(Vec<u8>) -> Fut,
580        Fut: std::future::Future<Output = PgResult<()>>,
581    {
582        check_access(
583            access_policy,
584            access_ctx,
585            &copy_export_table_command(table, columns),
586        )?;
587        self.copy_export_table_stream(table, columns, on_chunk)
588            .await
589    }
590
591    /// Execute a checked QAIL command and decode rows into typed structs.
592    pub async fn fetch_typed_checked<T: super::row::QailRow>(
593        &mut self,
594        cmd: &Qail,
595        access_ctx: &AccessContext,
596        access_policy: &AccessPolicy,
597    ) -> PgResult<Vec<T>> {
598        check_access(access_policy, access_ctx, cmd)?;
599        self.fetch_typed(cmd).await
600    }
601
602    /// Execute a checked QAIL command and decode typed rows using an explicit result format.
603    pub async fn fetch_typed_with_format_checked<T: super::row::QailRow>(
604        &mut self,
605        cmd: &Qail,
606        result_format: ResultFormat,
607        access_ctx: &AccessContext,
608        access_policy: &AccessPolicy,
609    ) -> PgResult<Vec<T>> {
610        check_access(access_policy, access_ctx, cmd)?;
611        self.fetch_typed_with_format(cmd, result_format).await
612    }
613
614    /// Execute a checked QAIL command and decode one typed row.
615    pub async fn fetch_one_typed_checked<T: super::row::QailRow>(
616        &mut self,
617        cmd: &Qail,
618        access_ctx: &AccessContext,
619        access_policy: &AccessPolicy,
620    ) -> PgResult<Option<T>> {
621        check_access(access_policy, access_ctx, cmd)?;
622        self.fetch_one_typed(cmd).await
623    }
624
625    /// Execute a checked QAIL command and decode one typed row using an explicit format.
626    pub async fn fetch_one_typed_with_format_checked<T: super::row::QailRow>(
627        &mut self,
628        cmd: &Qail,
629        result_format: ResultFormat,
630        access_ctx: &AccessContext,
631        access_policy: &AccessPolicy,
632    ) -> PgResult<Option<T>> {
633        check_access(access_policy, access_ctx, cmd)?;
634        self.fetch_one_typed_with_format(cmd, result_format).await
635    }
636
637    /// Execute checked AST commands in one pooled pipeline call.
638    pub async fn pipeline_execute_rows_ast_checked(
639        &mut self,
640        cmds: &[Qail],
641        access_ctx: &AccessContext,
642        access_policy: &AccessPolicy,
643    ) -> PgResult<Vec<Vec<Vec<Option<Vec<u8>>>>>> {
644        check_all_access(access_policy, access_ctx, cmds)?;
645        self.pipeline_execute_rows_ast(cmds).await
646    }
647}
648
649impl PgPool {
650    /// Execute checked commands with the pool auto strategy and return both count and plan.
651    pub async fn execute_count_auto_with_plan_checked(
652        &self,
653        cmds: &[Qail],
654        access_ctx: &AccessContext,
655        access_policy: &AccessPolicy,
656    ) -> PgResult<(usize, AutoCountPlan)> {
657        check_all_access(access_policy, access_ctx, cmds)?;
658        self.execute_count_auto_with_plan(cmds).await
659    }
660
661    /// Execute checked commands with the pool auto strategy.
662    pub async fn execute_count_auto_checked(
663        &self,
664        cmds: &[Qail],
665        access_ctx: &AccessContext,
666        access_policy: &AccessPolicy,
667    ) -> PgResult<usize> {
668        check_all_access(access_policy, access_ctx, cmds)?;
669        self.execute_count_auto(cmds).await
670    }
671}
672
673#[cfg(test)]
674mod tests {
675    use qail_core::access::{
676        AccessContext, AccessOperation, AccessPolicy, ColumnRule, TableAccessPolicy,
677    };
678    use qail_core::ast::{Expr, Qail};
679
680    use super::{check_access, check_all_access, copy_export_table_command};
681    use crate::driver::PgError;
682
683    #[test]
684    fn checked_pg_error_uses_existing_query_variant() {
685        let err = check_access(
686            &AccessPolicy::new(),
687            &AccessContext::anonymous(),
688            &Qail::get("orders"),
689        )
690        .expect_err("missing policy should fail closed");
691
692        match err {
693            PgError::Query(message) => {
694                assert!(message.contains("Access denied by policy"));
695                assert!(message.contains("orders"));
696            }
697            other => panic!("unexpected error variant: {other:?}"),
698        }
699    }
700
701    #[test]
702    fn checked_batch_rejects_denied_later_command_before_execution() {
703        let policy = AccessPolicy::new().with_table(
704            "orders",
705            TableAccessPolicy::new()
706                .allow_operations([AccessOperation::Read])
707                .read_columns(ColumnRule::only(["id"])),
708        );
709        let cmds = vec![
710            Qail::get("orders").columns(["id"]),
711            Qail::get("orders").columns(["id", "private_note"]),
712        ];
713
714        let err = check_all_access(&policy, &AccessContext::anonymous(), &cmds)
715            .expect_err("second command should deny before any wrapper executes");
716
717        assert!(matches!(err, PgError::Query(_)));
718    }
719
720    #[test]
721    fn checked_policy_recurses_into_subqueries() {
722        let policy = AccessPolicy::new().with_table(
723            "orders",
724            TableAccessPolicy::new().allow_operations([AccessOperation::Read]),
725        );
726        let cmd = Qail::get("orders").columns_expr([Expr::Subquery {
727            query: Box::new(Qail::get("users").columns(["id"])),
728            alias: None,
729        }]);
730
731        let err = check_access(&policy, &AccessContext::anonymous(), &cmd)
732            .expect_err("subquery table should require its own policy");
733
734        match err {
735            PgError::Query(message) => assert!(message.contains("users")),
736            other => panic!("unexpected error variant: {other:?}"),
737        }
738    }
739
740    #[test]
741    fn checked_copy_export_table_command_uses_read_column_policy() {
742        let policy = AccessPolicy::new().with_table(
743            "orders",
744            TableAccessPolicy::new()
745                .allow_operations([AccessOperation::Read])
746                .read_columns(ColumnRule::only(["id"])),
747        );
748        let columns = vec!["id".to_string(), "private_note".to_string()];
749        let cmd = copy_export_table_command("orders", &columns);
750
751        let err = check_access(&policy, &AccessContext::anonymous(), &cmd)
752            .expect_err("denied COPY export column should fail before execution");
753
754        match err {
755            PgError::Query(message) => assert!(message.contains("private_note")),
756            other => panic!("unexpected error variant: {other:?}"),
757        }
758    }
759}