Skip to content

Your first read-path policy

Run a test that returns the expected order, yet fails because the read path writes to the database. The example uses published QueryAudit 0.6.0 and in-memory H2. You need Java 17+, Git, and network access.

1. Run the unexpected-write failure

git clone https://github.com/haroya01/query-audit.git
cd query-audit
./gradlew -p examples/first-audit test -PextraWrite=true --rerun-tasks

The test permits one SELECT and zero INSERTs, UPDATEs, or DELETEs:

@ExpectQueries(select = 1, insert = 0, update = 0, delete = 0)

The order still has the expected status. The added UPDATE fails the policy:

QueryAudit: readsOnce() exceeded its query budget.
UPDATE: executed 1, expected at most 0.
  UPDATE orders SET status = 'NEW' WHERE id = 1

This is the expected failure. A download, compilation, or startup error does not verify capture. Use troubleshooting if you do not see the budget message.

2. Inspect the SQL and call site

Open the generated audit:

examples/first-audit/build/reports/query-audit/report.json
Field Result of the failing run
outcome FAIL
reports[0].summary.totalQueries 2: one SELECT and one UPDATE
UPDATE entry in reports[0].queries: sql UPDATE orders SET status = ? WHERE id = ?
Its stackTrace Starts with example.audit.FirstAuditTest.writeOnReadPath:43

JSON redacts literal values and retains application call sites. In this release the budget message's first call site can be a JDBC proxy; use the JSON evidence to locate the application code.

3. Remove the write and pass

Run the same test without the flag:

./gradlew -p examples/first-audit test --rerun-tasks
[QueryAudit] 1 tests, 1 queries — all clean
[QueryAudit] outcome: PASS

The new JSON contains one SELECT and no writes. The query policy and functional assertions both pass.

To try a query-count regression too:

./gradlew -p examples/first-audit test -PextraQuery=true --rerun-tasks
QueryAudit: readsOnce() exceeded its query budget.
SELECT: executed 2, expected at most 1.

Each run replaces report.json. To verify all three outcomes and retain their reports, run:

python3 .github/scripts/verify_first_audit.py

The verifier checks Gradle exits, JUnit results, captured SQL, application call sites, and the published artifact version. Results are saved under examples/first-audit/build/reports/first-audit-verification/.

4. Apply the budget to your code

Your setup Next step
Existing Spring Boot database test Install the starter, then verify capture
Plain JUnit 5 / JDBC Expose the instrumented DataSource

Keep the test's functional assertions. Add @EnableQueryInspector and @ExpectQueries directly to the test, and make the service or repository use the instrumented DataSource. Choose the SELECT limit for that operation; zero INSERT/UPDATE/DELETE limits protect a read path from those writes.

Budgets are upper bounds. select = 1 also permits zero captured SELECTs; omitted attributes are unchecked. Since 0.7.2, exact = true makes the declared attributes exact counts. Verify a deliberate violation through your real operation before trusting a pass. @EnableQueryInspector keeps findings informational while explicit query budgets still fail.

Complete runnable source

The fixture is prepared before the audit starts. Both flags change the operation under test; the assertions and policy stay the same.

package example.audit;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;

import io.queryaudit.junit5.EnableQueryInspector;
import io.queryaudit.junit5.ExpectQueries;
import javax.sql.DataSource;
import net.ttddyy.dsproxy.support.ProxyDataSourceBuilder;
import org.h2.jdbcx.JdbcDataSource;
import org.junit.jupiter.api.Test;

@EnableQueryInspector
class FirstAuditTest {

  static final DataSource DATA_SOURCE = dataSource();

  @Test
  @ExpectQueries(select = 1, insert = 0, update = 0, delete = 0)
  void readsOnce() throws Exception {
    readOne();
    if (Boolean.getBoolean("queryAudit.demo.extraQuery")) {
      readOne();
    }
    if (Boolean.getBoolean("queryAudit.demo.extraWrite")) {
      writeOnReadPath();
    }
  }

  private void readOne() throws Exception {
    try (var connection = DATA_SOURCE.getConnection();
        var statement = connection.createStatement();
        var result = statement.executeQuery("SELECT status FROM orders WHERE id = 1 LIMIT 1")) {
      assertTrue(result.next());
      assertEquals("NEW", result.getString("status"));
    }
  }

  private void writeOnReadPath() throws Exception {
    try (var connection = DATA_SOURCE.getConnection();
        var statement = connection.createStatement()) {
      // The returned order is unchanged, but the read path now executes a write.
      assertEquals(1, statement.executeUpdate("UPDATE orders SET status = 'NEW' WHERE id = 1"));
    }
  }

  private static DataSource dataSource() {
    var h2 = new JdbcDataSource();
    h2.setURL("jdbc:h2:mem:firstaudit;DB_CLOSE_DELAY=-1");
    // Prepare the fixture before the audited test starts.
    try (var connection = h2.getConnection();
        var statement = connection.createStatement()) {
      statement.execute("CREATE TABLE orders (id INT PRIMARY KEY, status VARCHAR(20))");
      statement.executeUpdate("INSERT INTO orders (id, status) VALUES (1, 'NEW')");
    } catch (java.sql.SQLException exception) {
      throw new IllegalStateException("Cannot prepare the order fixture", exception);
    }
    return ProxyDataSourceBuilder.create(h2).name("first-audit").build();
  }
}

Java source · Gradle build

Database and version scope

H2 proves capture and budgets. Index-dependent checks may print a missing-metadata notice; add the MySQL or PostgreSQL module when checking that database's indexes. The example downloads the published release independently of the repository build. Review known limitations before requiring a whole suite in CI.

Keep the policy across changes