QoreDB LogoQoreDB
Back to blog
ArchitectureTechnical journal

The Universal Query Interceptor: hooks, profiling and prevention

A database client runs queries. Lots of queries. In development, in staging, in production. Some are harmless, others can destroy data in a fraction of a second. The question is not whether a developer will one day run a DROP TABLE in the wrong place, but when. In QoreDB, every…

Raphaël – Creator of QoreDBRaphaël – Creator of QoreDB
5 min readUpdated Jul 18, 2026
The Universal Query Interceptor: hooks, profiling and prevention

A database client runs queries. Lots of queries. In development, in staging, in production. Some are harmless, others can destroy data in a fraction of a second. The question is not whether a developer will one day run a DROP TABLE in the wrong place, but when.

In QoreDB, every query passes through an interception pipeline before and after it runs. This pipeline, called the Universal Query Interceptor, fulfills three roles: logging each operation for traceability, measuring performance to detect slow queries, and applying safety rules to block or request confirmation on dangerous operations. All of it is implemented on the Rust side, in the Tauri backend.

Why everything happens in the backend

The first version of the interceptor was implemented on the frontend, in TypeScript. It worked, but it posed a fundamental problem: anything running in the render process of a Tauri application is, by nature, bypassable. A user who opens the developer console can alter the JavaScript behavior. Safety rules applied on the frontend are only suggestions.

The backend-first rewrite removed this attack surface. Interception now happens in the Rust process, between the moment the Tauri command receives the query and the moment it reaches the database driver. The frontend only displays the results and offers the configuration interface. It makes no security decision.

This choice also simplified the codebase. The old frontend implementation duplicated the backend logic without providing any additional guarantee. It was removed entirely.

The four-step pipeline

The interceptor is structured around a Rust struct InterceptorPipeline that orchestrates three components: AuditStore for logging, ProfilingStore for performance metrics, and SafetyEngine for safety rules. Each is protected by a read/write lock (via parking_lot) and shared across the Tokio threads.

A query's flow follows four steps. First, the pipeline builds a context that gathers the query's metadata: session identifier, driver used, target environment, detected operation type, database, and the result of the SQL analysis. Next, the pre-execution phase submits this context to the SafetyEngine, which evaluates the safety rules. If a rule blocks the query, execution stops immediately and the event is logged. If the query is allowed, it is passed to the database driver. Finally, in post-execution, the result (success, error, row count, duration) is recorded in the audit and in the profiling.

This design has a measured overhead of between 0.1 and 0.5 ms per query. On a query that takes 50 ms to run on PostgreSQL, that is negligible. And this measurement includes serializing the audit entry to JSONL.

Built-in, extensible safety rules

The SafetyEngine ships with six non-removable base rules that cover the most critical cases. DROP and TRUNCATE are blocked in production, with no way around it. DELETE in production requires explicit confirmation. UPDATE and DELETE without a WHERE clause also trigger a confirmation, in production as well as in staging. ALTER in production emits a warning.

Each rule targets one or more environments and one or more operation types. Some use a regex pattern to refine detection. For example, the rule that detects an UPDATE without a WHERE uses a regular expression that checks for the absence of a conditional clause after the SET.

Users can add their own rules through the configuration interface. A custom rule follows the same structure as a built-in rule: a name, target environments, operation types, an optional regex pattern, and an action (block, warn, or request confirmation). This makes it possible to tailor the guardrails to the context of each project.

Profiling and slow query detection

The ProfilingStore collects metrics for every query run: execution time, success or failure, operation type, environment. From this data, it computes the P50, P95, and P99 percentiles over the last 10,000 queries. These indicators give a realistic view of the latency distribution, not just the average.

The slow-query mechanism automatically captures any query whose execution time exceeds a configurable threshold (1000 ms by default). The last 100 slow queries are kept in memory with their full context: query, duration, target database, environment. These are bounded operational metrics, designed for real-time diagnosis, not an exhaustive history.

The whole set is exportable to JSON for external analysis. A developer who observes an abnormally high P95 can export the metrics, identify the queries responsible, and act directly.

Audit: complete traceability in JSONL

The AuditStore records each query with a set of metadata: timestamp, session identifier, driver, full query, environment, operation type, target database, result (success or error), execution time, number of affected rows, and whether the query was blocked by a rule.

Persistence uses the JSONL (JSON Lines) format, where each line is an independent JSON object. This format is suited to append writes, with no need to rewrite the entire file on each entry. The file is stored locally in the application's data directory, and automatic rotation kicks in when the number of entries exceeds the configured limit (10,000 by default).

An in-memory cache of the last 1,000 entries allows fast access from the interface. The API exposes filters to browse the logs by operation type, by environment, by result, along with aggregated statistics.

In practice: what the user sees

The interceptor is transparent in normal use. A SELECT runs without friction; profiling and audit run in the background. The user only sees the interceptor when they need to step in: a blocked query shows a clear message explaining which rule fired. A query requiring confirmation presents a dialog with the details of the operation and the environment concerned.

Three dedicated panels in the interface let you consult the audit logs, the profiling metrics along with the slow queries, and the safety rules with the ability to add or edit them. The full configuration (enabling modules, thresholds, custom rules) is persisted in a local JSON file and editable from the UI without a restart.

The choice to implement everything on the Rust side guarantees that these guardrails work independently of the frontend's state. Even if the interface crashes, the backend keeps logging and blocking dangerous queries. It is a safety net that does not depend on the display.

Newsletter

Stay updated on new releases

Subscribe to get product releases, new drivers notifications, and technical tutorials.

🎁 Bonus: Get our free SQL Performance Cheat Sheet — 9 pages, PostgreSQL / MySQL / SQLite (PDF)!
Share
The Universal Query Interceptor: hooks, profiling and prevention - Blog - QoreDB