1use super::core::PgDriver;
5use super::pipeline::AstPipelineMode;
6use super::prepared::PreparedStatement;
7use super::rls;
8use super::types::*;
9use super::{AutoCountPath, AutoCountPlan};
10use crate::protocol::AstEncoder;
11use qail_core::ast::Qail;
12
13impl PgDriver {
14 pub async fn begin(&mut self) -> PgResult<()> {
18 self.connection.begin_transaction().await
19 }
20
21 pub async fn commit(&mut self) -> PgResult<()> {
23 self.connection.commit().await
24 }
25
26 pub async fn rollback(&mut self) -> PgResult<()> {
28 self.connection.rollback().await
29 }
30
31 pub async fn savepoint(&mut self, name: &str) -> PgResult<()> {
44 self.connection.savepoint(name).await
45 }
46
47 pub async fn rollback_to(&mut self, name: &str) -> PgResult<()> {
51 self.connection.rollback_to(name).await
52 }
53
54 pub async fn release_savepoint(&mut self, name: &str) -> PgResult<()> {
57 self.connection.release_savepoint(name).await
58 }
59
60 pub async fn execute_batch(&mut self, cmds: &[Qail]) -> PgResult<Vec<u64>> {
74 self.begin().await?;
75 let mut results = Vec::with_capacity(cmds.len());
76 for cmd in cmds {
77 match self.execute(cmd).await {
78 Ok(n) => results.push(n),
79 Err(e) => {
80 if self.rollback().await.is_err() {
81 self.connection.mark_io_desynced();
82 }
83 return Err(e);
84 }
85 }
86 }
87 self.commit().await?;
88 Ok(results)
89 }
90
91 pub async fn set_statement_timeout(&mut self, ms: u32) -> PgResult<()> {
99 let cmd = Qail::session_set("statement_timeout", ms.to_string());
100 self.execute(&cmd).await.map(|_| ())
101 }
102
103 pub async fn reset_statement_timeout(&mut self) -> PgResult<()> {
105 let cmd = Qail::session_reset("statement_timeout");
106 self.execute(&cmd).await.map(|_| ())
107 }
108
109 pub async fn execute_simple(&mut self, sql: &str) -> PgResult<()> {
114 self.connection.execute_simple(sql).await
115 }
116
117 pub async fn simple_query(&mut self, sql: &str) -> PgResult<Vec<PgRow>> {
122 self.connection.simple_query(sql).await
123 }
124
125 pub async fn set_rls_context(&mut self, ctx: rls::RlsContext) -> PgResult<()> {
143 let sql = rls::context_to_sql(&ctx);
144 if sql.as_bytes().contains(&0) {
145 return Err(crate::PgError::Protocol(
146 "SQL contains NULL byte (0x00) which is invalid in PostgreSQL".to_string(),
147 ));
148 }
149 self.connection.execute_simple(&sql).await?;
150 self.rls_context = Some(ctx);
151 Ok(())
152 }
153
154 pub async fn clear_rls_context(&mut self) -> PgResult<()> {
159 let sql = rls::reset_sql();
160 if sql.as_bytes().contains(&0) {
161 return Err(crate::PgError::Protocol(
162 "SQL contains NULL byte (0x00) which is invalid in PostgreSQL".to_string(),
163 ));
164 }
165 self.connection.execute_simple(sql).await?;
166 self.rls_context = None;
167 Ok(())
168 }
169
170 pub fn rls_context(&self) -> Option<&rls::RlsContext> {
172 self.rls_context.as_ref()
173 }
174
175 pub async fn pipeline_execute_count(&mut self, cmds: &[Qail]) -> PgResult<usize> {
187 self.pipeline_execute_count_with_mode(cmds, AstPipelineMode::Auto)
188 .await
189 }
190
191 pub async fn execute_count_auto_with_plan(
198 &mut self,
199 cmds: &[Qail],
200 ) -> PgResult<(usize, AutoCountPlan)> {
201 let plan = AutoCountPlan::for_driver(cmds.len());
202
203 let completed = match plan.path {
204 AutoCountPath::SingleCached => {
205 if cmds.is_empty() {
206 0
207 } else {
208 let _ = self.fetch_all_cached(&cmds[0]).await?;
209 1
210 }
211 }
212 AutoCountPath::PipelineOneShot => {
213 self.connection
214 .pipeline_execute_count_ast_with_mode(cmds, AstPipelineMode::OneShot)
215 .await?
216 }
217 AutoCountPath::PipelineCached => {
218 self.connection
219 .pipeline_execute_count_ast_with_mode(cmds, AstPipelineMode::Cached)
220 .await?
221 }
222 AutoCountPath::PoolParallel => {
223 return Err(PgError::Protocol(
224 "driver auto planner returned pool-parallel path".to_string(),
225 ));
226 }
227 };
228
229 Ok((completed, plan))
230 }
231
232 #[inline]
234 pub async fn execute_count_auto(&mut self, cmds: &[Qail]) -> PgResult<usize> {
235 let (completed, _plan) = self.execute_count_auto_with_plan(cmds).await?;
236 Ok(completed)
237 }
238
239 pub async fn pipeline_execute_count_with_mode(
244 &mut self,
245 cmds: &[Qail],
246 mode: AstPipelineMode,
247 ) -> PgResult<usize> {
248 self.connection
249 .pipeline_execute_count_ast_with_mode(cmds, mode)
250 .await
251 }
252
253 pub async fn pipeline_execute_rows(&mut self, cmds: &[Qail]) -> PgResult<Vec<Vec<PgRow>>> {
255 let raw_results = self.connection.pipeline_execute_rows_ast(cmds).await?;
256
257 let results: Vec<Vec<PgRow>> = raw_results
258 .into_iter()
259 .map(|rows| {
260 rows.into_iter()
261 .map(|columns| PgRow {
262 columns,
263 column_info: None,
264 })
265 .collect()
266 })
267 .collect();
268
269 Ok(results)
270 }
271
272 pub async fn explain_estimate(
276 &mut self,
277 cmd: &Qail,
278 ) -> PgResult<Option<crate::driver::explain::ExplainEstimate>> {
279 let (sql, params) =
280 AstEncoder::encode_cmd_sql(cmd).map_err(|e| PgError::Encode(e.to_string()))?;
281 let explain_sql = format!("EXPLAIN (FORMAT JSON) {}", sql);
282 let rows = self.connection.query(&explain_sql, ¶ms).await?;
283
284 let mut json_output = String::new();
285 for row in &rows {
286 if let Some(Some(val)) = row.first()
287 && let Ok(text) = std::str::from_utf8(val)
288 {
289 json_output.push_str(text);
290 }
291 }
292
293 Ok(crate::driver::explain::parse_explain_json(&json_output))
294 }
295
296 pub async fn prepare(&mut self, sql: &str) -> PgResult<PreparedStatement> {
298 self.connection.prepare(sql).await
299 }
300
301 pub async fn pipeline_execute_prepared_count(
303 &mut self,
304 stmt: &PreparedStatement,
305 params_batch: &[Vec<Option<Vec<u8>>>],
306 ) -> PgResult<usize> {
307 self.connection
308 .pipeline_execute_prepared_count(stmt, params_batch)
309 .await
310 }
311
312 pub async fn copy_bulk(
328 &mut self,
329 cmd: &Qail,
330 rows: &[Vec<qail_core::ast::Value>],
331 ) -> PgResult<u64> {
332 use qail_core::ast::Action;
333
334 if cmd.action != Action::Add {
335 return Err(PgError::Query(
336 "copy_bulk requires Qail::Add action".to_string(),
337 ));
338 }
339
340 let table = &cmd.table;
341
342 let columns: Vec<String> = cmd
343 .columns
344 .iter()
345 .filter_map(|expr| {
346 use qail_core::ast::Expr;
347 match expr {
348 Expr::Named(name) => Some(name.clone()),
349 Expr::Aliased { name, .. } => Some(name.clone()),
350 Expr::Star => None, _ => None,
352 }
353 })
354 .collect();
355
356 if columns.is_empty() {
357 return Err(PgError::Query(
358 "copy_bulk requires columns in Qail".to_string(),
359 ));
360 }
361
362 self.connection.copy_in_fast(table, &columns, rows).await
364 }
365
366 pub async fn copy_bulk_bytes(&mut self, cmd: &Qail, data: &[u8]) -> PgResult<u64> {
379 use qail_core::ast::Action;
380
381 if cmd.action != Action::Add {
382 return Err(PgError::Query(
383 "copy_bulk_bytes requires Qail::Add action".to_string(),
384 ));
385 }
386
387 let table = &cmd.table;
388 let columns: Vec<String> = cmd
389 .columns
390 .iter()
391 .filter_map(|expr| {
392 use qail_core::ast::Expr;
393 match expr {
394 Expr::Named(name) => Some(name.clone()),
395 Expr::Aliased { name, .. } => Some(name.clone()),
396 _ => None,
397 }
398 })
399 .collect();
400
401 if columns.is_empty() {
402 return Err(PgError::Query(
403 "copy_bulk_bytes requires columns in Qail".to_string(),
404 ));
405 }
406
407 self.connection.copy_in_raw(table, &columns, data).await
409 }
410
411 pub async fn copy_export_table(
419 &mut self,
420 table: &str,
421 columns: &[String],
422 ) -> PgResult<Vec<u8>> {
423 let cols: Vec<String> = columns
424 .iter()
425 .map(|c| super::copy::quote_copy_column_ident(c))
426 .collect::<PgResult<_>>()?;
427 let sql = format!(
428 "COPY {} ({}) TO STDOUT",
429 super::copy::quote_copy_table_ref(table)?,
430 cols.join(", ")
431 );
432
433 self.connection.copy_out_raw(&sql).await
434 }
435
436 pub async fn copy_export_table_stream<F, Fut>(
440 &mut self,
441 table: &str,
442 columns: &[String],
443 on_chunk: F,
444 ) -> PgResult<()>
445 where
446 F: FnMut(Vec<u8>) -> Fut,
447 Fut: std::future::Future<Output = PgResult<()>>,
448 {
449 let cols: Vec<String> = columns
450 .iter()
451 .map(|c| super::copy::quote_copy_column_ident(c))
452 .collect::<PgResult<_>>()?;
453 let sql = format!(
454 "COPY {} ({}) TO STDOUT",
455 super::copy::quote_copy_table_ref(table)?,
456 cols.join(", ")
457 );
458 self.connection.copy_out_raw_stream(&sql, on_chunk).await
459 }
460
461 pub async fn copy_export_cmd_stream<F, Fut>(&mut self, cmd: &Qail, on_chunk: F) -> PgResult<()>
463 where
464 F: FnMut(Vec<u8>) -> Fut,
465 Fut: std::future::Future<Output = PgResult<()>>,
466 {
467 self.connection.copy_export_stream_raw(cmd, on_chunk).await
468 }
469
470 pub async fn copy_export_cmd_stream_rows<F>(&mut self, cmd: &Qail, on_row: F) -> PgResult<()>
472 where
473 F: FnMut(Vec<String>) -> PgResult<()>,
474 {
475 self.connection.copy_export_stream_rows(cmd, on_row).await
476 }
477
478 pub async fn stream_cmd(&mut self, cmd: &Qail, batch_size: usize) -> PgResult<Vec<Vec<PgRow>>> {
492 validate_stream_batch_size(batch_size)?;
493
494 use std::sync::atomic::{AtomicU64, Ordering};
495 static CURSOR_ID: AtomicU64 = AtomicU64::new(0);
496
497 let cursor_name = format!("qail_cursor_{}", CURSOR_ID.fetch_add(1, Ordering::SeqCst));
498
499 let mut sql_buf = bytes::BytesMut::with_capacity(256);
501 let mut params: Vec<Option<Vec<u8>>> = Vec::new();
502 AstEncoder::encode_select_sql(cmd, &mut sql_buf, &mut params)
503 .map_err(|e| PgError::Encode(e.to_string()))?;
504 let sql = std::str::from_utf8(&sql_buf)
505 .map_err(|e| PgError::Encode(format!("encoded SQL is not UTF-8: {}", e)))?
506 .to_string();
507
508 self.connection.begin_transaction().await?;
510
511 let stream_result = async {
512 self.connection
514 .declare_cursor(&cursor_name, &sql, ¶ms)
515 .await?;
516
517 let mut all_batches = Vec::new();
519 while let Some(rows) = self
520 .connection
521 .fetch_cursor(&cursor_name, batch_size)
522 .await?
523 {
524 let pg_rows: Vec<PgRow> = rows
525 .into_iter()
526 .map(|cols| PgRow {
527 columns: cols,
528 column_info: None,
529 })
530 .collect();
531 all_batches.push(pg_rows);
532 }
533
534 self.connection.close_cursor(&cursor_name).await?;
535 Ok(all_batches)
536 }
537 .await;
538
539 match stream_result {
540 Ok(all_batches) => {
541 self.connection.commit().await?;
542 Ok(all_batches)
543 }
544 Err(err) => {
545 if self.connection.rollback().await.is_err() {
546 self.connection.mark_io_desynced();
547 }
548 Err(err)
549 }
550 }
551 }
552}
553
554fn validate_stream_batch_size(batch_size: usize) -> PgResult<()> {
555 if batch_size == 0 {
556 return Err(PgError::Query(
557 "stream_cmd batch_size must be greater than 0".to_string(),
558 ));
559 }
560 Ok(())
561}
562
563#[cfg(test)]
564mod tests {
565 use super::validate_stream_batch_size;
566
567 #[test]
568 fn stream_batch_size_zero_is_rejected() {
569 let err = validate_stream_batch_size(0).expect_err("zero batch size must fail");
570 assert!(err.to_string().contains("batch_size"));
571 }
572
573 #[test]
574 fn stream_batch_size_positive_is_accepted() {
575 validate_stream_batch_size(1).expect("positive batch size should pass");
576 }
577}