MATIH Platform is in active MVP development. Documentation reflects current implementation status.
9. Query Engine & SQL
Caching
Invalidation

Cache Invalidation

The Query Engine supports multiple invalidation strategies to ensure cached results remain consistent with the underlying data. Invalidation can be triggered by specific query, data dependency, tenant-wide flush, or schema change.


Invalidation Strategies

StrategyEndpointScopeUse Case
Query-specificPOST /v1/cache/invalidate/query/{queryHash}Single cached resultKnown stale result
Dependency-basedPOST /v1/cache/invalidate/dependency/{dependency}All queries using a tableTable data updated
Tenant-widePOST /v1/cache/invalidate/tenantAll tenant cache entriesFull cache reset
Schema changePOST /v1/cache/invalidate/schema-changeQueries touching affected tablesDDL changes

Query-Specific Invalidation

Invalidate a single cached query result by its hash:

curl -X POST "http://query-engine:8080/v1/cache/invalidate/query/a3f2b9c1d4e5f6?reason=Data%20refreshed" \
  -H "Authorization: Bearer $JWT_TOKEN"
{
  "status": "invalidated",
  "queryHash": "a3f2b9c1d4e5f6",
  "tenantId": "550e8400-e29b-41d4-a716-446655440000"
}

Dependency-Based Invalidation

When a table's data changes, all cached queries that read from that table should be invalidated:

curl -X POST "http://query-engine:8080/v1/cache/invalidate/dependency/analytics.orders?reason=ETL%20completed" \
  -H "Authorization: Bearer $JWT_TOKEN"

The implementation tracks query-to-table dependencies in Redis sets:

public void invalidateByDependency(UUID tenantId, String dependency) {
    String dependencyKey = buildDependencyKey(tenantId, dependency);
    Set<String> cacheKeys = redisTemplate.opsForSet().members(dependencyKey);
    if (cacheKeys != null && !cacheKeys.isEmpty()) {
        for (String cacheKey : cacheKeys) {
            invalidateByKey(cacheKey);
        }
        redisTemplate.delete(dependencyKey);
    }
}

When a query result is cached, its table dependencies are recorded:

private void trackDependencies(UUID tenantId, String cacheKey, Set<String> dependencies) {
    for (String dependency : dependencies) {
        String dependencyKey = buildDependencyKey(tenantId, dependency);
        redisTemplate.opsForSet().add(dependencyKey, cacheKey);
        redisTemplate.expire(dependencyKey, cacheConfig.getL2().getTtl().multipliedBy(2));
    }
}

Tenant-Wide Invalidation

Flush all cache entries for a tenant (requires ADMIN or QUERY_ADMIN role):

curl -X POST "http://query-engine:8080/v1/cache/invalidate/tenant?reason=Configuration%20change" \
  -H "Authorization: Bearer $JWT_TOKEN"

The implementation scans for all Redis keys matching the tenant pattern:

public void invalidateTenant(UUID tenantId) {
    String pattern = cacheConfig.getL2().getKeyPrefix() + tenantId + ":*";
    Set<String> keys = redisTemplate.keys(pattern);
    if (keys != null && !keys.isEmpty()) {
        redisTemplate.delete(keys);
    }
    // Also clear L1 entries for this tenant
    if (cacheConfig.getL1().isEnabled()) {
        l1Cache.asMap().keySet().stream()
                .filter(k -> k.contains(tenantId.toString()))
                .forEach(l1Cache::invalidate);
    }
}

Schema Change Invalidation

When DDL operations change table schemas, all cached queries touching those tables must be invalidated:

curl -X POST http://query-engine:8080/v1/cache/invalidate/schema-change \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -d '{"affectedTables": ["analytics.orders", "analytics.order_items"]}'
{
  "status": "invalidated",
  "affectedTables": ["analytics.orders", "analytics.order_items"],
  "tenantId": "550e8400-e29b-41d4-a716-446655440000"
}

Event-Based Invalidation

The cache supports automatic invalidation via Kafka events. When the CacheInvalidationListener receives events on the query-cache-invalidation topic, it triggers the appropriate invalidation:

query:
  cache:
    invalidation:
      dependency-based-enabled: true
      event-based-enabled: true
      event-topic: "query-cache-invalidation"
      batch-size: 100

Invalidation Across Both Levels

Every invalidation operation removes entries from both L1 and L2:

private void invalidateByKey(String cacheKey) {
    if (cacheConfig.getL1().isEnabled()) {
        l1Cache.invalidate(cacheKey);
    }
    if (cacheConfig.getL2().isEnabled()) {
        redisTemplate.delete(cacheKey);
    }
    meterRegistry.counter("query.cache.invalidate").increment();
}