|
29 | 29 | {"id":"el-xih","title":"RETURNING Enhancement for Batch Operations","description":"Works for single operations, not batches. libSQL 3.45.1 supports RETURNING clause on INSERT/UPDATE/DELETE.\n\nDesired API:\n {count, rows} = Repo.insert_all(User, users, returning: [:id, :inserted_at])\n # Returns all inserted rows with IDs\n\nPRIORITY: Recommended as #9 in implementation order.\n\nEffort: 3-4 days.","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-30T17:35:53.70112+11:00","created_by":"drew","updated_at":"2025-12-30T17:43:32.892591+11:00"} |
30 | 30 | {"id":"el-xiy","title":"Implement Authorizer Hook for Row-Level Security","description":"Add support for authorizer hooks to enable row-level security and multi-tenant applications.\n\n**Context**: Authorizer hooks allow fine-grained access control at the SQL operation level. Essential for multi-tenant applications and row-level security (RLS).\n\n**Missing API** (from FEATURE_CHECKLIST.md):\n- authorizer() - Register callback that approves/denies SQL operations\n\n**Use Cases**:\n\n**1. Multi-Tenant Row-Level Security**:\n```elixir\n# Enforce tenant isolation at database level\nEctoLibSql.set_authorizer(repo, fn action, table, column, _context -\u003e\n case action do\n :read when table == \"users\" -\u003e\n if current_tenant_can_read?(table) do\n :ok\n else\n {:error, :unauthorized}\n end\n \n :write when table in [\"users\", \"posts\"] -\u003e\n if current_tenant_can_write?(table) do\n :ok\n else\n {:error, :unauthorized}\n end\n \n _ -\u003e :ok\n end\nend)\n```\n\n**2. Column-Level Access Control**:\n```elixir\n# Restrict access to sensitive columns\nEctoLibSql.set_authorizer(repo, fn action, table, column, _context -\u003e\n if column == \"ssn\" and !current_user_is_admin?() do\n {:error, :forbidden}\n else\n :ok\n end\nend)\n```\n\n**3. Audit Sensitive Operations**:\n```elixir\n# Log all DELETE operations\nEctoLibSql.set_authorizer(repo, fn action, table, _column, _context -\u003e\n if action == :delete do\n AuditLog.log_delete(current_user(), table)\n end\n :ok\nend)\n```\n\n**4. Prevent Dangerous Operations**:\n```elixir\n# Block DROP TABLE in production\nEctoLibSql.set_authorizer(repo, fn action, _table, _column, _context -\u003e\n if action in [:drop_table, :drop_index] and production?() do\n {:error, :forbidden}\n else\n :ok\n end\nend)\n```\n\n**SQLite Authorizer Actions**:\n- :read - SELECT from table/column\n- :insert - INSERT into table\n- :update - UPDATE table/column\n- :delete - DELETE from table\n- :create_table, :drop_table\n- :create_index, :drop_index\n- :alter_table\n- :transaction\n- And many more...\n\n**Implementation Challenge**:\nSimilar to update_hook, requires Rust → Elixir callbacks with additional complexity:\n- Authorizer must return result synchronously (blocking)\n- Called very frequently (every SQL operation)\n- Performance critical (adds overhead to all queries)\n- Thread-safety for concurrent connections\n\n**Implementation Options**:\n\n**Option 1: Synchronous Callback (Required)**:\n- Authorizer MUST return result synchronously\n- Block Rust thread while waiting for Elixir\n- Use message passing with timeout\n- Handle timeout as :deny\n\n**Option 2: Pre-Compiled Rules (Performance)**:\n- Instead of arbitrary Elixir callback\n- Define rules in config\n- Compile to Rust decision tree\n- Much faster but less flexible\n\n**Proposed Implementation (Hybrid)**:\n\n1. **Add NIF** (native/ecto_libsql/src/connection.rs):\n ```rust\n #[rustler::nif]\n fn set_authorizer(conn_id: \u0026str, pid: Pid) -\u003e NifResult\u003cAtom\u003e {\n // Store pid in connection metadata\n // Register libsql authorizer\n // On auth check: send sync message to pid, wait for response\n }\n \n #[rustler::nif]\n fn remove_authorizer(conn_id: \u0026str) -\u003e NifResult\u003cAtom\u003e\n ```\n\n2. **Add Elixir wrapper** (lib/ecto_libsql/native.ex):\n ```elixir\n def set_authorizer(state, callback_fn) do\n pid = spawn(fn -\u003e authorizer_loop(callback_fn) end)\n set_authorizer_nif(state.conn_id, pid)\n end\n \n defp authorizer_loop(callback_fn) do\n receive do\n {:authorize, from, action, table, column, context} -\u003e\n result = callback_fn.(action, table, column, context)\n send(from, {:auth_result, result})\n authorizer_loop(callback_fn)\n end\n end\n ```\n\n3. **Rust authorizer implementation**:\n ```rust\n fn authorizer_callback(action: i32, table: \u0026str, column: \u0026str) -\u003e i32 {\n // Send message to Elixir pid\n // Wait for response with timeout (100ms)\n // Return SQLITE_OK or SQLITE_DENY\n // On timeout: SQLITE_DENY (safe default)\n }\n ```\n\n**Performance Considerations**:\n- ⚠️ Adds ~1-5ms overhead per SQL operation\n- Critical for read-heavy workloads\n- Consider caching auth decisions\n- Consider pre-compiled rules for performance-critical paths\n\n**Files**:\n- native/ecto_libsql/src/connection.rs (authorizer implementation)\n- native/ecto_libsql/src/models.rs (store authorizer pid)\n- lib/ecto_libsql/native.ex (wrapper and authorizer process)\n- lib/ecto/adapters/libsql.ex (public API)\n- test/authorizer_test.exs (new tests)\n- AGENTS.md (update API docs)\n\n**Acceptance Criteria**:\n- [ ] set_authorizer() NIF implemented\n- [ ] remove_authorizer() NIF implemented\n- [ ] Authorizer can approve operations (return :ok)\n- [ ] Authorizer can deny operations (return {:error, reason})\n- [ ] Authorizer receives correct action types\n- [ ] Authorizer timeout doesn't crash VM\n- [ ] Performance overhead \u003c 5ms per operation\n- [ ] Comprehensive tests including error cases\n- [ ] Multi-tenant example in documentation\n\n**Test Requirements**:\n```elixir\ntest \"authorizer can block SELECT operations\" do\n EctoLibSql.set_authorizer(repo, fn action, _table, _column, _context -\u003e\n if action == :read do\n {:error, :forbidden}\n else\n :ok\n end\n end)\n \n assert {:error, _} = Repo.query(\"SELECT * FROM users\")\nend\n\ntest \"authorizer allows approved operations\" do\n EctoLibSql.set_authorizer(repo, fn _action, _table, _column, _context -\u003e\n :ok\n end)\n \n assert {:ok, _} = Repo.query(\"SELECT * FROM users\")\nend\n\ntest \"authorizer timeout defaults to deny\" do\n EctoLibSql.set_authorizer(repo, fn _action, _table, _column, _context -\u003e\n Process.sleep(200) # Timeout is 100ms\n :ok\n end)\n \n assert {:error, _} = Repo.query(\"SELECT * FROM users\")\nend\n```\n\n**References**:\n- FEATURE_CHECKLIST.md section \"Medium Priority\" item 5\n- LIBSQL_FEATURE_MATRIX_FINAL.md section 10\n- libsql API: conn.authorizer()\n- SQLite authorizer docs: https://www.sqlite.org/c3ref/set_authorizer.html\n\n**Dependencies**:\n- Similar to update_hook implementation\n- Can share callback infrastructure\n\n**Priority**: P2 - Enables advanced security patterns\n**Effort**: 5-7 days (complex synchronous Rust→Elixir callback)\n**Complexity**: High (performance-critical, blocking callbacks)\n**Security**: Critical - must handle timeouts safely","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-30T17:45:14.12598+11:00","created_by":"drew","updated_at":"2025-12-30T17:45:14.12598+11:00"} |
31 | 31 | {"id":"el-xkc","title":"Implement Update Hook for Change Data Capture","description":"Add support for update hooks to enable change data capture and real-time notifications.\n\n**Context**: Update hooks allow applications to receive notifications when database rows are modified. Critical for real-time updates, cache invalidation, and event sourcing patterns.\n\n**Missing API** (from FEATURE_CHECKLIST.md):\n- add_update_hook() - Register callback for INSERT/UPDATE/DELETE operations\n\n**Use Cases**:\n\n**1. Real-Time Updates**:\n```elixir\n# Broadcast changes via Phoenix PubSub\nEctoLibSql.set_update_hook(repo, fn action, _db, table, rowid -\u003e\n Phoenix.PubSub.broadcast(MyApp.PubSub, \"table:\\#{table}\", {action, rowid})\nend)\n```\n\n**2. Cache Invalidation**:\n```elixir\n# Invalidate cache on changes\nEctoLibSql.set_update_hook(repo, fn _action, _db, table, rowid -\u003e\n Cache.delete(\"table:\\#{table}:row:\\#{rowid}\")\nend)\n```\n\n**3. Audit Logging**:\n```elixir\n# Log all changes for compliance\nEctoLibSql.set_update_hook(repo, fn action, db, table, rowid -\u003e\n AuditLog.insert(%{action: action, db: db, table: table, rowid: rowid})\nend)\n```\n\n**4. Event Sourcing**:\n```elixir\n# Append to event stream\nEctoLibSql.set_update_hook(repo, fn action, _db, table, rowid -\u003e\n EventStore.append(table, %{type: action, rowid: rowid})\nend)\n```\n\n**Implementation Challenge**: \nCallbacks from Rust → Elixir are complex with NIFs. Requires:\n1. Register Elixir pid/function reference in Rust\n2. Send messages from Rust to Elixir process\n3. Handle callback results back in Rust (if needed)\n4. Thread-safety considerations for concurrent connections\n\n**Implementation Options**:\n\n**Option 1: Message Passing (Recommended)**:\n- Store Elixir pid in connection registry\n- Send messages to pid when updates occur\n- Elixir process handles messages asynchronously\n- No blocking in Rust code\n\n**Option 2: Synchronous Callback**:\n- Store function reference in registry\n- Call Elixir function from Rust\n- Wait for result (blocking)\n- More complex, potential deadlocks\n\n**Proposed Implementation (Option 1)**:\n\n1. **Add NIF** (native/ecto_libsql/src/connection.rs):\n ```rust\n #[rustler::nif]\n fn set_update_hook(conn_id: \u0026str, pid: Pid) -\u003e NifResult\u003cAtom\u003e {\n // Store pid in connection metadata\n // Register libsql update hook\n // On update: send message to pid\n }\n \n #[rustler::nif]\n fn remove_update_hook(conn_id: \u0026str) -\u003e NifResult\u003cAtom\u003e\n ```\n\n2. **Add Elixir wrapper** (lib/ecto_libsql/native.ex):\n ```elixir\n def set_update_hook(state, callback_fn) do\n pid = spawn(fn -\u003e update_hook_loop(callback_fn) end)\n set_update_hook_nif(state.conn_id, pid)\n end\n \n defp update_hook_loop(callback_fn) do\n receive do\n {:update, action, db, table, rowid} -\u003e\n callback_fn.(action, db, table, rowid)\n update_hook_loop(callback_fn)\n end\n end\n ```\n\n3. **Update connection lifecycle**:\n - Clean up hook process on connection close\n - Handle hook process crashes gracefully\n - Monitor hook process\n\n**Files**:\n- native/ecto_libsql/src/connection.rs (hook implementation)\n- native/ecto_libsql/src/models.rs (store hook pid in LibSQLConn)\n- lib/ecto_libsql/native.ex (wrapper and hook process)\n- lib/ecto/adapters/libsql.ex (public API)\n- test/update_hook_test.exs (new tests)\n- AGENTS.md (update API docs)\n\n**Acceptance Criteria**:\n- [ ] set_update_hook() NIF implemented\n- [ ] remove_update_hook() NIF implemented\n- [ ] Hook receives INSERT notifications\n- [ ] Hook receives UPDATE notifications\n- [ ] Hook receives DELETE notifications\n- [ ] Hook process cleaned up on connection close\n- [ ] Hook errors don't crash BEAM VM\n- [ ] Comprehensive tests including error cases\n- [ ] Documentation with examples\n\n**Test Requirements**:\n```elixir\ntest \"update hook receives INSERT notifications\" do\n ref = make_ref()\n EctoLibSql.set_update_hook(repo, fn action, db, table, rowid -\u003e\n send(self(), {ref, action, db, table, rowid})\n end)\n \n Repo.query(\"INSERT INTO users (name) VALUES ('Alice')\")\n \n assert_receive {^ref, :insert, \"main\", \"users\", rowid}\nend\n\ntest \"update hook doesn't crash VM on callback error\" do\n EctoLibSql.set_update_hook(repo, fn _, _, _, _ -\u003e\n raise \"callback error\"\n end)\n \n # Should not crash\n Repo.query(\"INSERT INTO users (name) VALUES ('Alice')\")\nend\n```\n\n**References**:\n- FEATURE_CHECKLIST.md section \"Medium Priority\" item 6\n- LIBSQL_FEATURE_MATRIX_FINAL.md section 10\n- libsql API: conn.update_hook()\n\n**Dependencies**:\n- None (can implement independently)\n\n**Priority**: P2 - Enables real-time and event-driven patterns\n**Effort**: 5-7 days (complex Rust→Elixir callback mechanism)\n**Complexity**: High (requires careful thread-safety design)","status":"open","priority":2,"issue_type":"feature","created_at":"2025-12-30T17:44:39.628+11:00","created_by":"drew","updated_at":"2025-12-30T17:44:39.628+11:00"} |
| 32 | +{"id":"el-yr6","title":"Strengthen security test validation","status":"closed","priority":1,"issue_type":"task","created_at":"2026-01-01T14:16:50.897859+11:00","created_by":"drew","updated_at":"2026-01-01T15:13:20.408399+11:00","closed_at":"2026-01-01T15:13:20.408399+11:00","close_reason":"Closed","labels":["security","testing","tests"]} |
32 | 33 | {"id":"el-z8u","title":"STRICT Tables (Type Enforcement)","description":"Not supported in migrations. SQLite 3.37+ (2021), libSQL 3.45.1 fully supports STRICT tables. Allowed types: INT, INTEGER, BLOB, TEXT, REAL. Rejects NULL types, unrecognised types, and generic types like TEXT(50) or DATE.\n\nDesired API:\n create table(:users, strict: true) do\n add :id, :integer, primary_key: true\n add :name, :string # Now MUST be text, not integer!\n end\n\nPRIORITY: Recommended as #5 in implementation order.\n\nEffort: 2-3 days.","status":"closed","priority":2,"issue_type":"feature","created_at":"2025-12-30T17:35:51.561346+11:00","created_by":"drew","updated_at":"2026-01-01T10:30:45.787433+11:00","closed_at":"2026-01-01T10:30:45.787433+11:00","close_reason":"Implemented STRICT Tables support in migrations. Tables now support strict: true option to enforce column type safety. Documentation added to AGENTS.md covering benefits, allowed types, usage examples, and error handling."} |
0 commit comments