use std::cell::Cell;
use std::sync::Arc;
use claviron_db::DbService;
use ext_php_rs::args::Arg;
use ext_php_rs::builders::FunctionBuilder;
use ext_php_rs::class::RegisteredClass;
use ext_php_rs::convert::IntoZval;
use ext_php_rs::flags::{ClassFlags, DataType};
use ext_php_rs::php_class;
use ext_php_rs::types::{ZendClassObject, Zval};
use ext_php_rs::zend::ExecuteData;
use super::{
Parameter, bridge, parse_parameters, session, set_query_result, throw,
};
use crate::extension::FunctionEntries;
const DEFAULT_BATCH: i64 = 1_000;
#[php_class]
#[php(name = "Claviron\\Runtime\\PostgresCursor")]
#[php(flags = ClassFlags::from_bits_retain(
ClassFlags::Final.bits()
| ClassFlags::NoDynamicProperties.bits()
| ClassFlags::NotSerializable.bits()
))]
pub(crate) struct PhpPostgresCursor {
service: Arc<DbService>,
pool: String,
name: String,
batch: i64,
open: Cell<bool>,
}
pub(super) fn append_function_entries(
entries: &mut FunctionEntries
) -> Result<(), String> {
entries.push(
FunctionBuilder::new("claviron_postgres_cursor", open_handler)
.arg(Arg::new(
"connection",
DataType::object(super::PhpPostgresConnection::CLASS_NAME),
))
.arg(Arg::new("sql", DataType::String))
.not_required()
.arg(Arg::new("parameters", DataType::Array).default("[]"))
.arg(Arg::new("batch", DataType::Long).default("1000"))
.returns(
DataType::object(PhpPostgresCursor::CLASS_NAME),
false,
false,
)
.build()
.map_err(|error| format!("claviron_postgres_cursor: {error}"))?,
);
entries.push(
FunctionBuilder::new("claviron_postgres_cursor_fetch", fetch_handler)
.arg(Arg::new(
"cursor",
DataType::object(PhpPostgresCursor::CLASS_NAME),
))
.returns(DataType::Array, false, false)
.build()
.map_err(|error| {
format!("claviron_postgres_cursor_fetch: {error}")
})?,
);
entries.push(
FunctionBuilder::new("claviron_postgres_cursor_close", close_handler)
.arg(Arg::new(
"cursor",
DataType::object(PhpPostgresCursor::CLASS_NAME),
))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| {
format!("claviron_postgres_cursor_close: {error}")
})?,
);
Ok(())
}
extern "C" fn open_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result: Result<PhpPostgresCursor, String> = (|| {
let mut connection = Arg::new(
"connection",
DataType::object(super::PhpPostgresConnection::CLASS_NAME),
);
let mut sql = Arg::new("sql", DataType::String);
let mut parameters = Arg::new("parameters", DataType::Array);
let mut batch = Arg::new("batch", DataType::Long);
ex.parser()
.arg(&mut connection)
.arg(&mut sql)
.not_required()
.arg(&mut parameters)
.arg(&mut batch)
.parse()
.map_err(|error| error.to_string())?;
let connection = super::connection_from_arg(&mut connection)?;
let sql = sql
.consume::<String>()
.map_err(|_| "PostgreSQL SQL must be a string".to_owned())?;
let parameters = parse_parameters(parameters)?;
let batch = batch
.zval()
.and_then(|value| value.long())
.unwrap_or(DEFAULT_BATCH);
if batch < 1 {
return Err("PostgreSQL cursor batch size must be at least 1"
.to_owned());
}
let bridge = bridge()?;
let name = next_name();
let hold = !session::in_transaction(&connection.name);
session::declare_cursor(
&connection.service,
&bridge.runtime,
&connection.name,
&declare(&name, hold, &sql),
parameters,
)?;
Ok(PhpPostgresCursor {
service: Arc::clone(&connection.service),
pool: connection.name,
name,
batch,
open: Cell::new(true),
})
})();
match result {
Ok(cursor) => {
if ZendClassObject::new(cursor).set_zval(retval, false).is_err()
{
retval.set_null();
throw("failed to create Claviron\\Runtime\\PostgresCursor");
}
}
Err(error) => {
retval.set_null();
throw(error);
}
}
}));
}
extern "C" fn fetch_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = with_cursor(ex, |cursor| {
if !cursor.open.get() {
return Err("PostgreSQL cursor is closed".to_owned());
}
let bridge = bridge()?;
let sql = format!(
"FETCH FORWARD {} FROM {}",
cursor.batch,
quote(&cursor.name)
);
session::execute(
&cursor.service,
&bridge.runtime,
&cursor.pool,
&sql,
Vec::<Parameter>::new(),
)
});
set_query_result(retval, result);
}));
}
extern "C" fn close_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = with_cursor(ex, |cursor| {
if !cursor.open.replace(false) {
return Ok(());
}
let bridge = bridge()?;
session::close_cursor(
&cursor.service,
&bridge.runtime,
&cursor.pool,
&format!("CLOSE {}", quote(&cursor.name)),
)
});
match result {
Ok(()) => retval.set_bool(true),
Err(error) => {
retval.set_bool(false);
throw(error);
}
}
}));
}
fn with_cursor<T>(
ex: &mut ExecuteData,
with: impl FnOnce(&PhpPostgresCursor) -> Result<T, String>,
) -> Result<T, String> {
let mut cursor =
Arg::new("cursor", DataType::object(PhpPostgresCursor::CLASS_NAME));
ex.parser().arg(&mut cursor).parse().map_err(|error| error.to_string())?;
let object =
cursor.zval().and_then(|value| value.object()).ok_or_else(|| {
"cursor must be a Claviron\\Runtime\\PostgresCursor".to_owned()
})?;
let handle = ZendClassObject::<PhpPostgresCursor>::from_zend_obj(object)
.ok_or_else(|| {
"cursor must be a Claviron\\Runtime\\PostgresCursor".to_owned()
})?;
with(handle)
}
fn declare(
name: &str,
hold: bool,
sql: &str,
) -> String {
format!(
"DECLARE {} NO SCROLL CURSOR {} FOR {sql}",
quote(name),
if hold { "WITH HOLD" } else { "WITHOUT HOLD" }
)
}
fn quote(name: &str) -> String {
format!("\"{}\"", name.replace('"', "\"\""))
}
fn next_name() -> String {
thread_local! {
static NEXT: Cell<u64> = const { Cell::new(0) };
}
NEXT.with(|next| {
let id = next.get().wrapping_add(1);
next.set(id);
format!("claviron_cursor_{id}")
})
}