query test progress
This commit is contained in:
111
src/database/executor.rs
Normal file
111
src/database/executor.rs
Normal file
@ -0,0 +1,111 @@
|
||||
use pgrx::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
/// An abstraction over database execution to allow for isolated unit testing
|
||||
/// without a live Postgres SPI connection.
|
||||
pub trait DatabaseExecutor: Send + Sync {
|
||||
/// Executes a query expecting a single JSONB return, representing rows.
|
||||
fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String>;
|
||||
|
||||
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
|
||||
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String>;
|
||||
|
||||
/// Returns the current authenticated user's ID
|
||||
fn auth_user_id(&self) -> Result<String, String>;
|
||||
|
||||
/// Returns the current transaction timestamp
|
||||
fn timestamp(&self) -> Result<String, String>;
|
||||
}
|
||||
|
||||
/// The production executor that wraps `pgrx::spi::Spi`.
|
||||
pub struct SpiExecutor;
|
||||
|
||||
impl SpiExecutor {
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl DatabaseExecutor for SpiExecutor {
|
||||
fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String> {
|
||||
let mut json_args = Vec::new();
|
||||
let mut args_with_oid: Vec<pgrx::datum::DatumWithOid> = Vec::new();
|
||||
if let Some(params) = args {
|
||||
for val in params {
|
||||
json_args.push(pgrx::JsonB(val.clone()));
|
||||
}
|
||||
for j_val in json_args.into_iter() {
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(j_val));
|
||||
}
|
||||
}
|
||||
|
||||
Spi::connect(|client| {
|
||||
match client.select(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Ok(tup_table) => {
|
||||
let mut results = Vec::new();
|
||||
for row in tup_table {
|
||||
if let Ok(Some(jsonb)) = row.get::<pgrx::JsonB>(1) {
|
||||
results.push(jsonb.0);
|
||||
}
|
||||
}
|
||||
Ok(Value::Array(results))
|
||||
}
|
||||
Err(e) => Err(format!("SPI Query Fetch Failure: {}", e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String> {
|
||||
let mut json_args = Vec::new();
|
||||
let mut args_with_oid: Vec<pgrx::datum::DatumWithOid> = Vec::new();
|
||||
if let Some(params) = args {
|
||||
for val in params {
|
||||
json_args.push(pgrx::JsonB(val.clone()));
|
||||
}
|
||||
for j_val in json_args.into_iter() {
|
||||
args_with_oid.push(pgrx::datum::DatumWithOid::from(j_val));
|
||||
}
|
||||
}
|
||||
|
||||
Spi::connect_mut(|client| {
|
||||
match client.update(sql, Some(args_with_oid.len() as i64), &args_with_oid) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => Err(format!("SPI Execution Failure: {}", e)),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn auth_user_id(&self) -> Result<String, String> {
|
||||
Spi::connect(|client| {
|
||||
let mut tup_table = client
|
||||
.select(
|
||||
"SELECT COALESCE(current_setting('auth.user_id', true), 'ffffffff-ffff-ffff-ffff-ffffffffffff')",
|
||||
None,
|
||||
&[],
|
||||
)
|
||||
.map_err(|e| format!("SPI Select Error: {}", e))?;
|
||||
|
||||
let row = tup_table
|
||||
.next()
|
||||
.ok_or("No user id setting returned from context".to_string())?;
|
||||
let user_id: Option<String> = row.get(1).map_err(|e| e.to_string())?;
|
||||
|
||||
user_id.ok_or("Missing user_id".to_string())
|
||||
})
|
||||
}
|
||||
|
||||
fn timestamp(&self) -> Result<String, String> {
|
||||
Spi::connect(|client| {
|
||||
let mut tup_table = client
|
||||
.select("SELECT clock_timestamp()::text", None, &[])
|
||||
.map_err(|e| format!("SPI Select Error: {}", e))?;
|
||||
|
||||
let row = tup_table
|
||||
.next()
|
||||
.ok_or("No clock timestamp returned".to_string())?;
|
||||
let timestamp: Option<String> = row.get(1).map_err(|e| e.to_string())?;
|
||||
|
||||
timestamp.ok_or("Missing timestamp".to_string())
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,23 +1,30 @@
|
||||
pub mod r#enum;
|
||||
pub mod executor;
|
||||
pub mod formats;
|
||||
pub mod page;
|
||||
pub mod punc;
|
||||
pub mod relation;
|
||||
pub mod schema;
|
||||
pub mod r#type;
|
||||
|
||||
use crate::database::r#enum::Enum;
|
||||
use crate::database::punc::Punc;
|
||||
use crate::database::executor::{DatabaseExecutor, SpiExecutor};
|
||||
use crate::database::punc::{Punc, Stem};
|
||||
use crate::database::relation::Relation;
|
||||
use crate::database::schema::Schema;
|
||||
use crate::database::r#type::Type;
|
||||
use serde_json::Value;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
pub struct Database {
|
||||
pub enums: HashMap<String, Enum>,
|
||||
pub types: HashMap<String, Type>,
|
||||
pub puncs: HashMap<String, Punc>,
|
||||
pub relations: HashMap<String, Relation>,
|
||||
pub schemas: HashMap<String, Schema>,
|
||||
pub descendants: HashMap<String, Vec<String>>,
|
||||
pub depths: HashMap<String, usize>,
|
||||
pub executor: Box<dyn DatabaseExecutor + Send + Sync>,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
@ -25,10 +32,12 @@ impl Database {
|
||||
let mut db = Self {
|
||||
enums: HashMap::new(),
|
||||
types: HashMap::new(),
|
||||
relations: HashMap::new(),
|
||||
puncs: HashMap::new(),
|
||||
schemas: HashMap::new(),
|
||||
descendants: HashMap::new(),
|
||||
depths: HashMap::new(),
|
||||
executor: Box::new(SpiExecutor::new()),
|
||||
};
|
||||
|
||||
if let Some(arr) = val.get("enums").and_then(|v| v.as_array()) {
|
||||
@ -47,6 +56,14 @@ impl Database {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arr) = val.get("relations").and_then(|v| v.as_array()) {
|
||||
for item in arr {
|
||||
if let Ok(def) = serde_json::from_value::<Relation>(item.clone()) {
|
||||
db.relations.insert(def.constraint.clone(), def);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(arr) = val.get("puncs").and_then(|v| v.as_array()) {
|
||||
for item in arr {
|
||||
if let Ok(def) = serde_json::from_value::<Punc>(item.clone()) {
|
||||
@ -73,12 +90,39 @@ impl Database {
|
||||
db
|
||||
}
|
||||
|
||||
/// Override the default executor for unit testing
|
||||
pub fn with_executor(mut self, executor: Box<dyn DatabaseExecutor + Send + Sync>) -> Self {
|
||||
self.executor = executor;
|
||||
self
|
||||
}
|
||||
|
||||
/// Executes a query expecting a single JSONB array return, representing rows.
|
||||
pub fn query(&self, sql: &str, args: Option<&[Value]>) -> Result<Value, String> {
|
||||
self.executor.query(sql, args)
|
||||
}
|
||||
|
||||
/// Executes an operation (INSERT, UPDATE, DELETE, or pg_notify) that does not return rows.
|
||||
pub fn execute(&self, sql: &str, args: Option<&[Value]>) -> Result<(), String> {
|
||||
self.executor.execute(sql, args)
|
||||
}
|
||||
|
||||
/// Returns the current authenticated user's ID
|
||||
pub fn auth_user_id(&self) -> Result<String, String> {
|
||||
self.executor.auth_user_id()
|
||||
}
|
||||
|
||||
/// Returns the current transaction timestamp
|
||||
pub fn timestamp(&self) -> Result<String, String> {
|
||||
self.executor.timestamp()
|
||||
}
|
||||
|
||||
/// Organizes the graph of the database, compiling regex, format functions, and caching relationships.
|
||||
fn compile(&mut self) -> Result<(), String> {
|
||||
self.collect_schemas();
|
||||
self.collect_depths();
|
||||
self.collect_descendants();
|
||||
self.compile_schemas();
|
||||
self.collect_stems();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -184,4 +228,154 @@ impl Database {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_stems(&mut self) {
|
||||
let mut st_map: HashMap<String, Vec<Stem>> = HashMap::new();
|
||||
for (name, _) in &self.puncs {
|
||||
let mut stems = Vec::new();
|
||||
let response_id = format!("{}.response", name);
|
||||
if let Some(resp_schema) = self.schemas.get(&response_id) {
|
||||
Self::discover_stems(
|
||||
&self.types,
|
||||
&self.schemas,
|
||||
&self.relations,
|
||||
&response_id,
|
||||
resp_schema,
|
||||
String::from(""),
|
||||
None,
|
||||
None,
|
||||
&mut stems,
|
||||
);
|
||||
}
|
||||
st_map.insert(name.clone(), stems);
|
||||
}
|
||||
for (name, stems) in st_map {
|
||||
if let Some(p) = self.puncs.get_mut(&name) {
|
||||
p.stems = stems;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn discover_stems(
|
||||
types: &HashMap<String, Type>,
|
||||
schemas: &HashMap<String, Schema>,
|
||||
relations: &HashMap<String, Relation>,
|
||||
_schema_id: &str,
|
||||
schema: &Schema,
|
||||
current_path: String,
|
||||
parent_type: Option<String>,
|
||||
property_name: Option<String>,
|
||||
stems: &mut Vec<Stem>,
|
||||
) {
|
||||
let mut is_entity = false;
|
||||
let mut entity_type = String::new();
|
||||
|
||||
// Check if this schema resolves to an Entity
|
||||
let mut current_ref = schema.obj.r#ref.clone();
|
||||
let mut depth = 0;
|
||||
while let Some(r) = current_ref {
|
||||
if types.contains_key(&r) {
|
||||
is_entity = true;
|
||||
entity_type = r.clone();
|
||||
break;
|
||||
}
|
||||
if let Some(s) = schemas.get(&r) {
|
||||
current_ref = s.obj.r#ref.clone();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
depth += 1;
|
||||
if depth > 20 {
|
||||
break;
|
||||
} // prevent infinite loop
|
||||
}
|
||||
|
||||
if is_entity {
|
||||
let final_path = if current_path.is_empty() {
|
||||
"/".to_string()
|
||||
} else {
|
||||
current_path.clone()
|
||||
};
|
||||
|
||||
let mut relation_col = None;
|
||||
if let (Some(pt), Some(prop)) = (&parent_type, &property_name) {
|
||||
let expected_col = format!("{}_id", prop);
|
||||
let mut found = false;
|
||||
|
||||
// Try to find the exact relation from the database schema
|
||||
for rel in relations.values() {
|
||||
if rel.source_type == *pt && rel.destination_type == entity_type {
|
||||
if rel.source_columns.contains(&expected_col) {
|
||||
relation_col = Some(expected_col.clone());
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
} else if rel.source_type == entity_type && rel.destination_type == *pt {
|
||||
if rel.source_columns.contains(&expected_col) {
|
||||
relation_col = Some(expected_col.clone());
|
||||
found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// Fallback guess if explicit matching fails
|
||||
relation_col = Some(expected_col);
|
||||
}
|
||||
}
|
||||
|
||||
stems.push(Stem {
|
||||
path: final_path,
|
||||
r#type: entity_type.clone(),
|
||||
relation: relation_col,
|
||||
});
|
||||
}
|
||||
|
||||
// Pass the new parent downwards
|
||||
let next_parent = if is_entity {
|
||||
Some(entity_type.clone())
|
||||
} else {
|
||||
parent_type.clone()
|
||||
};
|
||||
|
||||
if let Some(props) = &schema.obj.properties {
|
||||
for (k, v) in props {
|
||||
let next_path = format!(
|
||||
"{}/{}",
|
||||
if current_path.is_empty() {
|
||||
""
|
||||
} else {
|
||||
¤t_path
|
||||
},
|
||||
k
|
||||
);
|
||||
Self::discover_stems(
|
||||
types,
|
||||
schemas,
|
||||
relations,
|
||||
"",
|
||||
v,
|
||||
next_path,
|
||||
next_parent.clone(),
|
||||
Some(k.clone()),
|
||||
stems,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(items) = &schema.obj.items {
|
||||
Self::discover_stems(
|
||||
types,
|
||||
schemas,
|
||||
relations,
|
||||
"",
|
||||
items,
|
||||
current_path.clone(),
|
||||
next_parent.clone(),
|
||||
property_name.clone(),
|
||||
stems,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,6 +2,14 @@ use crate::database::page::Page;
|
||||
use crate::database::schema::Schema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Stem {
|
||||
pub path: String,
|
||||
pub r#type: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub relation: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Punc {
|
||||
@ -17,4 +25,6 @@ pub struct Punc {
|
||||
pub page: Option<Page>,
|
||||
#[serde(default)]
|
||||
pub schemas: Vec<Schema>,
|
||||
#[serde(default)]
|
||||
pub stems: Vec<Stem>,
|
||||
}
|
||||
|
||||
12
src/database/relation.rs
Normal file
12
src/database/relation.rs
Normal file
@ -0,0 +1,12 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
|
||||
#[serde(default)]
|
||||
pub struct Relation {
|
||||
pub constraint: String,
|
||||
pub source_type: String,
|
||||
pub source_columns: Vec<String>,
|
||||
pub destination_type: String,
|
||||
pub destination_columns: Vec<String>,
|
||||
pub prefix: Option<String>,
|
||||
}
|
||||
@ -23,7 +23,8 @@ pub struct Type {
|
||||
pub hierarchy: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub variations: HashSet<String>,
|
||||
pub relationship: Option<bool>,
|
||||
#[serde(default)]
|
||||
pub relationship: bool,
|
||||
#[serde(default)]
|
||||
pub fields: Vec<String>,
|
||||
pub grouped_fields: Option<Value>,
|
||||
|
||||
Reference in New Issue
Block a user