a database layer insipred by caqti and ecto

bd sync: 2026-01-05 11:17:11

Changed files
+7
.beads
+7
.beads/issues.jsonl
··· 1 1 {"id":"mlecto-0b6","title":"[EPIC] Repository Module","description":"Database operations gateway. Functor over Caqti/Eio connection. CRUD operations, transactions, error handling.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:21:10.437260357+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:44.810893227+01:00","closed_at":"2026-01-04T01:20:44.810893227+01:00","close_reason":"All child tasks completed"} 2 2 {"id":"mlecto-0hr","title":"Implement Query compilation to SQL","description":"Convert Query.t to SQL string + Caqti request. Parameter binding, SQL escaping, dialect-specific generation (Postgres first).","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:30.33363594+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:12:40.467657348+01:00","closed_at":"2026-01-04T01:12:40.467657348+01:00","close_reason":"Query compilation implemented in Query.to_sql - generates PostgreSQL SQL for SELECT/INSERT/UPDATE/DELETE.","dependencies":[{"issue_id":"mlecto-0hr","depends_on_id":"mlecto-7lc","type":"blocks","created_at":"2026-01-04T00:22:02.555339871+01:00","created_by":"gdiazlo"}]} 3 + {"id":"mlecto-1lq","title":"Create Cqrs module for read/write splitting","description":"Create a new lib/cqrs.ml module that provides transparent CQRS routing.\n\n## New File: lib/cqrs.ml\n\n### Types\n```ocaml\ntype replica_selection = RoundRobin | Random | LeastConnections\n\ntype 'conn config = {\n primary_conninfo: string;\n primary_max_size: int;\n replica_conninfos: string list; (* empty = CQRS disabled *)\n replica_max_size_each: int;\n replica_selection: replica_selection;\n validate: ('conn -\u003e bool) option;\n}\n\ntype 'conn t = {\n primary_pool: 'conn Pool.t;\n replica_pools: 'conn Pool.t array option; (* None when no replicas *)\n replica_index: int Kcas.Loc.t;\n selection: replica_selection;\n mutable in_transaction: bool; (* or use domain-local storage *)\n}\n\ntype intent = Read | Write\n```\n\n### Core API\n```ocaml\n(* Create CQRS-aware pool pair *)\nval create : (module Driver.S with type connection = 'conn) -\u003e 'conn config -\u003e 'conn t\n\n(* Auto-routing based on operation type *)\nval with_read : 'conn t -\u003e ('conn -\u003e 'a) -\u003e ('a, Pool.pool_error) result\nval with_write : 'conn t -\u003e ('conn -\u003e 'a) -\u003e ('a, Pool.pool_error) result\n\n(* Explicit routing *)\nval with_primary : 'conn t -\u003e ('conn -\u003e 'a) -\u003e ('a, Pool.pool_error) result\nval with_replica : 'conn t -\u003e ('conn -\u003e 'a) -\u003e ('a, Pool.pool_error) result\n\n(* Transaction - always uses primary, pins connection *)\nval transaction : 'conn t -\u003e ('conn -\u003e ('a, Error.db_error) result) -\u003e ('a, Error.db_error) result\n```\n\n### Functor Interface\n```ocaml\nmodule Make (D : Driver.S) : sig\n type t = D.connection t\n val create : D.connection config -\u003e t\n (* ... rest of API *)\nend\n```\n\n## Routing Logic\n\n### Read Operations (go to replica if available)\n- Repo.get, get_opt\n- Repo.all\n- Repo.all_query, one_query, one_query_opt\n\n### Write Operations (always go to primary)\n- Repo.insert, insert_returning\n- Repo.update\n- Repo.delete\n- Repo.insert_query, update_query, delete_query\n- All transaction operations\n\n### Query-Based Auto-Detection\n```ocaml\nlet route_query query =\n match query.Query.query_type with\n | Query.Select -\u003e Read\n | Query.Insert | Query.Update | Query.Delete -\u003e Write\n```\n\n## Edge Cases\n1. **No replicas configured**: All operations go to primary\n2. **All replicas down**: Fall back to primary with warning\n3. **Read after write**: Provide with_primary for consistency\n4. **Transaction context**: Pin all ops to primary connection\n\n## Acceptance Criteria\n- [ ] Cqrs.Make functor works with any Driver.S\n- [ ] Reads go to replicas when available\n- [ ] Writes always go to primary\n- [ ] Transactions pin to single primary connection\n- [ ] Graceful fallback when replicas unavailable\n- [ ] Round-robin across replicas","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-05T11:13:56.804354904+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:13:56.804354904+01:00","dependencies":[{"issue_id":"mlecto-1lq","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:30.146332625+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-1lq","depends_on_id":"mlecto-2gn","type":"blocks","created_at":"2026-01-05T11:16:02.678918762+01:00","created_by":"gdiazlo"}]} 3 4 {"id":"mlecto-1ud","title":"Implement Schema DSL (Mlecto.Schema)","description":"DSL for defining tables: field definitions, constraints (primary_key, not_null, unique, foreign_key, check), table_name generation.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:24.765735157+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:09:24.538059462+01:00","closed_at":"2026-01-04T01:09:24.538059462+01:00","close_reason":"Schema DSL complete: table definitions, column constraints (PrimaryKey, NotNull, Unique, Default, Check, ForeignKey), foreign key actions, timestamps helper, SQL generation.","dependencies":[{"issue_id":"mlecto-1ud","depends_on_id":"mlecto-z72","type":"blocks","created_at":"2026-01-04T00:22:02.524126267+01:00","created_by":"gdiazlo"}]} 4 5 {"id":"mlecto-1w9","title":"[EPIC] Core Type System \u0026 Schema DSL","description":"Define SQL types mapped to OCaml, schema definition DSL for tables/fields/constraints. Foundation for all other modules.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:21:05.917266549+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:44.799339065+01:00","closed_at":"2026-01-04T01:20:44.799339065+01:00","close_reason":"All child tasks completed"} 6 + {"id":"mlecto-2gn","title":"Implement MultiPool in pool.ml","description":"Extend the existing Pool module with multi-server support.\n\n## Changes to lib/pool.ml\n\n### New Types\n```ocaml\ntype server = {\n conninfo: string;\n weight: int; (* for weighted round-robin, default 1 *)\n}\n\ntype 'conn multi_config = {\n servers: server list;\n max_size_per_server: int;\n connect: string -\u003e ('conn, string) result;\n close: 'conn -\u003e unit;\n validate: ('conn -\u003e bool) option;\n}\n\ntype 'conn multi_t = {\n pools: 'conn t array;\n healthy: bool Kcas.Loc.t array;\n next_index: int Kcas.Loc.t;\n}\n```\n\n### New Functions\n- `create_multi : 'conn multi_config -\u003e 'conn multi_t`\n- `acquire_multi : 'conn multi_t -\u003e ('conn, pool_error) result`\n- `release_multi : 'conn multi_t -\u003e 'conn -\u003e unit`\n- `with_connection_multi : 'conn multi_t -\u003e ('conn -\u003e 'a) -\u003e ('a, pool_error) result`\n- `shutdown_multi : 'conn multi_t -\u003e unit`\n- `stats_multi : 'conn multi_t -\u003e stats list`\n- `mark_unhealthy : 'conn multi_t -\u003e int -\u003e unit`\n- `mark_healthy : 'conn multi_t -\u003e int -\u003e unit`\n\n### Implementation Notes\n- Use atomic counter with Kcas.Loc for lock-free round-robin\n- Skip unhealthy servers in round-robin selection\n- When all servers unhealthy, return Pool_empty error\n- Each server gets its own pool with max_size_per_server connections\n\n## Acceptance Criteria\n- [ ] All new types defined\n- [ ] Round-robin distributes evenly across healthy servers\n- [ ] Unhealthy servers are skipped\n- [ ] Backward compatible - existing Pool API unchanged\n- [ ] Unit tests pass","status":"open","priority":1,"issue_type":"task","created_at":"2026-01-05T11:13:34.299242118+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:13:34.299242118+01:00","dependencies":[{"issue_id":"mlecto-2gn","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:25.102411986+01:00","created_by":"gdiazlo"}]} 5 7 {"id":"mlecto-3ow","title":"cast_assoc and put_assoc for nested changesets","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-04T10:32:00.153765675+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T10:38:58.664932555+01:00","closed_at":"2026-01-04T10:38:58.664932555+01:00","close_reason":"Closed","dependencies":[{"issue_id":"mlecto-3ow","depends_on_id":"mlecto-d6f","type":"blocks","created_at":"2026-01-04T10:32:20.574240684+01:00","created_by":"gdiazlo"}]} 8 + {"id":"mlecto-3wf","title":"CQRS and Multi-Server Pool Support","description":"Add CQRS (Command Query Responsibility Segregation) support and multi-server connection pooling to repodb.\n\n## Goals\n1. **CQRS**: Transparent read/write splitting where reads go to replicas and writes go to primary\n2. **Multi-Server Pool**: Round-robin load balancing across multiple database servers\n3. **Backward Compatible**: Existing single-server users see no API changes\n\n## Key Features\n- Automatic routing based on query type (SELECT vs INSERT/UPDATE/DELETE)\n- Transaction pinning to primary (all ops in transaction use same connection)\n- Lock-free round-robin using kcas\n- Health checking to skip unhealthy servers\n- Fallback to primary when all replicas down\n\n## Testing\nIntegration tests with real PostgreSQL using Podman containers.","status":"open","priority":1,"issue_type":"epic","created_at":"2026-01-05T11:13:04.912905111+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:17:02.560139947+01:00"} 6 9 {"id":"mlecto-4g7","title":"Constraint error mapping from DB to changeset","status":"closed","priority":2,"issue_type":"feature","created_at":"2026-01-04T10:32:00.520652974+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T10:38:58.665466447+01:00","closed_at":"2026-01-04T10:38:58.665466447+01:00","close_reason":"Closed"} 7 10 {"id":"mlecto-5km","title":"Implement Changeset validators","description":"validate_required, validate_format (regex), validate_length (min/max/is), validate_inclusion, validate_exclusion, validate_number, validate_acceptance, validate_confirmation, validate_change (custom).","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:40.78012908+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:14:08.654694896+01:00","closed_at":"2026-01-04T01:14:08.654694896+01:00","close_reason":"Validators implemented in Changeset core: validate_required, validate_format, validate_length, validate_inclusion, validate_exclusion, validate_number, validate_acceptance, validate_confirmation, validate_change.","dependencies":[{"issue_id":"mlecto-5km","depends_on_id":"mlecto-m13","type":"blocks","created_at":"2026-01-04T00:22:03.958038572+01:00","created_by":"gdiazlo"}]} 8 11 {"id":"mlecto-7lc","title":"Implement Query builder (Mlecto.Query)","description":"SELECT, INSERT, UPDATE, DELETE builders. WHERE, JOIN, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET. Composable query pipelines.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:28.238632191+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:12:33.283719987+01:00","closed_at":"2026-01-04T01:12:33.283719987+01:00","close_reason":"Query builder complete: SELECT, INSERT, UPDATE, DELETE with WHERE, JOIN, ORDER BY, GROUP BY, HAVING, LIMIT, OFFSET. ON CONFLICT for upserts. RETURNING clause. Full SQL generation.","dependencies":[{"issue_id":"mlecto-7lc","depends_on_id":"mlecto-1ud","type":"blocks","created_at":"2026-01-04T00:22:02.538979501+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-7lc","depends_on_id":"mlecto-nv7","type":"blocks","created_at":"2026-01-04T00:22:02.547726588+01:00","created_by":"gdiazlo"}]} ··· 11 14 {"id":"mlecto-at2","title":"[EPIC] Migration System","description":"Versioned database migrations with up/down support. DSL for create_table, add_column, create_index, etc.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:21:12.288036672+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:44.812043932+01:00","closed_at":"2026-01-04T01:20:44.812043932+01:00","close_reason":"All child tasks completed"} 12 15 {"id":"mlecto-c1o","title":"Real Repo execution via Caqti/Eio","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T10:31:58.698847822+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T10:38:58.663072869+01:00","closed_at":"2026-01-04T10:38:58.663072869+01:00","close_reason":"Closed"} 13 16 {"id":"mlecto-d6f","title":"Schema associations (has_many, belongs_to, has_one, many_to_many)","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T10:31:59.069873954+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T10:38:58.661985306+01:00","closed_at":"2026-01-04T10:38:58.661985306+01:00","close_reason":"Closed"} 17 + {"id":"mlecto-dg4","title":"Add unit tests for MultiPool","description":"Add comprehensive unit tests for the MultiPool functionality.\n\n## New File: test/test_multi_pool.ml\n\n### Test Cases\n\n1. **Round-Robin Distribution**\n - Create multi-pool with 3 servers\n - Acquire N connections, verify even distribution\n - Assert each server gets ~N/3 connections\n\n2. **Health Check Skipping**\n - Create multi-pool with 3 servers\n - Mark one server unhealthy\n - Verify connections only go to healthy servers\n - Mark all unhealthy -\u003e expect Pool_empty error\n\n3. **Mark Healthy/Unhealthy**\n - Test mark_unhealthy and mark_healthy functions\n - Verify atomic updates work correctly\n\n4. **with_connection_multi**\n - Test automatic release on success\n - Test automatic release on exception\n\n5. **shutdown_multi**\n - Verify all pools are closed\n - Verify subsequent acquire returns Pool_closed\n\n6. **stats_multi**\n - Verify stats aggregation across all pools\n\n7. **Edge Cases**\n - Single server multi-pool (degenerate case)\n - Empty server list (should error)\n - All servers fail to connect\n\n## Mock Setup\nUse similar mock pattern to existing test_pool.ml:\n```ocaml\ntype mock_conn = { id : int; server_idx : int; mutable closed : bool }\n\nlet mock_multi_config ?(servers = 3) ?(max_size = 2) () = ...\n```\n\n## Acceptance Criteria\n- [ ] All test cases implemented\n- [ ] Tests pass with `dune test`\n- [ ] Edge cases covered","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-05T11:14:18.326292543+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:14:18.326292543+01:00","dependencies":[{"issue_id":"mlecto-dg4","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:35.191030964+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-dg4","depends_on_id":"mlecto-2gn","type":"blocks","created_at":"2026-01-05T11:16:07.722230492+01:00","created_by":"gdiazlo"}]} 18 + {"id":"mlecto-e6g","title":"Set up Podman-based PostgreSQL integration tests","description":"Create integration test infrastructure using Podman to run real PostgreSQL instances.\n\n## Goal\nTest CQRS and MultiPool against real PostgreSQL primary + replicas.\n\n## Infrastructure\n\n### Podman Compose File: test/integration/docker-compose.yml\n```yaml\nversion: '3.8'\nservices:\n primary:\n image: postgres:16\n environment:\n POSTGRES_USER: repodb\n POSTGRES_PASSWORD: repodb\n POSTGRES_DB: repodb_test\n ports:\n - '5432:5432'\n command: \u003e\n postgres\n -c wal_level=replica\n -c max_wal_senders=3\n -c max_replication_slots=3\n -c hot_standby=on\n volumes:\n - primary_data:/var/lib/postgresql/data\n healthcheck:\n test: ['CMD-SHELL', 'pg_isready -U repodb']\n interval: 5s\n timeout: 5s\n retries: 5\n\n replica1:\n image: postgres:16\n environment:\n POSTGRES_USER: repodb\n POSTGRES_PASSWORD: repodb\n POSTGRES_DB: repodb_test\n ports:\n - '5433:5432'\n depends_on:\n primary:\n condition: service_healthy\n command: \u003e\n bash -c \"\n until pg_basebackup -h primary -D /var/lib/postgresql/data -U repodb -Fp -Xs -P -R; do\n sleep 1\n done\n chmod 700 /var/lib/postgresql/data\n postgres\n \"\n volumes:\n - replica1_data:/var/lib/postgresql/data\n\n replica2:\n image: postgres:16\n environment:\n POSTGRES_USER: repodb\n POSTGRES_PASSWORD: repodb\n POSTGRES_DB: repodb_test\n ports:\n - '5434:5432'\n depends_on:\n primary:\n condition: service_healthy\n command: \u003e\n bash -c \"\n until pg_basebackup -h primary -D /var/lib/postgresql/data -U repodb -Fp -Xs -P -R; do\n sleep 1\n done\n chmod 700 /var/lib/postgresql/data\n postgres\n \"\n volumes:\n - replica2_data:/var/lib/postgresql/data\n\nvolumes:\n primary_data:\n replica1_data:\n replica2_data:\n```\n\n### Test Runner Script: test/integration/run_tests.sh\n```bash\n#!/bin/bash\nset -e\n\n# Start PostgreSQL cluster\npodman-compose -f test/integration/docker-compose.yml up -d\n\n# Wait for replicas to catch up\nsleep 10\n\n# Run integration tests\ndune exec test/integration/test_cqrs_integration.exe\n\n# Cleanup\npodman-compose -f test/integration/docker-compose.yml down -v\n```\n\n### Integration Test File: test/integration/test_cqrs_integration.ml\nTest cases:\n1. Write to primary, read from replica (verify replication)\n2. Transaction isolation (all ops on primary)\n3. Replica lag handling\n4. Connection pooling under load\n5. Failover when replica dies\n\n## Acceptance Criteria\n- [ ] Podman compose file creates working PG cluster\n- [ ] Replication works between primary and replicas\n- [ ] Integration tests pass\n- [ ] Cleanup script removes all containers/volumes","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-05T11:14:57.307024006+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:14:57.307024006+01:00","dependencies":[{"issue_id":"mlecto-e6g","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:45.282902638+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-e6g","depends_on_id":"mlecto-2gn","type":"blocks","created_at":"2026-01-05T11:16:17.810402811+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-e6g","depends_on_id":"mlecto-1lq","type":"blocks","created_at":"2026-01-05T11:16:22.852706838+01:00","created_by":"gdiazlo"}]} 14 19 {"id":"mlecto-flv","title":"Implement Multi module (Mlecto.Multi)","description":"Chain operations: Multi.new |\u003e Multi.insert |\u003e Multi.update |\u003e Multi.run. Named operations, access previous results, atomic execution.","status":"closed","priority":2,"issue_type":"task","created_at":"2026-01-04T00:21:50.583520239+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:32.435485703+01:00","closed_at":"2026-01-04T01:20:32.435485703+01:00","close_reason":"Multi with operation chaining, named results, merge, validate, execute_sync","dependencies":[{"issue_id":"mlecto-flv","depends_on_id":"mlecto-8ki","type":"blocks","created_at":"2026-01-04T00:22:07.51964609+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-flv","depends_on_id":"mlecto-hwj","type":"blocks","created_at":"2026-01-04T00:22:07.527438964+01:00","created_by":"gdiazlo"}]} 15 20 {"id":"mlecto-gfo","title":"[EPIC] Changeset System","description":"Casting, validation, and error accumulation. Separate validation from persistence with composable validators.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:21:07.363157981+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:44.808207655+01:00","closed_at":"2026-01-04T01:20:44.808207655+01:00","close_reason":"All child tasks completed"} 16 21 {"id":"mlecto-gxh","title":"Implement Changeset constraints","description":"unique_constraint, foreign_key_constraint, check_constraint, exclusion_constraint. Convert DB errors to changeset errors.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:42.297745588+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:14:08.663238895+01:00","closed_at":"2026-01-04T01:14:08.663238895+01:00","close_reason":"Constraints implemented in Changeset core: unique_constraint, foreign_key_constraint, check_constraint.","dependencies":[{"issue_id":"mlecto-gxh","depends_on_id":"mlecto-m13","type":"blocks","created_at":"2026-01-04T00:22:03.965402454+01:00","created_by":"gdiazlo"}]} 17 22 {"id":"mlecto-hw6","title":"[EPIC] mlecto - Ecto-like database toolkit for OCaml","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:20:55.991644266+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:49.927971061+01:00","closed_at":"2026-01-04T01:20:49.927971061+01:00","close_reason":"Core mlecto library complete: Type, Schema, Expr, Query, Changeset, Repo, Migration, Multi modules implemented"} 18 23 {"id":"mlecto-hwj","title":"Implement Repo transactions","description":"transaction/1 function wrapping Caqti transactions. Automatic rollback on error. Nested transaction support (savepoints).","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:46.455864854+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:19:20.791036484+01:00","closed_at":"2026-01-04T01:19:20.791036484+01:00","close_reason":"Transaction state machine with BEGIN/COMMIT/ROLLBACK and nested savepoint support","dependencies":[{"issue_id":"mlecto-hwj","depends_on_id":"mlecto-8ki","type":"blocks","created_at":"2026-01-04T00:22:05.217260647+01:00","created_by":"gdiazlo"}]} 24 + {"id":"mlecto-hwq","title":"Write CQRS documentation","description":"Create documentation for the CQRS and MultiPool features.\n\n## New File: docs/cqrs.md\n\n### Sections\n\n1. **Overview**\n - What is CQRS\n - When to use it\n - How repodb implements it\n\n2. **Quick Start**\n ```ocaml\n (* Basic CQRS setup *)\n module Cqrs = Cqrs.Make(Repodb_postgresql)\n\n let cqrs = Cqrs.create {\n primary_conninfo = \"postgresql://primary/mydb\";\n primary_max_size = 10;\n replica_conninfos = [\n \"postgresql://replica1/mydb\";\n \"postgresql://replica2/mydb\";\n ];\n replica_max_size_each = 5;\n replica_selection = RoundRobin;\n validate = None;\n }\n ```\n\n3. **Read Operations**\n - Which operations are routed to replicas\n - How to force read from primary\n\n4. **Write Operations**\n - Always go to primary\n - Transaction handling\n\n5. **Explicit Routing**\n - with_primary for read-after-write consistency\n - with_replica for specific use cases\n\n6. **Configuration Options**\n - Full config reference\n - Replica selection strategies\n\n7. **Best Practices**\n - When to use CQRS\n - Handling replica lag\n - Monitoring\n\n## Update: docs/pool.md\n\nAdd section on MultiPool:\n- Multi-server configuration\n- Round-robin behavior\n- Health checking\n\n## Acceptance Criteria\n- [ ] docs/cqrs.md complete with examples\n- [ ] docs/pool.md updated for MultiPool\n- [ ] README.md mentions new features\n- [ ] Examples compile and work","status":"open","priority":3,"issue_type":"task","created_at":"2026-01-05T11:15:13.481713538+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:15:13.481713538+01:00","dependencies":[{"issue_id":"mlecto-hwq","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:50.325885062+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-hwq","depends_on_id":"mlecto-1lq","type":"blocks","created_at":"2026-01-05T11:16:27.893913307+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-hwq","depends_on_id":"mlecto-2gn","type":"blocks","created_at":"2026-01-05T11:16:32.936486931+01:00","created_by":"gdiazlo"}]} 19 25 {"id":"mlecto-l6s","title":"[EPIC] Query DSL","description":"Type-safe SQL query builder using GADTs and phantom types. Composable queries as first-class values.","status":"closed","priority":1,"issue_type":"feature","created_at":"2026-01-04T00:21:08.537372834+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:20:44.809745142+01:00","closed_at":"2026-01-04T01:20:44.809745142+01:00","close_reason":"All child tasks completed"} 26 + {"id":"mlecto-lg9","title":"Add unit tests for Cqrs module","description":"Add comprehensive unit tests for the CQRS routing functionality.\n\n## New File: test/test_cqrs.ml\n\n### Test Cases\n\n1. **Read Routing**\n - Configure CQRS with primary + replicas\n - Execute with_read -\u003e verify goes to replica\n - Execute Repo.all -\u003e verify goes to replica\n\n2. **Write Routing**\n - Execute with_write -\u003e verify goes to primary\n - Execute Repo.insert -\u003e verify goes to primary\n\n3. **Query-Based Routing**\n - SELECT query -\u003e replica\n - INSERT query -\u003e primary\n - UPDATE query -\u003e primary\n - DELETE query -\u003e primary\n\n4. **Transaction Pinning**\n - Start transaction -\u003e all ops go to primary\n - Even read operations inside transaction go to primary\n - Same connection used throughout transaction\n\n5. **No Replicas Mode**\n - Configure with empty replica list\n - All operations go to primary\n - No errors, works like single-server pool\n\n6. **Replica Fallback**\n - All replicas unhealthy\n - Reads fall back to primary\n - Warning logged (if logging added)\n\n7. **with_primary / with_replica**\n - Explicit routing works as expected\n - with_primary always uses primary even for reads\n - with_replica errors if no replicas configured\n\n8. **Round-Robin Across Replicas**\n - Multiple replicas configured\n - Reads distributed evenly\n\n## Mock Setup\n```ocaml\n(* Track which pool each operation went to *)\ntype routing_tracker = {\n mutable primary_calls: int;\n mutable replica_calls: int list; (* per-replica counts *)\n}\n```\n\n## Acceptance Criteria\n- [ ] All test cases implemented\n- [ ] Tests pass with `dune test`\n- [ ] Transaction isolation tested\n- [ ] Fallback behavior tested","status":"open","priority":2,"issue_type":"task","created_at":"2026-01-05T11:14:37.578081717+01:00","created_by":"gdiazlo","updated_at":"2026-01-05T11:14:37.578081717+01:00","dependencies":[{"issue_id":"mlecto-lg9","depends_on_id":"mlecto-3wf","type":"blocks","created_at":"2026-01-05T11:15:40.237975143+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-lg9","depends_on_id":"mlecto-1lq","type":"blocks","created_at":"2026-01-05T11:16:12.765564629+01:00","created_by":"gdiazlo"}]} 20 27 {"id":"mlecto-m13","title":"Implement Changeset core (Mlecto.Changeset)","description":"Changeset record type: data, changes, errors, valid?, params, required. Cast function for external params. Change function for internal data.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:38.875652686+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:13:59.358336893+01:00","closed_at":"2026-01-04T01:13:59.358336893+01:00","close_reason":"Changeset core complete: type-safe field handling, validations (required, format, length, inclusion, exclusion, number, acceptance, confirmation, custom), constraints (unique, foreign_key, check), error handling.","dependencies":[{"issue_id":"mlecto-m13","depends_on_id":"mlecto-z72","type":"blocks","created_at":"2026-01-04T00:22:03.941717052+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-m13","depends_on_id":"mlecto-1ud","type":"blocks","created_at":"2026-01-04T00:22:03.95059939+01:00","created_by":"gdiazlo"}]} 21 28 {"id":"mlecto-nv7","title":"Implement Expression types (Mlecto.Expr)","description":"GADTs for SQL expressions: literals, columns, operators (+, -, =, \u003c\u003e, AND, OR), function calls, casts. Type-safe expression composition.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:26.662064082+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:11:24.632729849+01:00","closed_at":"2026-01-04T01:11:24.632729849+01:00","close_reason":"Expression types complete: GADT for all SQL expressions, operators (comparison, logical, arithmetic), functions (aggregate, string, date, math), CASE, BETWEEN, IN, CAST, SQL generation.","dependencies":[{"issue_id":"mlecto-nv7","depends_on_id":"mlecto-z72","type":"blocks","created_at":"2026-01-04T00:22:02.531940181+01:00","created_by":"gdiazlo"}]} 22 29 {"id":"mlecto-pvs","title":"Implement Migration runner","description":"schema_migrations table, version tracking, up/down execution, migrate/rollback commands, migration status reporting.","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-04T00:21:48.916746145+01:00","created_by":"gdiazlo","updated_at":"2026-01-04T01:18:41.847908163+01:00","closed_at":"2026-01-04T01:18:41.847908163+01:00","close_reason":"Migration runner with version tracking, plan_migrate, plan_rollback, format_status","dependencies":[{"issue_id":"mlecto-pvs","depends_on_id":"mlecto-uek","type":"blocks","created_at":"2026-01-04T00:22:06.667807096+01:00","created_by":"gdiazlo"},{"issue_id":"mlecto-pvs","depends_on_id":"mlecto-8ki","type":"blocks","created_at":"2026-01-04T00:22:06.6757586+01:00","created_by":"gdiazlo"}]}