GLib.List, GLib.HashSet, GLib.HashTable, or ANY Libgee structures ARE STRICTLY FORBIDDEN.
All collection types MUST use Invercargill.DataStructures:
Invercargill.DataStructures.Vector<T> instead of GLib.ListInvercargill.DataStructures.Series<T> for ordered collectionsInvercargill.DataStructures.Dictionary<K,V> instead of GLib.HashTableInvercargill.DataStructures.HashSet<T> for setsInvercargill.DataStructures.Buffer<T> for fixed-size indexed collectionsImplement core ORM functionality that leverages Invercargill.Mapping.PropertyMapper<T> for entity materialization and Invercargill.Expressions for query filtering.
src/
├── orm/
│ ├── orm-session.vala # Main ORM entry point
│ ├── entity-mapper.vala # EntityMapper class wrapping PropertyMapper
│ ├── entity-mapper-builder.vala # Fluent builder for EntityMapper
│ ├── column-builder.vala # Sub-builder for column constraints
│ ├── query.vala # Query class with materialise methods
│ ├── column-type.vala # ColumnType enum
│ ├── column-definition.vala # Column metadata class
│ └── index-definition.vala # Index metadata class
├── dialects/
│ ├── sql-dialect.vala # Interface for SQL dialects
│ └── sqlite-dialect.vala # SQLite implementation
└── expressions/
└── expression-to-sql-visitor.vala # Expression tree to SQL translator
Create src/orm/column-type.vala:
namespace InvercargillSql.Orm {
public enum ColumnType {
INT_32,
INT_64,
TEXT,
BOOLEAN,
DECIMAL,
DATETIME,
BINARY,
UUID;
public static ColumnType? from_gtype(Type type) {
if (type == typeof(int)) return INT_32;
if (type == typeof(int64)) return INT_64;
if (type == typeof(string)) return TEXT;
if (type == typeof(bool)) return BOOLEAN;
if (type == typeof(double) || type == typeof(float)) return DECIMAL;
if (type == typeof(DateTime)) return DATETIME;
if (type == typeof(uint8[])) return BINARY;
return null;
}
}
}
Create src/orm/column-definition.vala:
namespace InvercargillSql.Orm {
public class ColumnDefinition : Object {
public string name { get; set; }
public ColumnType column_type { get; set; }
public bool is_primary_key { get; set; default = false; }
public bool is_required { get; set; default = false; }
public bool is_unique { get; set; default = false; }
public bool has_index { get; set; default = false; }
public bool auto_increment { get; set; default = false; }
public Invercargill.Element? default_value { get; set; }
public bool default_now { get; set; default = false; }
}
}
Create src/orm/index-definition.vala:
using Invercargill.DataStructures;
namespace InvercargillSql.Orm {
public class IndexDefinition : Object {
public string name { get; set; }
public bool is_unique { get; set; default = false; }
public Vector<string> columns { get; set; }
public IndexDefinition() {
columns = new Vector<string>();
}
}
}
Create src/orm/column-builder.vala:
The ColumnBuilder must return to the parent EntityMapperBuilder after each constraint method, following the pattern from PropertyMappingBuilder in Invercargill.Mapping.
namespace InvercargillSql.Orm {
public class ColumnBuilder : Object {
private EntityMapperBuilder _parent;
private ColumnDefinition _column;
internal ColumnBuilder(EntityMapperBuilder parent, ColumnDefinition column) {
_parent = parent;
_column = column;
}
public EntityMapperBuilder primary_key() {
_column.is_primary_key = true;
return _parent;
}
public EntityMapperBuilder required() {
_column.is_required = true;
return _parent;
}
public EntityMapperBuilder unique() {
_column.is_unique = true;
return _parent;
}
public EntityMapperBuilder index() {
_column.has_index = true;
return _parent;
}
public EntityMapperBuilder auto_increment() {
_column.auto_increment = true;
return _parent;
}
public EntityMapperBuilder default_value<T>(T value) {
_column.default_value = new Invercargill.NativeElement<T>(value);
return _parent;
}
public EntityMapperBuilder default_now() {
_column.default_now = true;
return _parent;
}
}
}
Create src/orm/entity-mapper.vala and src/orm/entity-mapper-builder.vala:
The EntityMapper wraps Invercargill.Mapping.PropertyMapper<T> and adds ORM metadata.
// entity-mapper.vala
using Invercargill.DataStructures;
using Invercargill.Mapping;
namespace InvercargillSql.Orm {
public class EntityMapper<T> : Object {
public string table_name { get; construct; }
public PropertyMapper<T> property_mapper { get; construct; }
public Vector<ColumnDefinition> columns { get; construct; }
public Vector<IndexDefinition> indexes { get; construct; }
public string? primary_key_column { get; construct; }
public static EntityMapper<T> build_for(owned EntityMapperBuilder<T>.BuildFunc func) {
var builder = new EntityMapperBuilder<T>();
func(builder);
return builder.build();
}
public T materialise(Properties properties) throws Error {
return property_mapper.materialise(properties);
}
public Properties map_from(T entity) throws Error {
return property_mapper.map_from(entity);
}
}
}
// entity-mapper-builder.vala
using Invercargill.DataStructures;
using Invercargill.Mapping;
namespace InvercargillSql.Orm {
public delegate void BuildFunc<T>(EntityMapperBuilder<T> builder);
public class EntityMapperBuilder<T> : Object {
private string? _table_name;
private Vector<ColumnDefinition> _columns;
private Vector<IndexDefinition> _indexes;
private PropertyMapperBuilder<T> _mapper_builder;
private string? _primary_key_column;
public EntityMapperBuilder() {
_columns = new Vector<ColumnDefinition>();
_indexes = new Vector<IndexDefinition>();
_mapper_builder = new PropertyMapperBuilder<T>();
}
public EntityMapperBuilder<T> table(string name) {
_table_name = name;
return this;
}
public delegate TProp GetterFunc(T entity);
public delegate void SetterFunc(T entity, TProp value);
public ColumnBuilder column<TProp>(string name, owned GetterFunc<TProp> getter, owned SetterFunc<TProp> setter) {
var col_def = new ColumnDefinition() {
name = name,
column_type = ColumnType.from_gtype(typeof(TProp)) ?? ColumnType.TEXT
};
_columns.add(col_def);
// Add to underlying PropertyMapper
_mapper_builder.map<TProp>(name, getter, setter);
return new ColumnBuilder(this, col_def);
}
public EntityMapperBuilder<T> composite_index(string name, string[] columns) {
var index = new IndexDefinition() { name = name };
foreach (var col in columns) {
index.columns.add(col);
}
_indexes.add(index);
return this;
}
public EntityMapperBuilder<T> composite_unique(string name, string[] columns) {
var index = new IndexDefinition() { name = name, is_unique = true };
foreach (var col in columns) {
index.columns.add(col);
}
_indexes.add(index);
return this;
}
internal void set_primary_key(string column_name) {
_primary_key_column = column_name;
}
public EntityMapper<T> build() {
return new EntityMapper<T>() {
table_name = _table_name ?? typeof(T).name(),
property_mapper = _mapper_builder.build(),
columns = _columns,
indexes = _indexes,
primary_key_column = _primary_key_column
};
}
}
}
Create src/dialects/sql-dialect.vala:
using Invercargill.Expressions;
namespace InvercargillSql.Dialects {
public interface SqlDialect : Object {
// Type translation
public abstract string translate_type(ColumnType type);
// Expression translation
public abstract string translate_expression(Expression expr, EntityMapper mapper);
// CRUD SQL generation
public abstract string build_select(SelectQuery query);
public abstract string build_insert(InsertQuery query);
public abstract string build_update(UpdateQuery query);
public abstract string build_delete(DeleteQuery query);
}
}
Create src/dialects/sqlite-dialect.vala:
namespace InvercargillSql.Dialects {
public class SqliteDialect : Object, SqlDialect {
public string translate_type(ColumnType type) {
switch (type) {
case INT_32:
case INT_64:
case BOOLEAN:
return "INTEGER";
case TEXT:
case UUID:
return "TEXT";
case DECIMAL:
return "REAL";
case DATETIME:
return "INTEGER"; // Unix epoch
case BINARY:
return "BLOB";
default:
return "TEXT";
}
}
// ... implement other methods
}
}
Create src/expressions/expression-to-sql-visitor.vala:
Implements Invercargill.Expressions.ExpressionVisitor to translate expression trees to SQL WHERE clauses.
using Invercargill.Expressions;
using Invercargill.DataStructures;
namespace InvercargillSql.Expressions {
public class ExpressionToSqlVisitor : Object, ExpressionVisitor {
private StringBuilder _sql;
private SqlDialect _dialect;
private EntityMapper _entity_mapper;
private Vector<Invercargill.Element> _parameters;
public ExpressionToSqlVisitor(SqlDialect dialect, EntityMapper mapper) {
_dialect = dialect;
_entity_mapper = mapper;
_sql = new StringBuilder();
_parameters = new Vector<Invercargill.Element>();
}
public void visit_binary(BinaryExpression expr) {
_sql.append("(");
expr.left.accept(this);
_sql.append(get_operator_string(expr.op));
expr.right.accept(this);
_sql.append(")");
}
public void visit_property(PropertyExpression expr) {
var column_name = _entity_mapper.get_column_for_property(expr.property_name);
_sql.append(column_name ?? expr.property_name);
}
public void visit_literal(LiteralExpression expr) {
_sql.append("?");
_parameters.add(expr.value);
}
// ... implement other visit methods
public string get_sql() { return _sql.str; }
public Vector<Invercargill.Element> get_parameters() { return _parameters; }
}
}
Create src/orm/query.vala:
IMPORTANT: Query must NOT extend Enumerable<T>. Instead, provide materialise() methods that return Enumerable<T>.
using Invercargill.Expressions;
using Invercargill.DataStructures;
namespace InvercargillSql.Orm {
public class Query<T> : Object {
private OrmSession _session;
private Expression? _filter;
private Vector<OrderByClause> _orderings;
private int? _limit;
private int? _offset;
internal Query(OrmSession session) {
_session = session;
_orderings = new Vector<OrderByClause>();
}
public Query<T> where(string expression) {
_filter = ExpressionParser.parse(expression);
return this;
}
public Query<T> order_by(string expression) {
_orderings.add(new OrderByClause(expression, false));
return this;
}
public Query<T> order_by_desc(string expression) {
_orderings.add(new OrderByClause(expression, true));
return this;
}
public Query<T> limit(int count) {
_limit = count;
return this;
}
public Query<T> offset(int count) {
_offset = count;
return this;
}
public Invercargill.Enumerable<T> materialise() throws SqlError {
return _session.execute_query(this);
}
public async Invercargill.Enumerable<T> materialise_async() throws SqlError {
return yield _session.execute_query_async(this);
}
}
}
Create src/orm/orm-session.vala:
using Invercargill.DataStructures;
namespace InvercargillSql.Orm {
public class OrmSession : Object {
private Connection _connection;
private SqlDialect _dialect;
private Dictionary<Type, EntityMapper> _mappers;
public OrmSession(Connection connection, SqlDialect? dialect = null) {
_connection = connection;
_dialect = dialect ?? new SqliteDialect();
_mappers = new Dictionary<Type, EntityMapper>();
}
public void register_entity<T>(EntityMapper<T> mapper) {
_mappers.set(typeof(T), mapper);
}
public Query<T> query<T>() {
return new Query<T>(this);
}
public void insert<T>(T entity) throws SqlError {
var mapper = get_mapper<T>();
// Generate and execute INSERT
}
public void update<T>(T entity) throws SqlError {
var mapper = get_mapper<T>();
// Generate and execute UPDATE
}
public void delete<T>(T entity) throws SqlError {
var mapper = get_mapper<T>();
// Generate and execute DELETE
}
private EntityMapper<T> get_mapper<T>() throws SqlError {
var mapper = _mappers.get(typeof(T)) as EntityMapper<T>;
if (mapper == null) {
throw new SqlError.GENERAL_ERROR("Entity type not registered: " + typeof(T).name());
}
return mapper;
}
}
}
Invercargill.Mapping - PropertyMapper<T>, PropertyMapperBuilder<T>Invercargill.Expressions - Expression, ExpressionParser, ExpressionVisitorInvercargill.DataStructures - Vector<T>, Dictionary<K,V>, Series<T>Invercargill - Enumerable<T>, Properties, Element, NativeElement<T>var orm = new OrmSession(conn);
orm.register_entity(EntityMapper.build_for<User>(b => b
.table("users")
.column<int64>("id", u => u.id, (u, v) => u.id = v)
.primary_key()
.column<string>("name", u => u.name, (u, v) => u.name = v)
.required()
.column<string>("email", u => u.email, (u, v) => u.email = v)
.unique()
.index()
.column<DateTime>("created_at", u => u.created_at, (u, v) => u.created_at = v)
.default_now()));
// Insert
var user = new User() { name = "John", email = "john@example.com" };
orm.insert(user);
// Query
var results = orm.query<User>()
.where("u => u.name == 'John'")
.order_by("u => u.created_at")
.materialise();
foreach (var u in results) {
print("%s\n", u.name);
}