From f19457f532c6e706d52eb49a9f69246663b2a84b Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 5 Aug 2026 10:17:03 +0200 Subject: [PATCH 01/19] feat(store): add schema-based event loading --- .github/dependabot.yml | 1 + .github/workflows/rust.yml | 1 + Cargo.lock | 26 + Cargo.toml | 4 + crates/instrumentation-build/src/lib.rs | 19 +- crates/store-build/Cargo.toml | 16 + crates/store-build/example/Cargo.lock | 956 ++++++++++++++++++++++++ crates/store-build/example/Cargo.toml | 18 + crates/store-build/example/build.rs | 33 + crates/store-build/example/src/main.rs | 45 ++ crates/store-build/src/lib.rs | 184 +++++ crates/store/Cargo.toml | 17 + crates/store/src/filesystem/mod.rs | 408 ++++++++++ crates/store/src/lib.rs | 74 ++ 14 files changed, 1801 insertions(+), 1 deletion(-) create mode 100644 crates/store-build/Cargo.toml create mode 100644 crates/store-build/example/Cargo.lock create mode 100644 crates/store-build/example/Cargo.toml create mode 100644 crates/store-build/example/build.rs create mode 100644 crates/store-build/example/src/main.rs create mode 100644 crates/store-build/src/lib.rs create mode 100644 crates/store/Cargo.toml create mode 100644 crates/store/src/filesystem/mod.rs create mode 100644 crates/store/src/lib.rs diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 5dccfe1a3..e72e8b3dd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -22,6 +22,7 @@ updates: directories: - "/" - "/crates/instrumentation-build/example" + - "/crates/store-build/example" open-pull-requests-limit: 5 package-ecosystem: "cargo" schedule: diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 7711d2527..bd9f54a3c 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -63,6 +63,7 @@ jobs: # covered. - run: pixi run cargo clippy --manifest-path crates/instrumentation-build/example/Cargo.toml --all-targets --locked -- -D warnings - run: pixi run cargo run --manifest-path crates/instrumentation-build/example/Cargo.toml --locked + - run: pixi run cargo clippy --manifest-path crates/store-build/example/Cargo.toml --all-targets --locked -- -D warnings # Regression gate for `quent-open` backward compatibility: build a viewer for # a sidecar pinning the previous quent commit (the PR's base commit, or the diff --git a/Cargo.lock b/Cargo.lock index c763bbd0d..cb5840542 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3170,6 +3170,32 @@ dependencies = [ "uuid", ] +[[package]] +name = "quent-store" +version = "0.1.0" +dependencies = [ + "quent-build-info", + "quent-events", + "quent-io", + "serde", + "serde_json", + "tempfile", + "thiserror", + "uuid", +] + +[[package]] +name = "quent-store-build" +version = "0.1.0" +dependencies = [ + "prettyplease 0.3.0", + "quent-instrumentation-build", + "quent-schema", + "quote", + "syn 3.0.3", + "thiserror", +] + [[package]] name = "quent-time" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 43ba55763..142803c60 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,8 @@ members = [ "crates/model-macros", "crates/open", "crates/stdlib", + "crates/store", + "crates/store-build", "crates/time", "crates/ui", # NVTX integration crates @@ -98,6 +100,8 @@ default-members = [ "crates/model-macros", "crates/open", "crates/stdlib", + "crates/store", + "crates/store-build", "crates/time", "crates/ui", "domains/query_engine/analyzer", diff --git a/crates/instrumentation-build/src/lib.rs b/crates/instrumentation-build/src/lib.rs index 15e7254db..ef18074a7 100644 --- a/crates/instrumentation-build/src/lib.rs +++ b/crates/instrumentation-build/src/lib.rs @@ -54,8 +54,9 @@ mod runtime; use std::path::PathBuf; +use convert_case::Case; use quent_constraints::{BaseConstraintsError, Report, validate}; -use quent_schema::{Path, Schema}; +use quent_schema::{Entity, Path, Schema}; use quote::quote; /// Options controlling event and instrumentation source generation. @@ -166,6 +167,22 @@ pub struct GenerateInfo { pub warnings: Vec, } +/// Returns the model path generated for `schema` relative to the generated module root. +pub fn generated_model_path(schema: &Schema) -> proc_macro2::TokenStream { + let model = common::raw_ident(common::to_case(schema.name(), Case::Pascal)); + quote! { #model } +} + +/// Returns the entity marker path generated relative to the generated module root. +pub fn generated_entity_path(entity: &Entity) -> proc_macro2::TokenStream { + common::relative_type_path(entity.path(), &[], "") +} + +/// Returns the entity event path generated relative to the generated module root. +pub fn generated_entity_event_path(entity: &Entity) -> proc_macro2::TokenStream { + common::relative_type_path(entity.path(), &[], "Event") +} + /// Generate event source and, when enabled, instrumentation source for `schema`. pub fn generate(schema: &Schema, opts: &Options) -> Result { let Report { diff --git a/crates/store-build/Cargo.toml b/crates/store-build/Cargo.toml new file mode 100644 index 000000000..5d086cf56 --- /dev/null +++ b/crates/store-build/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "quent-store-build" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +prettyplease.workspace = true +quent-instrumentation-build = { path = "../instrumentation-build" } +quent-schema = { path = "../schema" } +quote = "1" +syn.workspace = true +thiserror.workspace = true + +[dev-dependencies] +quent-schema = { path = "../schema", features = ["test-utils"] } diff --git a/crates/store-build/example/Cargo.lock b/crates/store-build/example/Cargo.lock new file mode 100644 index 000000000..8be29da63 --- /dev/null +++ b/crates/store-build/example/Cargo.lock @@ -0,0 +1,956 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.4", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "annotate-snippets" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" +dependencies = [ + "anstyle", + "memchr", + "unicode-width", +] + +[[package]] +name = "anstyle" +version = "1.0.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "async-trait" +version = "0.1.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "bytes" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror", +] + +[[package]] +name = "convert_case" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "encoding_rs_io" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" +dependencies = [ + "encoding_rs", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + +[[package]] +name = "foldhash" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" + +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + +[[package]] +name = "getrandom" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" +dependencies = [ + "cfg-if", + "libc", + "r-efi 6.0.0", +] + +[[package]] +name = "granit-parser" +version = "0.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" +dependencies = [ + "arraydeque", + "smallvec", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" +dependencies = [ + "foldhash", +] + +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown 0.17.1", + "serde", + "serde_core", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.189" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" + +[[package]] +name = "log" +version = "0.4.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" + +[[package]] +name = "memchr" +version = "2.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "petgraph" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" +dependencies = [ + "fixedbitset", + "hashbrown 0.15.5", + "indexmap", + "serde", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + +[[package]] +name = "prettyplease" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" +dependencies = [ + "proc-macro2", + "syn 3.0.3", +] + +[[package]] +name = "proc-macro2" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quent-build-info" +version = "0.1.0" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "quent-constraints" +version = "0.1.0" +dependencies = [ + "petgraph", + "quent-schema", + "rustc-hash", +] + +[[package]] +name = "quent-dynamic-attributes" +version = "0.1.0" +dependencies = [ + "serde", + "thiserror", +] + +[[package]] +name = "quent-events" +version = "0.1.0" +dependencies = [ + "quent-build-info", + "quent-dynamic-attributes", + "quent-time", + "serde", + "uuid", +] + +[[package]] +name = "quent-fsm" +version = "0.1.0" +dependencies = [ + "petgraph", + "quent-constraints", + "quent-schema", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "quent-instrumentation-build" +version = "0.1.0" +dependencies = [ + "convert_case", + "prettyplease", + "proc-macro2", + "quent-constraints", + "quent-ref-target", + "quent-schema", + "quote", + "syn 3.0.3", + "thiserror", +] + +[[package]] +name = "quent-io" +version = "0.1.0" +dependencies = [ + "async-trait", + "quent-events", + "quent-io-msgpack", + "quent-io-ndjson", + "quent-io-postcard", + "quent-io-types", + "serde", + "uuid", +] + +[[package]] +name = "quent-io-msgpack" +version = "0.1.0" +dependencies = [ + "async-trait", + "quent-events", + "quent-io-types", + "rmp-serde", + "serde", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "quent-io-ndjson" +version = "0.1.0" +dependencies = [ + "async-trait", + "quent-events", + "quent-io-types", + "serde", + "serde_json", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "quent-io-postcard" +version = "0.1.0" +dependencies = [ + "async-trait", + "postcard", + "quent-events", + "quent-io-types", + "serde", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "quent-io-types" +version = "0.1.0" +dependencies = [ + "async-trait", + "quent-events", + "thiserror", + "tracing", + "uuid", +] + +[[package]] +name = "quent-ref-target" +version = "0.1.0" +dependencies = [ + "quent-constraints", + "quent-schema", + "thiserror", +] + +[[package]] +name = "quent-ref-tree" +version = "0.1.0" +dependencies = [ + "petgraph", + "quent-constraints", + "quent-ref-target", + "quent-schema", + "rustc-hash", + "thiserror", +] + +[[package]] +name = "quent-resource" +version = "0.1.0" +dependencies = [ + "indexmap", + "quent-constraints", + "quent-fsm", + "quent-schema", + "rustc-hash", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "quent-schema" +version = "0.1.0" +dependencies = [ + "indexmap", + "rustc-hash", + "serde", + "smallvec", + "thiserror", +] + +[[package]] +name = "quent-store" +version = "0.1.0" +dependencies = [ + "quent-build-info", + "quent-events", + "quent-io", + "serde", + "thiserror", + "uuid", +] + +[[package]] +name = "quent-store-build" +version = "0.1.0" +dependencies = [ + "prettyplease", + "quent-instrumentation-build", + "quent-schema", + "quote", + "syn 3.0.3", + "thiserror", +] + +[[package]] +name = "quent-store-build-example" +version = "0.1.0" +dependencies = [ + "quent-events", + "quent-store", + "quent-store-build", + "quent-yaml", + "serde", +] + +[[package]] +name = "quent-time" +version = "0.1.0" +dependencies = [ + "thiserror", +] + +[[package]] +name = "quent-yaml" +version = "0.1.0" +dependencies = [ + "indexmap", + "quent-constraints", + "quent-fsm", + "quent-ref-target", + "quent-ref-tree", + "quent-resource", + "quent-schema", + "serde", + "serde-saphyr", + "thiserror", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "quote" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "r-efi" +version = "6.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" + +[[package]] +name = "rmp" +version = "0.8.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" +dependencies = [ + "num-traits", +] + +[[package]] +name = "rmp-serde" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" +dependencies = [ + "rmp", + "serde", +] + +[[package]] +name = "rustc-hash" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" + +[[package]] +name = "rustversion" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" + +[[package]] +name = "serde" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-saphyr" +version = "0.0.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" +dependencies = [ + "ahash", + "annotate-snippets", + "base64", + "encoding_rs_io", + "getrandom 0.3.4", + "granit-parser", + "nohash-hasher", + "num-traits", + "serde_core", + "smallvec", + "zmij", +] + +[[package]] +name = "serde_core" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.229" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "serde_json" +version = "1.0.151" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] + +[[package]] +name = "syn" +version = "2.0.119" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "thiserror" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 3.0.3", +] + +[[package]] +name = "thread_local" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "tokio" +version = "1.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" +dependencies = [ + "bytes", + "pin-project-lite", +] + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" +dependencies = [ + "nu-ansi-term", + "sharded-slab", + "smallvec", + "thread_local", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + +[[package]] +name = "uuid" +version = "1.24.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" +dependencies = [ + "getrandom 0.4.3", + "js-sys", + "serde_core", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn 2.0.119", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" + +[[package]] +name = "zerocopy" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.55" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.119", +] + +[[package]] +name = "zmij" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/store-build/example/Cargo.toml b/crates/store-build/example/Cargo.toml new file mode 100644 index 000000000..7e2ca6933 --- /dev/null +++ b/crates/store-build/example/Cargo.toml @@ -0,0 +1,18 @@ +# Keep this example independent so it exercises the dependencies an external +# analysis crate must declare. +[workspace] + +[package] +name = "quent-store-build-example" +version = "0.1.0" +edition = "2024" +publish = false + +[dependencies] +quent-events = { path = "../../events", features = ["serde"] } +quent-store = { path = "../../store" } +serde = { version = "1", features = ["derive"] } + +[build-dependencies] +quent-store-build = { path = ".." } +quent-yaml = { path = "../../yaml" } diff --git a/crates/store-build/example/build.rs b/crates/store-build/example/build.rs new file mode 100644 index 000000000..0c6e9e68c --- /dev/null +++ b/crates/store-build/example/build.rs @@ -0,0 +1,33 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generates store types from the same schema as the instrumentation example. + +use std::path::Path; + +use quent_store_build::{Options, generate}; + +fn main() -> Result<(), Box> { + let model = Path::new(env!("CARGO_MANIFEST_DIR")) + .join("../../instrumentation-build/example/model.yaml"); + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed={}", model.display()); + + let parsed = quent_yaml::parse_from_file(&model)?; + for warning in &parsed.warnings { + println!("cargo:warning={warning}"); + } + + let options = Options { + // Generate `DemoEvent` so the example can load all model events through + // one iterator. Entity-specific loading does not require this option. + umbrella_event: true, + ..Options::default() + }; + let generated = generate(&parsed.schema, &options)?; + println!( + "cargo:warning=store model written to {}", + generated.path.display() + ); + Ok(()) +} diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs new file mode 100644 index 000000000..78df3a2a9 --- /dev/null +++ b/crates/store-build/example/src/main.rs @@ -0,0 +1,45 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Loads one recorded context produced by the instrumentation-build example. + +use std::io::{Error, ErrorKind}; +use std::path::PathBuf; + +use demo::{Demo, Query, Uuid}; +use quent_store::filesystem::Store; +use quent_store::{EntityEventStore, ModelEventStore}; + +#[allow(unused)] +mod demo { + include!(concat!(env!("OUT_DIR"), "/demo.rs")); +} + +fn main() -> Result<(), Box> { + let mut args = std::env::args_os().skip(1); + let root = PathBuf::from(args.next().ok_or_else(|| { + Error::new( + ErrorKind::InvalidInput, + "usage: quent-store-build-example ", + ) + })?); + let context_id = args + .next() + .and_then(|value| value.into_string().ok()) + .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "missing context ID"))? + .parse::()?; + + let store = Store::::new(root); + + // Load events for one entity type. + for event in store.entity_events::(context_id)? { + println!("{event:?}"); + } + + // Load all model events as `DemoEvent`. + for event in store.events(context_id)? { + println!("{event:?}"); + } + + Ok(()) +} diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs new file mode 100644 index 000000000..fbcfd74ba --- /dev/null +++ b/crates/store-build/src/lib.rs @@ -0,0 +1,184 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Generates event-store models from schemas. + +use std::path::PathBuf; + +use quent_schema::Schema; +use quote::quote; + +/// Options controlling event-store source generation. +pub struct Options { + /// Derive [`Debug`](std::fmt::Debug) on generated event and record types. + pub debug: bool, + + /// Derives applied to every generated event payload enum. + pub event_derives: &'static [&'static str], + + /// Derives applied to every generated record struct. + pub record_derives: &'static [&'static str], + + /// Generate a model-wide umbrella event and model-wide loading support. + pub umbrella_event: bool, + + /// Directory the generated file is written into. + pub out_dir: PathBuf, + + /// File name to write; defaults to the lowercase schema name with a `.rs` extension. + pub file_name: Option, +} + +impl Default for Options { + fn default() -> Self { + Self { + debug: true, + event_derives: Default::default(), + record_derives: Default::default(), + umbrella_event: false, + out_dir: PathBuf::from(std::env::var("OUT_DIR").unwrap_or_default()), + file_name: None, + } + } +} + +/// An error from generating event-store source. +#[derive(Debug, thiserror::Error)] +pub enum GenerateError { + #[error(transparent)] + EventModel(#[from] quent_instrumentation_build::GenerateError), + #[error("generated event-store code did not form a valid Rust file")] + InvalidGeneratedCode(#[source] syn::Error), + #[error("failed to write generated event-store source")] + Io(#[from] std::io::Error), +} + +/// Information about generated event-store source. +pub struct GenerateInfo { + /// Path of the generated Rust source file. + pub path: PathBuf, +} + +/// Generates an event model and its event-store descriptors. +/// +/// # Errors +/// +/// Returns an error when the schema cannot be generated or the output cannot be written. +pub fn generate(schema: &Schema, opts: &Options) -> Result { + let file_name = opts + .file_name + .clone() + .unwrap_or_else(|| format!("{}.rs", schema.name().to_string().to_lowercase())); + let path = opts.out_dir.join(file_name); + std::fs::write(&path, generate_str(schema, opts)?)?; + Ok(GenerateInfo { path }) +} + +/// Returns event-store model source for `schema`. +/// +/// # Errors +/// +/// Returns an error when event generation fails or the combined output is not valid Rust. +pub fn generate_str(schema: &Schema, opts: &Options) -> Result { + let event_opts = quent_instrumentation_build::Options { + instrumentation: false, + debug: opts.debug, + serde: true, + event_derives: opts.event_derives, + record_derives: opts.record_derives, + umbrella_event: opts.umbrella_event, + ..quent_instrumentation_build::Options::default() + }; + let events = quent_instrumentation_build::generate_str(schema, &event_opts)?; + let events = + syn::parse_str::(&events).map_err(GenerateError::InvalidGeneratedCode)?; + + let model = quent_instrumentation_build::generated_model_path(schema); + let stored_model = if opts.umbrella_event { + let streams = schema.entities().map(|entity| { + let event = quent_instrumentation_build::generated_entity_event_path(entity); + quote! { + ::quent_store::filesystem::EventStream::new( + <#event as ::quent_events::EntityEvent>::NAME, + ::quent_store::filesystem::import_event_files::<#model, #event>, + ) + } + }); + quote! { + impl ::quent_store::filesystem::Model for #model { + fn event_streams() -> &'static [::quent_store::filesystem::EventStream] { + static STREAMS: &[::quent_store::filesystem::EventStream<#model>] = &[ + #(#streams,)* + ]; + STREAMS + } + } + } + } else { + quote! {} + }; + let entities = schema.entities().map(|entity| { + let marker = quent_instrumentation_build::generated_entity_path(entity); + quote! { + impl ::quent_store::StoredEntity<#model> for #marker {} + } + }); + + let file = syn::parse2::(quote! { + #events + + #stored_model + + #(#entities)* + }) + .map_err(GenerateError::InvalidGeneratedCode)?; + + Ok(prettyplease::unparse(&file)) +} + +#[cfg(test)] +mod tests { + use quent_schema::builder::SchemaBuilder; + use quent_schema::test_utils::{entity, event}; + + use super::*; + + #[test] + fn generates_store_model_and_nested_entity_membership() { + let schema = SchemaBuilder::try_new("Demo") + .unwrap() + .with_entity(entity("Foo::Query", [event("created", [])])) + .with_entity(entity("Foo::Nested::Task", [event("created", [])])) + .build() + .unwrap(); + + let opts = Options { + umbrella_event: true, + ..Options::default() + }; + let source = generate_str(&schema, &opts).unwrap(); + + assert!(source.contains("impl ::quent_store::filesystem::Model for Demo")); + assert_eq!(source.matches("import_event_files::<").count(), 2); + assert!(source.contains("foo::QueryEvent")); + assert!(source.contains("foo::nested::TaskEvent")); + assert!(source.contains("StoredEntity for foo::Query")); + assert!(source.contains("StoredEntity for foo::nested::Task")); + assert!(!source.contains("quent_instrumentation")); + } + + #[test] + fn generates_entity_loading_without_an_umbrella_by_default() { + let schema = SchemaBuilder::try_new("Demo") + .unwrap() + .with_entity(entity("Query", [event("created", [])])) + .build() + .unwrap(); + + let source = generate_str(&schema, &Options::default()).unwrap(); + + assert!(source.contains("StoredEntity for Query")); + assert!(!source.contains("filesystem::Model for Demo")); + assert!(!source.contains("pub enum DemoEvent")); + } +} diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml new file mode 100644 index 000000000..eefea61ed --- /dev/null +++ b/crates/store/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "quent-store" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +quent-build-info = { path = "../build-info" } +quent-events = { path = "../events" } +quent-io = { path = "../io", default-features = false, features = ["ndjson", "msgpack", "postcard"] } +serde.workspace = true +thiserror.workspace = true +uuid.workspace = true + +[dev-dependencies] +serde_json.workspace = true +tempfile = "3" diff --git a/crates/store/src/filesystem/mod.rs b/crates/store/src/filesystem/mod.rs new file mode 100644 index 000000000..966ae262c --- /dev/null +++ b/crates/store/src/filesystem/mod.rs @@ -0,0 +1,408 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Filesystem-backed event storage. + +use std::marker::PhantomData; +use std::path::{Path, PathBuf}; + +use quent_build_info::ArtifactInfo; +use quent_events::{EntityEvent, Event, Model as EventModel, ModelEvents}; +use quent_io::ImporterProvider; +use quent_io::filesystem::{Format, importer}; +use serde::de::DeserializeOwned; +use uuid::Uuid; + +use crate::EventIterator; +use crate::{EntityEventLoader, EntityEventStore, ModelEventLoader, ModelEventStore, StoredEntity}; + +/// Result returned by filesystem event stores. +pub type Result = std::result::Result; + +/// An error encountered while loading filesystem events. +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("context `{0}` was not found")] + ContextNotFound(Uuid), + #[error("context model `{actual}` does not match expected model `{expected}`")] + ModelMismatch { expected: String, actual: String }, + #[error("context contains an unsupported event format `{0}`")] + UnsupportedFormat(String), + #[error(transparent)] + Io(#[from] std::io::Error), + #[error(transparent)] + Importer(#[from] quent_io::ImporterError), +} + +/// Associates a generated model with its filesystem entity-event streams. +#[doc(hidden)] +pub trait Model: ModelEvents { + /// Returns the streams generated from the model schema. + fn event_streams() -> &'static [EventStream] + where + Self: Sized; +} + +type ImportFn = fn( + Vec, +) + -> Result::UmbrellaEvent>>>>; + +/// Describes one entity-event stream in a generated analysis model. +pub struct EventStream { + entity: &'static str, + import: ImportFn, +} + +impl EventStream { + /// Creates a generated entity-event stream descriptor. + #[doc(hidden)] + pub const fn new(entity: &'static str, import: ImportFn) -> Self { + Self { entity, import } + } +} + +/// Identifies an event file and the importer required to decode it. +#[doc(hidden)] +pub struct EventFile { + format: Format, + path: PathBuf, +} + +/// Imports files containing entity events and converts them to the model umbrella type. +#[doc(hidden)] +pub fn import_event_files( + files: Vec, +) -> Result>>> +where + M: ModelEvents, + E: DeserializeOwned + Into + 'static, + M::UmbrellaEvent: 'static, +{ + let streams = import_files::(files)? + .map(|stream| { + Box::new(stream.map(|event| Event::new(event.id, event.timestamp, event.data.into()))) + as Box>> + }) + .collect::>(); + Ok(Box::new(streams.into_iter().flatten())) +} + +/// Loads model events from filesystem exporter output. +pub struct Store { + root: PathBuf, + model: PhantomData M>, +} + +impl Store { + /// Creates a store rooted at an exporter output directory. + pub fn new(root: impl Into) -> Self { + Self { + root: root.into(), + model: PhantomData, + } + } + + /// Returns the exporter output directory. + pub fn root(&self) -> &Path { + &self.root + } +} + +impl EntityEventStore for Store { + type Error = Error; +} + +impl EntityEventLoader for Store +where + M: EventModel, + E: StoredEntity, + E::Event: DeserializeOwned + 'static, +{ + type Error = Error; + + fn load_entity_events(&self, context_id: Uuid) -> Result> { + let context = self.context(context_id)?; + let streams = import_files::(event_files(&context, E::Event::NAME)?)?; + Ok(Box::new(streams.flatten())) + } +} + +impl ModelEventStore for Store {} + +impl ModelEventLoader for Store +where + M: EventModel + Model + 'static, +{ + type Error = Error; + + fn load_model_events(&self, context_id: Uuid) -> Result> { + let context = self.context(context_id)?; + let mut streams = Vec::new(); + for descriptor in M::event_streams() { + let files = event_files(&context, descriptor.entity)?; + streams.push((descriptor.import)(files)?); + } + Ok(Box::new(streams.into_iter().flatten())) + } +} + +impl Store +where + M: EventModel, +{ + fn context(&self, context_id: Uuid) -> Result { + let context = self.root.join(context_id.to_string()); + if !context.is_dir() { + return Err(Error::ContextNotFound(context_id)); + } + let artifact = ArtifactInfo::read_sidecar(&context)?; + if artifact.model.name != M::NAME { + return Err(Error::ModelMismatch { + expected: M::NAME.to_owned(), + actual: artifact.model.name, + }); + } + Ok(context) + } +} + +fn import_files( + files: Vec, +) -> Result>>>> +where + T: DeserializeOwned + 'static, +{ + files + .into_iter() + .map(|file| { + let importer = importer::Options { + format: file.format, + path: file.path, + } + .create_importer()?; + Ok(Box::new(importer) as Box>>) + }) + .collect::>>() + .map(Vec::into_iter) +} + +fn event_files(context: &Path, entity: &str) -> Result> { + let directory = context.join(entity); + if !directory.is_dir() { + return Ok(Vec::new()); + } + + std::fs::read_dir(directory)? + .filter_map(|entry| match entry { + Ok(entry) => match entry.file_type() { + Ok(file_type) if file_type.is_file() => Some(Ok(entry.path())), + Ok(_) => None, + Err(error) => Some(Err(Error::Io(error))), + }, + Err(error) => Some(Err(Error::Io(error))), + }) + .map(|path| { + let path = path?; + let Some(extension) = path.extension().and_then(|value| value.to_str()) else { + return Err(Error::UnsupportedFormat(String::new())); + }; + let format = Format::try_from(extension) + .map_err(|_| Error::UnsupportedFormat(extension.to_owned()))?; + Ok(EventFile { format, path }) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use std::fs; + + use quent_build_info::{ArtifactInfo, BuildInfo, ModelSource}; + use quent_events::{Entity, EntityEvent, Event, Model as EventModel, ModelEvents}; + use serde::{Deserialize, Serialize}; + + use super::*; + + struct TestModel; + + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct AlphaEvent(u8); + + impl EntityEvent for AlphaEvent { + const NAME: &'static str = "Alpha"; + } + + struct Alpha; + + impl Entity for Alpha { + type Event = AlphaEvent; + } + + impl StoredEntity for Alpha {} + + #[derive(Debug, Deserialize, PartialEq, Serialize)] + struct BetaEvent(u8); + + impl EntityEvent for BetaEvent { + const NAME: &'static str = "Beta"; + } + + #[derive(Debug, PartialEq)] + enum TestEvent { + Alpha(AlphaEvent), + Beta(BetaEvent), + } + + impl From for TestEvent { + fn from(event: AlphaEvent) -> Self { + Self::Alpha(event) + } + } + + impl From for TestEvent { + fn from(event: BetaEvent) -> Self { + Self::Beta(event) + } + } + + impl EventModel for TestModel { + const NAME: &'static str = "Test"; + } + + impl ModelSource for TestModel { + fn package() -> &'static str { + "quent-store" + } + + fn source() -> BuildInfo { + BuildInfo::unknown() + } + } + + impl ModelEvents for TestModel { + type UmbrellaEvent = TestEvent; + } + + impl Model for TestModel { + fn event_streams() -> &'static [EventStream] { + static STREAMS: &[EventStream] = &[ + EventStream::new( + AlphaEvent::NAME, + import_event_files::, + ), + EventStream::new(BetaEvent::NAME, import_event_files::), + ]; + STREAMS + } + } + + fn context(root: &Path, id: Uuid) -> PathBuf { + let path = root.join(id.to_string()); + fs::create_dir_all(&path).unwrap(); + ArtifactInfo::new(TestModel::model_info()) + .write_sidecar(&path) + .unwrap(); + path + } + + fn write_event(path: &Path, event: Event) { + fs::write( + path, + format!("{}\n", serde_json::to_string(&event).unwrap()), + ) + .unwrap(); + } + + #[test] + fn loads_all_model_events_without_relying_on_order() { + let root = tempfile::tempdir().unwrap(); + let id = Uuid::from_u128(2); + let context = context(root.path(), id); + fs::create_dir(context.join(AlphaEvent::NAME)).unwrap(); + fs::create_dir(context.join(BetaEvent::NAME)).unwrap(); + write_event( + &context.join(AlphaEvent::NAME).join("alpha.ndjson"), + Event::new(Uuid::from_u128(11), 11, AlphaEvent(1)), + ); + write_event( + &context.join(BetaEvent::NAME).join("beta.ndjson"), + Event::new(Uuid::from_u128(12), 1, BetaEvent(2)), + ); + + let store = Store::::new(root.path()); + let mut values = store + .events(id) + .unwrap() + .map(|event| match event.data { + TestEvent::Alpha(AlphaEvent(value)) | TestEvent::Beta(BetaEvent(value)) => value, + }) + .collect::>(); + values.sort_unstable(); + + assert_eq!(values, [1, 2]); + } + + #[test] + fn loads_one_entity_type_as_concrete_events() { + let root = tempfile::tempdir().unwrap(); + let id = Uuid::from_u128(2); + let context = context(root.path(), id); + fs::create_dir(context.join(AlphaEvent::NAME)).unwrap(); + fs::create_dir(context.join(BetaEvent::NAME)).unwrap(); + write_event( + &context.join(AlphaEvent::NAME).join("alpha.ndjson"), + Event::new(Uuid::from_u128(11), 11, AlphaEvent(1)), + ); + write_event( + &context.join(BetaEvent::NAME).join("beta.ndjson"), + Event::new(Uuid::from_u128(12), 1, BetaEvent(2)), + ); + + let store = Store::::new(root.path()); + let events = store + .entity_events::(id) + .unwrap() + .collect::>(); + + assert_eq!(events.len(), 1); + assert_eq!(events[0].data, AlphaEvent(1)); + } + + #[test] + fn validates_context_and_supported_formats() { + let root = tempfile::tempdir().unwrap(); + let store = Store::::new(root.path()); + + let missing = Uuid::from_u128(1); + assert!(matches!( + store.events(missing), + Err(Error::ContextNotFound(id)) if id == missing + )); + + let unsupported = Uuid::from_u128(2); + let unsupported_path = context(root.path(), unsupported); + fs::create_dir(unsupported_path.join(AlphaEvent::NAME)).unwrap(); + fs::write( + unsupported_path.join(AlphaEvent::NAME).join("events.csv"), + b"event", + ) + .unwrap(); + assert!(matches!( + store.events(unsupported), + Err(Error::UnsupportedFormat(format)) if format == "csv" + )); + + let mismatch = Uuid::from_u128(3); + let mismatch_path = context(root.path(), mismatch); + let mut info = TestModel::model_info(); + info.name = "Other".to_owned(); + ArtifactInfo::new(info) + .write_sidecar(&mismatch_path) + .unwrap(); + assert!(matches!( + store.events(mismatch), + Err(Error::ModelMismatch { actual, .. }) if actual == "Other" + )); + } +} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs new file mode 100644 index 000000000..6c5d594f8 --- /dev/null +++ b/crates/store/src/lib.rs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Typed access to stored model events. + +use quent_events::{Entity, Event, ModelEvents}; +use uuid::Uuid; + +pub mod filesystem; + +/// A dynamically dispatched iterator over stored events. +pub type EventIterator = Box>>; + +/// Loads events for individual entity types in model `M`. +pub trait EntityEventStore { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads events for entity type `E` without an ordering guarantee. + fn entity_events( + &self, + context_id: Uuid, + ) -> Result, >::Error> + where + E: StoredEntity, + Self: EntityEventLoader>::Error>, + { + self.load_entity_events(context_id) + } +} + +/// Loads model-wide umbrella events. +/// +/// Generated models support this trait only when +/// `quent_store_build::Options::umbrella_event` is enabled. +pub trait ModelEventStore: EntityEventStore { + /// Loads every event stored for `context_id` without an ordering guarantee. + fn events( + &self, + context_id: Uuid, + ) -> Result, >::Error> + where + Self: ModelEventLoader>::Error>, + { + self.load_model_events(context_id) + } +} + +/// Loads one concrete entity event type for an [`EntityEventStore`]. +#[doc(hidden)] +pub trait EntityEventLoader { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads events for `E` without an ordering guarantee. + fn load_entity_events(&self, context_id: Uuid) -> Result, Self::Error>; +} + +/// Loads umbrella events for a [`ModelEventStore`]. +#[doc(hidden)] +pub trait ModelEventLoader { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads model events without an ordering guarantee. + fn load_model_events( + &self, + context_id: Uuid, + ) -> Result, Self::Error>; +} + +/// Marks an entity as belonging to analysis model `M`. +#[doc(hidden)] +pub trait StoredEntity: Entity {} From 689945b68f0491870181fff9a41bff9bfaaf58f9 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 5 Aug 2026 10:42:59 +0200 Subject: [PATCH 02/19] test(store): exercise filesystem exporter path --- Cargo.lock | 2 +- crates/store/Cargo.toml | 2 +- crates/store/src/filesystem/mod.rs | 101 +++++++++++++++-------------- 3 files changed, 53 insertions(+), 52 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cb5840542..300ddefd2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3176,9 +3176,9 @@ version = "0.1.0" dependencies = [ "quent-build-info", "quent-events", + "quent-instrumentation", "quent-io", "serde", - "serde_json", "tempfile", "thiserror", "uuid", diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index eefea61ed..1f6a00626 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -13,5 +13,5 @@ thiserror.workspace = true uuid.workspace = true [dev-dependencies] -serde_json.workspace = true +quent-instrumentation = { path = "../instrumentation", features = ["io-ndjson"] } tempfile = "3" diff --git a/crates/store/src/filesystem/mod.rs b/crates/store/src/filesystem/mod.rs index 966ae262c..830a17b6f 100644 --- a/crates/store/src/filesystem/mod.rs +++ b/crates/store/src/filesystem/mod.rs @@ -218,8 +218,10 @@ fn event_files(context: &Path, entity: &str) -> Result> { mod tests { use std::fs; - use quent_build_info::{ArtifactInfo, BuildInfo, ModelSource}; + use quent_build_info::{BuildInfo, ModelSource}; use quent_events::{Entity, EntityEvent, Event, Model as EventModel, ModelEvents}; + use quent_instrumentation::{ContextExporter, ContextInner}; + use quent_io::{ExporterOptions, FileSystemExporterOptions, FileSystemFormat}; use serde::{Deserialize, Serialize}; use super::*; @@ -280,6 +282,22 @@ mod tests { } } + struct OtherModel; + + impl EventModel for OtherModel { + const NAME: &'static str = "Other"; + } + + impl ModelSource for OtherModel { + fn package() -> &'static str { + "quent-store" + } + + fn source() -> BuildInfo { + BuildInfo::unknown() + } + } + impl ModelEvents for TestModel { type UmbrellaEvent = TestEvent; } @@ -297,67 +315,55 @@ mod tests { } } - fn context(root: &Path, id: Uuid) -> PathBuf { - let path = root.join(id.to_string()); - fs::create_dir_all(&path).unwrap(); - ArtifactInfo::new(TestModel::model_info()) - .write_sidecar(&path) - .unwrap(); - path + fn context(root: &Path, id: Uuid) -> (ContextInner, ExporterOptions) + where + M: EventModel + ModelSource, + { + let context = ContextInner::try_new(id).unwrap(); + let options = ExporterOptions::FileSystem(FileSystemExporterOptions::new( + FileSystemFormat::Ndjson, + root.to_path_buf(), + )); + options.prepare_context(id, M::model_info()); + (context, options) } - fn write_event(path: &Path, event: Event) { - fs::write( - path, - format!("{}\n", serde_json::to_string(&event).unwrap()), - ) - .unwrap(); + fn export_events(root: &Path, id: Uuid) { + let (context, options) = context::(root, id); + let alpha = context + .block_on(context.observer::(&options)) + .unwrap(); + let beta = context + .block_on(context.observer::(&options)) + .unwrap(); + + alpha.send(Event::new(Uuid::from_u128(11), 11, AlphaEvent(1))); + beta.send(Event::new(Uuid::from_u128(12), 1, BetaEvent(2))); } #[test] fn loads_all_model_events_without_relying_on_order() { let root = tempfile::tempdir().unwrap(); let id = Uuid::from_u128(2); - let context = context(root.path(), id); - fs::create_dir(context.join(AlphaEvent::NAME)).unwrap(); - fs::create_dir(context.join(BetaEvent::NAME)).unwrap(); - write_event( - &context.join(AlphaEvent::NAME).join("alpha.ndjson"), - Event::new(Uuid::from_u128(11), 11, AlphaEvent(1)), - ); - write_event( - &context.join(BetaEvent::NAME).join("beta.ndjson"), - Event::new(Uuid::from_u128(12), 1, BetaEvent(2)), - ); + export_events(root.path(), id); let store = Store::::new(root.path()); - let mut values = store + let events = store .events(id) .unwrap() - .map(|event| match event.data { - TestEvent::Alpha(AlphaEvent(value)) | TestEvent::Beta(BetaEvent(value)) => value, - }) + .map(|event| event.data) .collect::>(); - values.sort_unstable(); - assert_eq!(values, [1, 2]); + assert_eq!(events.len(), 2); + assert!(events.contains(&TestEvent::Alpha(AlphaEvent(1)))); + assert!(events.contains(&TestEvent::Beta(BetaEvent(2)))); } #[test] fn loads_one_entity_type_as_concrete_events() { let root = tempfile::tempdir().unwrap(); let id = Uuid::from_u128(2); - let context = context(root.path(), id); - fs::create_dir(context.join(AlphaEvent::NAME)).unwrap(); - fs::create_dir(context.join(BetaEvent::NAME)).unwrap(); - write_event( - &context.join(AlphaEvent::NAME).join("alpha.ndjson"), - Event::new(Uuid::from_u128(11), 11, AlphaEvent(1)), - ); - write_event( - &context.join(BetaEvent::NAME).join("beta.ndjson"), - Event::new(Uuid::from_u128(12), 1, BetaEvent(2)), - ); + export_events(root.path(), id); let store = Store::::new(root.path()); let events = store @@ -381,8 +387,8 @@ mod tests { )); let unsupported = Uuid::from_u128(2); - let unsupported_path = context(root.path(), unsupported); - fs::create_dir(unsupported_path.join(AlphaEvent::NAME)).unwrap(); + export_events(root.path(), unsupported); + let unsupported_path = root.path().join(unsupported.to_string()); fs::write( unsupported_path.join(AlphaEvent::NAME).join("events.csv"), b"event", @@ -394,12 +400,7 @@ mod tests { )); let mismatch = Uuid::from_u128(3); - let mismatch_path = context(root.path(), mismatch); - let mut info = TestModel::model_info(); - info.name = "Other".to_owned(); - ArtifactInfo::new(info) - .write_sidecar(&mismatch_path) - .unwrap(); + context::(root.path(), mismatch); assert!(matches!( store.events(mismatch), Err(Error::ModelMismatch { actual, .. }) if actual == "Other" From 7a1d4e7b63f416026e6e42a1541769fa56be4b46 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 5 Aug 2026 15:59:06 +0200 Subject: [PATCH 03/19] refactor(store): organize event storage APIs Signed-off-by: Johan Peltenburg --- crates/store-build/example/build.rs | 2 +- crates/store-build/example/src/main.rs | 6 +- crates/store-build/src/lib.rs | 23 +++--- .../mod.rs => event/filesystem.rs} | 32 +++++++- crates/store/src/event/mod.rs | 74 +++++++++++++++++++ crates/store/src/lib.rs | 72 +----------------- 6 files changed, 121 insertions(+), 88 deletions(-) rename crates/store/src/{filesystem/mod.rs => event/filesystem.rs} (92%) create mode 100644 crates/store/src/event/mod.rs diff --git a/crates/store-build/example/build.rs b/crates/store-build/example/build.rs index 0c6e9e68c..afd5fc6ed 100644 --- a/crates/store-build/example/build.rs +++ b/crates/store-build/example/build.rs @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Generates store types from the same schema as the instrumentation example. +//! Generates store types from the schema shared with the instrumentation example. use std::path::Path; diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 78df3a2a9..3e720285b 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -1,14 +1,14 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Loads one recorded context produced by the instrumentation-build example. +//! Loads an existing filesystem-exported context matching the shared example schema. use std::io::{Error, ErrorKind}; use std::path::PathBuf; use demo::{Demo, Query, Uuid}; -use quent_store::filesystem::Store; -use quent_store::{EntityEventStore, ModelEventStore}; +use quent_store::event::{EntityEventStore, ModelEventStore}; +use quent_store::event::filesystem::Store; #[allow(unused)] mod demo { diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index fbcfd74ba..a68154fc1 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -98,16 +98,19 @@ pub fn generate_str(schema: &Schema, opts: &Options) -> Result::NAME, - ::quent_store::filesystem::import_event_files::<#model, #event>, + ::quent_store::event::filesystem::import_event_files::<#model, #event>, ) } }); quote! { - impl ::quent_store::filesystem::Model for #model { - fn event_streams() -> &'static [::quent_store::filesystem::EventStream] { - static STREAMS: &[::quent_store::filesystem::EventStream<#model>] = &[ + impl ::quent_store::event::filesystem::Model for #model { + fn event_streams( + ) -> &'static [::quent_store::event::filesystem::EventStream] { + static STREAMS: &[ + ::quent_store::event::filesystem::EventStream<#model> + ] = &[ #(#streams,)* ]; STREAMS @@ -120,7 +123,7 @@ pub fn generate_str(schema: &Schema, opts: &Options) -> Result for #marker {} + impl ::quent_store::event::StoredEntity<#model> for #marker {} } }); @@ -158,12 +161,12 @@ mod tests { }; let source = generate_str(&schema, &opts).unwrap(); - assert!(source.contains("impl ::quent_store::filesystem::Model for Demo")); + assert!(source.contains("impl ::quent_store::event::filesystem::Model for Demo")); assert_eq!(source.matches("import_event_files::<").count(), 2); assert!(source.contains("foo::QueryEvent")); assert!(source.contains("foo::nested::TaskEvent")); - assert!(source.contains("StoredEntity for foo::Query")); - assert!(source.contains("StoredEntity for foo::nested::Task")); + assert!(source.contains("event::StoredEntity for foo::Query")); + assert!(source.contains("event::StoredEntity for foo::nested::Task")); assert!(!source.contains("quent_instrumentation")); } @@ -177,7 +180,7 @@ mod tests { let source = generate_str(&schema, &Options::default()).unwrap(); - assert!(source.contains("StoredEntity for Query")); + assert!(source.contains("event::StoredEntity for Query")); assert!(!source.contains("filesystem::Model for Demo")); assert!(!source.contains("pub enum DemoEvent")); } diff --git a/crates/store/src/filesystem/mod.rs b/crates/store/src/event/filesystem.rs similarity index 92% rename from crates/store/src/filesystem/mod.rs rename to crates/store/src/event/filesystem.rs index 830a17b6f..26916f217 100644 --- a/crates/store/src/filesystem/mod.rs +++ b/crates/store/src/event/filesystem.rs @@ -13,8 +13,10 @@ use quent_io::filesystem::{Format, importer}; use serde::de::DeserializeOwned; use uuid::Uuid; -use crate::EventIterator; -use crate::{EntityEventLoader, EntityEventStore, ModelEventLoader, ModelEventStore, StoredEntity}; +use super::{ + EntityEventLoader, EntityEventStore, EventIterator, ModelEventLoader, ModelEventStore, + StoredEntity, +}; /// Result returned by filesystem event stores. pub type Result = std::result::Result; @@ -193,7 +195,7 @@ fn event_files(context: &Path, entity: &str) -> Result> { return Ok(Vec::new()); } - std::fs::read_dir(directory)? + let mut paths = std::fs::read_dir(directory)? .filter_map(|entry| match entry { Ok(entry) => match entry.file_type() { Ok(file_type) if file_type.is_file() => Some(Ok(entry.path())), @@ -202,8 +204,12 @@ fn event_files(context: &Path, entity: &str) -> Result> { }, Err(error) => Some(Err(Error::Io(error))), }) + .collect::>>()?; + paths.sort(); + + paths + .into_iter() .map(|path| { - let path = path?; let Some(extension) = path.extension().and_then(|value| value.to_str()) else { return Err(Error::UnsupportedFormat(String::new())); }; @@ -406,4 +412,22 @@ mod tests { Err(Error::ModelMismatch { actual, .. }) if actual == "Other" )); } + + #[test] + fn returns_event_files_in_path_order() { + let root = tempfile::tempdir().unwrap(); + let entity = root.path().join(AlphaEvent::NAME); + fs::create_dir(&entity).unwrap(); + for name in ["charlie.ndjson", "alpha.ndjson", "bravo.ndjson"] { + fs::write(entity.join(name), b"").unwrap(); + } + + let paths = event_files(root.path(), AlphaEvent::NAME) + .unwrap() + .into_iter() + .map(|file| file.path.file_name().unwrap().to_owned()) + .collect::>(); + + assert_eq!(paths, ["alpha.ndjson", "bravo.ndjson", "charlie.ndjson"]); + } } diff --git a/crates/store/src/event/mod.rs b/crates/store/src/event/mod.rs new file mode 100644 index 000000000..6c5d594f8 --- /dev/null +++ b/crates/store/src/event/mod.rs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Typed access to stored model events. + +use quent_events::{Entity, Event, ModelEvents}; +use uuid::Uuid; + +pub mod filesystem; + +/// A dynamically dispatched iterator over stored events. +pub type EventIterator = Box>>; + +/// Loads events for individual entity types in model `M`. +pub trait EntityEventStore { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads events for entity type `E` without an ordering guarantee. + fn entity_events( + &self, + context_id: Uuid, + ) -> Result, >::Error> + where + E: StoredEntity, + Self: EntityEventLoader>::Error>, + { + self.load_entity_events(context_id) + } +} + +/// Loads model-wide umbrella events. +/// +/// Generated models support this trait only when +/// `quent_store_build::Options::umbrella_event` is enabled. +pub trait ModelEventStore: EntityEventStore { + /// Loads every event stored for `context_id` without an ordering guarantee. + fn events( + &self, + context_id: Uuid, + ) -> Result, >::Error> + where + Self: ModelEventLoader>::Error>, + { + self.load_model_events(context_id) + } +} + +/// Loads one concrete entity event type for an [`EntityEventStore`]. +#[doc(hidden)] +pub trait EntityEventLoader { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads events for `E` without an ordering guarantee. + fn load_entity_events(&self, context_id: Uuid) -> Result, Self::Error>; +} + +/// Loads umbrella events for a [`ModelEventStore`]. +#[doc(hidden)] +pub trait ModelEventLoader { + /// Error returned when events cannot be loaded. + type Error; + + /// Loads model events without an ordering guarantee. + fn load_model_events( + &self, + context_id: Uuid, + ) -> Result, Self::Error>; +} + +/// Marks an entity as belonging to analysis model `M`. +#[doc(hidden)] +pub trait StoredEntity: Entity {} diff --git a/crates/store/src/lib.rs b/crates/store/src/lib.rs index 6c5d594f8..0ec4ba673 100644 --- a/crates/store/src/lib.rs +++ b/crates/store/src/lib.rs @@ -1,74 +1,6 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Typed access to stored model events. +//! Typed access to stored data. -use quent_events::{Entity, Event, ModelEvents}; -use uuid::Uuid; - -pub mod filesystem; - -/// A dynamically dispatched iterator over stored events. -pub type EventIterator = Box>>; - -/// Loads events for individual entity types in model `M`. -pub trait EntityEventStore { - /// Error returned when events cannot be loaded. - type Error; - - /// Loads events for entity type `E` without an ordering guarantee. - fn entity_events( - &self, - context_id: Uuid, - ) -> Result, >::Error> - where - E: StoredEntity, - Self: EntityEventLoader>::Error>, - { - self.load_entity_events(context_id) - } -} - -/// Loads model-wide umbrella events. -/// -/// Generated models support this trait only when -/// `quent_store_build::Options::umbrella_event` is enabled. -pub trait ModelEventStore: EntityEventStore { - /// Loads every event stored for `context_id` without an ordering guarantee. - fn events( - &self, - context_id: Uuid, - ) -> Result, >::Error> - where - Self: ModelEventLoader>::Error>, - { - self.load_model_events(context_id) - } -} - -/// Loads one concrete entity event type for an [`EntityEventStore`]. -#[doc(hidden)] -pub trait EntityEventLoader { - /// Error returned when events cannot be loaded. - type Error; - - /// Loads events for `E` without an ordering guarantee. - fn load_entity_events(&self, context_id: Uuid) -> Result, Self::Error>; -} - -/// Loads umbrella events for a [`ModelEventStore`]. -#[doc(hidden)] -pub trait ModelEventLoader { - /// Error returned when events cannot be loaded. - type Error; - - /// Loads model events without an ordering guarantee. - fn load_model_events( - &self, - context_id: Uuid, - ) -> Result, Self::Error>; -} - -/// Marks an entity as belonging to analysis model `M`. -#[doc(hidden)] -pub trait StoredEntity: Entity {} +pub mod event; From e200c10f9de7c8074ce5d09cf1355a33dfe32d63 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Thu, 6 Aug 2026 12:20:43 +0200 Subject: [PATCH 04/19] fix(store): propagate event import errors Signed-off-by: Johan Peltenburg --- crates/store-build/example/src/main.rs | 2 + crates/store/src/event/filesystem.rs | 55 +++++++++++++++++++------- crates/store/src/event/mod.rs | 21 +++++----- 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 3e720285b..6c7c589e0 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -33,11 +33,13 @@ fn main() -> Result<(), Box> { // Load events for one entity type. for event in store.entity_events::(context_id)? { + let event = event?; println!("{event:?}"); } // Load all model events as `DemoEvent`. for event in store.events(context_id)? { + let event = event?; println!("{event:?}"); } diff --git a/crates/store/src/event/filesystem.rs b/crates/store/src/event/filesystem.rs index 26916f217..589a62eb4 100644 --- a/crates/store/src/event/filesystem.rs +++ b/crates/store/src/event/filesystem.rs @@ -45,10 +45,8 @@ pub trait Model: ModelEvents { Self: Sized; } -type ImportFn = fn( - Vec, -) - -> Result::UmbrellaEvent>>>>; +type ImportFn = + fn(Vec) -> Result::UmbrellaEvent, Error>>; /// Describes one entity-event stream in a generated analysis model. pub struct EventStream { @@ -75,7 +73,7 @@ pub struct EventFile { #[doc(hidden)] pub fn import_event_files( files: Vec, -) -> Result>>> +) -> Result> where M: ModelEvents, E: DeserializeOwned + Into + 'static, @@ -83,8 +81,11 @@ where { let streams = import_files::(files)? .map(|stream| { - Box::new(stream.map(|event| Event::new(event.id, event.timestamp, event.data.into()))) - as Box>> + Box::new(stream.map(|event| { + event + .map(|event| Event::new(event.id, event.timestamp, event.data.into())) + .map_err(Error::from) + })) as EventIterator }) .collect::>(); Ok(Box::new(streams.into_iter().flatten())) @@ -123,10 +124,12 @@ where { type Error = Error; - fn load_entity_events(&self, context_id: Uuid) -> Result> { + fn load_entity_events(&self, context_id: Uuid) -> Result> { let context = self.context(context_id)?; let streams = import_files::(event_files(&context, E::Event::NAME)?)?; - Ok(Box::new(streams.flatten())) + Ok(Box::new( + streams.flatten().map(|event| event.map_err(Error::from)), + )) } } @@ -138,7 +141,10 @@ where { type Error = Error; - fn load_model_events(&self, context_id: Uuid) -> Result> { + fn load_model_events( + &self, + context_id: Uuid, + ) -> Result> { let context = self.context(context_id)?; let mut streams = Vec::new(); for descriptor in M::event_streams() { @@ -171,7 +177,7 @@ where fn import_files( files: Vec, -) -> Result>>>> +) -> Result>>> where T: DeserializeOwned + 'static, { @@ -183,7 +189,7 @@ where path: file.path, } .create_importer()?; - Ok(Box::new(importer) as Box>>) + Ok(importer) }) .collect::>>() .map(Vec::into_iter) @@ -357,8 +363,9 @@ mod tests { let events = store .events(id) .unwrap() - .map(|event| event.data) - .collect::>(); + .map(|event| event.map(|event| event.data)) + .collect::>>() + .unwrap(); assert_eq!(events.len(), 2); assert!(events.contains(&TestEvent::Alpha(AlphaEvent(1)))); @@ -375,7 +382,8 @@ mod tests { let events = store .entity_events::(id) .unwrap() - .collect::>(); + .collect::>>() + .unwrap(); assert_eq!(events.len(), 1); assert_eq!(events[0].data, AlphaEvent(1)); @@ -430,4 +438,21 @@ mod tests { assert_eq!(paths, ["alpha.ndjson", "bravo.ndjson", "charlie.ndjson"]); } + + #[test] + fn reports_import_failures_during_iteration() { + let root = tempfile::tempdir().unwrap(); + let id = Uuid::from_u128(2); + let context_path = root.path().join(id.to_string()); + context::(root.path(), id); + let entity = context_path.join(AlphaEvent::NAME); + fs::create_dir(&entity).unwrap(); + fs::write(entity.join("events.ndjson"), b"not json\n").unwrap(); + + let store = Store::::new(root.path()); + let mut events = store.entity_events::(id).unwrap(); + + assert!(matches!(events.next(), Some(Err(Error::Importer(_))))); + assert!(events.next().is_none()); + } } diff --git a/crates/store/src/event/mod.rs b/crates/store/src/event/mod.rs index 6c5d594f8..7a4aa0f4f 100644 --- a/crates/store/src/event/mod.rs +++ b/crates/store/src/event/mod.rs @@ -1,17 +1,20 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Typed access to stored model events. +//! Typed access to fully materialized model events. use quent_events::{Entity, Event, ModelEvents}; use uuid::Uuid; pub mod filesystem; -/// A dynamically dispatched iterator over stored events. -pub type EventIterator = Box>>; +/// An iterator yielding owned [`Event`](Event) values or read failures. +pub type EventIterator = Box, E>>>; -/// Loads events for individual entity types in model `M`. +/// The result of creating an [`EventIterator`]. +pub type EventIteratorResult = Result, E>; + +/// Loads stored events as owned values with payloads typed for an entity in model `M`. pub trait EntityEventStore { /// Error returned when events cannot be loaded. type Error; @@ -20,7 +23,7 @@ pub trait EntityEventStore { fn entity_events( &self, context_id: Uuid, - ) -> Result, >::Error> + ) -> EventIteratorResult>::Error> where E: StoredEntity, Self: EntityEventLoader>::Error>, @@ -29,7 +32,7 @@ pub trait EntityEventStore { } } -/// Loads model-wide umbrella events. +/// Loads model-wide stored events as owned values with umbrella-event payloads. /// /// Generated models support this trait only when /// `quent_store_build::Options::umbrella_event` is enabled. @@ -38,7 +41,7 @@ pub trait ModelEventStore: EntityEventStore { fn events( &self, context_id: Uuid, - ) -> Result, >::Error> + ) -> EventIteratorResult>::Error> where Self: ModelEventLoader>::Error>, { @@ -53,7 +56,7 @@ pub trait EntityEventLoader { type Error; /// Loads events for `E` without an ordering guarantee. - fn load_entity_events(&self, context_id: Uuid) -> Result, Self::Error>; + fn load_entity_events(&self, context_id: Uuid) -> EventIteratorResult; } /// Loads umbrella events for a [`ModelEventStore`]. @@ -66,7 +69,7 @@ pub trait ModelEventLoader { fn load_model_events( &self, context_id: Uuid, - ) -> Result, Self::Error>; + ) -> EventIteratorResult; } /// Marks an entity as belonging to analysis model `M`. From 69a2f79e1ba30bb1880fb4a358eb3a6009944eca Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Thu, 6 Aug 2026 20:45:54 +0200 Subject: [PATCH 05/19] fix(store): harden filesystem event loading Signed-off-by: Johan Peltenburg --- Cargo.lock | 22 + Cargo.toml | 17 +- .../instrumentation-build/example/Cargo.lock | 918 ----------------- .../instrumentation-build/example/Cargo.toml | 5 - crates/instrumentation-build/src/lib.rs | 28 +- crates/store-build/Cargo.toml | 1 + crates/store-build/example/Cargo.lock | 956 ------------------ crates/store-build/example/Cargo.toml | 4 - crates/store-build/example/build.rs | 3 + crates/store-build/example/src/main.rs | 2 +- crates/store-build/src/lib.rs | 59 +- crates/store/Cargo.toml | 9 +- crates/store/src/event/filesystem.rs | 245 +++-- crates/store/src/event/mod.rs | 1 + 14 files changed, 289 insertions(+), 1981 deletions(-) delete mode 100644 crates/instrumentation-build/example/Cargo.lock delete mode 100644 crates/store-build/example/Cargo.lock diff --git a/Cargo.lock b/Cargo.lock index 300ddefd2..111aa15a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2727,6 +2727,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "quent-instrumentation-build-example" +version = "0.1.0" +dependencies = [ + "quent-instrumentation", + "quent-instrumentation-build", + "quent-yaml", +] + [[package]] name = "quent-io" version = "0.1.0" @@ -3181,6 +3190,7 @@ dependencies = [ "serde", "tempfile", "thiserror", + "tracing", "uuid", ] @@ -3193,9 +3203,21 @@ dependencies = [ "quent-schema", "quote", "syn 3.0.3", + "tempfile", "thiserror", ] +[[package]] +name = "quent-store-build-example" +version = "0.1.0" +dependencies = [ + "quent-events", + "quent-store", + "quent-store-build", + "quent-yaml", + "serde", +] + [[package]] name = "quent-time" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 142803c60..407ecb670 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,9 @@ [workspace] resolver = "3" members = [ + # Legacy PoC crates, scheduled for removal + "crates/model", + "crates/model-macros", # Application-agnostic crates "crates/analyzer", "crates/dynamic-attributes", @@ -18,12 +21,8 @@ members = [ "crates/io/postcard", "crates/io/types", "crates/instrumentation", - "crates/model", - "crates/model-macros", "crates/open", "crates/stdlib", - "crates/store", - "crates/store-build", "crates/time", "crates/ui", # NVTX integration crates @@ -67,15 +66,13 @@ members = [ "crates/fsm", "crates/resource", "crates/instrumentation-build", + "crates/instrumentation-build/example", "crates/yaml", + "crates/store", + "crates/store-build", + "crates/store-build/example" ] -# The instrumentation-build example is deliberately its own workspace (see its -# Cargo.toml). Kept out of this one so `--workspace --all-features` doesn't -# unify the filesystem/collector exporters (and their `serde` bound) into its -# callback-only, `Serialize`-free graph. -exclude = ["crates/instrumentation-build/example"] - # default-members excludes any crate that activates `quent-time/__test-clock-override`. # `cargo build` and `cargo test` (no -p, no --workspace) skip those crates, preserving # the zero-cost guarantee for everything else. Use `cargo build -p ` to opt in. diff --git a/crates/instrumentation-build/example/Cargo.lock b/crates/instrumentation-build/example/Cargo.lock deleted file mode 100644 index 59f0a3180..000000000 --- a/crates/instrumentation-build/example/Cargo.lock +++ /dev/null @@ -1,918 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "annotate-snippets" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" -dependencies = [ - "anstyle", - "memchr", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "async-trait" -version = "0.1.89" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "futures-core" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" - -[[package]] -name = "futures-sink" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" - -[[package]] -name = "futures-task" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" - -[[package]] -name = "futures-util" -version = "0.3.32" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" -dependencies = [ - "futures-core", - "futures-task", - "pin-project-lite", - "slab", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "granit-parser" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" -dependencies = [ - "arraydeque", - "smallvec", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.103" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102" -dependencies = [ - "cfg-if", - "futures-util", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.186" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", - "serde", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "prettyplease" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" -dependencies = [ - "proc-macro2", - "syn 3.0.3", -] - -[[package]] -name = "proc-macro2" -version = "1.0.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quent-build-info" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "quent-constraints" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-schema", - "rustc-hash", -] - -[[package]] -name = "quent-dynamic-attributes" -version = "0.1.0" -dependencies = [ - "thiserror", -] - -[[package]] -name = "quent-events" -version = "0.1.0" -dependencies = [ - "quent-build-info", - "quent-dynamic-attributes", - "quent-time", - "uuid", -] - -[[package]] -name = "quent-fsm" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-constraints", - "quent-schema", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "quent-instrumentation" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-build-info", - "quent-dynamic-attributes", - "quent-events", - "quent-io", - "quent-io-callback", - "thiserror", - "tokio", - "tokio-util", - "tracing", - "uuid", -] - -[[package]] -name = "quent-instrumentation-build" -version = "0.1.0" -dependencies = [ - "convert_case", - "prettyplease", - "proc-macro2", - "quent-constraints", - "quent-ref-target", - "quent-schema", - "quote", - "syn 3.0.3", - "thiserror", -] - -[[package]] -name = "quent-instrumentation-build-example" -version = "0.1.0" -dependencies = [ - "quent-instrumentation", - "quent-instrumentation-build", - "quent-yaml", -] - -[[package]] -name = "quent-io" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "quent-io-types", - "uuid", -] - -[[package]] -name = "quent-io-callback" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "quent-io-types", - "uuid", -] - -[[package]] -name = "quent-io-types" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "thiserror", - "tracing", - "uuid", -] - -[[package]] -name = "quent-ref-target" -version = "0.1.0" -dependencies = [ - "quent-constraints", - "quent-schema", - "thiserror", -] - -[[package]] -name = "quent-ref-tree" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-constraints", - "quent-ref-target", - "quent-schema", - "rustc-hash", - "thiserror", -] - -[[package]] -name = "quent-resource" -version = "0.1.0" -dependencies = [ - "indexmap", - "quent-constraints", - "quent-fsm", - "quent-schema", - "rustc-hash", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "quent-schema" -version = "0.1.0" -dependencies = [ - "indexmap", - "rustc-hash", - "serde", - "smallvec", - "thiserror", -] - -[[package]] -name = "quent-time" -version = "0.1.0" -dependencies = [ - "thiserror", -] - -[[package]] -name = "quent-yaml" -version = "0.1.0" -dependencies = [ - "indexmap", - "quent-constraints", - "quent-fsm", - "quent-ref-target", - "quent-ref-tree", - "quent-resource", - "quent-schema", - "serde", - "serde-saphyr", - "thiserror", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "quote" -version = "1.0.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfbc457d0c7a0759a614551b11a6409e5951f6c7537be1f1b7682b9ae9230368" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-saphyr" -version = "0.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" -dependencies = [ - "ahash", - "annotate-snippets", - "base64", - "encoding_rs_io", - "getrandom 0.3.4", - "granit-parser", - "nohash-hasher", - "num-traits", - "serde_core", - "smallvec", - "zmij", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.150" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "slab" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "syn" -version = "2.0.118" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tokio" -version = "1.52.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" -dependencies = [ - "pin-project-lite", - "tokio-macros", -] - -[[package]] -name = "tokio-macros" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "tokio-util" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.4+wasi-0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.118", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.126" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.57.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" - -[[package]] -name = "zerocopy" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b7cbbc0a705a0fd05cc3676525980d2bf5a9bc4adac6d6475209a7887cf59d19" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.54" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e817b7b52d0c7358d3246da9d69935ebb18116b2b102b4230dac079b4862f5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.118", -] - -[[package]] -name = "zmij" -version = "1.0.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/crates/instrumentation-build/example/Cargo.toml b/crates/instrumentation-build/example/Cargo.toml index 02fdb4f43..184efe9e0 100644 --- a/crates/instrumentation-build/example/Cargo.toml +++ b/crates/instrumentation-build/example/Cargo.toml @@ -1,8 +1,3 @@ -# Its own workspace, excluded from the repo workspace, so its exporter feature -# graph stays callback-only (`Serialize`-free) instead of being unified with the -# repo's filesystem/collector exporters under `--workspace --all-features`. -[workspace] - [package] name = "quent-instrumentation-build-example" version = "0.1.0" diff --git a/crates/instrumentation-build/src/lib.rs b/crates/instrumentation-build/src/lib.rs index ef18074a7..b905574b1 100644 --- a/crates/instrumentation-build/src/lib.rs +++ b/crates/instrumentation-build/src/lib.rs @@ -55,7 +55,7 @@ mod runtime; use std::path::PathBuf; use convert_case::Case; -use quent_constraints::{BaseConstraintsError, Report, validate}; +use quent_constraints::{BaseConstraintsError, Report}; use quent_schema::{Entity, Path, Schema}; use quote::quote; @@ -167,6 +167,20 @@ pub struct GenerateInfo { pub warnings: Vec, } +/// Validates the schema requirements shared by generated event models. +/// +/// Returns constraint names without registered validators as warnings. +pub fn validate_schema(schema: &Schema) -> Result, GenerateError> { + let Report { + base_constraints, + unregistered_constraints, + results: _, + } = quent_constraints::validate::<()>(schema); + + base_constraints?; + Ok(unregistered_constraints) +} + /// Returns the model path generated for `schema` relative to the generated module root. pub fn generated_model_path(schema: &Schema) -> proc_macro2::TokenStream { let model = common::raw_ident(common::to_case(schema.name(), Case::Pascal)); @@ -185,17 +199,7 @@ pub fn generated_entity_event_path(entity: &Entity) -> proc_macro2::TokenStream /// Generate event source and, when enabled, instrumentation source for `schema`. pub fn generate(schema: &Schema, opts: &Options) -> Result { - let Report { - base_constraints, - unregistered_constraints, - results: _, // unused for now, but built-in constraints go here later - // and will add to either errors or warnings. - } = validate::<()>(schema); - - let warnings = unregistered_constraints; - - // Fail if base constraints aren't met. - base_constraints?; + let warnings = validate_schema(schema)?; let file_name = opts .file_name diff --git a/crates/store-build/Cargo.toml b/crates/store-build/Cargo.toml index 5d086cf56..4e70ad2ad 100644 --- a/crates/store-build/Cargo.toml +++ b/crates/store-build/Cargo.toml @@ -14,3 +14,4 @@ thiserror.workspace = true [dev-dependencies] quent-schema = { path = "../schema", features = ["test-utils"] } +tempfile = "3" diff --git a/crates/store-build/example/Cargo.lock b/crates/store-build/example/Cargo.lock deleted file mode 100644 index 8be29da63..000000000 --- a/crates/store-build/example/Cargo.lock +++ /dev/null @@ -1,956 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "ahash" -version = "0.8.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" -dependencies = [ - "cfg-if", - "getrandom 0.3.4", - "once_cell", - "version_check", - "zerocopy", -] - -[[package]] -name = "annotate-snippets" -version = "0.12.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f211a51805bc641f3ad5b7664c77d2547af685cc33b4cd8d31964027a46f13f1" -dependencies = [ - "anstyle", - "memchr", - "unicode-width", -] - -[[package]] -name = "anstyle" -version = "1.0.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" - -[[package]] -name = "arraydeque" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" - -[[package]] -name = "async-trait" -version = "0.1.91" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae36dc4177970ef04fde5178d3e2429882def40e57a451f919c098f72baa6cec" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "autocfg" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "bumpalo" -version = "3.20.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" - -[[package]] -name = "bytes" -version = "1.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04" - -[[package]] -name = "cfg-if" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" - -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror", -] - -[[package]] -name = "convert_case" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "affbf0190ed2caf063e3def54ff444b449371d55c58e513a95ab98eca50adb49" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - -[[package]] -name = "encoding_rs" -version = "0.8.35" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "encoding_rs_io" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83" -dependencies = [ - "encoding_rs", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "fixedbitset" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" - -[[package]] -name = "foldhash" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi 5.3.0", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" -dependencies = [ - "cfg-if", - "libc", - "r-efi 6.0.0", -] - -[[package]] -name = "granit-parser" -version = "0.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d03f81ad4732830d85cfd417a9f62cde6dadda4354d37d078a6084a19560aa2d" -dependencies = [ - "arraydeque", - "smallvec", -] - -[[package]] -name = "hashbrown" -version = "0.15.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" -dependencies = [ - "foldhash", -] - -[[package]] -name = "hashbrown" -version = "0.17.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" - -[[package]] -name = "indexmap" -version = "2.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" -dependencies = [ - "equivalent", - "hashbrown 0.17.1", - "serde", - "serde_core", -] - -[[package]] -name = "itoa" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" - -[[package]] -name = "js-sys" -version = "0.3.85" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "libc" -version = "0.2.189" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2" - -[[package]] -name = "log" -version = "0.4.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad" - -[[package]] -name = "memchr" -version = "2.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98" - -[[package]] -name = "nohash-hasher" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" - -[[package]] -name = "nu-ansi-term" -version = "0.50.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" - -[[package]] -name = "petgraph" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8701b58ea97060d5e5b155d383a69952a60943f0e6dfe30b04c287beb0b27455" -dependencies = [ - "fixedbitset", - "hashbrown 0.15.5", - "indexmap", - "serde", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - -[[package]] -name = "prettyplease" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bfe0f4c752e450fc2faf62654f1c134747922825d5b04ca717b8874f41a40c0" -dependencies = [ - "proc-macro2", - "syn 3.0.3", -] - -[[package]] -name = "proc-macro2" -version = "1.0.107" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "quent-build-info" -version = "0.1.0" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "quent-constraints" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-schema", - "rustc-hash", -] - -[[package]] -name = "quent-dynamic-attributes" -version = "0.1.0" -dependencies = [ - "serde", - "thiserror", -] - -[[package]] -name = "quent-events" -version = "0.1.0" -dependencies = [ - "quent-build-info", - "quent-dynamic-attributes", - "quent-time", - "serde", - "uuid", -] - -[[package]] -name = "quent-fsm" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-constraints", - "quent-schema", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "quent-instrumentation-build" -version = "0.1.0" -dependencies = [ - "convert_case", - "prettyplease", - "proc-macro2", - "quent-constraints", - "quent-ref-target", - "quent-schema", - "quote", - "syn 3.0.3", - "thiserror", -] - -[[package]] -name = "quent-io" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "quent-io-msgpack", - "quent-io-ndjson", - "quent-io-postcard", - "quent-io-types", - "serde", - "uuid", -] - -[[package]] -name = "quent-io-msgpack" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "quent-io-types", - "rmp-serde", - "serde", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "quent-io-ndjson" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "quent-io-types", - "serde", - "serde_json", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "quent-io-postcard" -version = "0.1.0" -dependencies = [ - "async-trait", - "postcard", - "quent-events", - "quent-io-types", - "serde", - "tokio", - "tracing", - "uuid", -] - -[[package]] -name = "quent-io-types" -version = "0.1.0" -dependencies = [ - "async-trait", - "quent-events", - "thiserror", - "tracing", - "uuid", -] - -[[package]] -name = "quent-ref-target" -version = "0.1.0" -dependencies = [ - "quent-constraints", - "quent-schema", - "thiserror", -] - -[[package]] -name = "quent-ref-tree" -version = "0.1.0" -dependencies = [ - "petgraph", - "quent-constraints", - "quent-ref-target", - "quent-schema", - "rustc-hash", - "thiserror", -] - -[[package]] -name = "quent-resource" -version = "0.1.0" -dependencies = [ - "indexmap", - "quent-constraints", - "quent-fsm", - "quent-schema", - "rustc-hash", - "serde", - "serde_json", - "thiserror", -] - -[[package]] -name = "quent-schema" -version = "0.1.0" -dependencies = [ - "indexmap", - "rustc-hash", - "serde", - "smallvec", - "thiserror", -] - -[[package]] -name = "quent-store" -version = "0.1.0" -dependencies = [ - "quent-build-info", - "quent-events", - "quent-io", - "serde", - "thiserror", - "uuid", -] - -[[package]] -name = "quent-store-build" -version = "0.1.0" -dependencies = [ - "prettyplease", - "quent-instrumentation-build", - "quent-schema", - "quote", - "syn 3.0.3", - "thiserror", -] - -[[package]] -name = "quent-store-build-example" -version = "0.1.0" -dependencies = [ - "quent-events", - "quent-store", - "quent-store-build", - "quent-yaml", - "serde", -] - -[[package]] -name = "quent-time" -version = "0.1.0" -dependencies = [ - "thiserror", -] - -[[package]] -name = "quent-yaml" -version = "0.1.0" -dependencies = [ - "indexmap", - "quent-constraints", - "quent-fsm", - "quent-ref-target", - "quent-ref-tree", - "quent-resource", - "quent-schema", - "serde", - "serde-saphyr", - "thiserror", - "tracing", - "tracing-subscriber", -] - -[[package]] -name = "quote" -version = "1.0.47" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" - -[[package]] -name = "r-efi" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" - -[[package]] -name = "rmp" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c" -dependencies = [ - "num-traits", -] - -[[package]] -name = "rmp-serde" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155" -dependencies = [ - "rmp", - "serde", -] - -[[package]] -name = "rustc-hash" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d" - -[[package]] -name = "rustversion" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" - -[[package]] -name = "serde" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde-saphyr" -version = "0.0.29" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7bd22781911de0ca6debda95f073c8f18bec65d1a94f1fa9573f3102e514cea4" -dependencies = [ - "ahash", - "annotate-snippets", - "base64", - "encoding_rs_io", - "getrandom 0.3.4", - "granit-parser", - "nohash-hasher", - "num-traits", - "serde_core", - "smallvec", - "zmij", -] - -[[package]] -name = "serde_core" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.229" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "serde_json" -version = "1.0.151" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "smallvec" -version = "1.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "syn" -version = "2.0.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "syn" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "thiserror" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd" -dependencies = [ - "proc-macro2", - "quote", - "syn 3.0.3", -] - -[[package]] -name = "thread_local" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "tokio" -version = "1.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed" -dependencies = [ - "bytes", - "pin-project-lite", -] - -[[package]] -name = "tracing" -version = "0.1.44" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" -dependencies = [ - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "tracing-core" -version = "0.1.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "unicode-ident" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" - -[[package]] -name = "unicode-segmentation" -version = "1.13.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" - -[[package]] -name = "unicode-width" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" - -[[package]] -name = "uuid" -version = "1.24.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239" -dependencies = [ - "getrandom 0.4.3", - "js-sys", - "serde_core", - "wasm-bindgen", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn 2.0.119", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.108" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "zerocopy" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a105cd7b140f6eeec8acff2ea38135d3cab283ada58540f629fe51e46696eb" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fe976fb70c78cd64cccfe3a6fc142244e8a77b70959b30faf9d0ac37ee228eb" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "zmij" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b" diff --git a/crates/store-build/example/Cargo.toml b/crates/store-build/example/Cargo.toml index 7e2ca6933..fc923b268 100644 --- a/crates/store-build/example/Cargo.toml +++ b/crates/store-build/example/Cargo.toml @@ -1,7 +1,3 @@ -# Keep this example independent so it exercises the dependencies an external -# analysis crate must declare. -[workspace] - [package] name = "quent-store-build-example" version = "0.1.0" diff --git a/crates/store-build/example/build.rs b/crates/store-build/example/build.rs index afd5fc6ed..868e05378 100644 --- a/crates/store-build/example/build.rs +++ b/crates/store-build/example/build.rs @@ -25,6 +25,9 @@ fn main() -> Result<(), Box> { ..Options::default() }; let generated = generate(&parsed.schema, &options)?; + if !generated.warnings.is_empty() { + println!("cargo:warning= {}", generated.warnings.join("\n")); + } println!( "cargo:warning=store model written to {}", generated.path.display() diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 6c7c589e0..94a95fbc1 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -7,8 +7,8 @@ use std::io::{Error, ErrorKind}; use std::path::PathBuf; use demo::{Demo, Query, Uuid}; -use quent_store::event::{EntityEventStore, ModelEventStore}; use quent_store::event::filesystem::Store; +use quent_store::event::{EntityEventStore, ModelEventStore}; #[allow(unused)] mod demo { diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index a68154fc1..5e1bf7cb4 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -2,6 +2,11 @@ // SPDX-License-Identifier: Apache-2.0 //! Generates event-store models from schemas. +//! +//! Add `quent-store-build` to `[build-dependencies]`, call [`generate`] from +//! `build.rs`, and include the generated file from Cargo's `OUT_DIR`. The +//! consuming crate requires normal dependencies on `quent-store`, +//! serde-enabled `quent-events`, and derive-enabled `serde`. use std::path::PathBuf; @@ -57,6 +62,8 @@ pub enum GenerateError { pub struct GenerateInfo { /// Path of the generated Rust source file. pub path: PathBuf, + /// Constraint names without registered validators. + pub warnings: Vec, } /// Generates an event model and its event-store descriptors. @@ -65,13 +72,14 @@ pub struct GenerateInfo { /// /// Returns an error when the schema cannot be generated or the output cannot be written. pub fn generate(schema: &Schema, opts: &Options) -> Result { + let warnings = quent_instrumentation_build::validate_schema(schema)?; let file_name = opts .file_name .clone() .unwrap_or_else(|| format!("{}.rs", schema.name().to_string().to_lowercase())); let path = opts.out_dir.join(file_name); std::fs::write(&path, generate_str(schema, opts)?)?; - Ok(GenerateInfo { path }) + Ok(GenerateInfo { path, warnings }) } /// Returns event-store model source for `schema`. @@ -141,8 +149,8 @@ pub fn generate_str(schema: &Schema, opts: &Options) -> Result = std::result::Result; pub enum Error { #[error("context `{0}` was not found")] ContextNotFound(Uuid), + #[error("context path `{0}` is not a directory")] + ContextNotDirectory(PathBuf), #[error("context model `{actual}` does not match expected model `{expected}`")] ModelMismatch { expected: String, actual: String }, - #[error("context contains an unsupported event format `{0}`")] - UnsupportedFormat(String), - #[error(transparent)] - Io(#[from] std::io::Error), - #[error(transparent)] - Importer(#[from] quent_io::ImporterError), + #[error("event file `{path}` requires the `{feature}` feature for `{format}` data")] + DisabledFormat { + path: PathBuf, + format: String, + feature: &'static str, + }, + #[error("failed to {operation} `{path}`: {source}")] + Io { + operation: &'static str, + path: PathBuf, + #[source] + source: std::io::Error, + }, + #[error("failed to import events from `{path}`: {source}")] + Importer { + path: PathBuf, + #[source] + source: quent_io::ImporterError, + }, } /// Associates a generated model with its filesystem entity-event streams. @@ -49,6 +64,7 @@ type ImportFn = fn(Vec) -> Result::UmbrellaEvent, Error>>; /// Describes one entity-event stream in a generated analysis model. +#[doc(hidden)] pub struct EventStream { entity: &'static str, import: ImportFn, @@ -79,16 +95,9 @@ where E: DeserializeOwned + Into + 'static, M::UmbrellaEvent: 'static, { - let streams = import_files::(files)? - .map(|stream| { - Box::new(stream.map(|event| { - event - .map(|event| Event::new(event.id, event.timestamp, event.data.into())) - .map_err(Error::from) - })) as EventIterator - }) - .collect::>(); - Ok(Box::new(streams.into_iter().flatten())) + Ok(Box::new(import_files::(files).map(|event| { + event.map(|event| Event::new(event.id, event.timestamp, event.data.into())) + }))) } /// Loads model events from filesystem exporter output. @@ -126,10 +135,10 @@ where fn load_entity_events(&self, context_id: Uuid) -> Result> { let context = self.context(context_id)?; - let streams = import_files::(event_files(&context, E::Event::NAME)?)?; - Ok(Box::new( - streams.flatten().map(|event| event.map_err(Error::from)), - )) + Ok(import_files::(event_files( + &context, + E::Event::NAME, + )?)) } } @@ -161,10 +170,25 @@ where { fn context(&self, context_id: Uuid) -> Result { let context = self.root.join(context_id.to_string()); - if !context.is_dir() { - return Err(Error::ContextNotFound(context_id)); + match std::fs::metadata(&context) { + Ok(metadata) if metadata.is_dir() => {} + Ok(_) => return Err(Error::ContextNotDirectory(context)), + Err(source) if source.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::ContextNotFound(context_id)); + } + Err(source) => { + return Err(Error::Io { + operation: "inspect context path", + path: context, + source, + }); + } } - let artifact = ArtifactInfo::read_sidecar(&context)?; + let artifact = ArtifactInfo::read_sidecar(&context).map_err(|source| Error::Io { + operation: "read context metadata from", + path: context.join(SIDECAR_FILE_NAME), + source, + })?; if artifact.model.name != M::NAME { return Err(Error::ModelMismatch { expected: M::NAME.to_owned(), @@ -175,55 +199,111 @@ where } } -fn import_files( - files: Vec, -) -> Result>>> +fn import_files(files: Vec) -> EventIterator where T: DeserializeOwned + 'static, { - files - .into_iter() - .map(|file| { + Box::new(files.into_iter().flat_map(|file| { + let stream = || { + let path = file.path; let importer = importer::Options { format: file.format, - path: file.path, + path: path.clone(), } - .create_importer()?; - Ok(importer) - }) - .collect::>>() - .map(Vec::into_iter) + .create_importer() + .map_err(|source| Error::Importer { + path: path.clone(), + source, + })?; + Ok::<_, Error>(Box::new(importer.map(move |event| { + event.map_err(|source| Error::Importer { + path: path.clone(), + source, + }) + })) as EventIterator) + }; + + stream().unwrap_or_else(|error| Box::new(std::iter::once(Err(error)))) + })) } fn event_files(context: &Path, entity: &str) -> Result> { let directory = context.join(entity); - if !directory.is_dir() { - return Ok(Vec::new()); + match std::fs::metadata(&directory) { + Ok(metadata) if metadata.is_dir() => {} + Ok(_) => return Ok(Vec::new()), + Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(Vec::new()), + Err(source) => { + return Err(Error::Io { + operation: "inspect event directory", + path: directory, + source, + }); + } } - let mut paths = std::fs::read_dir(directory)? - .filter_map(|entry| match entry { - Ok(entry) => match entry.file_type() { - Ok(file_type) if file_type.is_file() => Some(Ok(entry.path())), - Ok(_) => None, - Err(error) => Some(Err(Error::Io(error))), - }, - Err(error) => Some(Err(Error::Io(error))), - }) - .collect::>>()?; + let entries = std::fs::read_dir(&directory).map_err(|source| Error::Io { + operation: "read event directory", + path: directory.clone(), + source, + })?; + let mut paths = Vec::new(); + for entry in entries { + let entry = entry.map_err(|source| Error::Io { + operation: "read entry in event directory", + path: directory.clone(), + source, + })?; + let path = entry.path(); + let file_type = entry.file_type().map_err(|source| Error::Io { + operation: "inspect event file", + path: path.clone(), + source, + })?; + if file_type.is_file() { + paths.push(path); + } + } paths.sort(); - paths - .into_iter() - .map(|path| { - let Some(extension) = path.extension().and_then(|value| value.to_str()) else { - return Err(Error::UnsupportedFormat(String::new())); - }; - let format = Format::try_from(extension) - .map_err(|_| Error::UnsupportedFormat(extension.to_owned()))?; - Ok(EventFile { format, path }) - }) - .collect() + let mut files = Vec::new(); + for path in paths { + let Some(extension) = path.extension().and_then(|value| value.to_str()) else { + tracing::debug!( + path = %path.display(), + "ignoring file with unsupported event format" + ); + continue; + }; + match Format::try_from(extension) { + Ok(format) => files.push(EventFile { format, path }), + Err(_) => { + let normalized = extension.to_ascii_lowercase(); + let Some(feature) = format_feature(&normalized) else { + tracing::debug!( + path = %path.display(), + "ignoring file with unsupported event format" + ); + continue; + }; + return Err(Error::DisabledFormat { + path, + format: normalized, + feature, + }); + } + } + } + Ok(files) +} + +fn format_feature(extension: &str) -> Option<&'static str> { + match extension { + "ndjson" => Some("io-ndjson"), + "msgpack" => Some("io-msgpack"), + "postcard" => Some("io-postcard"), + _ => None, + } } #[cfg(test)] @@ -390,7 +470,7 @@ mod tests { } #[test] - fn validates_context_and_supported_formats() { + fn validates_context_and_model() { let root = tempfile::tempdir().unwrap(); let store = Store::::new(root.path()); @@ -400,17 +480,17 @@ mod tests { Err(Error::ContextNotFound(id)) if id == missing )); - let unsupported = Uuid::from_u128(2); - export_events(root.path(), unsupported); - let unsupported_path = root.path().join(unsupported.to_string()); - fs::write( - unsupported_path.join(AlphaEvent::NAME).join("events.csv"), - b"event", - ) - .unwrap(); + let missing_sidecar = Uuid::from_u128(2); + let missing_sidecar_path = root.path().join(missing_sidecar.to_string()); + fs::create_dir(&missing_sidecar_path).unwrap(); assert!(matches!( - store.events(unsupported), - Err(Error::UnsupportedFormat(format)) if format == "csv" + store.events(missing_sidecar), + Err(Error::Io { + operation: "read context metadata from", + path, + source, + }) if path == missing_sidecar_path.join(SIDECAR_FILE_NAME) + && source.kind() == std::io::ErrorKind::NotFound )); let mismatch = Uuid::from_u128(3); @@ -439,6 +519,25 @@ mod tests { assert_eq!(paths, ["alpha.ndjson", "bravo.ndjson", "charlie.ndjson"]); } + #[cfg(not(feature = "io-msgpack"))] + #[test] + fn rejects_event_files_for_disabled_formats() { + let root = tempfile::tempdir().unwrap(); + let entity = root.path().join(AlphaEvent::NAME); + fs::create_dir(&entity).unwrap(); + let path = entity.join("events.msgpack"); + fs::write(&path, b"").unwrap(); + + assert!(matches!( + event_files(root.path(), AlphaEvent::NAME), + Err(Error::DisabledFormat { + path: error_path, + format, + feature: "io-msgpack", + }) if error_path == path && format == "msgpack" + )); + } + #[test] fn reports_import_failures_during_iteration() { let root = tempfile::tempdir().unwrap(); @@ -452,7 +551,11 @@ mod tests { let store = Store::::new(root.path()); let mut events = store.entity_events::(id).unwrap(); - assert!(matches!(events.next(), Some(Err(Error::Importer(_))))); + assert!(matches!( + events.next(), + Some(Err(Error::Importer { path, .. })) + if path == entity.join("events.ndjson") + )); assert!(events.next().is_none()); } } diff --git a/crates/store/src/event/mod.rs b/crates/store/src/event/mod.rs index 7a4aa0f4f..81ecf8a15 100644 --- a/crates/store/src/event/mod.rs +++ b/crates/store/src/event/mod.rs @@ -6,6 +6,7 @@ use quent_events::{Entity, Event, ModelEvents}; use uuid::Uuid; +#[cfg(any(feature = "io-ndjson", feature = "io-msgpack", feature = "io-postcard"))] pub mod filesystem; /// An iterator yielding owned [`Event`](Event) values or read failures. From 3102b678d77dbe946585cb84e7d834bfe9714958 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 08:31:22 +0200 Subject: [PATCH 06/19] docs(store): add event round-trip example Signed-off-by: Johan Peltenburg --- .github/workflows/rust.yml | 8 +- Cargo.lock | 3 + .../instrumentation-build/example/Cargo.toml | 3 +- crates/instrumentation-build/example/build.rs | 2 + .../instrumentation-build/example/src/lib.rs | 100 ++++++++++++++++++ .../instrumentation-build/example/src/main.rs | 73 +------------ crates/instrumentation/src/lib.rs | 2 + crates/store-build/example/Cargo.toml | 2 + crates/store-build/example/src/main.rs | 25 ++--- 9 files changed, 120 insertions(+), 98 deletions(-) create mode 100644 crates/instrumentation-build/example/src/lib.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index bd9f54a3c..b83b8618b 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -58,12 +58,8 @@ jobs: - run: pixi run cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - run: pixi run cargo test --workspace --all-features --locked --all-targets - run: pixi run cargo build --workspace --all-features --locked --release - # The instrumentation-build example is its own workspace (callback-only, - # Serialize-free), excluded above; build and run it separately so it stays - # covered. - - run: pixi run cargo clippy --manifest-path crates/instrumentation-build/example/Cargo.toml --all-targets --locked -- -D warnings - - run: pixi run cargo run --manifest-path crates/instrumentation-build/example/Cargo.toml --locked - - run: pixi run cargo clippy --manifest-path crates/store-build/example/Cargo.toml --all-targets --locked -- -D warnings + - run: pixi run cargo run -p quent-instrumentation-build-example --locked + - run: pixi run cargo run -p quent-store-build-example --locked # Regression gate for `quent-open` backward compatibility: build a viewer for # a sidecar pinning the previous quent commit (the PR's base commit, or the diff --git a/Cargo.lock b/Cargo.lock index 111aa15a7..0d3558f80 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2734,6 +2734,7 @@ dependencies = [ "quent-instrumentation", "quent-instrumentation-build", "quent-yaml", + "serde", ] [[package]] @@ -3212,10 +3213,12 @@ name = "quent-store-build-example" version = "0.1.0" dependencies = [ "quent-events", + "quent-instrumentation-build-example", "quent-store", "quent-store-build", "quent-yaml", "serde", + "tempfile", ] [[package]] diff --git a/crates/instrumentation-build/example/Cargo.toml b/crates/instrumentation-build/example/Cargo.toml index 184efe9e0..51b7b2754 100644 --- a/crates/instrumentation-build/example/Cargo.toml +++ b/crates/instrumentation-build/example/Cargo.toml @@ -5,7 +5,8 @@ edition = "2024" publish = false [dependencies] -quent-instrumentation = { path = "../../instrumentation", features = ["io-callback"] } +quent-instrumentation = { path = "../../instrumentation", features = ["io-callback", "io-ndjson"] } +serde = { version = "1", features = ["derive"] } [build-dependencies] quent-instrumentation-build = { path = ".." } diff --git a/crates/instrumentation-build/example/build.rs b/crates/instrumentation-build/example/build.rs index a149d4127..258c890d3 100644 --- a/crates/instrumentation-build/example/build.rs +++ b/crates/instrumentation-build/example/build.rs @@ -21,6 +21,8 @@ fn main() -> Result<(), Box> { // Schema -> generated Rust instrumentation source. let opts = Options { + // Filesystem exporters serialize generated events. + serde: true, // Generate `DemoEvent`, which lets one typed callback receive events // from every entity in the model. umbrella_event: true, diff --git a/crates/instrumentation-build/example/src/lib.rs b/crates/instrumentation-build/example/src/lib.rs new file mode 100644 index 000000000..44781c61a --- /dev/null +++ b/crates/instrumentation-build/example/src/lib.rs @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +//! Emits a small generated instrumentation model through selectable exporters. + +use std::path::PathBuf; + +use quent_instrumentation::{ + ContextExporter, EventCallback, ExporterOptions, FileSystemExporterOptions, FileSystemFormat, + ObserverBuilder, +}; + +use demo::{Connection, Context, Demo, DemoEvent, Handle, Observer, Query, Server, Uuid}; + +#[allow(unused)] +mod demo { + include!(concat!(env!("OUT_DIR"), "/demo.rs")); +} + +/// Emits the demo events through a debug-printing callback. +pub fn run_callback() -> Result> { + emit(EventCallback::::new(|event| { + println!("{event:?}") + })) +} + +/// Exports the demo events as NDJSON and returns their context ID. +pub fn export_ndjson(root: impl Into) -> Result> { + emit(ExporterOptions::FileSystem(FileSystemExporterOptions::new( + FileSystemFormat::Ndjson, + root.into(), + ))) +} + +fn emit

(provider: P) -> Result> +where + P: ContextExporter, + Demo: ObserverBuilder

, +{ + // The context builds one exporter pipeline per entity event type and + // exposes the corresponding typed observers. + let context: Context = Context::try_new(provider)?; + let context_id = context.id(); + + // `observer.handle()` creates a fresh entity instance to emit events for. + let mut server = context.observer::().handle(); + server.booted()?; + + let observer: Observer = context.observer::(); + // Once-cardinality events take `&mut self` and may fire only once, tracked + // by the handle, hence it is mut: + let mut conn: Handle = observer.handle(); + + // One method per entity event: + conn.opened( + demo::Endpoint { + host: "localhost".to_owned(), + port: 8080, + }, + Uuid::nil(), + // A handle can deal out a reference to the entity it represents: + server.as_entity_ref(), + )?; + conn.data(1234, None)?; + + // A `dynamic` schema field maps to `DynamicAttributes`, which are + // dynamically-typed key-value pairs: + let mut extra = demo::DynamicAttributes::new(); + extra.add_string("peer_agent", "curl/8.4"); + extra.add_u64("chunk_index", 3); + extra.add_bool("compressed", true); + conn.data( + 5678, + Some(demo::Meta { + tags: vec!["tls".to_string(), "keepalive".to_string()], + extra, + }), + )?; + + // `as_entity_ref_with` produces an entity ref that also carries data: + conn.routed(server.as_entity_ref_with(demo::Route { hops: 3 }))?; + + // An FSM entity's events are transitions into its states. + // Their cardinality is derived from the topology at build time. + // + // FSMs will get typestate pattern handles in the future, also see + // https://github.com/rapidsai/quent/issues/416 + let mut query = context.observer::().handle(); + query.submitted("select 1".to_owned(), conn.as_entity_ref())?; + query.running(10)?; + query.ready(true)?; + + conn.closed()?; + + // A once-event returns an error if emitted again. + assert!(conn.closed_emitted()); + assert!(conn.closed().is_err()); + + Ok(context_id) +} diff --git a/crates/instrumentation-build/example/src/main.rs b/crates/instrumentation-build/example/src/main.rs index 724ff71ba..bf4eb82a1 100644 --- a/crates/instrumentation-build/example/src/main.rs +++ b/crates/instrumentation-build/example/src/main.rs @@ -1,78 +1,7 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -use quent_instrumentation::EventCallback; - -use crate::demo::{Connection, Context, Demo, DemoEvent, Handle, Observer, Query, Server, Uuid}; - -#[allow(unused)] -mod demo { - include!(concat!(env!("OUT_DIR"), "/demo.rs")); -} - fn main() -> Result<(), Box> { - // The context builds one exporter pipeline per entity event type and - // exposes the corresponding typed observers. - let context: Context = Context::try_new(println_exporter())?; - - // `observer.handle()` creates a fresh entity instance to events emit for. - let mut server = context.observer::().handle(); - server.booted()?; - - let observer: Observer = context.observer::(); - // Once-cardinality events take `&mut self` and may fire only once, tracked - // by the handle, hence it is mut: - let mut conn: Handle = observer.handle(); - - // One method per entity event: - conn.opened( - demo::Endpoint { - host: "localhost".to_owned(), - port: 8080, - }, - Uuid::nil(), - // A handle can deal out a reference to the entity it represents: - server.as_entity_ref(), - )?; - conn.data(1234, None)?; - - // A `dynamic` schema field maps to `DynamicAttributes`, which are - // dynamically-typed key-value pairs: - let mut extra = demo::DynamicAttributes::new(); - extra.add_string("peer_agent", "curl/8.4"); - extra.add_u64("chunk_index", 3); - extra.add_bool("compressed", true); - conn.data( - 5678, - Some(demo::Meta { - tags: vec!["tls".to_string(), "keepalive".to_string()], - extra, - }), - )?; - - // `as_entity_ref_with` produces an entity ref that also carries data: - conn.routed(server.as_entity_ref_with(demo::Route { hops: 3 }))?; - - // An FSM entity's events are transitions into its states. - // Their cardinality is derived from the topology at build time. - // - // FSMs will get typestate pattern handles in the future, also see - // https://github.com/rapidsai/quent/issues/416 - let mut query = context.observer::().handle(); - query.submitted("select 1".to_owned(), conn.as_entity_ref())?; - query.running(10)?; - query.ready(true)?; - - conn.closed()?; - - // A once-event returns an error if emitted again. - assert!(conn.closed_emitted()); - assert!(conn.closed().is_err()); - + quent_instrumentation_build_example::run_callback()?; Ok(()) } - -/// Return a callback that debug-prints each emitted event. -fn println_exporter() -> EventCallback { - EventCallback::new(|event| println!("{event:?}")) -} diff --git a/crates/instrumentation/src/lib.rs b/crates/instrumentation/src/lib.rs index 725b6ab07..89ab5dae3 100644 --- a/crates/instrumentation/src/lib.rs +++ b/crates/instrumentation/src/lib.rs @@ -32,6 +32,8 @@ pub use quent_dynamic_attributes::DynamicAttributes; pub use quent_events as events; pub use quent_events::{AnyEntity, EntityEvent, EntityRef, Event, Model, ModelEvents}; pub use quent_io::{ExporterOptions, ExporterProvider}; +#[cfg(any(feature = "io-ndjson", feature = "io-msgpack", feature = "io-postcard"))] +pub use quent_io::{FileSystemExporterOptions, FileSystemFormat}; pub use uuid::Uuid; /// A caller-supplied typed event sink, selected via the `io-callback` feature. diff --git a/crates/store-build/example/Cargo.toml b/crates/store-build/example/Cargo.toml index fc923b268..67f6997dc 100644 --- a/crates/store-build/example/Cargo.toml +++ b/crates/store-build/example/Cargo.toml @@ -6,8 +6,10 @@ publish = false [dependencies] quent-events = { path = "../../events", features = ["serde"] } +quent-instrumentation-build-example = { path = "../../instrumentation-build/example" } quent-store = { path = "../../store" } serde = { version = "1", features = ["derive"] } +tempfile = "3" [build-dependencies] quent-store-build = { path = ".." } diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 94a95fbc1..93b8b05e2 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -1,12 +1,9 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Loads an existing filesystem-exported context matching the shared example schema. +//! Runs instrumentation and loads its filesystem-exported events. -use std::io::{Error, ErrorKind}; -use std::path::PathBuf; - -use demo::{Demo, Query, Uuid}; +use demo::{Demo, Query}; use quent_store::event::filesystem::Store; use quent_store::event::{EntityEventStore, ModelEventStore}; @@ -16,20 +13,10 @@ mod demo { } fn main() -> Result<(), Box> { - let mut args = std::env::args_os().skip(1); - let root = PathBuf::from(args.next().ok_or_else(|| { - Error::new( - ErrorKind::InvalidInput, - "usage: quent-store-build-example ", - ) - })?); - let context_id = args - .next() - .and_then(|value| value.into_string().ok()) - .ok_or_else(|| Error::new(ErrorKind::InvalidInput, "missing context ID"))? - .parse::()?; - - let store = Store::::new(root); + let output = tempfile::tempdir()?; + let context_id = quent_instrumentation_build_example::export_ndjson(output.path())?; + + let store = Store::::new(output.path()); // Load events for one entity type. for event in store.entity_events::(context_id)? { From 9578232fd7ac973e1c70538198de0945ae334bd2 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 08:46:26 +0200 Subject: [PATCH 07/19] docs(instrumentation): simplify shared example runner Signed-off-by: Johan Peltenburg --- .../instrumentation-build/example/src/lib.rs | 28 +++++++++---------- .../instrumentation-build/example/src/main.rs | 2 +- crates/store-build/example/src/main.rs | 2 +- 3 files changed, 15 insertions(+), 17 deletions(-) diff --git a/crates/instrumentation-build/example/src/lib.rs b/crates/instrumentation-build/example/src/lib.rs index 44781c61a..83f2f1e71 100644 --- a/crates/instrumentation-build/example/src/lib.rs +++ b/crates/instrumentation-build/example/src/lib.rs @@ -6,8 +6,7 @@ use std::path::PathBuf; use quent_instrumentation::{ - ContextExporter, EventCallback, ExporterOptions, FileSystemExporterOptions, FileSystemFormat, - ObserverBuilder, + EventCallback, ExporterOptions, FileSystemExporterOptions, FileSystemFormat, }; use demo::{Connection, Context, Demo, DemoEvent, Handle, Observer, Query, Server, Uuid}; @@ -18,28 +17,27 @@ mod demo { } /// Emits the demo events through a debug-printing callback. -pub fn run_callback() -> Result> { - emit(EventCallback::::new(|event| { +pub fn run_with_debug_print() -> Result> { + let context = Context::try_new(EventCallback::::new(|event| { println!("{event:?}") - })) + }))?; + emit_events(context) } /// Exports the demo events as NDJSON and returns their context ID. -pub fn export_ndjson(root: impl Into) -> Result> { - emit(ExporterOptions::FileSystem(FileSystemExporterOptions::new( +pub fn run_with_ndjson( + root_export_path: impl Into, +) -> Result> { + let context = Context::try_new(ExporterOptions::FileSystem(FileSystemExporterOptions::new( FileSystemFormat::Ndjson, - root.into(), - ))) + root_export_path.into(), + )))?; + emit_events(context) } -fn emit

(provider: P) -> Result> -where - P: ContextExporter, - Demo: ObserverBuilder

, -{ +fn emit_events(context: Context) -> Result> { // The context builds one exporter pipeline per entity event type and // exposes the corresponding typed observers. - let context: Context = Context::try_new(provider)?; let context_id = context.id(); // `observer.handle()` creates a fresh entity instance to emit events for. diff --git a/crates/instrumentation-build/example/src/main.rs b/crates/instrumentation-build/example/src/main.rs index bf4eb82a1..ed396a042 100644 --- a/crates/instrumentation-build/example/src/main.rs +++ b/crates/instrumentation-build/example/src/main.rs @@ -2,6 +2,6 @@ // SPDX-License-Identifier: Apache-2.0 fn main() -> Result<(), Box> { - quent_instrumentation_build_example::run_callback()?; + quent_instrumentation_build_example::run_with_debug_print()?; Ok(()) } diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 93b8b05e2..6b0538011 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -14,7 +14,7 @@ mod demo { fn main() -> Result<(), Box> { let output = tempfile::tempdir()?; - let context_id = quent_instrumentation_build_example::export_ndjson(output.path())?; + let context_id = quent_instrumentation_build_example::run_with_ndjson(output.path())?; let store = Store::::new(output.path()); From a376fe0d8869c52dc812f6aa84a8e3b03dd17ad9 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 08:49:20 +0200 Subject: [PATCH 08/19] docs(store): label round-trip example output Signed-off-by: Johan Peltenburg --- crates/store-build/example/src/main.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 6b0538011..a1de51458 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -18,12 +18,16 @@ fn main() -> Result<(), Box> { let store = Store::::new(output.path()); + println!("--- Query events ---"); + // Load events for one entity type. for event in store.entity_events::(context_id)? { let event = event?; println!("{event:?}"); } + println!("\n--- All model events ---"); + // Load all model events as `DemoEvent`. for event in store.events(context_id)? { let event = event?; From bdae9d658470af825e3a927b02bf48ab7f47bfde Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 09:18:59 +0200 Subject: [PATCH 09/19] feat(store-build): make serde generation optional --- crates/store-build/example/build.rs | 4 ++- crates/store-build/src/lib.rs | 54 ++++++++++++++++++++++------- 2 files changed, 45 insertions(+), 13 deletions(-) diff --git a/crates/store-build/example/build.rs b/crates/store-build/example/build.rs index 868e05378..cf7b47f3d 100644 --- a/crates/store-build/example/build.rs +++ b/crates/store-build/example/build.rs @@ -19,6 +19,8 @@ fn main() -> Result<(), Box> { } let options = Options { + // The NDJSON importer requires serde-deserializable event types. + serde: true, // Generate `DemoEvent` so the example can load all model events through // one iterator. Entity-specific loading does not require this option. umbrella_event: true, @@ -29,7 +31,7 @@ fn main() -> Result<(), Box> { println!("cargo:warning= {}", generated.warnings.join("\n")); } println!( - "cargo:warning=store model written to {}", + "cargo:warning=stored-event retrieval API written to {}", generated.path.display() ); Ok(()) diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index 5e1bf7cb4..e53882430 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -1,27 +1,33 @@ // SPDX-FileCopyrightText: Copyright (c) 2026, NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 -//! Generates event-store models from schemas. +//! Generates schema-based typed APIs for retrieving stored events. //! //! Add `quent-store-build` to `[build-dependencies]`, call [`generate`] from -//! `build.rs`, and include the generated file from Cargo's `OUT_DIR`. The -//! consuming crate requires normal dependencies on `quent-store`, -//! serde-enabled `quent-events`, and derive-enabled `serde`. +//! `build.rs`, and include the generated file from Cargo's `OUT_DIR`. use std::path::PathBuf; use quent_schema::Schema; use quote::quote; -/// Options controlling event-store source generation. +/// Options controlling stored-event retrieval source generation. pub struct Options { /// Derive [`Debug`](std::fmt::Debug) on generated event and record types. pub debug: bool, + /// Derive `serde::Serialize` and `serde::Deserialize` on generated event + /// and record types. + pub serde: bool, + /// Derives applied to every generated event payload enum. + /// + /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. pub event_derives: &'static [&'static str], /// Derives applied to every generated record struct. + /// + /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. pub record_derives: &'static [&'static str], /// Generate a model-wide umbrella event and model-wide loading support. @@ -38,6 +44,7 @@ impl Default for Options { fn default() -> Self { Self { debug: true, + serde: false, event_derives: Default::default(), record_derives: Default::default(), umbrella_event: false, @@ -47,18 +54,18 @@ impl Default for Options { } } -/// An error from generating event-store source. +/// An error from generating stored-event retrieval source. #[derive(Debug, thiserror::Error)] pub enum GenerateError { #[error(transparent)] EventModel(#[from] quent_instrumentation_build::GenerateError), - #[error("generated event-store code did not form a valid Rust file")] + #[error("generated stored-event retrieval code did not form a valid Rust file")] InvalidGeneratedCode(#[source] syn::Error), - #[error("failed to write generated event-store source")] + #[error("failed to write generated stored-event retrieval source")] Io(#[from] std::io::Error), } -/// Information about generated event-store source. +/// Information about generated stored-event retrieval source. pub struct GenerateInfo { /// Path of the generated Rust source file. pub path: PathBuf, @@ -66,7 +73,7 @@ pub struct GenerateInfo { pub warnings: Vec, } -/// Generates an event model and its event-store descriptors. +/// Generates event types and their typed stored-event retrieval API. /// /// # Errors /// @@ -82,7 +89,7 @@ pub fn generate(schema: &Schema, opts: &Options) -> Result Result Date: Tue, 25 Aug 2026 09:29:44 +0200 Subject: [PATCH 10/19] test(store-build): focus generation coverage --- crates/store-build/src/lib.rs | 73 +++++------------------------------ 1 file changed, 9 insertions(+), 64 deletions(-) diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index e53882430..322616c17 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -157,12 +157,12 @@ pub fn generate_str(schema: &Schema, opts: &Options) -> Result for foo::Query")); - assert!(source.contains("event::StoredEntity for foo::nested::Task")); - assert!(!source.contains("quent_instrumentation")); - } - - #[test] - fn generates_entity_loading_without_an_umbrella_by_default() { - let schema = SchemaBuilder::try_new("Demo") - .unwrap() - .with_entity(entity("Query", [event("created", [])])) - .build() - .unwrap(); - - let source = generate_str(&schema, &Options::default()).unwrap(); - - assert!(source.contains("event::StoredEntity for Query")); - assert!(!source.contains("filesystem::Model for Demo")); - assert!(!source.contains("pub enum DemoEvent")); - } - - #[test] - fn forwards_the_serde_option_to_event_generation() { - let schema = SchemaBuilder::try_new("Demo") - .unwrap() - .with_entity(entity("Query", [event("created", [])])) - .build() - .unwrap(); - - let default_source = generate_str(&schema, &Options::default()).unwrap(); - let serde_source = generate_str( - &schema, - &Options { - serde: true, - ..Options::default() - }, - ) - .unwrap(); + let umbrella_source = generate_str(&schema, &opts).unwrap(); + assert!(default_source.contains("event::StoredEntity for foo::Query")); + assert!(default_source.contains("event::StoredEntity for foo::nested::Task")); + assert!(!default_source.contains("filesystem::Model for Demo")); assert!(!default_source.contains("::serde::")); - assert!(serde_source.contains("::serde::Serialize")); - assert!(serde_source.contains("::serde::Deserialize")); - } - - #[test] - fn generate_rejects_invalid_schemas() { - let schema = unchecked_schema("Demo", [eventless_entity("Query")], []); - let output = tempfile::tempdir().unwrap(); - let options = Options { - out_dir: output.path().to_owned(), - ..Options::default() - }; - - assert!(matches!( - generate(&schema, &options), - Err(GenerateError::EventModel( - quent_instrumentation_build::GenerateError::InvalidSchema(_) - )) - )); + assert!(umbrella_source.contains("impl ::quent_store::event::filesystem::Model for Demo")); + assert_eq!(umbrella_source.matches("import_event_files::<").count(), 2); } #[test] From a2e7568dc58e048fa89afe221f179bf093ef26a7 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 09:44:55 +0200 Subject: [PATCH 11/19] test(store): simplify filesystem coverage --- Cargo.lock | 1 - crates/store/Cargo.toml | 1 - crates/store/src/event/filesystem.rs | 209 ++++----------------------- 3 files changed, 28 insertions(+), 183 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9e8a08504..7eb93dc88 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3239,7 +3239,6 @@ version = "0.1.0" dependencies = [ "quent-build-info", "quent-events", - "quent-instrumentation", "quent-io", "serde", "tempfile", diff --git a/crates/store/Cargo.toml b/crates/store/Cargo.toml index 85296f24e..565b73ab0 100644 --- a/crates/store/Cargo.toml +++ b/crates/store/Cargo.toml @@ -20,5 +20,4 @@ tracing.workspace = true uuid.workspace = true [dev-dependencies] -quent-instrumentation = { path = "../instrumentation", features = ["io-ndjson"] } tempfile = "3" diff --git a/crates/store/src/event/filesystem.rs b/crates/store/src/event/filesystem.rs index ff9b1d999..bcbf1ea1f 100644 --- a/crates/store/src/event/filesystem.rs +++ b/crates/store/src/event/filesystem.rs @@ -310,165 +310,19 @@ fn format_feature(extension: &str) -> Option<&'static str> { mod tests { use std::fs; - use quent_build_info::{BuildInfo, ModelSource}; - use quent_events::{Entity, EntityEvent, Event, Model as EventModel, ModelEvents}; - use quent_instrumentation::{ContextExporter, ContextInner}; - use quent_io::{ExporterOptions, FileSystemExporterOptions, FileSystemFormat}; - use serde::{Deserialize, Serialize}; + use quent_build_info::ModelInfo; + use quent_events::Model as EventModel; + #[cfg(feature = "io-ndjson")] + use serde::Deserialize; use super::*; struct TestModel; - #[derive(Debug, Deserialize, PartialEq, Serialize)] - struct AlphaEvent(u8); - - impl EntityEvent for AlphaEvent { - const NAME: &'static str = "Alpha"; - } - - struct Alpha; - - impl Entity for Alpha { - type Event = AlphaEvent; - } - - impl StoredEntity for Alpha {} - - #[derive(Debug, Deserialize, PartialEq, Serialize)] - struct BetaEvent(u8); - - impl EntityEvent for BetaEvent { - const NAME: &'static str = "Beta"; - } - - #[derive(Debug, PartialEq)] - enum TestEvent { - Alpha(AlphaEvent), - Beta(BetaEvent), - } - - impl From for TestEvent { - fn from(event: AlphaEvent) -> Self { - Self::Alpha(event) - } - } - - impl From for TestEvent { - fn from(event: BetaEvent) -> Self { - Self::Beta(event) - } - } - impl EventModel for TestModel { const NAME: &'static str = "Test"; } - impl ModelSource for TestModel { - fn package() -> &'static str { - "quent-store" - } - - fn source() -> BuildInfo { - BuildInfo::unknown() - } - } - - struct OtherModel; - - impl EventModel for OtherModel { - const NAME: &'static str = "Other"; - } - - impl ModelSource for OtherModel { - fn package() -> &'static str { - "quent-store" - } - - fn source() -> BuildInfo { - BuildInfo::unknown() - } - } - - impl ModelEvents for TestModel { - type UmbrellaEvent = TestEvent; - } - - impl Model for TestModel { - fn event_streams() -> &'static [EventStream] { - static STREAMS: &[EventStream] = &[ - EventStream::new( - AlphaEvent::NAME, - import_event_files::, - ), - EventStream::new(BetaEvent::NAME, import_event_files::), - ]; - STREAMS - } - } - - fn context(root: &Path, id: Uuid) -> (ContextInner, ExporterOptions) - where - M: EventModel + ModelSource, - { - let context = ContextInner::try_new(id).unwrap(); - let options = ExporterOptions::FileSystem(FileSystemExporterOptions::new( - FileSystemFormat::Ndjson, - root.to_path_buf(), - )); - options.prepare_context(id, M::model_info()); - (context, options) - } - - fn export_events(root: &Path, id: Uuid) { - let (context, options) = context::(root, id); - let alpha = context - .block_on(context.observer::(&options)) - .unwrap(); - let beta = context - .block_on(context.observer::(&options)) - .unwrap(); - - alpha.send(Event::new(Uuid::from_u128(11), 11, AlphaEvent(1))); - beta.send(Event::new(Uuid::from_u128(12), 1, BetaEvent(2))); - } - - #[test] - fn loads_all_model_events_without_relying_on_order() { - let root = tempfile::tempdir().unwrap(); - let id = Uuid::from_u128(2); - export_events(root.path(), id); - - let store = Store::::new(root.path()); - let events = store - .events(id) - .unwrap() - .map(|event| event.map(|event| event.data)) - .collect::>>() - .unwrap(); - - assert_eq!(events.len(), 2); - assert!(events.contains(&TestEvent::Alpha(AlphaEvent(1)))); - assert!(events.contains(&TestEvent::Beta(BetaEvent(2)))); - } - - #[test] - fn loads_one_entity_type_as_concrete_events() { - let root = tempfile::tempdir().unwrap(); - let id = Uuid::from_u128(2); - export_events(root.path(), id); - - let store = Store::::new(root.path()); - let events = store - .entity_events::(id) - .unwrap() - .collect::>>() - .unwrap(); - - assert_eq!(events.len(), 1); - assert_eq!(events[0].data, AlphaEvent(1)); - } - #[test] fn validates_context_and_model() { let root = tempfile::tempdir().unwrap(); @@ -476,41 +330,33 @@ mod tests { let missing = Uuid::from_u128(1); assert!(matches!( - store.events(missing), + store.context(missing), Err(Error::ContextNotFound(id)) if id == missing )); - let missing_sidecar = Uuid::from_u128(2); - let missing_sidecar_path = root.path().join(missing_sidecar.to_string()); - fs::create_dir(&missing_sidecar_path).unwrap(); + let mismatch = Uuid::from_u128(2); + let context = root.path().join(mismatch.to_string()); + fs::create_dir(&context).unwrap(); + let mut model = ModelInfo::unknown(); + model.name = "Other".to_owned(); + ArtifactInfo::new(model).write_sidecar(&context).unwrap(); assert!(matches!( - store.events(missing_sidecar), - Err(Error::Io { - operation: "read context metadata from", - path, - source, - }) if path == missing_sidecar_path.join(SIDECAR_FILE_NAME) - && source.kind() == std::io::ErrorKind::NotFound - )); - - let mismatch = Uuid::from_u128(3); - context::(root.path(), mismatch); - assert!(matches!( - store.events(mismatch), + store.context(mismatch), Err(Error::ModelMismatch { actual, .. }) if actual == "Other" )); } + #[cfg(feature = "io-ndjson")] #[test] fn returns_event_files_in_path_order() { let root = tempfile::tempdir().unwrap(); - let entity = root.path().join(AlphaEvent::NAME); + let entity = root.path().join("Alpha"); fs::create_dir(&entity).unwrap(); for name in ["charlie.ndjson", "alpha.ndjson", "bravo.ndjson"] { fs::write(entity.join(name), b"").unwrap(); } - let paths = event_files(root.path(), AlphaEvent::NAME) + let paths = event_files(root.path(), "Alpha") .unwrap() .into_iter() .map(|file| file.path.file_name().unwrap().to_owned()) @@ -523,13 +369,13 @@ mod tests { #[test] fn rejects_event_files_for_disabled_formats() { let root = tempfile::tempdir().unwrap(); - let entity = root.path().join(AlphaEvent::NAME); + let entity = root.path().join("Alpha"); fs::create_dir(&entity).unwrap(); let path = entity.join("events.msgpack"); fs::write(&path, b"").unwrap(); assert!(matches!( - event_files(root.path(), AlphaEvent::NAME), + event_files(root.path(), "Alpha"), Err(Error::DisabledFormat { path: error_path, format, @@ -538,23 +384,24 @@ mod tests { )); } + #[cfg(feature = "io-ndjson")] #[test] fn reports_import_failures_during_iteration() { let root = tempfile::tempdir().unwrap(); - let id = Uuid::from_u128(2); - let context_path = root.path().join(id.to_string()); - context::(root.path(), id); - let entity = context_path.join(AlphaEvent::NAME); - fs::create_dir(&entity).unwrap(); - fs::write(entity.join("events.ndjson"), b"not json\n").unwrap(); + let path = root.path().join("events.ndjson"); + fs::write(&path, b"not json\n").unwrap(); - let store = Store::::new(root.path()); - let mut events = store.entity_events::(id).unwrap(); + #[derive(Deserialize)] + struct TestEvent; + + let mut events = import_files::(vec![EventFile { + format: Format::Ndjson, + path: path.clone(), + }]); assert!(matches!( events.next(), - Some(Err(Error::Importer { path, .. })) - if path == entity.join("events.ndjson") + Some(Err(Error::Importer { path: error_path, .. })) if error_path == path )); assert!(events.next().is_none()); } From db4917aa19d350aefd4d18ca327a916bfb7fe5b8 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 11:18:23 +0200 Subject: [PATCH 12/19] fix(store-build): require serde event generation Signed-off-by: Johan Peltenburg --- crates/store-build/example/build.rs | 2 -- crates/store-build/src/lib.rs | 19 ++++++------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/crates/store-build/example/build.rs b/crates/store-build/example/build.rs index cf7b47f3d..6d9f20ad1 100644 --- a/crates/store-build/example/build.rs +++ b/crates/store-build/example/build.rs @@ -19,8 +19,6 @@ fn main() -> Result<(), Box> { } let options = Options { - // The NDJSON importer requires serde-deserializable event types. - serde: true, // Generate `DemoEvent` so the example can load all model events through // one iterator. Entity-specific loading does not require this option. umbrella_event: true, diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index 322616c17..8cc810fbb 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -12,22 +12,17 @@ use quent_schema::Schema; use quote::quote; /// Options controlling stored-event retrieval source generation. +/// +/// Generated event and record types always derive `serde::Serialize` and +/// `serde::Deserialize`. pub struct Options { /// Derive [`Debug`](std::fmt::Debug) on generated event and record types. pub debug: bool, - /// Derive `serde::Serialize` and `serde::Deserialize` on generated event - /// and record types. - pub serde: bool, - - /// Derives applied to every generated event payload enum. - /// - /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. + /// Additional derives applied to every generated event payload enum. pub event_derives: &'static [&'static str], - /// Derives applied to every generated record struct. - /// - /// Use [`Self::debug`] and [`Self::serde`] for the built-in derives. + /// Additional derives applied to every generated record struct. pub record_derives: &'static [&'static str], /// Generate a model-wide umbrella event and model-wide loading support. @@ -44,7 +39,6 @@ impl Default for Options { fn default() -> Self { Self { debug: true, - serde: false, event_derives: Default::default(), record_derives: Default::default(), umbrella_event: false, @@ -98,7 +92,7 @@ pub fn generate_str(schema: &Schema, opts: &Options) -> Result for foo::Query")); assert!(default_source.contains("event::StoredEntity for foo::nested::Task")); assert!(!default_source.contains("filesystem::Model for Demo")); - assert!(!default_source.contains("::serde::")); assert!(umbrella_source.contains("impl ::quent_store::event::filesystem::Model for Demo")); assert_eq!(umbrella_source.matches("import_event_files::<").count(), 2); } From b904b9c9df379db3f4678b95e2658f6468e81408 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 11:22:54 +0200 Subject: [PATCH 13/19] test(store): cover disabled format configuration Signed-off-by: Johan Peltenburg --- .github/workflows/rust.yml | 1 + crates/store/src/event/filesystem.rs | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index b83b8618b..27c24cfa1 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -57,6 +57,7 @@ jobs: - run: pixi run cargo fmt --all -- --check - run: pixi run cargo clippy --workspace --all-targets --all-features --locked -- -D warnings - run: pixi run cargo test --workspace --all-features --locked --all-targets + - run: pixi run cargo test -p quent-store --no-default-features --features io-ndjson --locked - run: pixi run cargo build --workspace --all-features --locked --release - run: pixi run cargo run -p quent-instrumentation-build-example --locked - run: pixi run cargo run -p quent-store-build-example --locked diff --git a/crates/store/src/event/filesystem.rs b/crates/store/src/event/filesystem.rs index bcbf1ea1f..4abfe6e48 100644 --- a/crates/store/src/event/filesystem.rs +++ b/crates/store/src/event/filesystem.rs @@ -365,9 +365,12 @@ mod tests { assert_eq!(paths, ["alpha.ndjson", "bravo.ndjson", "charlie.ndjson"]); } - #[cfg(not(feature = "io-msgpack"))] #[test] fn rejects_event_files_for_disabled_formats() { + if Format::try_from("msgpack").is_ok() { + return; + } + let root = tempfile::tempdir().unwrap(); let entity = root.path().join("Alpha"); fs::create_dir(&entity).unwrap(); From 48c4236177751f272e62f0880d9e86242f59092b Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 11:33:35 +0200 Subject: [PATCH 14/19] docs(store): clarify filesystem retrieval requirements Signed-off-by: Johan Peltenburg --- crates/store-build/example/src/main.rs | 6 ++---- crates/store-build/src/lib.rs | 4 +++- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index a1de51458..63217a660 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -22,16 +22,14 @@ fn main() -> Result<(), Box> { // Load events for one entity type. for event in store.entity_events::(context_id)? { - let event = event?; - println!("{event:?}"); + println!("{:?}", event?); } println!("\n--- All model events ---"); // Load all model events as `DemoEvent`. for event in store.events(context_id)? { - let event = event?; - println!("{event:?}"); + println!("{:?}", event?); } Ok(()) diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index 8cc810fbb..f71970595 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -25,7 +25,9 @@ pub struct Options { /// Additional derives applied to every generated record struct. pub record_derives: &'static [&'static str], - /// Generate a model-wide umbrella event and model-wide loading support. + /// Generate a model-wide umbrella event and model-wide filesystem loading support. + /// + /// The consuming crate must enable at least one `quent-store` `io-*` feature. pub umbrella_event: bool, /// Directory the generated file is written into. From 54a9211e57e56e6fd1b8691172a83a8b6fdbc54b Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 11:37:11 +0200 Subject: [PATCH 15/19] build: keep preliminary store crates opt-in Signed-off-by: Johan Peltenburg --- Cargo.toml | 2 -- 1 file changed, 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1e2d8b03d..ef0cdf7ba 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -100,8 +100,6 @@ default-members = [ "crates/model-macros", "crates/open", "crates/stdlib", - "crates/store", - "crates/store-build", "crates/time", "crates/ui", "domains/query_engine/analyzer", From c561cbdfa0c9710f2da32416ac8b96407e92f922 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Tue, 25 Aug 2026 12:08:31 +0200 Subject: [PATCH 16/19] docs(store-build): document generated dependencies Signed-off-by: Johan Peltenburg --- crates/store-build/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index f71970595..03e0a81de 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -5,6 +5,8 @@ //! //! Add `quent-store-build` to `[build-dependencies]`, call [`generate`] from //! `build.rs`, and include the generated file from Cargo's `OUT_DIR`. +//! The crate including that source needs normal dependencies on `quent-store`, +//! serde-enabled `quent-events`, and derive-enabled `serde`. use std::path::PathBuf; From 08de2c50f732bc27d5502c0e1630d64648fa7989 Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 26 Aug 2026 13:19:06 +0200 Subject: [PATCH 17/19] docs(store-build): narrow generated import allowance --- crates/store-build/example/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/store-build/example/src/main.rs b/crates/store-build/example/src/main.rs index 63217a660..57737a7b4 100644 --- a/crates/store-build/example/src/main.rs +++ b/crates/store-build/example/src/main.rs @@ -7,7 +7,7 @@ use demo::{Demo, Query}; use quent_store::event::filesystem::Store; use quent_store::event::{EntityEventStore, ModelEventStore}; -#[allow(unused)] +#[allow(unused_imports)] mod demo { include!(concat!(env!("OUT_DIR"), "/demo.rs")); } From 743756437fdb7dec9aa6352ad4d98f9a6a42371b Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 26 Aug 2026 13:33:59 +0200 Subject: [PATCH 18/19] docs(store-build): add setup example --- crates/store-build/src/lib.rs | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/crates/store-build/src/lib.rs b/crates/store-build/src/lib.rs index 03e0a81de..3d149b686 100644 --- a/crates/store-build/src/lib.rs +++ b/crates/store-build/src/lib.rs @@ -7,6 +7,34 @@ //! `build.rs`, and include the generated file from Cargo's `OUT_DIR`. //! The crate including that source needs normal dependencies on `quent-store`, //! serde-enabled `quent-events`, and derive-enabled `serde`. +//! +//! ```ignore +//! // build.rs +//! use quent_store_build::{Options, generate}; +//! +//! fn main() -> Result<(), Box> { +//! let schema = todo!("load a quent_schema::Schema"); +//! generate(&schema, &Options::default())?; +//! Ok(()) +//! } +//! ``` +//! +//! ```ignore +//! // src/lib.rs +//! mod model { +//! include!(concat!(env!("OUT_DIR"), "/demo.rs")); +//! } +//! ``` +//! +//! ```toml +//! [build-dependencies] +//! quent-store-build = { path = "../quent/crates/store-build" } +//! +//! [dependencies] +//! quent-events = { path = "../quent/crates/events", features = ["serde"] } +//! quent-store = { path = "../quent/crates/store" } +//! serde = { version = "1", features = ["derive"] } +//! ``` use std::path::PathBuf; From e19d1fcacd98501fe1243bd168e66bece49ef9ea Mon Sep 17 00:00:00 2001 From: Johan Peltenburg Date: Wed, 26 Aug 2026 13:39:08 +0200 Subject: [PATCH 19/19] docs(store): document filesystem helpers --- crates/store/src/event/filesystem.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/store/src/event/filesystem.rs b/crates/store/src/event/filesystem.rs index 4abfe6e48..13c75938f 100644 --- a/crates/store/src/event/filesystem.rs +++ b/crates/store/src/event/filesystem.rs @@ -168,6 +168,7 @@ impl Store where M: EventModel, { + /// Returns the context directory after verifying that it exists and belongs to `M`. fn context(&self, context_id: Uuid) -> Result { let context = self.root.join(context_id.to_string()); match std::fs::metadata(&context) { @@ -199,6 +200,7 @@ where } } +/// Imports event files in their supplied order and yields importer failures as iterator items. fn import_files(files: Vec) -> EventIterator where T: DeserializeOwned + 'static, @@ -227,6 +229,10 @@ where })) } +/// Returns recognized event files for `entity` in path order. +/// +/// A missing or non-directory entity path produces an empty list. A recognized format whose +/// feature is disabled produces an error. fn event_files(context: &Path, entity: &str) -> Result> { let directory = context.join(entity); match std::fs::metadata(&directory) {