use nodedb_types::DatabaseId;
use pgwire::api::Type;
use pgwire::api::results::FieldInfo;
pub(super) fn is_dsl_statement(sql: &str) -> bool {
let upper = sql.trim().to_uppercase();
if upper.starts_with("SEARCH ") && upper.contains("USING VECTOR") {
return false;
}
let first_token = upper.split_whitespace().next().unwrap_or("");
let may_be_ddl = matches!(
first_token,
"CREATE"
| "DROP"
| "ALTER"
| "SHOW"
| "DESCRIBE"
| "GRANT"
| "REVOKE"
| "ANALYZE"
| "COPY"
| "BACKUP"
| "RESTORE"
| "UNDROP"
| "REINDEX"
| "REMOVE"
| "REBALANCE"
| "COMPACT"
);
if may_be_ddl && nodedb_sql::ddl_ast::parse(sql).is_some() {
return true;
}
if may_be_ddl
&& (upper.starts_with("CREATE OR REPLACE FUNCTION ")
|| upper.starts_with("CREATE FUNCTION ")
|| upper.starts_with("CREATE OR REPLACE AGGREGATE FUNCTION ")
|| upper.starts_with("CREATE AGGREGATE FUNCTION ")
|| upper.starts_with("CREATE OR REPLACE PROCEDURE ")
|| upper.starts_with("CREATE PROCEDURE ")
|| upper.starts_with("DROP FUNCTION ")
|| upper.starts_with("DROP PROCEDURE ")
|| upper.starts_with("ALTER FUNCTION ")
|| upper.starts_with("CALL "))
{
return true;
}
upper.starts_with("SEARCH ")
|| upper.starts_with("GRAPH ")
|| upper.starts_with("MATCH ")
|| upper.starts_with("OPTIONAL MATCH ")
|| upper.starts_with("CRDT MERGE ")
|| upper.starts_with("UPSERT INTO ")
|| upper.starts_with("CREATE VECTOR INDEX ")
|| upper.starts_with("CREATE FULLTEXT INDEX ")
|| upper.starts_with("CREATE SEARCH INDEX ")
|| upper.starts_with("CREATE SPARSE INDEX ")
}
pub(super) fn substitute_placeholders_with_null(sql: &str) -> String {
let ranges = crate::control::server::shared::sql::placeholder::placeholder_ranges(sql);
if ranges.is_empty() {
return sql.to_owned();
}
let mut out = String::with_capacity(sql.len());
let mut cursor = 0usize;
for (start, end, _idx) in ranges {
out.push_str(&sql[cursor..start]);
out.push_str("NULL");
cursor = end;
}
out.push_str(&sql[cursor..]);
out
}
pub(super) fn count_placeholders(sql: &str) -> usize {
let mut max_idx = 0usize;
for (_, _, idx) in crate::control::server::shared::sql::placeholder::placeholder_ranges(sql) {
if idx > max_idx {
max_idx = max_idx.max(idx);
}
}
max_idx
}
pub(super) fn result_fields_for_returning(
spec: &nodedb_physical::physical_plan::ReturningSpec,
plan: Option<&nodedb_sql::SqlPlan>,
catalog: &dyn nodedb_sql::SqlCatalog,
) -> Option<Vec<FieldInfo>> {
use nodedb_physical::physical_plan::{ReturningColumns, ReturningItem};
use nodedb_sql::types::SqlDataType;
use pgwire::api::results::FieldFormat;
fn returning_col_type_to_pg(dt: &SqlDataType) -> Type {
match dt {
SqlDataType::Int64 => Type::INT8,
SqlDataType::Float64 => Type::FLOAT8,
SqlDataType::String => Type::TEXT,
SqlDataType::Bool => Type::BOOL,
SqlDataType::Bytes => Type::BYTEA,
SqlDataType::Timestamp => Type::TIMESTAMP,
SqlDataType::Timestamptz => Type::TIMESTAMPTZ,
SqlDataType::Decimal => Type::NUMERIC,
SqlDataType::Uuid => Type::TEXT,
SqlDataType::Vector(_) => Type::BYTEA,
SqlDataType::Geometry => Type::BYTEA,
}
}
let collection = match plan? {
nodedb_sql::SqlPlan::Update { collection, .. } => collection.as_str(),
nodedb_sql::SqlPlan::Delete { collection, .. } => collection.as_str(),
_ => return None,
};
let info = catalog
.get_collection(DatabaseId::DEFAULT, collection)
.ok()
.flatten()?;
let columns_to_field_info = |columns: &[nodedb_sql::ColumnInfo]| -> Vec<FieldInfo> {
columns
.iter()
.map(|c| {
FieldInfo::new(
c.name.clone(),
None,
None,
returning_col_type_to_pg(&c.data_type),
FieldFormat::Text,
)
})
.collect()
};
let fields = match &spec.columns {
ReturningColumns::Star => columns_to_field_info(&info.columns),
ReturningColumns::Named(items) => items
.iter()
.map(|item: &ReturningItem| {
let display_name = item.alias.clone().unwrap_or_else(|| item.name.clone());
let pg_type = info
.columns
.iter()
.find(|c| c.name == item.name)
.map(|c| returning_col_type_to_pg(&c.data_type))
.unwrap_or(Type::TEXT);
FieldInfo::new(display_name, None, None, pg_type, FieldFormat::Text)
})
.collect(),
};
Some(fields)
}