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

Synchronous Query Execution

Synchronous execution is the primary mode for interactive queries. The client submits a SQL statement and blocks until results are returned or the timeout is reached. This mode is used by dashboard queries, ad-hoc exploration, and the AI Service's SQL execution pipeline.


Endpoint

POST /v1/queries/execute

Required Headers

HeaderTypeDescription
X-Tenant-IDUUIDThe tenant identifier for data isolation
X-User-IDUUIDThe user performing the query
AuthorizationStringBearer JWT token

Execution Flow

The QueryExecutionService.executeSync() method orchestrates the synchronous execution pipeline. The following sequence occurs for every synchronous query:

Client Request
    |
    v
1. Validate concurrent query limit
    |
    v
2. Calculate query hash (SHA-256)
    |
    v
3. Check cache (if useCache=true)
    |--- Cache hit ---> Return cached result (cacheHit=true)
    |
    v
4. Route to engine (SmartQueryRouter)
    |
    v
5. Create QueryExecution record (status=PENDING)
    |
    v
6. Mark RUNNING, execute via engine strategy
    |
    v
7. Apply result limit
    |
    v
8. Mark COMPLETED, cache result
    |
    v
9. Return QueryResponse

Source Implementation

From QueryExecutionService.java:

@Transactional
public QueryResponse executeSync(UUID tenantId, UUID userId, QueryRequest request) {
    log.info("Executing sync query for tenant: {}, user: {}", tenantId, userId);
 
    // Check concurrent query limit
    validateConcurrentQueryLimit(tenantId);
 
    // Calculate query hash for caching
    String queryHash = calculateQueryHash(request.getSql());
 
    // Check cache first
    if (Boolean.TRUE.equals(request.getUseCache())) {
        QueryResponse cachedResult = cacheService.get(tenantId, queryHash);
        if (cachedResult != null) {
            log.debug("Cache hit for query hash: {}", queryHash);
            recordCacheHit(tenantId);
            return cachedResult.toBuilder().cacheHit(true).build();
        }
    }
 
    // Route to appropriate engine
    EngineType engineType = queryRouter.route(request);
 
    // Create execution record
    QueryExecution execution = createExecution(tenantId, userId, request, queryHash, engineType);
    execution = executionRepository.save(execution);
 
    // Execute query
    Timer.Sample timer = Timer.start(meterRegistry);
    try {
        execution.markRunning();
        executionRepository.save(execution);
 
        QueryEngineStrategy strategy = strategyFactory.getStrategy(engineType);
        int timeout = request.getTimeoutSeconds() != null
                ? request.getTimeoutSeconds()
                : syncTimeoutSeconds;
 
        QueryResponse result = strategy.execute(request, timeout);
        // ... mark completed, cache result, return
    } catch (QueryTimeoutException e) {
        // ... handle timeout
    } catch (QueryCancelledException e) {
        // ... handle cancellation
    }
}

Timeout Handling

The default synchronous timeout is controlled by the query.execution.sync-timeout-seconds property (default: 300 seconds). Clients can override this per-query via the timeoutSeconds field in the request.

# application.yml
query:
  execution:
    sync-timeout-seconds: 300
    max-concurrent-queries: 100

When a query exceeds the timeout:

  1. The engine strategy throws QueryTimeoutException
  2. The execution record is marked as FAILED with a timeout message
  3. A Micrometer timer is recorded with status=timeout
  4. The exception propagates to the client as an HTTP 408 or 500 response

curl Example

# Execute a synchronous query with custom timeout
curl -X POST http://query-engine:8080/v1/queries/execute \
  -H "Content-Type: application/json" \
  -H "X-Tenant-ID: 550e8400-e29b-41d4-a716-446655440000" \
  -H "X-User-ID: 6ba7b810-9dad-11d1-80b4-00c04fd430c8" \
  -H "Authorization: Bearer $JWT_TOKEN" \
  -d '{
    "sql": "SELECT customer_id, COUNT(*) as order_count FROM orders GROUP BY customer_id ORDER BY order_count DESC LIMIT 10",
    "catalog": "iceberg",
    "schema": "sales",
    "useCache": true,
    "timeoutSeconds": 60
  }'

Response (Success)

{
  "executionId": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "status": "COMPLETED",
  "engineType": "TRINO",
  "columns": [
    { "name": "customer_id", "type": "VARCHAR", "nullable": true },
    { "name": "order_count", "type": "BIGINT", "nullable": false }
  ],
  "data": [
    { "customer_id": "C-1042", "order_count": 156 },
    { "customer_id": "C-0887", "order_count": 143 }
  ],
  "rowCount": 10,
  "totalRows": 10,
  "bytesScanned": 52428800,
  "executionTimeMs": 1847,
  "cacheHit": false,
  "hasMore": false,
  "submittedAt": "2026-02-12T10:15:00Z",
  "completedAt": "2026-02-12T10:15:01.847Z"
}

Concurrent Query Limit

Before execution begins, the service validates that the tenant has not exceeded the concurrent query limit:

private void validateConcurrentQueryLimit(UUID tenantId) {
    long activeQueries = executionRepository.countActiveQueries(tenantId);
    if (activeQueries >= maxConcurrentQueries) {
        throw new QueryExecutionException(
            "Too many concurrent queries. Limit: " + maxConcurrentQueries);
    }
}

The default limit of 100 concurrent queries per tenant can be configured via query.execution.max-concurrent-queries.


Cache Integration

When useCache is true (the default), the execution service checks the cache before routing to an engine. The query hash is computed as a SHA-256 digest of the raw SQL text:

private String calculateQueryHash(String sql) {
    MessageDigest digest = MessageDigest.getInstance("SHA-256");
    byte[] hash = digest.digest(sql.getBytes(StandardCharsets.UTF_8));
    return HexFormat.of().formatHex(hash);
}

On a cache hit, the cached QueryResponse is returned immediately with cacheHit: true. On a cache miss, the result is cached after successful execution for future lookups.


Metrics Emitted

Every synchronous execution emits the following Micrometer metrics:

MetricTypeTagsDescription
query.execution.timeTimerengine, statusTotal execution time
query.cache.hitCountertenantCache hit count
query.cancelledCountertenantCancellation count

Error Handling

ExceptionHTTP StatusCause
QueryTimeoutException408Query exceeded timeout
QueryCancelledException499Query was cancelled
QueryExecutionException500General execution failure
Concurrent limit exceeded429Too many active queries for tenant