use lru::LruCache;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::num::NonZeroUsize;
use std::sync::{Arc, OnceLock, RwLock};
use crate::analyze::ShapePathAlias;
use crate::error::PyQLError;
use crate::schema::SchemaDescriptor;
use crate::{analyze, ir, parse, sql};
const CACHE_CAPACITY: usize = 1024;
const CACHE_SHARDS: usize = 16;
struct CacheEntry {
query: String,
config: ir::SessionConfig,
compiled: Arc<CompiledQuery>,
}
type CacheShard = RwLock<LruCache<u64, CacheEntry>>;
static QUERY_CACHE: OnceLock<Vec<CacheShard>> = OnceLock::new();
fn query_cache() -> &'static [CacheShard] {
QUERY_CACHE.get_or_init(|| {
let per_shard = NonZeroUsize::new(CACHE_CAPACITY / CACHE_SHARDS).unwrap();
(0..CACHE_SHARDS)
.map(|_| RwLock::new(LruCache::new(per_shard)))
.collect()
})
}
fn cache_key_hash(query: &str, config: &ir::SessionConfig) -> u64 {
let mut hasher = DefaultHasher::new();
query.hash(&mut hasher);
config.hash(&mut hasher);
hasher.finish()
}
pub fn clear_query_cache() {
if let Some(shards) = QUERY_CACHE.get() {
for shard in shards {
shard.write().unwrap().clear();
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Cardinality {
Required,
Optional,
Many,
}
#[derive(Debug, Clone)]
pub enum ShapeNode {
Scalar { name: String, position: usize },
RawScalar,
JsonScalar,
Object {
name: String,
type_name: Option<String>,
position: usize,
cardinality: Cardinality,
pointers: Vec<ShapeNode>,
has_implicit_id: bool,
},
Array {
name: String,
position: usize,
element: Box<ShapeNode>,
},
Tuple {
position: usize,
elements: Vec<ShapeNode>,
names: Option<Vec<String>>,
},
NamedTuple {
name: String,
position: usize,
type_name: Option<String>,
members: Option<Vec<JsonMember>>,
is_free_object: bool,
},
Enum {
name: String,
position: usize,
enum_type: String,
},
VectorSearch {
object_position: usize,
distance_position: usize,
object_node: Box<ShapeNode>,
},
FtsSearch {
object_position: usize,
rank_position: usize,
object_node: Box<ShapeNode>,
},
Group {
key_nodes: Vec<ShapeNode>,
grouping_position: usize,
elements_position: usize,
element: Box<ShapeNode>,
},
}
#[derive(Debug, Clone)]
pub struct JsonMember {
pub key: Option<String>,
pub kind: JsonMemberKind,
}
#[derive(Debug, Clone)]
pub enum JsonMemberKind {
Scalar,
Enum { enum_type: String },
Tuple {
type_name: Option<String>,
members: Vec<JsonMember>,
},
}
#[derive(Debug, Clone)]
pub struct ShapeDescriptor {
pub root: ShapeNode,
}
#[derive(Debug, Clone)]
pub enum QueryParam {
Null,
Bool(bool),
Int(i64),
Float(f64),
Text(String),
Bytes(Vec<u8>),
Uuid([u8; 16]),
}
#[derive(Debug, Clone)]
pub enum InferencePlan {
Search {
backend: String,
index_name: String,
query_param_name: String,
query_literal: Option<String>,
size: Option<usize>,
},
Embedding {
model_name: String,
type_name: String,
index_name: Option<String>,
query_param_name: String,
query_literal: Option<String>,
},
}
#[derive(Debug, Clone)]
pub struct CompiledQuery {
pub sql: String,
pub param_names: Vec<String>,
pub params: Vec<QueryParam>,
pub shape: ShapeDescriptor,
pub warnings: Vec<String>,
pub inference_plan: Option<InferencePlan>,
pub tags: Vec<String>,
pub mutates: bool,
pub analyze_paths: Option<Vec<ShapePathAlias>>,
pub shape_id: Arc<str>,
}
pub fn derive_shape_id(sql: &str, shape: &ShapeDescriptor) -> Arc<str> {
crate::shape_id::query_shape_id(sql, &format!("{shape:?}")).into()
}
impl CompiledQuery {
pub fn shape_id(&self) -> Arc<str> {
self.shape_id.clone()
}
pub fn sql_id(&self) -> &str {
&self.shape_id
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorClass {
ConstraintViolation,
Contention,
Timeout,
Connection,
Compile,
Other,
}
impl ErrorClass {
pub fn as_label(self) -> &'static str {
match self {
ErrorClass::ConstraintViolation => "constraint_violation",
ErrorClass::Contention => "contention",
ErrorClass::Timeout => "timeout",
ErrorClass::Connection => "connection",
ErrorClass::Compile => "compile",
ErrorClass::Other => "other",
}
}
pub fn from_sqlstate(code: &str) -> Self {
match code {
"40001" => ErrorClass::Contention,
"40P01" => ErrorClass::Contention,
"57014" => ErrorClass::Timeout,
"55P03" => ErrorClass::Timeout,
_ => match code.get(..2) {
Some("23") => ErrorClass::ConstraintViolation,
Some("08") => ErrorClass::Connection,
Some("40") => ErrorClass::Contention,
_ => ErrorClass::Other,
},
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Outcome {
Ok,
Error(ErrorClass),
}
impl Outcome {
pub fn as_label(self) -> &'static str {
match self {
Outcome::Ok => "success",
Outcome::Error(_) => "error",
}
}
}
#[derive(Debug, Clone)]
pub struct ExecutionMetadata {
pub compile_duration: std::time::Duration,
pub execute_duration: std::time::Duration,
pub query_shape_id: String,
pub rows_returned: Option<u64>,
pub outcome: Outcome,
}
impl ExecutionMetadata {
pub fn total_duration(&self) -> std::time::Duration {
self.compile_duration + self.execute_duration
}
}
pub fn compile_fill_expr(
type_name: &str,
expr_str: &str,
schema: &SchemaDescriptor,
) -> Result<String, crate::error::PyQLError> {
let expr_ast = parse::parse_expr(expr_str)?;
let (ir_expr, params) = ir::compile_expr_unaliased(&expr_ast, type_name, schema)?;
if !params.is_empty() {
return Err(crate::error::PyQLError::Syntax(crate::error::PyQLSyntaxError {
message: "fill expressions may not contain query parameters".into(),
position: crate::error::Position { line: 0, col: 0 },
}));
}
Ok(sql::emit_expr(&ir_expr))
}
pub fn compile_trigger_handler(
handler: &str,
type_name: &str,
on_mask: u8,
schema: &SchemaDescriptor,
) -> Result<String, crate::error::PyQLError> {
let ir_out = ir::compile_trigger_handler(handler, type_name, on_mask, schema)?;
if !ir_out.params.is_empty() {
return Err(crate::error::PyQLError::Syntax(crate::error::PyQLSyntaxError {
message: "trigger handlers may not contain query parameters".into(),
position: crate::error::Position { line: 0, col: 0 },
}));
}
Ok(sql::emit(&ir_out).sql)
}
pub fn compile(query: &str, schema: &SchemaDescriptor) -> Result<Arc<CompiledQuery>, PyQLError> {
compile_with_config(query, schema, &ir::SessionConfig::default())
}
pub fn compile_script(
query: &str,
schema: &SchemaDescriptor,
config: &ir::SessionConfig,
) -> Result<Vec<Arc<CompiledQuery>>, PyQLError> {
let statements = parse::parse_script(query)?;
if statements.len() == 1 {
return Ok(vec![compile_with_config(query, schema, config)?]);
}
statements
.iter()
.map(|ast| compile_ast(ast, schema, config).map(Arc::new))
.collect()
}
pub fn compile_with_config(
query: &str,
schema: &SchemaDescriptor,
config: &ir::SessionConfig,
) -> Result<Arc<CompiledQuery>, PyQLError> {
let hash = cache_key_hash(query, config);
let shard = &query_cache()[(hash as usize) % CACHE_SHARDS];
{
let mut cache = shard.write().unwrap();
if let Some(entry) = cache.get(&hash) {
if entry.query == query && &entry.config == config {
return Ok(entry.compiled.clone());
}
}
}
let compiled = Arc::new(compile_uncached(query, schema, config)?);
shard.write().unwrap().put(
hash,
CacheEntry {
query: query.to_string(),
config: config.clone(),
compiled: compiled.clone(),
},
);
Ok(compiled)
}
fn compile_uncached(
query: &str,
schema: &SchemaDescriptor,
config: &ir::SessionConfig,
) -> Result<CompiledQuery, PyQLError> {
let ast = parse::parse(query)?;
compile_ast(&ast, schema, config)
}
fn compile_ast(
ast: &parse::Stmt,
schema: &SchemaDescriptor,
config: &ir::SessionConfig,
) -> Result<CompiledQuery, PyQLError> {
let is_analyze = matches!(ast, parse::Stmt::Analyze(_));
let ir_out = ir::compile_with_config(ast, schema, config)?;
let analyze_paths = is_analyze.then(|| {
let mut paths = analyze::collect_shape_path_aliases(&ir_out.stmt);
if let Some(root) = paths.iter_mut().find(|p| p.path == "root") {
root.marker_offset = analyze::root_marker_offset(ast);
}
paths
});
let tags = ir::tags::collect_tags(&ir_out);
let mutates = stmt_mutates(&ir_out.stmt) || ir_out.ctes.iter().any(|c| stmt_mutates(&c.stmt));
let sql_out = sql::emit(&ir_out);
let shape_id = derive_shape_id(&sql_out.sql, &sql_out.shape);
Ok(CompiledQuery {
sql: sql_out.sql,
param_names: ir_out.params,
params: Vec::new(),
shape: sql_out.shape,
warnings: ir_out.warnings,
inference_plan: sql_out.inference_plan,
tags,
mutates,
analyze_paths,
shape_id,
})
}
fn stmt_mutates(stmt: &ir::IrStmt) -> bool {
match stmt {
ir::IrStmt::Insert(_) | ir::IrStmt::Update(_) | ir::IrStmt::Delete(_) => true,
ir::IrStmt::For(f) => stmt_mutates(&f.body),
ir::IrStmt::Select(sel) => sel.dml_source.as_deref().is_some_and(stmt_mutates),
_ => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::schema::{PropertyDescriptor, TypeDescriptor};
fn make_schema() -> SchemaDescriptor {
SchemaDescriptor {
types: vec![TypeDescriptor {
name: "Person".into(),
module: "default".into(),
table: "person".into(),
abstract_: false,
materialized: false,
description: None,
parents: vec![],
interfaces: vec![],
bases: vec![],
properties: vec![PropertyDescriptor {
name: "id".into(),
pg_type: "uuid".into(),
nullable: false,
default_sql: Some("uuidv7()".into()),
default_pyql: None,
description: None,
check_constraints: vec![],
is_exclusive: true,
is_pk: true,
is_readonly: true,
rewrites: vec![],
tuple_members: None,
column_type: None,
}],
links: vec![],
multilinks: vec![],
computed: vec![],
constraints: vec![],
indexes: vec![],
partition: None,
vector_indexes: vec![],
search_indexes: vec![],
triggers: vec![],
junction: false,
signals: vec![],
}],
scalars: vec![],
enums: vec![],
named_tuples: vec![],
globals: vec![],
functions: vec![],
aliases: vec![],
channels: vec![],
..Default::default()
}
}
#[test]
fn test_analyze_paths_is_none_for_a_plain_query() {
let schema = make_schema();
let compiled = compile("select Person { id }", &schema).unwrap();
assert!(compiled.analyze_paths.is_none());
}
#[test]
fn test_analyze_paths_is_populated_for_an_analyze_query() {
let schema = make_schema();
let query = "analyze select Person { id }";
let compiled = compile(query, &schema).unwrap();
let paths = compiled
.analyze_paths
.as_ref()
.expect("analyze query should populate analyze_paths");
assert_eq!(paths.len(), 1);
assert_eq!(paths[0].path, "root");
let offset = paths[0].marker_offset.expect("root path should carry a marker offset");
assert_eq!(&query[offset..offset + "Person".len()], "Person");
}
}