phase-1-implementation-summary.md 17 KB

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<T> instead of GLib.List
  • Invercargill.DataStructures.Series<T> for ordered collections
  • Invercargill.DataStructures.Dictionary<K,V> instead of GLib.HashTable
  • Invercargill.DataStructures.HashSet<T> for sets
  • Invercargill.DataStructures.Buffer<T> for fixed-size indexed collections

Overview

Implement core ORM functionality that leverages Invercargill.Mapping.PropertyMapper<T> 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:

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:

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>();
        }
    }
}

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.

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;
        }
    }
}

Task 4: EntityMapper and EntityMapperBuilder

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
            };
        }
    }
}

Task 5: SqlDialect Interface

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);
    }
}

Task 6: SqliteDialect Implementation

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
    }
}

Task 7: ExpressionToSqlVisitor

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; }
    }
}

Task 8: Query Class

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);
        }
    }
}

Task 9: OrmSession

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;
        }
    }
}

Dependencies

  • Invercargill.Mapping - PropertyMapper<T>, PropertyMapperBuilder<T>
  • Invercargill.Expressions - Expression, ExpressionParser, ExpressionVisitor
  • Invercargill.DataStructures - Vector<T>, Dictionary<K,V>, Series<T>
  • Invercargill - Enumerable<T>, Properties, Element, NativeElement<T>

Example Usage (Target API)

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);
}