Data · Rows, schemas and migrations
Invercargill-Sql
A generic SQL abstraction built on Invercargill data structures. Connections are provider-neutral, query results return as lazy Enumerable rows, and above that sits a migration system and an ORM with entity mappers and projections. SQLite is the engine beneath today.
- Version
0.1- Licence
- LGPL-3.0-or-later
- Package
usm install invercargill-sql- Language
- Vala
Overview
Why it exists
Databases in Vala usually mean provider-specific APIs and eager row fetching. Invercargill-Sql takes the other road: a small public surface — ConnectionFactory, Connection, Command, Transaction — under which the concrete engine is an internal detail. Query results come back as Enumerable<Properties>, so a result set is not a materialised list you then re-shape but a lazy sequence you query with the same operations as everything else in the stack.
On that ground stands the rest of the data story: a fluent migration system that builds and versions schemas, an ORM that maps GObject classes onto tables with schema introspection, and projections that shape joins and aggregates into purpose-built read models. Spry's user and permission tables are defined here, as migrations.
Features
What it hands you
- Generic engine interface. Abstract away specific SQL server implementations; SQLite ships as the first-class engine.
- Lazy Enumerable rows. Query results return as
Enumerable<Properties>— filter, project and cache them with Invercargill itself. - Connection strings. URI-style strings such as
sqlite::memory:orsqlite:///db.sqlite?mode=ro, parsed into scheme, database and options. - Parameterized queries. A fluent API for type-safe parameter binding.
- Transactions. Full commit/rollback management.
- Async throughout. Every operation has an async variant for non-blocking I/O.
- Migrations. A fluent builder for tables, columns, indexes and foreign keys, with dependency-aware ordering and
up/downreversibility. - ORM. Entity mappers, sessions, typed queries and projections — including join and aggregate read models.
Installation
Getting the library
usm install invercargill
usm install invercargill-sql
From source:
git clone <the Invercargill-Sql repository>
cd Invercargill-Sql
meson setup build
ninja -C build
ninja -C build install
meson test -C build.valac --pkg invercargill-sql my-program.vala
.deps chain automatically.Usage & documentation
Connecting and querying
Use ConnectionFactory to create connections — no database-specific types in sight:
using InvercargillSql;
void main() throws SqlError {
// Create and open a connection using a connection string
var conn = ConnectionFactory.create_and_open("sqlite:///mydb.db");
// Or create and open separately
var conn2 = ConnectionFactory.create("sqlite:///another.db");
conn2.open();
// Execute commands...
conn.close();
conn2.close();
}
sqlite://./relative/path.db, sqlite::memory: and read-only or read-write modes via ?mode=.DDL and inserts go through fluent commands with bound parameters:
// Create a table
conn.create_command("""
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
email TEXT
)
""").execute_non_query();
// Insert data with parameters
conn.create_command("INSERT INTO users (name, email) VALUES (:name, :email)")
.with_parameter("name", "Alice")
.with_parameter("email", "alice@example.com")
.execute_non_query();
Query results are lazy — enumerate them, read columns by name, or keep querying:
var results = conn.create_command("SELECT * FROM users")
.execute_query();
foreach (var row in results) {
string? name = row.get("name")?.as_string_or_null();
string? email = row.get("email")?.as_string_or_null();
int64? id = row.get("id")?.as<int64?>();
print(@"User: $name ($email)\n");
}
// Results are Enumerable<Properties> — the whole toolkit applies
var activeUsers = results.where(row => row.get("active")?.as<bool>() ?? false);
var names = results.select(row => row.get("name")?.as_string_or_null() ?? "unknown");
var cached = results.cache();
Transactions commit or roll back as a unit, and every operation has an async twin:
var txn = conn.begin_transaction();
try {
conn.create_command("INSERT INTO users (name) VALUES ('Charlie')")
.execute_non_query();
conn.create_command("INSERT INTO users (name) VALUES ('Diana')")
.execute_non_query();
txn.commit();
} catch (SqlError e) {
txn.rollback();
warning("Transaction failed: %s", e.message);
}
conn.open_async(), conn.execute_query_async(...), conn.close_async().Migrations — schemas as code
Subclass Migration, declare a namespace, serial and name, and build your schema with the fluent builder:
using InvercargillSql;
using InvercargillSql.Migrations;
public class V001_CreateUsers : Migration {
public override string migration_namespace { get { return "app"; } }
public override uint64 serial { get { return 1; } }
public override string name { get { return "CreateUsers"; } }
public override void up(MigrationBuilder b) throws SqlError {
b.create_table("users", t => {
t.column<int64?>("id")
.primary_key()
.auto_increment();
t.column<string>("name")
.not_null();
t.column<string>("email")
.unique();
t.column<int64?>("age");
t.column<bool?>("is_active");
// Create index on email for fast lookups
t.index("idx_users_email").on_column("email");
});
}
public override void down(MigrationBuilder b) throws SqlError {
b.drop_table("users");
}
}
examples/migrations/v001-create-users.vala.var runner = new MigrationRunner(conn, dialect);
runner.register_migration(new V001_CreateUsers());
runner.register_migration(new V002_CreateProducts());
runner.register_migration(new V003_CreateOrders());
runner.migrate_to_latest();
var applied = runner.get_applied_migrations();
examples/demo.vala — the runner orders migrations and remembers what is applied.The ORM — entities, sessions, projections
Map a GObject class onto a table once; sessions do the rest:
public class User : Object {
public int64 id { get; set; }
public string name { get; set; }
public string email { get; set; }
public int64 age { get; set; }
public bool is_active { get; set; }
public static void configure_mapper(EntityMapperBuilder<User> b) {
b.table("users")
.column<int64?>("id", u => u.id, (u, v) => u.id = v)
.column<string>("name", u => u.name, (u, v) => u.name = v)
.column<string>("email", u => u.email, (u, v) => u.email = v)
.column<int64?>("age", u => u.age, (u, v) => u.age = v)
.column<bool?>("is_active", u => u.is_active, (u, v) => u.is_active = v);
}
}
examples/entities/user.vala — the mapper introspects the live schema at registration.var registry = new TypeRegistry();
registry.register_with_schema<User>("users", conn, dialect, User.configure_mapper);
var session = new OrmSession(conn, registry, dialect);
// Insert
var alice = new User();
alice.name = "Alice";
alice.email = "alice@example.com";
session.insert(alice);
// Query with typed predicates and ordering
var users_over_25 = session.query<User>()
.where(expr("age > $0", new NativeElement<int?>(25)))
.materialise();
var active_users = session.query<User>()
.where(expr("is_active == $0", new NativeElement<bool?>(true)))
.order_by(expr("age"))
.materialise();
// Update and delete
alice.age = 31;
session.update(alice);
session.delete(alice);
examples/demo.vala — expr() builds parameterised predicates; pagination is .limit(2).offset(1).Projections complete the ORM: purpose-built read models — simple, joined and aggregate — registered per type and queried through the same sessions, as UserSummary, OrderDetail and SalesReport in the demo show.
The repository's examples/demo.vala runs the whole story — migrations, registration, inserts, queries, updates, deletes, projections — against an in-memory database; its docs/ARCHITECTURE.md and plans/ directory record how the layered design was reached.
Package & source
The ledger
- Version
0.1(fromMANIFEST.usm)- Licence
- LGPL-3.0-or-later — free to run, study, share and improve; the LGPL keeps it free to link from any program.
- Package
usm install invercargill-sql- Provides
libinvercargill-sql.so,invercargill-sql.h,invercargill-sql.vapi+.deps,invercargill_sql-1.0.gir,invercargill_sql-1.0.typelib,invercargill-sql.pc- Needs at runtime
- GLib, GObject, Gio,
libinvercargill-1.soandlibsqlite3.so.0. - Source
- The repository
Tilo15/Invercargill-Sqlon the Astrologue forge (git over ssh, port 222). Free software: the licence guarantees your right to the source.