use std::collections::HashSet;
use std::sync::{Arc, OnceLock};
use arc_swap::ArcSwapOption;
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::exception::PhpException;
use ext_php_rs::flags::{ClassFlags, DataType};
use ext_php_rs::php_class;
use ext_php_rs::types::{ZendClassObject, ZendHashTable, Zval};
use ext_php_rs::zend::ExecuteData;
use futures_util::TryStreamExt;
use serde_json::Value as JsonValue;
use sqlx::sqlite::{SqliteConnection, SqliteRow};
use sqlx::{AssertSqlSafe, Column, Either, Row, TypeInfo};
use tokio::runtime::Handle;
use tracing::debug;
use crate::extension::FunctionEntries;
mod session;
mod types;
pub(crate) struct ExecutionCleanup;
impl Drop for ExecutionCleanup {
fn drop(&mut self) {
end_request();
}
}
pub(crate) fn begin_request() {
if let Ok(bridge) = bridge() {
session::begin_request(&bridge.runtime);
}
}
pub(crate) fn end_request() {
if let Ok(bridge) = bridge() {
session::end_request(&bridge.runtime);
}
}
struct PhpSqliteBridge {
service: Arc<DbService>,
runtime: Handle,
}
const MAX_JSON_DEPTH: usize = 64;
#[php_class]
#[php(name = "Claviron\\Runtime\\SqliteConnection")]
#[php(flags = ClassFlags::from_bits_retain(
ClassFlags::Final.bits()
| ClassFlags::NoDynamicProperties.bits()
| ClassFlags::NotSerializable.bits()
))]
pub(crate) struct PhpSqliteConnection {
service: Arc<DbService>,
name: String,
}
#[derive(Debug)]
enum Parameter {
Null,
Boolean(bool),
I32(i32),
I64(i64),
F64(f64),
String(String),
Bytes(Vec<u8>),
Json(JsonValue),
}
#[derive(Debug)]
enum Value {
Null,
I64(i64),
F64(f64),
String(String),
Bytes(Vec<u8>),
}
#[derive(Debug)]
struct ColumnInfo {
name: String,
type_name: String,
}
#[derive(Debug, Default)]
struct QueryResult {
columns: Vec<ColumnInfo>,
rows: Vec<Vec<Value>>,
affected_rows: u64,
insert_id: i64,
}
static PHP_SQLITE: OnceLock<ArcSwapOption<PhpSqliteBridge>> = OnceLock::new();
fn bridge_slot() -> &'static ArcSwapOption<PhpSqliteBridge> {
PHP_SQLITE.get_or_init(ArcSwapOption::const_empty)
}
pub(crate) fn configure(
provider: Option<Arc<DbService>>
) -> claviron_runtime::registry::Result<()> {
let bridge = provider
.map(|service| {
Handle::try_current()
.map(|runtime| Arc::new(PhpSqliteBridge { service, runtime }))
.map_err(|error| {
claviron_runtime::registry::Error::msg(format!(
"PHP SQLite runtime handle is unavailable: {error}"
))
})
})
.transpose()?;
let enabled = bridge.is_some();
bridge_slot().store(bridge);
debug!(enabled, "PHP SQLite host functions configured");
Ok(())
}
fn bridge() -> Result<Arc<PhpSqliteBridge>, String> {
bridge_slot()
.load_full()
.ok_or_else(|| "Claviron SQLite is not configured".to_owned())
}
pub(crate) fn append_function_entries(
entries: &mut FunctionEntries
) -> Result<(), String> {
let functions = [
FunctionBuilder::new("claviron_sqlite_connection", connection_handler)
.not_required()
.arg(
Arg::new("name", DataType::String).allow_null().default("null"),
)
.returns(
DataType::object(PhpSqliteConnection::CLASS_NAME),
false,
false,
)
.build()
.map_err(|error| format!("claviron_sqlite_connection: {error}"))?,
FunctionBuilder::new("claviron_sqlite_execute", execute_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.arg(Arg::new("sql", DataType::String))
.not_required()
.arg(Arg::new("parameters", DataType::Array).default("[]"))
.returns(DataType::Array, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_execute: {error}"))?,
FunctionBuilder::new("claviron_sqlite_unprepared", unprepared_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.arg(Arg::new("sql", DataType::String))
.returns(DataType::Long, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_unprepared: {error}"))?,
FunctionBuilder::new("claviron_sqlite_begin", begin_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.not_required()
.arg(Arg::new("mode", DataType::String).default("'DEFERRED'"))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_begin: {error}"))?,
FunctionBuilder::new("claviron_sqlite_commit", commit_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_commit: {error}"))?,
FunctionBuilder::new("claviron_sqlite_rollback", rollback_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_rollback: {error}"))?,
FunctionBuilder::new(
"claviron_sqlite_in_transaction",
in_transaction_handler,
)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_in_transaction: {error}"))?,
];
entries.extend(functions);
entries.push(
FunctionBuilder::new("claviron_sqlite_disconnect", disconnect_handler)
.arg(Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
))
.returns(DataType::Bool, false, false)
.build()
.map_err(|error| format!("claviron_sqlite_disconnect: {error}"))?,
);
Ok(())
}
extern "C" fn disconnect_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = (|| {
let connection = parse_connection(ex)?;
session::disconnect(&connection.name);
Ok(())
})();
set_bool_result(retval, result);
}));
}
extern "C" fn connection_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result: Result<PhpSqliteConnection, String> = (|| {
let mut name = Arg::new("name", DataType::String).allow_null();
ex.parser()
.not_required()
.arg(&mut name)
.parse()
.map_err(|error| error.to_string())?;
let name = match name.zval() {
None => "default".to_owned(),
Some(value) if value.is_null() => "default".to_owned(),
Some(_) => name.consume::<String>().map_err(|_| {
"SQLite connection name must be a string".to_owned()
})?,
};
if name.is_empty() {
return Err(
"SQLite connection name must not be empty".to_owned()
);
}
let bridge = bridge()?;
if bridge.service.sqlite_named(&name).is_none() {
return Err(format!(
"SQLite connection is not registered: {name}"
));
}
Ok(PhpSqliteConnection {
service: Arc::clone(&bridge.service),
name,
})
})();
match result {
Ok(connection) => {
if ZendClassObject::new(connection)
.set_zval(retval, false)
.is_err()
{
retval.set_null();
throw(
"failed to create Claviron\\Runtime\\SqliteConnection",
);
}
}
Err(error) => {
retval.set_null();
throw(error);
}
}
}));
}
extern "C" fn execute_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = (|| {
let (connection, sql, parameters) = parse_connection_query(ex)?;
let bridge = bridge()?;
session::execute(
&connection.service,
&bridge.runtime,
&connection.name,
&sql,
parameters,
)
})();
set_query_result(retval, result);
}));
}
extern "C" fn unprepared_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = (|| {
let mut connection = Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
);
let mut sql = Arg::new("sql", DataType::String);
ex.parser()
.arg(&mut connection)
.arg(&mut sql)
.parse()
.map_err(|error| error.to_string())?;
let connection = connection_from_arg(&mut connection)?;
let sql = sql
.consume::<String>()
.map_err(|_| "SQLite SQL must be a string".to_owned())?;
let bridge = bridge()?;
session::execute_unprepared(
&connection.service,
&bridge.runtime,
&connection.name,
&sql,
)
})();
match result {
Ok(affected) => {
retval.set_long(i64::try_from(affected).unwrap_or(i64::MAX))
}
Err(error) => {
retval.set_null();
throw(error);
}
}
}));
}
extern "C" fn begin_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
let result = (|| {
let mut connection = Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
);
let mut mode = Arg::new("mode", DataType::String);
ex.parser()
.arg(&mut connection)
.not_required()
.arg(&mut mode)
.parse()
.map_err(|error| error.to_string())?;
let connection = connection_from_arg(&mut connection)?;
let mode =
mode.consume::<String>().unwrap_or_else(|_| "DEFERRED".into());
let statement = transaction_statement(&mode)?;
let bridge = bridge()?;
session::begin(
&connection.service,
&bridge.runtime,
&connection.name,
statement,
)
})();
set_bool_result(retval, result);
}));
}
extern "C" fn commit_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
finish_handler(ex, retval, true);
}));
}
extern "C" fn rollback_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
finish_handler(ex, retval, false);
}));
}
fn finish_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
commit: bool,
) {
let result = (|| {
let connection = parse_connection(ex)?;
let bridge = bridge()?;
session::finish(
&connection.service,
&bridge.runtime,
&connection.name,
commit,
)
})();
set_bool_result(retval, result);
}
extern "C" fn in_transaction_handler(
ex: &mut ExecuteData,
retval: &mut Zval,
) {
ext_php_rs::zend::run_handler(std::panic::AssertUnwindSafe(|| {
match parse_connection(ex) {
Ok(connection) => {
retval.set_bool(session::in_transaction(&connection.name))
}
Err(error) => {
retval.set_null();
throw(error);
}
}
}));
}
fn set_bool_result(
retval: &mut Zval,
result: Result<(), String>,
) {
match result {
Ok(()) => retval.set_bool(true),
Err(error) => {
retval.set_bool(false);
throw(error);
}
}
}
async fn execute_on(
connection: &mut SqliteConnection,
sql: &str,
parameters: Vec<Parameter>,
) -> Result<QueryResult, sqlx::Error> {
let mut query = sqlx::query(AssertSqlSafe(sql));
for parameter in parameters {
query = types::bind(query, parameter);
}
#[allow(deprecated)]
let mut stream = query.fetch_many(connection);
let mut result = QueryResult::default();
while let Some(item) = stream.try_next().await? {
match item {
Either::Left(done) => {
result.affected_rows =
result.affected_rows.saturating_add(done.rows_affected());
result.insert_id = done.last_insert_rowid();
}
Either::Right(row) => push_row(&mut result, row)?,
}
}
Ok(result)
}
async fn execute_unprepared_on(
connection: &mut SqliteConnection,
sql: &str,
) -> Result<u64, sqlx::Error> {
sqlx::raw_sql(AssertSqlSafe(sql))
.execute(connection)
.await
.map(|done| done.rows_affected())
}
fn push_row(
result: &mut QueryResult,
row: SqliteRow,
) -> Result<(), sqlx::Error> {
if result.columns.is_empty() {
result.columns = row
.columns()
.iter()
.map(|column| ColumnInfo {
name: column.name().to_owned(),
type_name: column.type_info().name().to_owned(),
})
.collect();
}
let values = row
.columns()
.iter()
.enumerate()
.map(|(index, column)| {
types::decode(&row, index, column.name(), column.type_info().name())
})
.collect::<Result<Vec<_>, _>>()?;
result.rows.push(values);
Ok(())
}
fn parse_connection_query(
ex: &mut ExecuteData
) -> Result<(PhpSqliteConnection, String, Vec<Parameter>), String> {
let mut connection = Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
);
let mut sql = Arg::new("sql", DataType::String);
let mut parameters = Arg::new("parameters", DataType::Array);
ex.parser()
.arg(&mut connection)
.arg(&mut sql)
.not_required()
.arg(&mut parameters)
.parse()
.map_err(|error| error.to_string())?;
Ok((
connection_from_arg(&mut connection)?,
sql.consume::<String>()
.map_err(|_| "SQLite SQL must be a string".to_owned())?,
parse_parameters(parameters)?,
))
}
fn parse_connection(
ex: &mut ExecuteData
) -> Result<PhpSqliteConnection, String> {
let mut connection = Arg::new(
"connection",
DataType::object(PhpSqliteConnection::CLASS_NAME),
);
ex.parser()
.arg(&mut connection)
.parse()
.map_err(|error| error.to_string())?;
connection_from_arg(&mut connection)
}
fn connection_from_arg(
arg: &mut Arg<'_>
) -> Result<PhpSqliteConnection, String> {
let object =
arg.zval().and_then(|value| value.object()).ok_or_else(|| {
"connection must be a Claviron\\Runtime\\SqliteConnection"
.to_owned()
})?;
let handle = ZendClassObject::<PhpSqliteConnection>::from_zend_obj(object)
.ok_or_else(|| {
"connection must be a Claviron\\Runtime\\SqliteConnection"
.to_owned()
})?;
Ok(PhpSqliteConnection {
service: Arc::clone(&handle.service),
name: handle.name.clone(),
})
}
fn parse_parameters(mut arg: Arg<'_>) -> Result<Vec<Parameter>, String> {
let Some(value) = arg.zval() else {
return Ok(Vec::new());
};
let array = value
.array()
.ok_or_else(|| "SQLite parameters must be an array".to_owned())?;
array.values().map(parse_parameter).collect()
}
fn parse_parameter(value: &Zval) -> Result<Parameter, String> {
let value = value.dereference();
if value.is_null() {
return Ok(Parameter::Null);
}
if let Some(value) = value.bool() {
return Ok(Parameter::Boolean(value));
}
if let Some(value) = value.long() {
return Ok(Parameter::I64(value));
}
if let Some(value) = value.double() {
return value.is_finite().then_some(Parameter::F64(value)).ok_or_else(
|| "SQLite float parameters must be finite".to_owned(),
);
}
if let Some(value) = value.binary_slice::<u8>() {
return Ok(match String::from_utf8(value.to_vec()) {
Ok(value) => Parameter::String(value),
Err(value) => Parameter::Bytes(value.into_bytes()),
});
}
let array = value.array().ok_or_else(|| {
"unsupported SQLite parameter; use a scalar or tagged parameter array".to_owned()
})?;
parse_tagged_parameter(array)
}
fn parse_tagged_parameter(array: &ZendHashTable) -> Result<Parameter, String> {
let kind = array
.get("type")
.and_then(|value| value.binary_slice::<u8>())
.and_then(|value| std::str::from_utf8(value).ok())
.ok_or_else(|| {
"tagged SQLite parameter requires a string 'type'".to_owned()
})?;
let value = array.get("value");
match kind {
"null" => Ok(Parameter::Null),
"boolean" => {
value.and_then(Zval::bool).map(Parameter::Boolean).ok_or_else(
|| "boolean SQLite parameter requires a bool value".to_owned(),
)
}
"i32" => value
.and_then(Zval::long)
.and_then(|value| i32::try_from(value).ok())
.map(Parameter::I32)
.ok_or_else(|| {
"i32 SQLite parameter is outside the 32-bit range".to_owned()
}),
"i64" => {
value.and_then(Zval::long).map(Parameter::I64).ok_or_else(|| {
"i64 SQLite parameter requires an integer value".to_owned()
})
}
"f64" => value
.and_then(Zval::double)
.filter(|value| value.is_finite())
.map(Parameter::F64)
.ok_or_else(|| {
"f64 SQLite parameter requires a finite float value".to_owned()
}),
"string" => value
.and_then(Zval::string)
.map(Parameter::String)
.ok_or_else(|| {
"string SQLite parameter requires a UTF-8 string value"
.to_owned()
}),
"bytes" => value
.and_then(|value| value.binary::<u8>())
.map(Parameter::Bytes)
.ok_or_else(|| {
"bytes SQLite parameter requires a string value".to_owned()
}),
"json" => value
.ok_or_else(|| "json SQLite parameter requires a value".to_owned())
.and_then(json_from_zval)
.map(Parameter::Json),
_ => Err(format!("unsupported tagged SQLite parameter type: {kind}")),
}
}
fn json_from_zval(value: &Zval) -> Result<JsonValue, String> {
json_from_zval_inner(value, 0, &mut HashSet::new())
}
fn json_from_zval_inner(
value: &Zval,
depth: usize,
ancestors: &mut HashSet<usize>,
) -> Result<JsonValue, String> {
if depth > MAX_JSON_DEPTH {
return Err(format!(
"JSON SQLite parameter exceeds maximum nesting depth {MAX_JSON_DEPTH}"
));
}
let value = value.dereference();
if value.is_null() {
return Ok(JsonValue::Null);
}
if let Some(value) = value.bool() {
return Ok(JsonValue::Bool(value));
}
if let Some(value) = value.long() {
return Ok(JsonValue::from(value));
}
if let Some(value) = value.double() {
return serde_json::Number::from_f64(value)
.map(JsonValue::Number)
.ok_or_else(|| "JSON floats must be finite".to_owned());
}
if let Some(value) = value.string() {
return Ok(JsonValue::String(value));
}
let array = value.array().ok_or_else(|| {
"JSON SQLite parameter supports only null, bool, int, float, string, or array".to_owned()
})?;
let identity = std::ptr::from_ref(array) as usize;
if !ancestors.insert(identity) {
return Err(
"JSON SQLite parameter contains a recursive array".to_owned()
);
}
if array.has_sequential_keys() {
let result = array
.values()
.map(|value| json_from_zval_inner(value, depth + 1, ancestors))
.collect::<Result<Vec<_>, _>>()
.map(JsonValue::Array);
ancestors.remove(&identity);
return result;
}
let mut object = serde_json::Map::with_capacity(array.len());
for (key, value) in array {
object.insert(
key.to_string(),
json_from_zval_inner(value, depth + 1, ancestors)?,
);
}
ancestors.remove(&identity);
Ok(JsonValue::Object(object))
}
fn transaction_statement(mode: &str) -> Result<&'static str, String> {
match mode {
"DEFERRED" => Ok("BEGIN DEFERRED"),
"IMMEDIATE" => Ok("BEGIN IMMEDIATE"),
"EXCLUSIVE" => Ok("BEGIN EXCLUSIVE"),
_ => Err(format!("unsupported SQLite transaction mode: {mode}")),
}
}
fn set_query_result(
retval: &mut Zval,
result: Result<QueryResult, String>,
) {
match result {
Ok(result) => {
if let Err(error) = query_result_zval(result, retval) {
retval.set_null();
throw(error);
}
}
Err(error) => {
retval.set_null();
throw(error);
}
}
}
fn query_result_zval(
result: QueryResult,
retval: &mut Zval,
) -> Result<(), String> {
let mut output = ZendHashTable::new();
output
.insert("columns", columns_zval(result.columns)?)
.map_err(|error| error.to_string())?;
output
.insert("rows", rows_zval(result.rows)?)
.map_err(|error| error.to_string())?;
let mut affected = Zval::new();
if let Ok(value) = i64::try_from(result.affected_rows) {
affected.set_long(value);
} else {
affected
.set_string(&result.affected_rows.to_string(), false)
.map_err(|error| error.to_string())?;
}
output
.insert("affected_rows", affected)
.map_err(|error| error.to_string())?;
output
.insert("insert_id", result.insert_id.to_string())
.map_err(|error| error.to_string())?;
retval.set_hashtable(output);
Ok(())
}
fn columns_zval(columns: Vec<ColumnInfo>) -> Result<Zval, String> {
let mut output = ZendHashTable::with_capacity(
columns.len().try_into().unwrap_or(u32::MAX),
);
for column in columns {
let mut value = ZendHashTable::new();
value.insert("name", column.name).map_err(|error| error.to_string())?;
value
.insert("type", column.type_name)
.map_err(|error| error.to_string())?;
output.push(value).map_err(|error| error.to_string())?;
}
let mut value = Zval::new();
value.set_hashtable(output);
Ok(value)
}
fn rows_zval(rows: Vec<Vec<Value>>) -> Result<Zval, String> {
let mut output =
ZendHashTable::with_capacity(rows.len().try_into().unwrap_or(u32::MAX));
for row in rows {
let mut values = ZendHashTable::with_capacity(
row.len().try_into().unwrap_or(u32::MAX),
);
for value in row {
values
.push(value_zval(value)?)
.map_err(|error| error.to_string())?;
}
output.push(values).map_err(|error| error.to_string())?;
}
let mut value = Zval::new();
value.set_hashtable(output);
Ok(value)
}
fn value_zval(value: Value) -> Result<Zval, String> {
let mut output = Zval::new();
match value {
Value::Null => output.set_null(),
Value::I64(value) => output.set_long(value),
Value::F64(value) => output.set_double(value),
Value::String(value) => {
output
.set_string(&value, false)
.map_err(|error| error.to_string())?;
}
Value::Bytes(value) => output.set_binary(value),
}
Ok(output)
}
pub(super) fn describe(error: &sqlx::Error) -> String {
if let sqlx::Error::Database(database) = error {
let code = database
.code()
.and_then(|code| code.parse::<i32>().ok())
.unwrap_or(0);
return describe_database(code, database.message());
}
error.to_string()
}
fn describe_database(
code: i32,
message: &str,
) -> String {
let (state, label) = if code & 0xff == 19 {
("23000", "Integrity constraint violation")
} else {
("HY000", "General error")
};
format!("SQLSTATE[{state}]: {label}: {code} {message}")
}
fn throw(error: impl std::fmt::Display) {
PhpException::from_message(format!(
"Claviron SQLite operation failed: {error}"
))
.throw();
}
#[cfg(test)]
#[path = "../../tests/functions/sqlite.rs"]
mod tests;