⚠️ CRITICAL IMPLEMENTATION NOTES - READ FIRST ⚠️
Code Style Requirements
DO NOT use GLib.List, GLib.HashTable, or Libgee collections
- Use ONLY
Invercargill.DataStructuresfor all collectionsVector<T>instead ofGLib.List<T>or arraysDictionary<TKey, TValue>instead ofGLib.HashTableorGLib.MapHashSet<T>for set operationsReference the Invercargill Library
- Analyze
../Invercargill/src/lib/for DataStructures and Expressions patterns- Key directories:
DataStructures/,Expressions/,Mapping/- Use
Invercargill.Expressionsfor all expression handlingNo Raw SQL in High-Level APIs
- All expression parameters use Invercargill.Expressions syntax
- Raw SQL is only generated internally by dialect implementations
Async Pattern Requirements
Thread Offloading at Lowest Level
- SQLite has no native async API - thread offloading is acceptable
- Thread offloading should happen in interfaces (
Command,Connection,Transaction)- Higher-level code should use
yieldto propagate async correctlyNo Blocking Async Methods
- Async methods must NOT simply call their sync counterparts
- Use
yieldto call async methods on dependencies- Maintain the async chain all the way down to the interface level
This document provides a detailed implementation plan for Phase 5: Async Support Refactoring. The goal is to fix async method stubs throughout the project that currently block and call their sync counterparts, instead of properly using async/yield patterns.
The codebase has two categories of async implementations:
These are implemented correctly - they spawn a thread and yield back to the main loop:
| File | Method | Pattern |
|---|---|---|
command.vala |
execute_query_async() |
Thread + Idle.add + yield |
command.vala |
execute_non_query_async() |
Thread + Idle.add + yield |
command.vala |
execute_scalar_async() |
Thread + Idle.add + yield |
command.vala |
execute_batch_async() |
Thread + Idle.add + yield |
connection.vala |
open_async() |
Thread + Idle.add + yield |
connection.vala |
begin_transaction_async() |
Thread + Idle.add + yield |
connection.vala |
execute_async() |
Thread + Idle.add + yield |
transaction.vala |
commit_async() |
Thread + Idle.add + yield |
transaction.vala |
rollback_async() |
Thread + Idle.add + yield |
connection-provider.vala |
create_connection_async() |
Thread + Idle.add + yield |
These are acceptable because SQLite has no native async API. Thread offloading at the interface level is the correct approach.
These methods just call their sync counterparts, blocking the calling thread:
| File | Method | Current Implementation |
|---|---|---|
orm-session.vala:328 |
execute_query_async<T>() |
return execute_query<T>(query); |
projection-query.vala:274 |
materialise_async() |
return materialise(); |
projection-query.vala:323 |
first_async() |
return first(); |
These methods correctly use yield to call async dependencies:
| File | Method | Implementation |
|---|---|---|
query.vala:175 |
materialise_async() |
yield _session.execute_query_async<T>(this) |
query.vala:206 |
first_async() |
yield _session.execute_query_async<T>(limited_query) |
connection-factory.vala:69 |
create_and_open_async() |
yield provider.create_connection_async(cs) |
flowchart TB
subgraph High-Level API
PQ[ProjectionQuery.materialise_async]
Q[Query.materialise_async]
QF[Query.first_async]
end
subgraph Session Layer
OS[OrmSession.execute_query_async T]
end
subgraph Interface Layer - Thread Boundary
CMD[Command.execute_query_async]
CONN[Connection.open_async]
TX[Transaction.commit_async]
end
subgraph SQLite Implementation
SC[SqliteCommand]
SCON[SqliteConnection]
STX[SqliteTransaction]
end
PQ --> OS
Q --> OS
QF --> OS
OS --> CMD
CMD --> SC
CONN --> SCON
TX --> STX
File: src/orm/orm-session.vala
Current Implementation:
internal async Invercargill.Enumerable<T> execute_query_async<T>(Query<T> query) throws SqlError {
// For now, delegate to synchronous version
// TODO: Implement true async execution when needed
return execute_query<T>(query);
}
Required Changes:
The method needs to be refactored to use async/await properly. The key insight is that the sync version does three things:
We need to use the async version of command execution:
internal async Invercargill.Enumerable<T> execute_query_async<T>(Query<T> query) throws SqlError {
var mapper = get_mapper<T>();
// Build SELECT SQL - same as sync version
var sql = new StringBuilder();
sql.append("SELECT * FROM ");
sql.append(mapper.table_name);
// Add WHERE clause if filter exists
if (query.filter != null) {
var visitor = new ExpressionToSqlVisitor(_dialect, mapper);
query.filter.accept(visitor);
sql.append(" WHERE ");
sql.append(visitor.get_sql());
}
// Add ORDER BY
if (query.orderings.length > 0) {
sql.append(" ORDER BY ");
bool first = true;
foreach (var ordering in query.orderings) {
if (!first) {
sql.append(", ");
}
sql.append(ordering.expression);
if (ordering.descending) {
sql.append(" DESC");
}
first = false;
}
}
// Add LIMIT and OFFSET
if (query.limit_value != null) {
sql.append_printf(" LIMIT %d", query.limit_value);
}
if (query.offset_value != null) {
sql.append_printf(" OFFSET %d", query.offset_value);
}
var command = _connection.create_command(sql.str);
// Add parameters from expression visitor if filter exists
if (query.filter != null) {
var visitor = new ExpressionToSqlVisitor(_dialect, mapper);
query.filter.accept(visitor);
var parameters = visitor.get_parameters();
var param_names = visitor.get_parameter_names();
// Bind parameters using their generated names
var param_array = parameters.to_array();
var name_array = param_names.to_array();
for (int i = 0; i < param_array.length && i < name_array.length; i++) {
command.with_parameter<Invercargill.Element>(name_array[i], param_array[i]);
}
}
// Execute asynchronously - THIS IS THE KEY CHANGE
var results = yield command.execute_query_async();
// Materialize results
var entities = new Vector<T>();
foreach (var row in results) {
try {
var entity = mapper.materialise(row);
entities.add(entity);
} catch (Error e) {
throw new SqlError.GENERAL_ERROR("Failed to materialize entity: %s".printf(e.message));
}
}
return entities;
}
File: src/orm/projections/projection-query.vala
Current Implementation:
public async Vector<TProjection> materialise_async() throws SqlError {
// For now, delegate to synchronous version
// TODO: Implement true async execution when needed
return materialise();
}
Required Changes:
The async version should use async command execution:
public async Vector<TProjection> materialise_async() throws SqlError {
// Build the SQL using the projection SQL builder
string? where_expr = get_combined_where();
BuiltQuery built = _query_sql_builder.build_with_split(
where_expr,
_order_by_clauses,
_limit_value,
_offset_value
);
string sql = built.sql;
// Execute through session's connection
var connection = get_connection_from_session();
var command = connection.create_command(sql);
// Execute asynchronously - THIS IS THE KEY CHANGE
var results = yield command.execute_query_async();
// Materialize the results using the projection mapper
var mapper = new ProjectionMapper<TProjection>(_query_definition);
try {
return mapper.map_all(results);
} catch (ProjectionError e) {
throw new SqlError.GENERAL_ERROR(
@"Failed to materialize projection: $(e.message)"
);
}
}
File: src/orm/projections/projection-query.vala
Current Implementation:
public async TProjection? first_async() throws SqlError {
// For now, delegate to synchronous version
return first();
}
Required Changes:
The async version should use the async materialise:
public async TProjection? first_async() throws SqlError {
// Create a copy with limit 1
var limited_query = new ProjectionQuery<TProjection>(
_query_session,
_query_definition,
_query_sql_builder
);
// Copy state
foreach (var clause in _where_clauses) {
limited_query._where_clauses.add(clause);
}
foreach (var order in _order_by_clauses) {
limited_query._order_by_clauses.add(order);
}
limited_query._use_or = _use_or;
limited_query._offset_value = _offset_value;
limited_query._limit_value = 1; // Override limit
// Use async materialise - THIS IS THE KEY CHANGE
var results = yield limited_query.materialise_async();
if (results.length > 0) {
return results.get(0);
}
return null;
}
| File | Lines | Changes |
|---|---|---|
src/orm/orm-session.vala |
328-332 | Replace stub with proper async implementation using yield command.execute_query_async() |
src/orm/projections/projection-query.vala |
274-278 | Replace stub with proper async implementation using yield command.execute_query_async() |
src/orm/projections/projection-query.vala |
323-326 | Replace stub with proper async implementation using yield limited_query.materialise_async() |
Step 1: Refactor OrmSession.execute_query_async<T>()
Query<T> async methodsQuery.materialise_async() and Query.first_async()Step 2: Refactor ProjectionQuery.materialise_async()
Step 3: Refactor ProjectionQuery.first_async()
Test async query execution doesn't block
Query.materialise_async() actually yieldsQuery.first_async() actually yieldsTest async projection query execution doesn't block
ProjectionQuery.materialise_async() actually yieldsProjectionQuery.first_async() actually yieldsTest async results match sync results
src/tests/orm-test.vala - ORM async testssrc/tests/projection-test.vala - Projection async testsSQLite is a file-based database with no network communication. It has no native async API. The only way to achieve non-blocking behavior is to offload to a thread. Doing this at the interface level (Command, Connection, Transaction) is the correct approach because:
yield, regardless of the underlying implementationHigher-level code (ORM, projections) should use yield to call async methods because:
sequenceDiagram
participant User Code
participant Query T
participant OrmSession
participant Command
participant Thread Pool
participant SQLite
User Code->>Query T: materialise_async
Query T->>OrmSession: execute_query_async T
OrmSession->>OrmSession: Build SQL
OrmSession->>Command: create_command
OrmSession->>Command: execute_query_async
Command->>Thread Pool: new Thread
Thread Pool->>SQLite: execute_query
SQLite-->>Thread Pool: results
Thread Pool->>Command: Idle.add callback
Command-->>OrmSession: yield returns
OrmSession->>OrmSession: Materialize entities
OrmSession-->>Query T: Enumerable T
Query T-->>User Code: Enumerable T
This refactoring ensures that async methods throughout the project properly use the async/yield pattern instead of blocking on sync calls. The key changes are:
OrmSession.execute_query_async<T>() - Use yield command.execute_query_async()ProjectionQuery.materialise_async() - Use yield command.execute_query_async()ProjectionQuery.first_async() - Use yield limited_query.materialise_async()These changes maintain the thread-offloading at the lowest level (interfaces) while ensuring higher-level code properly propagates async behavior using yield.