From 5930c72a324df94cc39622398fcaff9bcc4d6604 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:40:30 +0200 Subject: [PATCH 01/14] feat: remove read module chore: add new dependency chore: format feat: error module feat: introduce hashql_eval interner chore: checkpoint feat: checkpoint feat: checkpoint chore: remove old value module feat: checkpoint feat: checkpoint feat: checkpoint feat: checkpoint feat: checkpoint chore: checkpoint feat: move entity query into its own modul fix: query request feat: checkpoint (it compiles!) feat: checkpoint feat: checkpoint feat: checkpoint fix: issue around cached thunking feat: covariance for opaque inners fix: cfgattr serde chore: remove graph module fix: merge fuckup --- apps/hash-graph/src/subcommand/server.rs | 108 +++++++- libs/@local/graph/api/src/lib.rs | 1 + .../graph/api/src/rest/hashql/compile.rs | 190 ++++++++++++++ .../@local/graph/api/src/rest/hashql/error.rs | 148 +++++++++++ libs/@local/graph/api/src/rest/hashql/mod.rs | 236 ++++++++++++++++++ .../@local/graph/api/src/rest/hashql/value.rs | 124 +++++++++ libs/@local/graph/api/src/rest/mod.rs | 37 ++- .../postgres-store/src/store/postgres/mod.rs | 11 +- tests/graph/http/test.sh | 2 + tests/graph/http/tests/hashql.http | 188 ++++++++++++++ 10 files changed, 1037 insertions(+), 8 deletions(-) create mode 100644 libs/@local/graph/api/src/rest/hashql/compile.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/error.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/mod.rs create mode 100644 libs/@local/graph/api/src/rest/hashql/value.rs create mode 100644 tests/graph/http/tests/hashql.http diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index 8c2dbeb27ab..c3133969198 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -2,7 +2,7 @@ use alloc::sync::Arc; use core::{ fmt, net::{AddrParseError, SocketAddr}, - str::FromStr as _, + str::FromStr, time::Duration, }; use std::path::PathBuf; @@ -14,7 +14,10 @@ use harpc_codec::json::JsonCodec; use harpc_server::Server; use hash_codec::bytes::JsonLinesEncoder; use hash_graph_api::{ - rest::{ApiConfig, QueryLogger, RestApiStore, RestRouterDependencies, rest_api_router}, + rest::{ + ApiConfig, QueryLogger, RestApiStore, RestRouterDependencies, hashql::CompilerContext, + rest_api_router, + }, rpc::Dependencies, }; use hash_graph_authorization::policies::store::{PolicyStore, PrincipalStore}; @@ -103,6 +106,73 @@ pub struct TemporalConfig { pub address: TemporalAddress, } +/// A pool size that can be either a concrete count or unbounded. +/// +/// Parses positive integers as a bounded size and `-1` as unbounded. +#[derive(Debug, Copy, Clone)] +pub struct PoolSize(Option); + +impl PoolSize { + #[inline] + const fn get(self) -> Option { + self.0 + } +} + +impl fmt::Display for PoolSize { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.0 { + Some(size) => write!(fmt, "{size}"), + None => write!(fmt, "-1"), + } + } +} + +impl FromStr for PoolSize { + type Err = ::Err; + + #[expect( + clippy::cast_sign_loss, + clippy::cast_possible_truncation, + reason = "negative values produce None, and pool sizes never approach u32::MAX" + )] + fn from_str(s: &str) -> Result { + let value = s.parse::()?; + if value < 0 { + Ok(Self(None)) + } else { + Ok(Self(Some(value as usize))) + } + } +} + +/// Configuration for the HashQL compiler and execution pool. +#[derive(Debug, Clone, Parser)] +pub struct CompilerConfig { + /// Number of pre-allocated heap/scratch instances in the compiler memory pool. + /// + /// Set to -1 for an unbounded pool that grows without limit. + #[clap( + long, + default_value = "16", + env = "HASH_GRAPH_COMPILER_MEMORY_POOL_SIZE", + allow_hyphen_values = true + )] + pub compiler_memory_pool_size: PoolSize, + + /// Number of threads in the compiler execution pool. + /// + /// Each thread runs a `LocalSet` for `!Send` query execution. Set to -1 to use the number + /// of available CPU cores. + #[clap( + long, + default_value = "-1", + env = "HASH_GRAPH_COMPILER_EXEC_POOL_SIZE", + allow_hyphen_values = true + )] + pub compiler_exec_pool_size: PoolSize, +} + /// Configuration for the main graph API server. /// /// Groups HTTP address, RPC address, temporal client, store behavior, and @@ -167,6 +237,9 @@ pub struct ServerConfig { #[clap(flatten)] pub api_config: ApiConfig, + #[clap(flatten)] + pub compiler: CompilerConfig, + /// Outputs the queries made to the graph to the specified file. #[clap(long)] pub log_queries: Option, @@ -233,7 +306,12 @@ async fn run_rest_server( async fn create_temporal_client( config: &TemporalConfig, ) -> Result, Report> { - if let Some(host) = &config.address.temporal_host { + if let Some(host) = config + .address + .temporal_host + .as_deref() + .filter(|host| !host.is_empty()) + { TemporalClientConfig::new( Url::from_str(&format!("{host}:{}", config.address.temporal_port)) .change_context(GraphError)?, @@ -314,6 +392,8 @@ where /// Starts the main graph API server (REST + optional RPC). async fn start_server( pool: S, + postgres: PostgresStorePool, + compiler: Arc, config: ServerConfig, query_logger: Option, lifecycle: &ServerLifecycle, @@ -343,10 +423,12 @@ where let router = rest_api_router(RestRouterDependencies { store, - domain_regex: DomainValidator::new(config.allowed_url_domain), + postgres, temporal_client, + domain_regex: DomainValidator::new(config.allowed_url_domain), query_logger, api_config: config.api_config, + compiler, }); start_rest_server(router, config.http_address, lifecycle); @@ -405,6 +487,8 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report> { let lifecycle = ServerLifecycle::new(); + let postgres = pool.clone(); + if args.embed_admin { start_admin_server(pool.clone(), args.admin, &lifecycle); } @@ -441,7 +525,21 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report> { None }; - if let Err(error) = start_server(pool, args.config, query_logger, &lifecycle).await { + let compiler = Arc::new(CompilerContext::new( + args.config.compiler.compiler_memory_pool_size.get(), + args.config.compiler.compiler_exec_pool_size.get(), + )); + + if let Err(error) = start_server( + pool, + postgres, + compiler, + args.config, + query_logger, + &lifecycle, + ) + .await + { lifecycle.shutdown_and_wait().await; return Err(error); } diff --git a/libs/@local/graph/api/src/lib.rs b/libs/@local/graph/api/src/lib.rs index 7b4903ec731..6eced828a20 100644 --- a/libs/@local/graph/api/src/lib.rs +++ b/libs/@local/graph/api/src/lib.rs @@ -9,6 +9,7 @@ // Library Features error_generic_member_access, + allocator_api )] extern crate alloc; diff --git a/libs/@local/graph/api/src/rest/hashql/compile.rs b/libs/@local/graph/api/src/rest/hashql/compile.rs new file mode 100644 index 00000000000..0e12fbd2a47 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/compile.rs @@ -0,0 +1,190 @@ +use hashql_ast::error::AstDiagnosticCategory; +use hashql_core::{ + heap::{Heap, ResetAllocator as _, Scratch}, + module::ModuleRegistry, + span::{SpanId, SpanTable}, + r#type::environment::Environment, +}; +use hashql_diagnostics::{DiagnosticIssues, IntoStatus as _, Status, StatusExt as _, Success}; +use hashql_eval::{ + context::{CodeExecutionContext, CodeGenerationContext}, + postgres::{PostgresCompiler, PreparedQueries}, +}; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::{ + body::Body, + def::{DefId, DefIdVec}, + error::MirDiagnosticCategory, + pass::{LowerConfig, execution::ExecutionAnalysisResidual}, +}; +use hashql_syntax_jexpr::span::Span; + +use super::error::HashQlDiagnosticCategory; + +pub(crate) struct CodeCompilationArtifact<'heap> { + pub assignment: DefIdVec>, &'heap Heap>, + + pub interpreter: DefIdVec, &'heap Heap>, + pub postgres: PreparedQueries<'heap, &'heap Heap>, +} + +pub(crate) struct Compilation<'heap> { + pub heap: &'heap Heap, + + pub root_span: SpanId, + + pub interner: hashql_eval::intern::Interner<'heap>, + pub env: Environment<'heap>, + + pub entrypoint: DefId, + pub artifact: CodeCompilationArtifact<'heap>, +} + +impl<'heap> Compilation<'heap> { + #[expect(clippy::too_many_lines, reason = "orchestration of sequential tasks")] + pub(crate) fn compile( + heap: &'heap Heap, + scratch: &mut Scratch, + spans: &mut SpanTable, + query: &[u8], + ) -> Status { + // Parse the query + let mut parser = hashql_syntax_jexpr::Parser::new(heap, spans); + let Success { + value: mut ast, + advisories, + } = parser + .parse_expr(query) + .into_status() + .map_category(HashQlDiagnosticCategory::JExpr)?; + + let root_span = ast.span; + + let mut env = Environment::new(heap); + let modules = ModuleRegistry::new(&env); + + // Lower the AST + let Success { + value: types, + advisories, + } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) + .map_category(|category| { + HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) + }) + .with_diagnostics(advisories)?; + + let interner = hashql_hir::intern::Interner::new(heap); + let mut hir_context = hashql_hir::context::HirContext::new(&interner, &modules); + + // Reify the HIR from the AST + let Success { + value: hir, + advisories, + } = hashql_hir::node::NodeData::from_ast(ast, &mut hir_context, &types) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Reification(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the HIR + let Success { + value: hir, + advisories, + } = hashql_hir::lower::lower(hir, &types, &mut env, &mut hir_context) + .map_category(|category| { + HashQlDiagnosticCategory::Hir(HirDiagnosticCategory::Lowering(category)) + }) + .with_diagnostics(advisories)?; + + let interner = hashql_mir::intern::Interner::new(heap); + let mut bodies = DefIdVec::new_in(heap); + let mut mir_context = hashql_mir::context::MirContext { + heap, + env: &env, + interner: &interner, + diagnostics: DiagnosticIssues::new(), + }; + let mut reify_context = hashql_mir::reify::ReifyContext { + bodies: &mut bodies, + mir: &mut mir_context, + hir: &hir_context, + scratch: &*scratch, + }; + + // Reify the MIR from the HIR + let Success { + value: entrypoint, + advisories, + } = hashql_mir::reify::from_hir(hir, &mut reify_context) + .map_category(|category| { + HashQlDiagnosticCategory::Mir(MirDiagnosticCategory::Reify(category)) + }) + .with_diagnostics(advisories)?; + + // Lower the MIR + let Success { + value: (), + advisories, + } = hashql_mir::pass::lower( + &mut mir_context, + scratch, + &mut bodies, + &LowerConfig::default(), + ) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Plan the execution + let Success { + value: execution, + advisories, + } = hashql_mir::pass::place(&mut mir_context, scratch, &mut bodies) + .map_category(HashQlDiagnosticCategory::Mir) + .with_diagnostics(advisories)?; + + // Build the postgres artifacts + let interner = interner.into(); + let mut context = CodeGenerationContext::new_in( + &env, + &interner, + &bodies, + &execution, + heap, + &mut *scratch, + ); + + let mut postgres = PostgresCompiler::new_in(&mut context, &mut *scratch); + let queries = postgres.compile(); + scratch.reset(); + + context + .diagnostics + .into_status(()) + .map_category(HashQlDiagnosticCategory::Eval) + .with_diagnostics(advisories)?; + + Status::success(Self { + heap, + + root_span, + env, + interner, + entrypoint, + artifact: CodeCompilationArtifact { + assignment: execution, + interpreter: bodies, + postgres: queries, + }, + }) + } + + pub(crate) fn context(&self) -> CodeExecutionContext<'_, 'heap, &'heap Heap> { + CodeExecutionContext { + env: &self.env, + interner: &self.interner, + bodies: &self.artifact.interpreter, + execution: &self.artifact.assignment, + alloc: self.heap, + } + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/error.rs b/libs/@local/graph/api/src/rest/hashql/error.rs new file mode 100644 index 00000000000..0fb9d7ed609 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/error.rs @@ -0,0 +1,148 @@ +use alloc::borrow::Cow; +use core::ops::Range; + +use axum::response::{Html, IntoResponse as _}; +use hashql_ast::error::AstDiagnosticCategory; +use hashql_core::span::{SpanId, SpanTable}; +use hashql_diagnostics::{ + DiagnosticCategory, Failure, Sources, Status, Success, + category::{TerminalDiagnosticCategory, canonical_category_id}, + diagnostic::render::{Format, RenderOptions}, + severity::Critical, +}; +use hashql_eval::error::EvalDiagnosticCategory; +use hashql_hir::error::HirDiagnosticCategory; +use hashql_mir::error::MirDiagnosticCategory; +use hashql_syntax_jexpr::{error::JExprDiagnosticCategory, span::Span}; +use http::StatusCode; + +use super::{ + CompilationOutputOptions, + value::{JsonValueSerialize, OwnedValue}, +}; +use crate::rest::{json::Json, status::BoxedResponse}; + +const INFRASTRUCTURE_CATEGORY: TerminalDiagnosticCategory = TerminalDiagnosticCategory { + id: "infrastructure", + name: "Infrastructure", +}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub(crate) enum HashQlDiagnosticCategory { + JExpr(JExprDiagnosticCategory), + Ast(AstDiagnosticCategory), + Hir(HirDiagnosticCategory), + Mir(MirDiagnosticCategory), + Eval(EvalDiagnosticCategory), + Infrastructure, +} + +impl serde::Serialize for HashQlDiagnosticCategory { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(&canonical_category_id(self)) + } +} + +impl DiagnosticCategory for HashQlDiagnosticCategory { + fn id(&self) -> Cow<'_, str> { + Cow::Borrowed("hashql") + } + + fn name(&self) -> Cow<'_, str> { + Cow::Borrowed("HashQL") + } + + fn subcategory(&self) -> Option<&dyn DiagnosticCategory> { + match self { + Self::JExpr(jexpr) => Some(jexpr), + Self::Ast(ast) => Some(ast), + Self::Hir(hir) => Some(hir), + Self::Mir(mir) => Some(mir), + Self::Eval(eval) => Some(eval), + Self::Infrastructure => Some(&INFRASTRUCTURE_CATEGORY), + } + } +} + +#[derive(Debug, serde::Serialize)] +struct PointerSpan { + pub range: Range, + pub pointer: Option, +} + +impl PointerSpan { + fn resolve(id: SpanId, spans: &SpanTable) -> Option { + let absolute = spans.absolute(id)?; + + let mut pointer = None; + + for ancestor in spans.ancestors(id) { + let Some(span) = spans.get(ancestor) else { + continue; + }; + + if let Some(span_pointer) = &span.pointer { + pointer = Some(span_pointer.as_str().to_owned()); + break; + } + } + + Some(Self { + range: absolute.range().into(), + pointer, + }) + } +} + +pub(crate) fn status_to_response( + status: Status, + sources: &Sources<'_>, + mut spans: &SpanTable, + options: &CompilationOutputOptions, +) -> BoxedResponse { + match status { + Ok(Success { value, advisories }) => { + let advisories = advisories.map_spans(|span| PointerSpan::resolve(span, spans)); + + if options.json_compat { + Json(Success { + value: JsonValueSerialize(&value), + advisories, + }) + .into_response() + .into() + } else { + Json(Success { value, advisories }).into_response().into() + } + } + Err(Failure { primary, secondary }) => { + let severity = primary.severity; + let status_code = if severity == Critical::ERROR { + StatusCode::BAD_REQUEST + } else { + StatusCode::INTERNAL_SERVER_ERROR + }; + + let mut response = if options.interactive { + let mut diagnostics = secondary.generalize(); + diagnostics.insert_front(primary.generalize()); + + let output = + diagnostics.render(RenderOptions::new(Format::Html, sources), &mut spans); + Html(output).into_response() + } else { + Json(Failure { + primary: Box::new(primary.map_spans(|span| PointerSpan::resolve(span, spans))), + secondary: secondary.map_spans(|span| PointerSpan::resolve(span, spans)), + }) + .into_response() + }; + + *response.status_mut() = status_code; + response.into() + } + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs new file mode 100644 index 00000000000..747b7dba904 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -0,0 +1,236 @@ +//! HashQL query endpoint. +//! +//! Accepts a HashQL query as raw JSON, compiles it through the full pipeline +//! (parse, type-check, optimize, codegen), executes the generated SQL, and returns +//! the result. Compilation errors are reported as structured diagnostics with +//! source spans. + +mod compile; +mod error; +mod value; + +use alloc::sync::Arc; +use core::num::NonZero; +use std::thread::available_parallelism; + +use axum::{Extension, Router, response::IntoResponse as _, routing::post}; +use hash_graph_postgres_store::store::PostgresStorePool; +use hash_graph_store::pool::StorePool as _; +use hash_temporal_client::TemporalClient; +use hashql_core::{ + heap::{HeapPool, ScratchPool}, + span::{SpanId, SpanTable}, +}; +use hashql_diagnostics::{ + Diagnostic, IntoStatus as _, Label, Message, Source, Sources, Status, StatusExt as _, Success, + severity::Critical, +}; +use hashql_eval::{error::EvalDiagnosticCategory, orchestrator::Orchestrator}; +use hashql_mir::interpret::Inputs; +use hashql_syntax_jexpr::span::Span; +use serde_json::value::RawValue; +use tokio_util::task::LocalPoolHandle; +use utoipa::OpenApi; + +use self::{ + compile::Compilation, + error::{HashQlDiagnosticCategory, status_to_response}, + value::OwnedValue, +}; +use crate::rest::{InteractiveHeader, JsonCompatHeader, json::Json, status::BoxedResponse}; + +/// Shared resources for HashQL query compilation and execution, created once at server startup. +pub struct CompilerContext { + pub scratches: ScratchPool, + pub heaps: HeapPool, + pub pool: LocalPoolHandle, +} + +impl CompilerContext { + /// Creates a new compiler context. + /// + /// `memory_pool_size` bounds the heap and scratch pools; `None` leaves them unbounded. + /// `exec_pool_size` sets the thread count; `None` uses the number of available CPU cores. + pub fn new(memory_pool_size: Option, exec_pool_size: Option) -> Self { + let scratches = memory_pool_size.map_or_else(ScratchPool::new, ScratchPool::bounded); + let heaps = memory_pool_size.map_or_else(HeapPool::new, HeapPool::bounded); + + let thread_count = + exec_pool_size.unwrap_or_else(|| available_parallelism().map_or(4, NonZero::get)); + + let pool = LocalPoolHandle::new(thread_count); + Self { + scratches, + heaps, + pool, + } + } +} + +/// Per-request database context. +struct ExecutionContext { + postgres: PostgresStorePool, + temporal: Option>, +} + +/// Controls the response format for a HashQL query. +pub(crate) struct CompilationOutputOptions { + /// Render errors as HTML with source annotations instead of structured JSON. + pub interactive: bool, + /// Serialize the result as plain JSON values, stripping HashQL-specific type wrappers. + pub json_compat: bool, +} + +/// Compiles and executes a HashQL query, returning the result as a [`Status`]. +#[expect(clippy::future_not_send)] +async fn query_local_impl( + ctx: Arc, + exec: ExecutionContext, + spans: &mut SpanTable, + query: &[u8], +) -> Status { + // Heap and scratch must be created inside this function because `spawn_pinned` requires + // `'static`. Moving them across the spawn boundary isn't possible since they borrow from + // the pool guards. + let mut scratch = ctx.scratches.get(); + let heap = ctx.heaps.get(); + + let inputs = Inputs::new(); // TODO: https://linear.app/hash/issue/BE-41/hashql-expose-input-in-graph-api + + let Success { + value: compilation, + advisories, + } = Compilation::compile(&heap, &mut scratch, spans, query)?; + + let context = compilation.context(); + + let Success { + value: client, + advisories, + } = exec + .postgres + .acquire(exec.temporal) + .await + .map_err(|report| { + let mut diagnostic = + Diagnostic::new(HashQlDiagnosticCategory::Infrastructure, Critical::BUG).primary( + Label::new(compilation.root_span, "failed to acquire postgres client"), + ); + + diagnostic.add_message(Message::note(format!("{report:?}"))); + diagnostic + }) + .into_status() + .with_diagnostics(advisories)?; + + let orchestrator = Orchestrator::new(client, &compilation.artifact.postgres, &context); + orchestrator + .run(&inputs, compilation.entrypoint, []) + .await + .into_status() + .map_category(|category| { + HashQlDiagnosticCategory::Eval(EvalDiagnosticCategory::Orchestrator(category)) + }) + .with_diagnostics(advisories) + .map_value(OwnedValue::from) +} + +#[expect(clippy::future_not_send)] +async fn query_local( + ctx: Arc, + exec: ExecutionContext, + query: Arc, + options: CompilationOutputOptions, +) -> BoxedResponse { + let mut sources = Sources::new(); + let source_id = sources.push(Source::new(query.get())); + + let mut spans = SpanTable::new(source_id); + + let status = query_local_impl(ctx, exec, &mut spans, query.get().as_bytes()).await; + status_to_response(status, &sources, &spans, &options) +} + +/// Spawns a query onto the local thread pool and awaits the response. +async fn run_query( + ctx: Arc, + exec: ExecutionContext, + query: Arc, + options: CompilationOutputOptions, +) -> BoxedResponse { + // The compiler and interpreter hold references into bump-allocated heaps, making their + // futures `!Send`. `spawn_pinned` runs them on a dedicated thread; the returned handle + // is `Send` so the HTTP handler can await it normally. + let pool = ctx.pool.clone(); + let result = pool + .spawn_pinned(|| query_local(ctx, exec, query, options)) + .await; + + result.unwrap_or_else(|_| { + Json(serde_json::json!({"fatal": "internal error: query execution failed"})) + .into_response() + .into() + }) +} + +/// Request body for the `/hashql` endpoint. +#[derive(serde::Deserialize, utoipa::ToSchema)] +pub(crate) struct HashQlRequest { + query: Arc, + /// Input values for the query. Must be an empty list until input support ships. + #[expect( + dead_code, + reason = "inputs will be required once HashQL input support ships" + )] + inputs: Vec<()>, +} + +#[utoipa::path( + post, + path = "/hashql", + request_body = HashQlRequest, + tag = "HashQL", + params( + ("Interactive" = Option, Header, description = "When true, error responses are rendered as HTML instead of JSON"), + ("Json-Compat" = Option, Header, description = "When true, serializes the result as plain JSON values, stripping HashQL-specific type wrappers"), + ), + responses( + (status = 200, content_type = "application/json", description = "Query executed successfully"), + (status = 400, content_type = "application/json", description = "Query compilation or validation error"), + (status = 500, description = "Internal compiler or database error"), + ) +)] +pub(crate) async fn query_hashql( + Extension(compiler): Extension>, + Extension(postgres): Extension>, + Extension(temporal): Extension>>, + InteractiveHeader(interactive): InteractiveHeader, + JsonCompatHeader(json_compat): JsonCompatHeader, + Json(request): Json, +) -> BoxedResponse { + let exec = ExecutionContext { + postgres: (*postgres).clone(), + temporal, + }; + + let options = CompilationOutputOptions { + interactive, + json_compat, + }; + + run_query(compiler, exec, request.query, options).await +} + +#[derive(OpenApi)] +#[openapi( + paths(query_hashql), + components(schemas(HashQlRequest)), + tags((name = "HashQL", description = "HashQL query execution API")) +)] +pub(crate) struct HashQlResource; + +impl HashQlResource { + pub(crate) fn routes() -> Router { + Router::new().route("/hashql", post(query_hashql)) + } +} diff --git a/libs/@local/graph/api/src/rest/hashql/value.rs b/libs/@local/graph/api/src/rest/hashql/value.rs new file mode 100644 index 00000000000..1c6c2c9bfa6 --- /dev/null +++ b/libs/@local/graph/api/src/rest/hashql/value.rs @@ -0,0 +1,124 @@ +use alloc::{collections::BTreeMap, sync::Arc}; +use core::alloc::Allocator; + +use hashql_core::id::Id as _; +use hashql_mir::interpret::value::{Int, Num, Ptr, Value}; +use serde::Serialize as _; + +fn serialize_int(int: &Int, serializer: S) -> Result { + int.as_int().serialize(serializer) +} + +#[expect(clippy::trivially_copy_pass_by_ref)] +fn serialize_num(num: &Num, serializer: S) -> Result { + num.as_f64().serialize(serializer) +} + +#[expect(clippy::trivially_copy_pass_by_ref)] +fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result { + ptr.def().as_u32().serialize(serializer) +} + +// This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] +pub(crate) enum OwnedValue { + /// The unit value. + Unit, + /// An integer value (also represents booleans). + Integer(#[serde(serialize_with = "serialize_int")] Int), + /// A floating-point number. + Number(#[serde(serialize_with = "serialize_num")] Num), + /// A string value. + String(Arc), + /// A function pointer. + Pointer(#[serde(serialize_with = "serialize_ptr")] Ptr), + + /// An opaque/newtype wrapper. + Opaque(Arc, Box), + /// A named-field struct. + Struct(Vec<(Arc, Self)>), + /// A positional tuple. + Tuple(Vec), + /// An ordered list. + List(Vec), + /// An ordered dictionary. + Dict(BTreeMap), +} + +impl<'heap, A: Allocator + Clone> From> for OwnedValue { + fn from(value: Value<'heap, A>) -> Self { + match value { + Value::Unit => Self::Unit, + Value::Integer(int) => Self::Integer(int), + Value::Number(num) => Self::Number(num), + Value::String(str) => Self::String(Arc::from(str.as_str())), + Value::Pointer(ptr) => Self::Pointer(ptr), + Value::Opaque(opaque) => Self::Opaque( + Arc::from(opaque.name().as_str()), + Box::new(opaque.into_value().into()), + ), + Value::Struct(r#struct) => { + debug_assert_eq!(r#struct.fields().len(), r#struct.values().len()); + + Self::Struct( + r#struct + .fields() + .iter() + .zip(r#struct.values()) + .map(|(field, value)| { + (Arc::from(field.as_str()), Self::from(value.clone())) + }) + .collect(), + ) + } + Value::Tuple(tuple) => Self::Tuple( + tuple + .values() + .iter() + .map(|value| Self::from(value.clone())) + .collect(), + ), + Value::List(list) => { + Self::List(list.iter().map(|value| Self::from(value.clone())).collect()) + } + Value::Dict(dict) => Self::Dict( + dict.iter() + .map(|(key, value)| (Self::from(key.clone()), Self::from(value.clone()))) + .collect(), + ), + } + } +} + +#[derive(Copy, Clone)] +pub(crate) struct JsonValueSerialize<'value>(pub &'value OwnedValue); + +impl serde::Serialize for JsonValueSerialize<'_> { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + match self.0 { + OwnedValue::Unit => serializer.serialize_unit(), + OwnedValue::Integer(int) => serialize_int(int, serializer), + OwnedValue::Number(num) => serialize_num(num, serializer), + OwnedValue::String(str) => serde::Serialize::serialize(str.as_ref(), serializer), + OwnedValue::Pointer(ptr) => serialize_ptr(ptr, serializer), + OwnedValue::Opaque(_, owned_value) => { + serde::Serialize::serialize(&Self(owned_value), serializer) + } + OwnedValue::Struct(items) => { + serializer.collect_map(items.iter().map(|(key, value)| (key, Self(value)))) + } + OwnedValue::Tuple(owned_values) => { + serializer.collect_seq(owned_values.iter().map(Self)) + } + OwnedValue::List(owned_values) => serializer.collect_seq(owned_values.iter().map(Self)), + OwnedValue::Dict(btree_map) => serializer.collect_map( + btree_map + .iter() + .map(|(key, value)| (Self(key), Self(value))), + ), + } + } +} diff --git a/libs/@local/graph/api/src/rest/mod.rs b/libs/@local/graph/api/src/rest/mod.rs index c67668ab898..38bbce2ab3e 100644 --- a/libs/@local/graph/api/src/rest/mod.rs +++ b/libs/@local/graph/api/src/rest/mod.rs @@ -16,6 +16,7 @@ pub mod admin; pub mod http_tracing_layer; pub mod jwt; +pub mod hashql; mod json; mod utoipa_typedef; use alloc::{borrow::Cow, sync::Arc}; @@ -37,7 +38,7 @@ use error_stack::{Report, ResultExt as _}; use futures::{SinkExt as _, channel::mpsc::Sender}; use hash_codec::numeric::Real; use hash_graph_authorization::policies::store::{PolicyStore, PrincipalStore}; -use hash_graph_postgres_store::store::error::VersionedUrlAlreadyExists; +use hash_graph_postgres_store::store::{PostgresStorePool, error::VersionedUrlAlreadyExists}; use hash_graph_store::{ account::AccountStore, data_type::DataTypeStore, @@ -169,6 +170,32 @@ impl FromRequestParts for InteractiveHeader { } } +pub struct JsonCompatHeader(pub bool); + +impl FromRequestParts for JsonCompatHeader { + type Rejection = (StatusCode, Cow<'static, str>); + + async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + let Some(value) = parts.headers.get("Json-Compat") else { + return Ok(Self(false)); + }; + + let bytes = value.as_ref(); + if bytes.eq_ignore_ascii_case(b"true") || bytes.eq_ignore_ascii_case(b"1") { + return Ok(Self(true)); + } + + if bytes.eq_ignore_ascii_case(b"false") || bytes.eq_ignore_ascii_case(b"0") { + return Ok(Self(false)); + } + + Err(( + StatusCode::BAD_REQUEST, + Cow::Borrowed("`Json-Compat` header must be either `true` (`1`) or `false` (`0`)"), + )) + } +} + #[derive(Debug, Serialize, Deserialize, ToSchema)] pub struct PermissionResponse { pub has_permission: bool, @@ -250,6 +277,7 @@ fn api_documentation() -> Vec { entity::EntityResource::openapi(), permissions::PermissionResource::openapi(), principal::PrincipalResource::openapi(), + hashql::HashQlResource::openapi(), ] } @@ -406,10 +434,12 @@ where S: StorePool + Send + Sync + 'static, { pub store: Arc, + pub postgres: PostgresStorePool, pub temporal_client: Option>, pub domain_regex: DomainValidator, pub query_logger: Option, pub api_config: ApiConfig, + pub compiler: Arc, } /// A [`Router`] that only serves the `OpenAPI` specification (JSON, and necessary subschemas) for @@ -437,6 +467,7 @@ where let merged_routes = api_resources::() .into_iter() .fold(Router::new(), Router::merge) + .merge(hashql::HashQlResource::routes()) .fallback(|| { tracing::error!("404: Not found"); async { StatusCode::NOT_FOUND } @@ -454,9 +485,11 @@ where ) .layer(http_tracing_layer::HttpTracingLayer) .layer(Extension(dependencies.store)) + .layer(Extension(Arc::new(dependencies.postgres))) .layer(Extension(dependencies.temporal_client)) .layer(Extension(dependencies.domain_regex)) - .layer(Extension(dependencies.api_config)); + .layer(Extension(dependencies.api_config)) + .layer(Extension(dependencies.compiler)); if let Some(query_logger) = dependencies.query_logger { router = router.layer(Extension(query_logger)); diff --git a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs index b5aa3517a5a..48d57109be8 100644 --- a/libs/@local/graph/postgres-store/src/store/postgres/mod.rs +++ b/libs/@local/graph/postgres-store/src/store/postgres/mod.rs @@ -51,7 +51,7 @@ use hash_status::StatusCode; use hash_temporal_client::TemporalClient; use postgres_types::{Json, ToSql}; use time::OffsetDateTime; -use tokio_postgres::{GenericClient as _, error::SqlState}; +use tokio_postgres::{Client, GenericClient as _, error::SqlState}; use tracing::Instrument as _; use type_system::{ Valid, @@ -112,6 +112,15 @@ pub struct PostgresStore { pub settings: Arc, } +impl AsRef for PostgresStore +where + C: AsClient, +{ + fn as_ref(&self) -> &Client { + self.client.as_client() + } +} + impl PostgresStore> { /// Inserts multiple policies into the database. /// diff --git a/tests/graph/http/test.sh b/tests/graph/http/test.sh index f97a1f651e5..d66e0992030 100755 --- a/tests/graph/http/test.sh +++ b/tests/graph/http/test.sh @@ -11,3 +11,5 @@ yarn httpyac send --all tests/ambiguous.http -o none yarn reset-database -o none yarn httpyac send --all tests/link-inheritance.http -o none yarn reset-database -o none +yarn httpyac send --all tests/hashql.http -o none +yarn reset-database -o none diff --git a/tests/graph/http/tests/hashql.http b/tests/graph/http/tests/hashql.http new file mode 100644 index 00000000000..5dfe5041548 --- /dev/null +++ b/tests/graph/http/tests/hashql.http @@ -0,0 +1,188 @@ +# This file either runs with JetBrains' http requests or using httpYac (https://httpyac.github.io). + +### Seed default policies +GET http://127.0.0.1:4000/policies/seed +Content-Type: application/json + +> {% + client.test("status", function() { + client.assert(response.status === 204, "Response status is not 204"); + }); +%} + +### Get system user +GET http://127.0.0.1:4000/actors/machine/identifier/system/h +Content-Type: application/json + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.global.set("system_machine_id", response.body); +%} + +### Create account +POST http://127.0.0.1:4000/actors/user +Content-Type: application/json +X-Authenticated-User-Actor-Id: {{system_machine_id}} + +{ + "shortname": "alice", + "registrationComplete": true +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.global.set("user_id", response.body.userId); +%} + +### HashQL: filter-false returns empty list +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": ["let", "axes", + ["::graph::temporal::PinnedTransactionTimeTemporalAxes", + {"#struct": { + "pinned": ["::graph::temporal::TransactionTime", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ], + "variable": ["::graph::temporal::DecisionTime", + ["::graph::temporal::Interval", {"#struct": { + "start": ["::graph::temporal::UnboundedTemporalBound"], + "end": ["::graph::temporal::ExclusiveTemporalBound", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ] + }}] + ] + }} + ], + ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", "axes"], + ["fn", {"#tuple": []}, {"#struct": {"vertex": "_"}}, "_", + {"#literal": false} + ] + ] + ] + ], + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.test("empty list", function() { + client.assert(response.body.value !== undefined, "Expected 'value' field"); + client.assert(response.body.value.List !== undefined, "Expected 'List' in value"); + client.assert(response.body.value.List.length === 0, "Expected empty list for filter-false"); + client.assert(response.body.advisories !== undefined, "Expected 'advisories' field"); + }); +%} + +### HashQL: parse error returns 400 with diagnostics +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": {"not": "a valid query"}, + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 400, "Expected 400 for invalid query"); + }); + client.test("has diagnostic", function() { + client.assert(response.body.primary !== undefined, "Expected primary diagnostic"); + client.assert(response.body.primary.category !== undefined, "Expected diagnostic category"); + }); +%} + +### HashQL: missing inputs field returns 400 +POST http://127.0.0.1:4000/hashql +Content-Type: application/json + +{ + "query": {"#literal": 1} +} + +> {% + client.test("status", function() { + client.assert(response.status === 400, "Expected 400 for missing inputs field"); + }); + client.test("error message mentions inputs", function() { + client.assert(response.body.message !== undefined, "Expected error message"); + client.assert(response.body.contents[0].Error.metadata.deserializationError.includes("inputs"), + "Expected error to mention missing 'inputs' field"); + }); +%} + +### HashQL: json-compat wraps result in envelope with advisories +POST http://127.0.0.1:4000/hashql +Content-Type: application/json +Json-Compat: true + +{ + "query": ["let", "axes", + ["::graph::temporal::PinnedTransactionTimeTemporalAxes", + {"#struct": { + "pinned": ["::graph::temporal::TransactionTime", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ], + "variable": ["::graph::temporal::DecisionTime", + ["::graph::temporal::Interval", {"#struct": { + "start": ["::graph::temporal::UnboundedTemporalBound"], + "end": ["::graph::temporal::ExclusiveTemporalBound", + ["::graph::temporal::Timestamp", {"#literal": 4102444800000}] + ] + }}] + ] + }} + ], + ["::graph::tail::collect", + ["::graph::body::filter", + ["::graph::head::entities", "axes"], + ["fn", {"#tuple": []}, {"#struct": {"vertex": "_"}}, "_", + {"#literal": false} + ] + ] + ] + ], + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 200, "Response status is not 200"); + }); + client.test("envelope shape", function() { + client.assert(response.body.value !== undefined, "Expected 'value' field in envelope"); + client.assert(response.body.advisories !== undefined, "Expected 'advisories' field in envelope"); + client.assert(Array.isArray(response.body.value), "Expected value to be an array"); + client.assert(response.body.value.length === 0, "Expected empty list for filter-false"); + }); +%} + +### HashQL: interactive header returns HTML on error +POST http://127.0.0.1:4000/hashql +Content-Type: application/json +Interactive: true + +{ + "query": {"not": "a valid query"}, + "inputs": [] +} + +> {% + client.test("status", function() { + client.assert(response.status === 400, "Expected 400 for invalid query"); + }); + client.test("html response", function() { + var contentType = response.headers.valueOf("content-type"); + client.assert(contentType.includes("text/html"), "Expected HTML content type, got: " + contentType); + }); +%} From ebd61ed337e24b42b556979f9ccb7350b7e1a288 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 8 Jun 2026 15:05:37 +0200 Subject: [PATCH 02/14] chore: regen schema --- libs/@local/graph/api/openapi/openapi.json | 75 ++++++++++++++++++++ libs/@local/graph/api/src/rest/hashql/mod.rs | 1 + 2 files changed, 76 insertions(+) diff --git a/libs/@local/graph/api/openapi/openapi.json b/libs/@local/graph/api/openapi/openapi.json index 0f29ddb09ae..c7a27649d87 100644 --- a/libs/@local/graph/api/openapi/openapi.json +++ b/libs/@local/graph/api/openapi/openapi.json @@ -2516,6 +2516,58 @@ } } }, + "/hashql": { + "post": { + "tags": [ + "Graph", + "HashQL" + ], + "operationId": "query_hashql", + "parameters": [ + { + "name": "Interactive", + "in": "header", + "description": "When true, error responses are rendered as HTML instead of JSON", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + }, + { + "name": "Json-Compat", + "in": "header", + "description": "When true, serializes the result as plain JSON values, stripping HashQL-specific type wrappers", + "required": false, + "schema": { + "type": "boolean", + "nullable": true + } + } + ], + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/HashQlRequest" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Query executed successfully" + }, + "400": { + "description": "Query compilation or validation error" + }, + "500": { + "description": "Internal compiler or database error" + } + } + } + }, "/policies": { "post": { "tags": [ @@ -5519,6 +5571,25 @@ }, "additionalProperties": false }, + "HashQlRequest": { + "type": "object", + "description": "Request body for the `/hashql` endpoint.", + "required": [ + "query", + "inputs" + ], + "properties": { + "inputs": { + "type": "array", + "items": { + "default": null, + "nullable": true + }, + "description": "Input values for the query. Must be an empty list until input support ships." + }, + "query": {} + } + }, "IncludeEntityTypeOption": { "type": "string", "enum": [ @@ -9694,6 +9765,10 @@ { "name": "Principal", "description": "Principal management API" + }, + { + "name": "HashQL", + "description": "HashQL query execution API" } ] } diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 747b7dba904..4724067eb8c 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -176,6 +176,7 @@ async fn run_query( /// Request body for the `/hashql` endpoint. #[derive(serde::Deserialize, utoipa::ToSchema)] pub(crate) struct HashQlRequest { + #[schema(value_type = serde_json::Value)] query: Arc, /// Input values for the query. Must be an empty list until input support ships. #[expect( From aeee8ada23b5b221de6af84d0cf156c70166431b Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:05:15 +0200 Subject: [PATCH 03/14] feat: add 0 as a possibility instead of -1 --- apps/hash-graph/src/subcommand/server.rs | 37 +++++++++----------- libs/@local/graph/api/src/rest/hashql/mod.rs | 9 ++--- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index c3133969198..1efd4b9f772 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -2,6 +2,7 @@ use alloc::sync::Arc; use core::{ fmt, net::{AddrParseError, SocketAddr}, + num::NonZero, str::FromStr, time::Duration, }; @@ -108,41 +109,37 @@ pub struct TemporalConfig { /// A pool size that can be either a concrete count or unbounded. /// -/// Parses positive integers as a bounded size and `-1` as unbounded. +/// Parses positive integers as a bounded size and `0` as unbounded. #[derive(Debug, Copy, Clone)] -pub struct PoolSize(Option); +pub struct PoolSize(Option>); impl PoolSize { #[inline] - const fn get(self) -> Option { + const fn get(self) -> Option> { self.0 } + + #[inline] + fn as_usize(self) -> Option { + self.0.map(NonZero::get) + } } impl fmt::Display for PoolSize { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { match self.0 { Some(size) => write!(fmt, "{size}"), - None => write!(fmt, "-1"), + None => write!(fmt, "0"), } } } impl FromStr for PoolSize { - type Err = ::Err; + type Err = ::Err; - #[expect( - clippy::cast_sign_loss, - clippy::cast_possible_truncation, - reason = "negative values produce None, and pool sizes never approach u32::MAX" - )] fn from_str(s: &str) -> Result { - let value = s.parse::()?; - if value < 0 { - Ok(Self(None)) - } else { - Ok(Self(Some(value as usize))) - } + let value = s.parse::()?; + Ok(Self(NonZero::new(value))) } } @@ -151,7 +148,7 @@ impl FromStr for PoolSize { pub struct CompilerConfig { /// Number of pre-allocated heap/scratch instances in the compiler memory pool. /// - /// Set to -1 for an unbounded pool that grows without limit. + /// Set to 0 for an unbounded pool that grows without limit. #[clap( long, default_value = "16", @@ -162,11 +159,11 @@ pub struct CompilerConfig { /// Number of threads in the compiler execution pool. /// - /// Each thread runs a `LocalSet` for `!Send` query execution. Set to -1 to use the number + /// Each thread runs a `LocalSet` for `!Send` query execution. Set to 0 to use the number /// of available CPU cores. #[clap( long, - default_value = "-1", + default_value = "0", env = "HASH_GRAPH_COMPILER_EXEC_POOL_SIZE", allow_hyphen_values = true )] @@ -526,7 +523,7 @@ pub async fn server(mut args: ServerArgs) -> Result<(), Report> { }; let compiler = Arc::new(CompilerContext::new( - args.config.compiler.compiler_memory_pool_size.get(), + args.config.compiler.compiler_memory_pool_size.as_usize(), args.config.compiler.compiler_exec_pool_size.get(), )); diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 4724067eb8c..9017b65ba31 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -51,14 +51,15 @@ impl CompilerContext { /// /// `memory_pool_size` bounds the heap and scratch pools; `None` leaves them unbounded. /// `exec_pool_size` sets the thread count; `None` uses the number of available CPU cores. - pub fn new(memory_pool_size: Option, exec_pool_size: Option) -> Self { + pub fn new(memory_pool_size: Option, exec_pool_size: Option>) -> Self { let scratches = memory_pool_size.map_or_else(ScratchPool::new, ScratchPool::bounded); let heaps = memory_pool_size.map_or_else(HeapPool::new, HeapPool::bounded); - let thread_count = - exec_pool_size.unwrap_or_else(|| available_parallelism().map_or(4, NonZero::get)); + let thread_count = exec_pool_size.unwrap_or_else(|| { + available_parallelism().unwrap_or(const { NonZero::new(4).unwrap() }) + }); - let pool = LocalPoolHandle::new(thread_count); + let pool = LocalPoolHandle::new(thread_count.get()); Self { scratches, heaps, From e458373caf61d11eae144fa0b09ec3774821c29a Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:12:41 +0200 Subject: [PATCH 04/14] fix: value serialization --- apps/hash-graph/src/subcommand/server.rs | 2 +- .../graph/api/src/rest/hashql/compile.rs | 3 +- libs/@local/graph/api/src/rest/hashql/mod.rs | 6 +++- .../@local/graph/api/src/rest/hashql/value.rs | 28 +++++++++++++++---- libs/@local/hashql/core/src/symbol/sym.rs | 3 +- 5 files changed, 33 insertions(+), 9 deletions(-) diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index 1efd4b9f772..8ff79ea3edb 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -146,7 +146,7 @@ impl FromStr for PoolSize { /// Configuration for the HashQL compiler and execution pool. #[derive(Debug, Clone, Parser)] pub struct CompilerConfig { - /// Number of pre-allocated heap/scratch instances in the compiler memory pool. + /// Number of retained heap/scratch instances in the compiler memory pool. /// /// Set to 0 for an unbounded pool that grows without limit. #[clap( diff --git a/libs/@local/graph/api/src/rest/hashql/compile.rs b/libs/@local/graph/api/src/rest/hashql/compile.rs index 0e12fbd2a47..a18ed91e4d0 100644 --- a/libs/@local/graph/api/src/rest/hashql/compile.rs +++ b/libs/@local/graph/api/src/rest/hashql/compile.rs @@ -3,6 +3,7 @@ use hashql_core::{ heap::{Heap, ResetAllocator as _, Scratch}, module::ModuleRegistry, span::{SpanId, SpanTable}, + symbol::sym, r#type::environment::Environment, }; use hashql_diagnostics::{DiagnosticIssues, IntoStatus as _, Status, StatusExt as _, Success}; @@ -67,7 +68,7 @@ impl<'heap> Compilation<'heap> { let Success { value: types, advisories, - } = hashql_ast::lowering::lower(heap.intern_symbol("main"), &mut ast, &env, &modules) + } = hashql_ast::lowering::lower(sym::path::main, &mut ast, &env, &modules) .map_category(|category| { HashQlDiagnosticCategory::Ast(AstDiagnosticCategory::Lowering(category)) }) diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 9017b65ba31..04d2ef5ef7a 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -28,6 +28,7 @@ use hashql_diagnostics::{ use hashql_eval::{error::EvalDiagnosticCategory, orchestrator::Orchestrator}; use hashql_mir::interpret::Inputs; use hashql_syntax_jexpr::span::Span; +use http::StatusCode; use serde_json::value::RawValue; use tokio_util::task::LocalPoolHandle; use utoipa::OpenApi; @@ -168,7 +169,10 @@ async fn run_query( .await; result.unwrap_or_else(|_| { - Json(serde_json::json!({"fatal": "internal error: query execution failed"})) + ( + StatusCode::INTERNAL_SERVER_ERROR, + Json(serde_json::json!({"fatal": "internal error: query execution failed"})), + ) .into_response() .into() }) diff --git a/libs/@local/graph/api/src/rest/hashql/value.rs b/libs/@local/graph/api/src/rest/hashql/value.rs index 1c6c2c9bfa6..9e06baa1889 100644 --- a/libs/@local/graph/api/src/rest/hashql/value.rs +++ b/libs/@local/graph/api/src/rest/hashql/value.rs @@ -19,6 +19,13 @@ fn serialize_ptr(ptr: &Ptr, serializer: S) -> Result( + dict: &BTreeMap, + serializer: S, +) -> Result { + serializer.collect_seq(dict) +} + // This is only here until https://linear.app/hash/issue/BE-540/hashql-register-based-bytecode-vm #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize)] pub(crate) enum OwnedValue { @@ -42,7 +49,7 @@ pub(crate) enum OwnedValue { /// An ordered list. List(Vec), /// An ordered dictionary. - Dict(BTreeMap), + Dict(#[serde(serialize_with = "serialize_dict")] BTreeMap), } impl<'heap, A: Allocator + Clone> From> for OwnedValue { @@ -114,11 +121,22 @@ impl serde::Serialize for JsonValueSerialize<'_> { serializer.collect_seq(owned_values.iter().map(Self)) } OwnedValue::List(owned_values) => serializer.collect_seq(owned_values.iter().map(Self)), - OwnedValue::Dict(btree_map) => serializer.collect_map( - btree_map + OwnedValue::Dict(btree_map) => { + let iter = btree_map .iter() - .map(|(key, value)| (Self(key), Self(value))), - ), + .map(|(key, value)| (Self(key), Self(value))); + + // If all the keys are strings we can collect a map, otherwise we must fallback + // to collecting as a sequence + if btree_map + .keys() + .all(|key| matches!(key, OwnedValue::String(_))) + { + serializer.collect_map(iter) + } else { + serializer.collect_seq(iter) + } + } } } } diff --git a/libs/@local/hashql/core/src/symbol/sym.rs b/libs/@local/hashql/core/src/symbol/sym.rs index d4809e9ed2b..1379e05f3a2 100644 --- a/libs/@local/hashql/core/src/symbol/sym.rs +++ b/libs/@local/hashql/core/src/symbol/sym.rs @@ -243,10 +243,11 @@ hashql_macros::define_symbols! { Interval: "::graph::temporal::Interval", LeftClosedTemporalInterval: "::graph::temporal::LeftClosedTemporalInterval", LinkData: "::graph::types::knowledge::entity::LinkData", + main: "::main", None: "::core::option::None", OntologyTypeVersion: "::graph::ontology::OntologyTypeVersion", OpenTemporalBound: "::graph::temporal::OpenTemporalBound", - option: "::core::option::Option", + Option: "::core::option::Option", PinnedDecisionTimeTemporalAxes: "::graph::temporal::PinnedDecisionTimeTemporalAxes", PinnedTransactionTimeTemporalAxes: "::graph::temporal::PinnedTransactionTimeTemporalAxes", PropertyObjectMetadata: "::graph::types::knowledge::entity::PropertyObjectMetadata", From c9629fb4d537146dc59138d0621b4cc3d42bc353 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 8 Jun 2026 16:26:16 +0200 Subject: [PATCH 05/14] chore: regen turborepo --- libs/@local/graph/api/package.json | 9 ++++++++- yarn.lock | 7 +++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/libs/@local/graph/api/package.json b/libs/@local/graph/api/package.json index 2d795d6ce7c..acafac48ec2 100644 --- a/libs/@local/graph/api/package.json +++ b/libs/@local/graph/api/package.json @@ -30,6 +30,13 @@ "@rust/hash-graph-types": "workspace:*", "@rust/hash-graph-validation": "workspace:*", "@rust/hash-status": "workspace:*", - "@rust/hash-temporal-client": "workspace:*" + "@rust/hash-temporal-client": "workspace:*", + "@rust/hashql-ast": "workspace:*", + "@rust/hashql-core": "workspace:*", + "@rust/hashql-diagnostics": "workspace:*", + "@rust/hashql-eval": "workspace:*", + "@rust/hashql-hir": "workspace:*", + "@rust/hashql-mir": "workspace:*", + "@rust/hashql-syntax-jexpr": "workspace:*" } } diff --git a/yarn.lock b/yarn.lock index 90d21fd7ab6..a3a87f56ea0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13093,6 +13093,13 @@ __metadata: "@rust/hash-graph-validation": "workspace:*" "@rust/hash-status": "workspace:*" "@rust/hash-temporal-client": "workspace:*" + "@rust/hashql-ast": "workspace:*" + "@rust/hashql-core": "workspace:*" + "@rust/hashql-diagnostics": "workspace:*" + "@rust/hashql-eval": "workspace:*" + "@rust/hashql-hir": "workspace:*" + "@rust/hashql-mir": "workspace:*" + "@rust/hashql-syntax-jexpr": "workspace:*" languageName: unknown linkType: soft From 9edc89dc7546f99be2ee843ffc04ed8f6e562c23 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:20:04 +0200 Subject: [PATCH 06/14] chore: remove allow-hyphen-value --- apps/hash-graph/src/subcommand/server.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/hash-graph/src/subcommand/server.rs b/apps/hash-graph/src/subcommand/server.rs index 8ff79ea3edb..196f94d8a99 100644 --- a/apps/hash-graph/src/subcommand/server.rs +++ b/apps/hash-graph/src/subcommand/server.rs @@ -152,8 +152,7 @@ pub struct CompilerConfig { #[clap( long, default_value = "16", - env = "HASH_GRAPH_COMPILER_MEMORY_POOL_SIZE", - allow_hyphen_values = true + env = "HASH_GRAPH_COMPILER_MEMORY_POOL_SIZE" )] pub compiler_memory_pool_size: PoolSize, @@ -161,12 +160,7 @@ pub struct CompilerConfig { /// /// Each thread runs a `LocalSet` for `!Send` query execution. Set to 0 to use the number /// of available CPU cores. - #[clap( - long, - default_value = "0", - env = "HASH_GRAPH_COMPILER_EXEC_POOL_SIZE", - allow_hyphen_values = true - )] + #[clap(long, default_value = "0", env = "HASH_GRAPH_COMPILER_EXEC_POOL_SIZE")] pub compiler_exec_pool_size: PoolSize, } From 66cd043e16026ad523b29fbe7b13f98e1e6c5acd Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:28:45 +0200 Subject: [PATCH 07/14] chore: add tracing error logs --- libs/@local/graph/api/src/rest/hashql/mod.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 04d2ef5ef7a..491f39ecc7f 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -168,7 +168,9 @@ async fn run_query( .spawn_pinned(|| query_local(ctx, exec, query, options)) .await; - result.unwrap_or_else(|_| { + result.unwrap_or_else(|error| { + tracing::error!(?error, "panicked by trying to execute query"); + ( StatusCode::INTERNAL_SERVER_ERROR, Json(serde_json::json!({"fatal": "internal error: query execution failed"})), From d7cbf87a929bc25aa02144736232cdb88b32bdf8 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Tue, 9 Jun 2026 09:35:22 +0200 Subject: [PATCH 08/14] fix: clippy --- libs/@local/graph/api/src/rest/mod.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/libs/@local/graph/api/src/rest/mod.rs b/libs/@local/graph/api/src/rest/mod.rs index 38bbce2ab3e..9ab72c616ae 100644 --- a/libs/@local/graph/api/src/rest/mod.rs +++ b/libs/@local/graph/api/src/rest/mod.rs @@ -175,24 +175,27 @@ pub struct JsonCompatHeader(pub bool); impl FromRequestParts for JsonCompatHeader { type Rejection = (StatusCode, Cow<'static, str>); - async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result { + fn from_request_parts( + parts: &mut Parts, + _state: &S, + ) -> impl Future> + Send { let Some(value) = parts.headers.get("Json-Compat") else { - return Ok(Self(false)); + return core::future::ready(Ok(Self(false))); }; let bytes = value.as_ref(); if bytes.eq_ignore_ascii_case(b"true") || bytes.eq_ignore_ascii_case(b"1") { - return Ok(Self(true)); + return core::future::ready(Ok(Self(true))); } if bytes.eq_ignore_ascii_case(b"false") || bytes.eq_ignore_ascii_case(b"0") { - return Ok(Self(false)); + return core::future::ready(Ok(Self(false))); } - Err(( + core::future::ready(Err(( StatusCode::BAD_REQUEST, Cow::Borrowed("`Json-Compat` header must be either `true` (`1`) or `false` (`0`)"), - )) + ))) } } From 796227660f17d4c2c95c888ff99139538c91fd3f Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:17:10 +0200 Subject: [PATCH 09/14] fix: suggestions from code review --- libs/@local/graph/api/src/rest/hashql/mod.rs | 24 ++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 491f39ecc7f..a8ed5e12ed4 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -119,7 +119,12 @@ async fn query_local_impl( Label::new(compilation.root_span, "failed to acquire postgres client"), ); - diagnostic.add_message(Message::note(format!("{report:?}"))); + if cfg!(debug_assertions) { + diagnostic.add_message(Message::note(format!("{report:?}"))); + } else { + tracing::error!(?report, "failed to acquire postgres client"); + } + diagnostic }) .into_status() @@ -169,7 +174,7 @@ async fn run_query( .await; result.unwrap_or_else(|error| { - tracing::error!(?error, "panicked by trying to execute query"); + tracing::error!(?error, "panicked while executing query"); ( StatusCode::INTERNAL_SERVER_ERROR, @@ -180,6 +185,20 @@ async fn run_query( }) } +fn deserialize_empty_inputs<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let inputs = serde::Deserialize::<'de>::deserialize(deserializer)?; + if inputs.is_empty() { + return Ok(inputs); + } + + Err(serde::de::Error::custom( + "`inputs` must be an empty array until input support ships", + )) +} + /// Request body for the `/hashql` endpoint. #[derive(serde::Deserialize, utoipa::ToSchema)] pub(crate) struct HashQlRequest { @@ -190,6 +209,7 @@ pub(crate) struct HashQlRequest { dead_code, reason = "inputs will be required once HashQL input support ships" )] + #[serde(deserialize_with = "deserialize_empty_inputs")] inputs: Vec<()>, } From b88b8d13b9b3a8d097a53672e513b0781f4f6220 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Thu, 11 Jun 2026 14:18:10 +0200 Subject: [PATCH 10/14] fix: type annotation --- libs/@local/graph/api/src/rest/hashql/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index a8ed5e12ed4..62912a8e832 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -189,7 +189,7 @@ fn deserialize_empty_inputs<'de, D>(deserializer: D) -> Result, D::Error where D: serde::Deserializer<'de>, { - let inputs = serde::Deserialize::<'de>::deserialize(deserializer)?; + let inputs: Vec<()> = serde::Deserialize::<'de>::deserialize(deserializer)?; if inputs.is_empty() { return Ok(inputs); } From 96720c5e3a558963d126033b8f380e19ab745e75 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 6 Jul 2026 09:48:04 +0200 Subject: [PATCH 11/14] fix: dependencies --- Cargo.lock | 7 +++++++ libs/@local/graph/api/Cargo.toml | 7 +++++++ 2 files changed, 14 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index e28b7ad8bb4..e0de9ebea5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3570,6 +3570,13 @@ dependencies = [ "hash-graph-validation", "hash-status", "hash-temporal-client", + "hashql-ast", + "hashql-core", + "hashql-diagnostics", + "hashql-eval", + "hashql-hir", + "hashql-mir", + "hashql-syntax-jexpr", "http 1.4.2", "hyper", "include_dir", diff --git a/libs/@local/graph/api/Cargo.toml b/libs/@local/graph/api/Cargo.toml index 2880099f590..5589b893052 100644 --- a/libs/@local/graph/api/Cargo.toml +++ b/libs/@local/graph/api/Cargo.toml @@ -26,6 +26,7 @@ hash-temporal-client = { workspace = true, public = true } axum = { workspace = true, public = true } axum-core = { workspace = true, public = true } futures-channel = { workspace = true, public = true } +hashql-core = { workspace = true, public = true } http = { workspace = true, public = true } jsonwebtoken = { workspace = true, public = true, features = ["aws_lc_rs"] } tower-http = { workspace = true, public = true } @@ -41,6 +42,12 @@ hash-graph-store = { workspace = true, features = ["utoipa"] } hash-graph-temporal-versioning = { workspace = true } hash-graph-type-defs = { workspace = true } hash-graph-validation = { workspace = true } +hashql-ast = { workspace = true } +hashql-diagnostics = { workspace = true, features = ["serde", "render"] } +hashql-eval = { workspace = true } +hashql-hir = { workspace = true } +hashql-mir = { workspace = true } +hashql-syntax-jexpr = { workspace = true } type-system = { workspace = true, features = ["utoipa"] } # Private third-party dependencies (optional) From d0d1f077ed92fb04e2804c67205c1e4dca8fc1e6 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:16:22 +0200 Subject: [PATCH 12/14] fix: dependency diagram --- apps/hash-graph/docs/dependency-diagram.mmd | 66 +++++--- .../rust/docs/dependency-diagram.mmd | 3 +- libs/@local/codec/docs/dependency-diagram.mmd | 3 +- .../codegen/docs/dependency-diagram.mmd | 3 +- .../graph/api/docs/dependency-diagram.mmd | 70 ++++++--- .../authorization/docs/dependency-diagram.mmd | 3 +- .../docs/dependency-diagram.mmd | 3 +- .../migrations/docs/dependency-diagram.mmd | 3 +- .../docs/dependency-diagram.mmd | 3 +- .../graph/store/docs/dependency-diagram.mmd | 3 +- .../docs/dependency-diagram.mmd | 3 +- .../graph/types/docs/dependency-diagram.mmd | 3 +- .../validation/docs/dependency-diagram.mmd | 3 +- .../harpc/types/docs/dependency-diagram.mmd | 3 +- .../wire-protocol/docs/dependency-diagram.mmd | 3 +- .../hashql/ast/docs/dependency-diagram.mmd | 145 +++++++++--------- .../compiletest/docs/dependency-diagram.mmd | 145 +++++++++--------- .../hashql/core/docs/dependency-diagram.mmd | 83 +++++----- .../diagnostics/docs/dependency-diagram.mmd | 43 +++--- .../hashql/eval/docs/dependency-diagram.mmd | 145 +++++++++--------- .../hashql/hir/docs/dependency-diagram.mmd | 145 +++++++++--------- .../hashql/mir/docs/dependency-diagram.mmd | 145 +++++++++--------- .../syntax-jexpr/docs/dependency-diagram.mmd | 145 +++++++++--------- .../status/rust/docs/dependency-diagram.mmd | 3 +- .../telemetry/docs/dependency-diagram.mmd | 3 +- .../docs/dependency-diagram.mmd | 3 +- .../rust/docs/dependency-diagram.mmd | 3 +- 27 files changed, 658 insertions(+), 525 deletions(-) diff --git a/apps/hash-graph/docs/dependency-diagram.mmd b/apps/hash-graph/docs/dependency-diagram.mmd index 117a14558df..8814115ea5f 100644 --- a/apps/hash-graph/docs/dependency-diagram.mmd +++ b/apps/hash-graph/docs/dependency-diagram.mmd @@ -32,37 +32,51 @@ graph TD 20[harpc-tower] 21[harpc-types] 22[harpc-wire-protocol] - 23[hash-status] - 24[hash-telemetry] - 25[hash-temporal-client] - 26[error-stack] - 27[hash-graph-test-data] + 23[hashql-ast] + 24[hashql-compiletest] + 25[hashql-core] + 26[hashql-diagnostics] + 27[hashql-eval] + 28[hashql-hir] + 29[hashql-macros] + 30[hashql-mir] + 31[hashql-syntax-jexpr] + 32[hash-status] + 33[hash-telemetry] + 34[hash-temporal-client] + 35[darwin-kperf] + 36[darwin-kperf-criterion] + 37[darwin-kperf-events] + 38[darwin-kperf-sys] + 39[error-stack] + 40[hash-graph-test-data] 0 --> 4 1 --> 10 - 1 -.-> 27 + 1 -.-> 40 2 -.-> 3 2 --> 22 - 4 --> 8 4 --> 11 4 --> 12 4 --> 18 + 4 --> 27 + 4 --> 31 5 --> 1 6 --> 7 - 6 --> 24 + 6 --> 33 8 -.-> 6 8 --> 14 - 8 --> 23 + 8 --> 32 9 --> 5 9 --> 13 - 9 --> 25 + 9 --> 34 10 --> 2 - 11 --> 23 + 11 --> 32 12 --> 9 - 13 -.-> 27 - 14 -.-> 27 + 13 -.-> 40 + 14 -.-> 40 15 --> 19 16 --> 21 - 16 --> 26 + 16 --> 39 17 --> 2 17 -.-> 16 17 --> 16 @@ -73,7 +87,23 @@ graph TD 20 --> 17 22 -.-> 21 22 --> 21 - 22 --> 26 - 24 --> 26 - 25 --> 1 - 27 --> 9 + 22 --> 39 + 23 -.-> 24 + 24 --> 27 + 24 --> 31 + 25 --> 2 + 25 --> 26 + 25 --> 29 + 25 -.-> 36 + 27 --> 8 + 27 --> 30 + 28 -.-> 24 + 30 --> 28 + 31 --> 23 + 31 --> 25 + 33 --> 39 + 34 --> 1 + 35 --> 37 + 35 --> 38 + 36 --> 35 + 40 --> 9 diff --git a/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd b/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd index 60e5dc7313b..b9a27b2f1e8 100644 --- a/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd +++ b/libs/@blockprotocol/type-system/rust/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/codec/docs/dependency-diagram.mmd b/libs/@local/codec/docs/dependency-diagram.mmd index 6f3bc699d0a..e40e05fce7a 100644 --- a/libs/@local/codec/docs/dependency-diagram.mmd +++ b/libs/@local/codec/docs/dependency-diagram.mmd @@ -45,9 +45,10 @@ graph TD 1 -.-> 30 2 -.-> 3 2 --> 18 - 4 --> 6 4 --> 9 4 --> 14 + 4 --> 22 + 4 --> 25 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/codegen/docs/dependency-diagram.mmd b/libs/@local/codegen/docs/dependency-diagram.mmd index 4d1bad0e4c0..601be9ff4ba 100644 --- a/libs/@local/codegen/docs/dependency-diagram.mmd +++ b/libs/@local/codegen/docs/dependency-diagram.mmd @@ -41,9 +41,10 @@ graph TD 1 --> 8 1 -.-> 27 2 -.-> 3 - 4 --> 6 4 --> 9 4 --> 14 + 4 --> 20 + 4 --> 23 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/graph/api/docs/dependency-diagram.mmd b/libs/@local/graph/api/docs/dependency-diagram.mmd index 1f93e114811..57d0ebd98d4 100644 --- a/libs/@local/graph/api/docs/dependency-diagram.mmd +++ b/libs/@local/graph/api/docs/dependency-diagram.mmd @@ -32,38 +32,52 @@ graph TD 20[harpc-tower] 21[harpc-types] 22[harpc-wire-protocol] - 23[hash-status] - 24[hash-telemetry] - 25[hash-temporal-client] - 26[error-stack] - 27[hash-graph-benches] - 28[hash-graph-test-data] + 23[hashql-ast] + 24[hashql-compiletest] + 25[hashql-core] + 26[hashql-diagnostics] + 27[hashql-eval] + 28[hashql-hir] + 29[hashql-macros] + 30[hashql-mir] + 31[hashql-syntax-jexpr] + 32[hash-status] + 33[hash-telemetry] + 34[hash-temporal-client] + 35[darwin-kperf] + 36[darwin-kperf-criterion] + 37[darwin-kperf-events] + 38[darwin-kperf-sys] + 39[error-stack] + 40[hash-graph-benches] + 41[hash-graph-test-data] 0 --> 4 1 --> 10 - 1 -.-> 28 + 1 -.-> 41 2 -.-> 3 2 --> 22 - 4 --> 8 4 --> 11 4 --> 12 4 --> 18 + 4 --> 27 + 4 --> 31 5 --> 1 6 --> 7 - 6 --> 24 + 6 --> 33 8 -.-> 6 8 --> 14 - 8 --> 23 + 8 --> 32 9 --> 5 9 --> 13 - 9 --> 25 + 9 --> 34 10 --> 2 - 11 --> 23 + 11 --> 32 12 --> 9 - 13 -.-> 28 - 14 -.-> 28 + 13 -.-> 41 + 14 -.-> 41 15 --> 19 16 --> 21 - 16 --> 26 + 16 --> 39 17 --> 2 17 -.-> 16 17 --> 16 @@ -74,8 +88,24 @@ graph TD 20 --> 17 22 -.-> 21 22 --> 21 - 22 --> 26 - 24 --> 26 - 25 --> 1 - 27 -.-> 4 - 28 --> 9 + 22 --> 39 + 23 -.-> 24 + 24 --> 27 + 24 --> 31 + 25 --> 2 + 25 --> 26 + 25 --> 29 + 25 -.-> 36 + 27 --> 8 + 27 --> 30 + 28 -.-> 24 + 30 --> 28 + 31 --> 23 + 31 --> 25 + 33 --> 39 + 34 --> 1 + 35 --> 37 + 35 --> 38 + 36 --> 35 + 40 -.-> 4 + 41 --> 9 diff --git a/libs/@local/graph/authorization/docs/dependency-diagram.mmd b/libs/@local/graph/authorization/docs/dependency-diagram.mmd index f0dc208fa91..ce8b50f65b7 100644 --- a/libs/@local/graph/authorization/docs/dependency-diagram.mmd +++ b/libs/@local/graph/authorization/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/graph/migrations-macros/docs/dependency-diagram.mmd b/libs/@local/graph/migrations-macros/docs/dependency-diagram.mmd index 746154ea368..427345a63b3 100644 --- a/libs/@local/graph/migrations-macros/docs/dependency-diagram.mmd +++ b/libs/@local/graph/migrations-macros/docs/dependency-diagram.mmd @@ -23,7 +23,8 @@ graph TD 11[hash-graph-benches] 12[hash-graph-integration] 0 --> 1 - 1 --> 4 + 1 --> 7 + 1 --> 10 2 --> 3 4 -.-> 2 5 -.-> 6 diff --git a/libs/@local/graph/migrations/docs/dependency-diagram.mmd b/libs/@local/graph/migrations/docs/dependency-diagram.mmd index 3abf48d0891..9e314eca731 100644 --- a/libs/@local/graph/migrations/docs/dependency-diagram.mmd +++ b/libs/@local/graph/migrations/docs/dependency-diagram.mmd @@ -25,7 +25,8 @@ graph TD 13[hash-graph-benches] 14[hash-graph-integration] 0 --> 1 - 1 --> 4 + 1 --> 7 + 1 --> 10 2 --> 3 2 --> 11 4 -.-> 2 diff --git a/libs/@local/graph/postgres-store/docs/dependency-diagram.mmd b/libs/@local/graph/postgres-store/docs/dependency-diagram.mmd index c7c6a94b004..84ed6a1efa0 100644 --- a/libs/@local/graph/postgres-store/docs/dependency-diagram.mmd +++ b/libs/@local/graph/postgres-store/docs/dependency-diagram.mmd @@ -42,7 +42,8 @@ graph TD 1 -.-> 27 2 -.-> 3 2 --> 14 - 4 --> 8 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 7 6 --> 22 diff --git a/libs/@local/graph/store/docs/dependency-diagram.mmd b/libs/@local/graph/store/docs/dependency-diagram.mmd index 263eb7b198b..d0627e3dd8b 100644 --- a/libs/@local/graph/store/docs/dependency-diagram.mmd +++ b/libs/@local/graph/store/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd b/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd index 738f70891d2..146d1f192df 100644 --- a/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd +++ b/libs/@local/graph/temporal-versioning/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/graph/types/docs/dependency-diagram.mmd b/libs/@local/graph/types/docs/dependency-diagram.mmd index cc37bd19123..5ddabb57b44 100644 --- a/libs/@local/graph/types/docs/dependency-diagram.mmd +++ b/libs/@local/graph/types/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/libs/@local/graph/validation/docs/dependency-diagram.mmd b/libs/@local/graph/validation/docs/dependency-diagram.mmd index a2c6e933d2f..a84b3c63ba9 100644 --- a/libs/@local/graph/validation/docs/dependency-diagram.mmd +++ b/libs/@local/graph/validation/docs/dependency-diagram.mmd @@ -38,7 +38,8 @@ graph TD 1 -.-> 23 2 -.-> 3 2 --> 12 - 4 --> 6 + 4 --> 15 + 4 --> 18 5 --> 1 6 --> 10 7 --> 5 diff --git a/libs/@local/harpc/types/docs/dependency-diagram.mmd b/libs/@local/harpc/types/docs/dependency-diagram.mmd index b357dab2b77..a33077303be 100644 --- a/libs/@local/harpc/types/docs/dependency-diagram.mmd +++ b/libs/@local/harpc/types/docs/dependency-diagram.mmd @@ -43,9 +43,10 @@ graph TD 1 --> 7 1 -.-> 29 2 --> 18 - 3 --> 5 3 --> 8 3 --> 14 + 3 --> 22 + 3 --> 25 4 --> 1 5 --> 10 6 --> 4 diff --git a/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd b/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd index 3d92ab96acb..4bb5a7ed3f0 100644 --- a/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd +++ b/libs/@local/harpc/wire-protocol/docs/dependency-diagram.mmd @@ -43,9 +43,10 @@ graph TD 1 --> 7 1 -.-> 29 2 --> 17 - 3 --> 5 3 --> 8 3 --> 13 + 3 --> 21 + 3 --> 24 4 --> 1 5 --> 10 6 --> 4 diff --git a/libs/@local/hashql/ast/docs/dependency-diagram.mmd b/libs/@local/hashql/ast/docs/dependency-diagram.mmd index d77b81ad850..1c4dfe4d89f 100644 --- a/libs/@local/hashql/ast/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/ast/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - class 13 root - 14[hashql-compiletest] - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - 18[hashql-hir] - 19[hashql-macros] - 20[hashql-mir] - 21[hashql-syntax-jexpr] - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + class 15 root + 16[hashql-compiletest] + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + 20[hashql-hir] + 21[hashql-macros] + 22[hashql-mir] + 23[hashql-syntax-jexpr] + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/hashql/compiletest/docs/dependency-diagram.mmd b/libs/@local/hashql/compiletest/docs/dependency-diagram.mmd index 6a7e3b37ce8..4d2d74f3e20 100644 --- a/libs/@local/hashql/compiletest/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/compiletest/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - 14[hashql-compiletest] - class 14 root - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - 18[hashql-hir] - 19[hashql-macros] - 20[hashql-mir] - 21[hashql-syntax-jexpr] - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + 16[hashql-compiletest] + class 16 root + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + 20[hashql-hir] + 21[hashql-macros] + 22[hashql-mir] + 23[hashql-syntax-jexpr] + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/hashql/core/docs/dependency-diagram.mmd b/libs/@local/hashql/core/docs/dependency-diagram.mmd index 0416c5a3818..af15c76c147 100644 --- a/libs/@local/hashql/core/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/core/docs/dependency-diagram.mmd @@ -8,42 +8,49 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[hash-codec] - 1[hash-codegen] - 2[harpc-types] - 3[harpc-wire-protocol] - 4[hashql-ast] - 5[hashql-compiletest] - 6[hashql-core] - class 6 root - 7[hashql-diagnostics] - 8[hashql-eval] - 9[hashql-hir] - 10[hashql-macros] - 11[hashql-mir] - 12[hashql-syntax-jexpr] - 13[darwin-kperf] - 14[darwin-kperf-criterion] - 15[darwin-kperf-events] - 16[darwin-kperf-sys] - 17[error-stack] - 0 -.-> 1 + 0[hash-graph] + 1[hash-codec] + 2[hash-codegen] + 3[hash-graph-api] + 4[harpc-types] + 5[harpc-wire-protocol] + 6[hashql-ast] + 7[hashql-compiletest] + 8[hashql-core] + class 8 root + 9[hashql-diagnostics] + 10[hashql-eval] + 11[hashql-hir] + 12[hashql-macros] + 13[hashql-mir] + 14[hashql-syntax-jexpr] + 15[darwin-kperf] + 16[darwin-kperf-criterion] + 17[darwin-kperf-events] + 18[darwin-kperf-sys] + 19[error-stack] + 20[hash-graph-benches] 0 --> 3 - 3 -.-> 2 - 3 --> 2 - 3 --> 17 - 4 -.-> 5 - 5 --> 8 - 5 --> 12 - 6 --> 0 - 6 --> 7 - 6 --> 10 - 6 -.-> 14 - 8 --> 11 - 9 -.-> 5 - 11 --> 9 - 12 --> 4 - 12 --> 6 - 13 --> 15 - 13 --> 16 - 14 --> 13 + 1 -.-> 2 + 1 --> 5 + 3 --> 10 + 3 --> 14 + 5 -.-> 4 + 5 --> 4 + 5 --> 19 + 6 -.-> 7 + 7 --> 10 + 7 --> 14 + 8 --> 1 + 8 --> 9 + 8 --> 12 + 8 -.-> 16 + 10 --> 13 + 11 -.-> 7 + 13 --> 11 + 14 --> 6 + 14 --> 8 + 15 --> 17 + 15 --> 18 + 16 --> 15 + 20 -.-> 3 diff --git a/libs/@local/hashql/diagnostics/docs/dependency-diagram.mmd b/libs/@local/hashql/diagnostics/docs/dependency-diagram.mmd index 5f20bd758d0..c586b711b54 100644 --- a/libs/@local/hashql/diagnostics/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/diagnostics/docs/dependency-diagram.mmd @@ -8,21 +8,28 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[hashql-ast] - 1[hashql-compiletest] - 2[hashql-core] - 3[hashql-diagnostics] - class 3 root - 4[hashql-eval] - 5[hashql-hir] - 6[hashql-mir] - 7[hashql-syntax-jexpr] - 0 -.-> 1 - 1 --> 4 - 1 --> 7 - 2 --> 3 - 4 --> 6 - 5 -.-> 1 - 6 --> 5 - 7 --> 0 - 7 --> 2 + 0[hash-graph] + 1[hash-graph-api] + 2[hashql-ast] + 3[hashql-compiletest] + 4[hashql-core] + 5[hashql-diagnostics] + class 5 root + 6[hashql-eval] + 7[hashql-hir] + 8[hashql-mir] + 9[hashql-syntax-jexpr] + 10[hash-graph-benches] + 0 --> 1 + 1 --> 6 + 1 --> 9 + 2 -.-> 3 + 3 --> 6 + 3 --> 9 + 4 --> 5 + 6 --> 8 + 7 -.-> 3 + 8 --> 7 + 9 --> 2 + 9 --> 4 + 10 -.-> 1 diff --git a/libs/@local/hashql/eval/docs/dependency-diagram.mmd b/libs/@local/hashql/eval/docs/dependency-diagram.mmd index 1788e09b973..09fbd7269eb 100644 --- a/libs/@local/hashql/eval/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/eval/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - 14[hashql-compiletest] - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - class 17 root - 18[hashql-hir] - 19[hashql-macros] - 20[hashql-mir] - 21[hashql-syntax-jexpr] - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + 16[hashql-compiletest] + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + class 19 root + 20[hashql-hir] + 21[hashql-macros] + 22[hashql-mir] + 23[hashql-syntax-jexpr] + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/hashql/hir/docs/dependency-diagram.mmd b/libs/@local/hashql/hir/docs/dependency-diagram.mmd index 1ec3169585e..979a0dc9dc4 100644 --- a/libs/@local/hashql/hir/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/hir/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - 14[hashql-compiletest] - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - 18[hashql-hir] - class 18 root - 19[hashql-macros] - 20[hashql-mir] - 21[hashql-syntax-jexpr] - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + 16[hashql-compiletest] + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + 20[hashql-hir] + class 20 root + 21[hashql-macros] + 22[hashql-mir] + 23[hashql-syntax-jexpr] + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/hashql/mir/docs/dependency-diagram.mmd b/libs/@local/hashql/mir/docs/dependency-diagram.mmd index 69ae470425b..58b887e6182 100644 --- a/libs/@local/hashql/mir/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/mir/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - 14[hashql-compiletest] - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - 18[hashql-hir] - 19[hashql-macros] - 20[hashql-mir] - class 20 root - 21[hashql-syntax-jexpr] - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + 16[hashql-compiletest] + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + 20[hashql-hir] + 21[hashql-macros] + 22[hashql-mir] + class 22 root + 23[hashql-syntax-jexpr] + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/hashql/syntax-jexpr/docs/dependency-diagram.mmd b/libs/@local/hashql/syntax-jexpr/docs/dependency-diagram.mmd index cb3792e5d65..d4de1967175 100644 --- a/libs/@local/hashql/syntax-jexpr/docs/dependency-diagram.mmd +++ b/libs/@local/hashql/syntax-jexpr/docs/dependency-diagram.mmd @@ -8,73 +8,80 @@ graph TD %% --> : Normal dependency %% -.-> : Dev dependency %% ---> : Build dependency - 0[type-system] - 1[hash-codec] - 2[hash-codegen] - 3[hash-graph-authorization] - 4[hash-graph-migrations] - 5[hash-graph-migrations-macros] - 6[hash-graph-postgres-store] - 7[hash-graph-store] - 8[hash-graph-temporal-versioning] - 9[hash-graph-types] - 10[hash-graph-validation] - 11[harpc-types] - 12[harpc-wire-protocol] - 13[hashql-ast] - 14[hashql-compiletest] - 15[hashql-core] - 16[hashql-diagnostics] - 17[hashql-eval] - 18[hashql-hir] - 19[hashql-macros] - 20[hashql-mir] - 21[hashql-syntax-jexpr] - class 21 root - 22[hash-status] - 23[hash-telemetry] - 24[hash-temporal-client] - 25[darwin-kperf] - 26[darwin-kperf-criterion] - 27[darwin-kperf-events] - 28[darwin-kperf-sys] - 29[error-stack] - 30[hash-graph-test-data] - 0 --> 8 - 0 -.-> 30 - 1 -.-> 2 - 1 --> 12 - 3 --> 0 - 4 --> 5 + 0[hash-graph] + 1[type-system] + 2[hash-codec] + 3[hash-codegen] + 4[hash-graph-api] + 5[hash-graph-authorization] + 6[hash-graph-migrations] + 7[hash-graph-migrations-macros] + 8[hash-graph-postgres-store] + 9[hash-graph-store] + 10[hash-graph-temporal-versioning] + 11[hash-graph-types] + 12[hash-graph-validation] + 13[harpc-types] + 14[harpc-wire-protocol] + 15[hashql-ast] + 16[hashql-compiletest] + 17[hashql-core] + 18[hashql-diagnostics] + 19[hashql-eval] + 20[hashql-hir] + 21[hashql-macros] + 22[hashql-mir] + 23[hashql-syntax-jexpr] + class 23 root + 24[hash-status] + 25[hash-telemetry] + 26[hash-temporal-client] + 27[darwin-kperf] + 28[darwin-kperf-criterion] + 29[darwin-kperf-events] + 30[darwin-kperf-sys] + 31[error-stack] + 32[hash-graph-benches] + 33[hash-graph-test-data] + 0 --> 4 + 1 --> 10 + 1 -.-> 33 + 2 -.-> 3 + 2 --> 14 + 4 --> 19 4 --> 23 - 6 -.-> 4 - 6 --> 10 - 6 --> 22 - 7 --> 3 - 7 --> 9 - 7 --> 24 - 8 --> 1 - 9 -.-> 30 - 10 -.-> 30 - 12 -.-> 11 - 12 --> 11 - 12 --> 29 - 13 -.-> 14 - 14 --> 17 - 14 --> 21 - 15 --> 1 - 15 --> 16 - 15 --> 19 - 15 -.-> 26 - 17 --> 6 - 17 --> 20 - 18 -.-> 14 - 20 --> 18 - 21 --> 13 - 21 --> 15 - 23 --> 29 - 24 --> 0 - 25 --> 27 - 25 --> 28 - 26 --> 25 - 30 --> 7 + 5 --> 1 + 6 --> 7 + 6 --> 25 + 8 -.-> 6 + 8 --> 12 + 8 --> 24 + 9 --> 5 + 9 --> 11 + 9 --> 26 + 10 --> 2 + 11 -.-> 33 + 12 -.-> 33 + 14 -.-> 13 + 14 --> 13 + 14 --> 31 + 15 -.-> 16 + 16 --> 19 + 16 --> 23 + 17 --> 2 + 17 --> 18 + 17 --> 21 + 17 -.-> 28 + 19 --> 8 + 19 --> 22 + 20 -.-> 16 + 22 --> 20 + 23 --> 15 + 23 --> 17 + 25 --> 31 + 26 --> 1 + 27 --> 29 + 27 --> 30 + 28 --> 27 + 32 -.-> 4 + 33 --> 9 diff --git a/libs/@local/status/rust/docs/dependency-diagram.mmd b/libs/@local/status/rust/docs/dependency-diagram.mmd index 123cea737eb..a9df8779a27 100644 --- a/libs/@local/status/rust/docs/dependency-diagram.mmd +++ b/libs/@local/status/rust/docs/dependency-diagram.mmd @@ -23,8 +23,9 @@ graph TD 11[hash-graph-benches] 12[hash-graph-integration] 0 --> 1 - 1 --> 2 1 --> 3 + 1 --> 6 + 1 --> 9 2 --> 10 3 --> 10 4 -.-> 5 diff --git a/libs/@local/telemetry/docs/dependency-diagram.mmd b/libs/@local/telemetry/docs/dependency-diagram.mmd index 7cf2e93873c..42bf020869c 100644 --- a/libs/@local/telemetry/docs/dependency-diagram.mmd +++ b/libs/@local/telemetry/docs/dependency-diagram.mmd @@ -25,7 +25,8 @@ graph TD 13[hash-graph-benches] 14[hash-graph-integration] 0 --> 1 - 1 --> 3 + 1 --> 6 + 1 --> 9 2 --> 11 3 -.-> 2 4 -.-> 5 diff --git a/libs/@local/temporal-client/docs/dependency-diagram.mmd b/libs/@local/temporal-client/docs/dependency-diagram.mmd index 0733723aa30..2550d3da4f3 100644 --- a/libs/@local/temporal-client/docs/dependency-diagram.mmd +++ b/libs/@local/temporal-client/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 diff --git a/tests/graph/test-data/rust/docs/dependency-diagram.mmd b/tests/graph/test-data/rust/docs/dependency-diagram.mmd index d927eb64c10..2ac8e9e7481 100644 --- a/tests/graph/test-data/rust/docs/dependency-diagram.mmd +++ b/tests/graph/test-data/rust/docs/dependency-diagram.mmd @@ -40,9 +40,10 @@ graph TD 1 -.-> 25 2 -.-> 3 2 --> 14 - 4 --> 6 4 --> 9 4 --> 12 + 4 --> 17 + 4 --> 20 5 --> 1 6 --> 11 7 --> 5 From 5074076c5ba994cd9f7acf561d07cea896f827d7 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:26:00 +0200 Subject: [PATCH 13/14] fix: serialization --- libs/@local/graph/api/src/rest/hashql/mod.rs | 2 ++ .../@local/graph/api/src/rest/hashql/value.rs | 32 +++++++++++++++---- libs/@local/hashql/core/src/heap/pool.rs | 2 ++ .../hashql/mir/src/interpret/value/num.rs | 19 +++++++++++ 4 files changed, 49 insertions(+), 6 deletions(-) diff --git a/libs/@local/graph/api/src/rest/hashql/mod.rs b/libs/@local/graph/api/src/rest/hashql/mod.rs index 62912a8e832..2a0bcc38459 100644 --- a/libs/@local/graph/api/src/rest/hashql/mod.rs +++ b/libs/@local/graph/api/src/rest/hashql/mod.rs @@ -103,6 +103,7 @@ async fn query_local_impl( value: compilation, advisories, } = Compilation::compile(&heap, &mut scratch, spans, query)?; + drop(scratch); let context = compilation.context(); @@ -201,6 +202,7 @@ where /// Request body for the `/hashql` endpoint. #[derive(serde::Deserialize, utoipa::ToSchema)] +#[serde(deny_unknown_fields)] pub(crate) struct HashQlRequest { #[schema(value_type = serde_json::Value)] query: Arc, diff --git a/libs/@local/graph/api/src/rest/hashql/value.rs b/libs/@local/graph/api/src/rest/hashql/value.rs index 9e06baa1889..c63f662f445 100644 --- a/libs/@local/graph/api/src/rest/hashql/value.rs +++ b/libs/@local/graph/api/src/rest/hashql/value.rs @@ -1,5 +1,5 @@ use alloc::{collections::BTreeMap, sync::Arc}; -use core::alloc::Allocator; +use core::{alloc::Allocator, num::FpCategory}; use hashql_core::id::Id as _; use hashql_mir::interpret::value::{Int, Num, Ptr, Value}; @@ -11,7 +11,15 @@ fn serialize_int(int: &Int, serializer: S) -> Result(num: &Num, serializer: S) -> Result { - num.as_f64().serialize(serializer) + let value = num.as_f64(); + match value.classify() { + FpCategory::Infinite if value.is_sign_negative() => "-inf".serialize(serializer), + FpCategory::Infinite => "inf".serialize(serializer), + FpCategory::Nan => "nan".serialize(serializer), + FpCategory::Zero | FpCategory::Subnormal | FpCategory::Normal => { + value.serialize(serializer) + } + } } #[expect(clippy::trivially_copy_pass_by_ref)] @@ -97,6 +105,21 @@ impl<'heap, A: Allocator + Clone> From> for OwnedValue { } } +fn is_string(value: &OwnedValue) -> bool { + match value { + OwnedValue::String(_) => true, + OwnedValue::Opaque(_, opaque) => is_string(opaque), + OwnedValue::Unit + | OwnedValue::Integer(_) + | OwnedValue::Number(_) + | OwnedValue::Pointer(_) + | OwnedValue::Struct(_) + | OwnedValue::Tuple(_) + | OwnedValue::List(_) + | OwnedValue::Dict(_) => false, + } +} + #[derive(Copy, Clone)] pub(crate) struct JsonValueSerialize<'value>(pub &'value OwnedValue); @@ -128,10 +151,7 @@ impl serde::Serialize for JsonValueSerialize<'_> { // If all the keys are strings we can collect a map, otherwise we must fallback // to collecting as a sequence - if btree_map - .keys() - .all(|key| matches!(key, OwnedValue::String(_))) - { + if btree_map.keys().all(is_string) { serializer.collect_map(iter) } else { serializer.collect_seq(iter) diff --git a/libs/@local/hashql/core/src/heap/pool.rs b/libs/@local/hashql/core/src/heap/pool.rs index 3b441a8999d..b6b69f8802b 100644 --- a/libs/@local/hashql/core/src/heap/pool.rs +++ b/libs/@local/hashql/core/src/heap/pool.rs @@ -80,6 +80,7 @@ impl Default for ScratchPool { /// /// Derefs to [`Scratch`], so it can be used anywhere a `&Scratch` is expected. /// On drop, the allocator is reset and returned to the pool for reuse. +#[clippy::has_significant_drop] pub struct ScratchPoolGuard<'pool> { pool: &'pool ScratchPool, scratch: ManuallyDrop, @@ -181,6 +182,7 @@ impl Default for HeapPool { /// /// Derefs to [`Heap`], so it can be used anywhere a `&Heap` is expected. /// On drop, the heap is reset and returned to the pool for reuse. +#[clippy::has_significant_drop] pub struct HeapPoolGuard<'pool> { pool: &'pool HeapPool, heap: ManuallyDrop, diff --git a/libs/@local/hashql/mir/src/interpret/value/num.rs b/libs/@local/hashql/mir/src/interpret/value/num.rs index f30fcc22b13..29afacf13f3 100644 --- a/libs/@local/hashql/mir/src/interpret/value/num.rs +++ b/libs/@local/hashql/mir/src/interpret/value/num.rs @@ -50,6 +50,25 @@ impl Num { self.value } + /// Returns `true` if this value is finite (i.e., not NaN or infinity). + /// + /// # Examples + /// + /// ``` + /// use hashql_mir::interpret::value::Num; + /// + /// let n = Num::from(2.5); + /// assert!(n.is_finite()); + /// + /// let n = Num::from(f64::NAN); + /// assert!(!n.is_finite()); + /// ``` + #[inline] + #[must_use] + pub const fn is_finite(self) -> bool { + self.value.is_finite() + } + /// Attempts to convert this value to [`i128`] by truncation toward zero. /// /// Returns [`None`] for values outside the representable range, including From 2a72e577e1ec53e27202d87874932d67bb14ad55 Mon Sep 17 00:00:00 2001 From: Bilal Mahmoud <7252775+indietyp@users.noreply.github.com> Date: Mon, 6 Jul 2026 11:42:57 +0200 Subject: [PATCH 14/14] fix: openapi --- libs/@local/graph/api/openapi/openapi.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/libs/@local/graph/api/openapi/openapi.json b/libs/@local/graph/api/openapi/openapi.json index c7a27649d87..459205307da 100644 --- a/libs/@local/graph/api/openapi/openapi.json +++ b/libs/@local/graph/api/openapi/openapi.json @@ -5588,7 +5588,8 @@ "description": "Input values for the query. Must be an empty list until input support ships." }, "query": {} - } + }, + "additionalProperties": false }, "IncludeEntityTypeOption": { "type": "string",