# Phase 1: ORM Core - Implementation Summary ## ⚠️ CRITICAL CONSTRAINT **`GLib.List`, `GLib.HashSet`, `GLib.HashTable`, or ANY `Libgee` structures ARE STRICTLY FORBIDDEN.** All collection types MUST use `Invercargill.DataStructures`: - `Invercargill.DataStructures.Vector` instead of GLib.List - `Invercargill.DataStructures.Series` for ordered collections - `Invercargill.DataStructures.Dictionary` instead of GLib.HashTable - `Invercargill.DataStructures.HashSet` for sets - `Invercargill.DataStructures.Buffer` for fixed-size indexed collections --- ## Overview Implement core ORM functionality that leverages `Invercargill.Mapping.PropertyMapper` for entity materialization and `Invercargill.Expressions` for query filtering. ## Files to Create ``` 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 ``` --- ## Implementation Tasks ### Task 1: ColumnType Enum and Type Mapping Create `src/orm/column-type.vala`: ```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; } } } ``` ### Task 2: ColumnDefinition and IndexDefinition Create `src/orm/column-definition.vala`: ```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`: ```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 columns { get; set; } public IndexDefinition() { columns = new Vector(); } } } ``` ### Task 3: ColumnBuilder (Sub-Builder Pattern) 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. ```vala 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 value) { _column.default_value = new Invercargill.NativeElement(value); return _parent; } public EntityMapperBuilder default_now() { _column.default_now = true; return _parent; } } } ``` ### Task 4: EntityMapper and EntityMapperBuilder Create `src/orm/entity-mapper.vala` and `src/orm/entity-mapper-builder.vala`: The EntityMapper wraps `Invercargill.Mapping.PropertyMapper` and adds ORM metadata. ```vala // entity-mapper.vala using Invercargill.DataStructures; using Invercargill.Mapping; namespace InvercargillSql.Orm { public class EntityMapper : Object { public string table_name { get; construct; } public PropertyMapper property_mapper { get; construct; } public Vector columns { get; construct; } public Vector indexes { get; construct; } public string? primary_key_column { get; construct; } public static EntityMapper build_for(owned EntityMapperBuilder.BuildFunc func) { var builder = new EntityMapperBuilder(); 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); } } } ``` ```vala // entity-mapper-builder.vala using Invercargill.DataStructures; using Invercargill.Mapping; namespace InvercargillSql.Orm { public delegate void BuildFunc(EntityMapperBuilder builder); public class EntityMapperBuilder : Object { private string? _table_name; private Vector _columns; private Vector _indexes; private PropertyMapperBuilder _mapper_builder; private string? _primary_key_column; public EntityMapperBuilder() { _columns = new Vector(); _indexes = new Vector(); _mapper_builder = new PropertyMapperBuilder(); } public EntityMapperBuilder 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(string name, owned GetterFunc getter, owned SetterFunc 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(name, getter, setter); return new ColumnBuilder(this, col_def); } public EntityMapperBuilder 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 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 build() { return new EntityMapper() { table_name = _table_name ?? typeof(T).name(), property_mapper = _mapper_builder.build(), columns = _columns, indexes = _indexes, primary_key_column = _primary_key_column }; } } } ``` ### Task 5: SqlDialect Interface Create `src/dialects/sql-dialect.vala`: ```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); } } ``` ### Task 6: SqliteDialect Implementation Create `src/dialects/sqlite-dialect.vala`: ```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 } } ``` ### Task 7: ExpressionToSqlVisitor Create `src/expressions/expression-to-sql-visitor.vala`: Implements `Invercargill.Expressions.ExpressionVisitor` to translate expression trees to SQL WHERE clauses. ```vala 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 _parameters; public ExpressionToSqlVisitor(SqlDialect dialect, EntityMapper mapper) { _dialect = dialect; _entity_mapper = mapper; _sql = new StringBuilder(); _parameters = new Vector(); } 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 get_parameters() { return _parameters; } } } ``` ### Task 8: Query Class Create `src/orm/query.vala`: **IMPORTANT:** Query must NOT extend `Enumerable`. Instead, provide `materialise()` methods that return `Enumerable`. ```vala using Invercargill.Expressions; using Invercargill.DataStructures; namespace InvercargillSql.Orm { public class Query : Object { private OrmSession _session; private Expression? _filter; private Vector _orderings; private int? _limit; private int? _offset; internal Query(OrmSession session) { _session = session; _orderings = new Vector(); } public Query where(string expression) { _filter = ExpressionParser.parse(expression); return this; } public Query order_by(string expression) { _orderings.add(new OrderByClause(expression, false)); return this; } public Query order_by_desc(string expression) { _orderings.add(new OrderByClause(expression, true)); return this; } public Query limit(int count) { _limit = count; return this; } public Query offset(int count) { _offset = count; return this; } public Invercargill.Enumerable materialise() throws SqlError { return _session.execute_query(this); } public async Invercargill.Enumerable materialise_async() throws SqlError { return yield _session.execute_query_async(this); } } } ``` ### Task 9: OrmSession Create `src/orm/orm-session.vala`: ```vala using Invercargill.DataStructures; namespace InvercargillSql.Orm { public class OrmSession : Object { private Connection _connection; private SqlDialect _dialect; private Dictionary _mappers; public OrmSession(Connection connection, SqlDialect? dialect = null) { _connection = connection; _dialect = dialect ?? new SqliteDialect(); _mappers = new Dictionary(); } public void register_entity(EntityMapper mapper) { _mappers.set(typeof(T), mapper); } public Query query() { return new Query(this); } public void insert(T entity) throws SqlError { var mapper = get_mapper(); // Generate and execute INSERT } public void update(T entity) throws SqlError { var mapper = get_mapper(); // Generate and execute UPDATE } public void delete(T entity) throws SqlError { var mapper = get_mapper(); // Generate and execute DELETE } private EntityMapper get_mapper() throws SqlError { var mapper = _mappers.get(typeof(T)) as EntityMapper; if (mapper == null) { throw new SqlError.GENERAL_ERROR("Entity type not registered: " + typeof(T).name()); } return mapper; } } } ``` --- ## Dependencies - `Invercargill.Mapping` - `PropertyMapper`, `PropertyMapperBuilder` - `Invercargill.Expressions` - `Expression`, `ExpressionParser`, `ExpressionVisitor` - `Invercargill.DataStructures` - `Vector`, `Dictionary`, `Series` - `Invercargill` - `Enumerable`, `Properties`, `Element`, `NativeElement` --- ## Example Usage (Target API) ```vala var orm = new OrmSession(conn); orm.register_entity(EntityMapper.build_for(b => b .table("users") .column("id", u => u.id, (u, v) => u.id = v) .primary_key() .column("name", u => u.name, (u, v) => u.name = v) .required() .column("email", u => u.email, (u, v) => u.email = v) .unique() .index() .column("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() .where("u => u.name == 'John'") .order_by("u => u.created_at") .materialise(); foreach (var u in results) { print("%s\n", u.name); } ```