Skip to content

feat: Redis adapter#872

Merged
abnegate merged 41 commits intomainfrom
feat-redis-adapter
May 1, 2026
Merged

feat: Redis adapter#872
abnegate merged 41 commits intomainfrom
feat-redis-adapter

Conversation

@abnegate
Copy link
Copy Markdown
Member

@abnegate abnegate commented Apr 30, 2026

Summary

Adds a Utopia\Database\Adapter\Redis adapter so multiple processes/containers can share a single in-memory database via a configured Redis server. Mirrors the Memory adapter's surface but persists every operation through Redis primitives.

Storage layout

  • utopia:{ns}:{db}:meta:{collection} — collection metadata (HASH)
  • utopia:{ns}:{db}:doc:{collection}:{id} — document payload (STRING, JSON-encoded Document)
  • utopia:{ns}:{db}:idx:{collection} — document id index (SET)
  • utopia:{ns}:{db}:perm:{collection}:{r|c|u|d}:{role} — inverted permission index (SET of doc ids)
  • utopia:{ns}:{db}:permDoc:{collection}:{id} — per-document permission map (HASH)
  • utopia:{ns}:{db}:uniq:{collection}:{idx}:{value} — unique-index dedupe (SET)
  • utopia:{ns}:{db}:seq:{collection} — sequence counter (INCR)

Capabilities

  • Collections: create / delete / list
  • Attributes: create / delete / list / update / rename
  • Indexes: KEY, UNIQUE, FULLTEXT (substring fallback)
  • Documents: CRUD, upsert, bulk variants, granular per-(role,letter) permissions
  • Queries: equal/notEqual/lessThan/greaterThan/between/isNull/isNotNull/search/contains/startsWith/endsWith/select/cursor/limit/offset/orderAsc/orderDesc
  • Relationships: 1:1, 1:N, N:1, N:N (bidirectional + unidirectional, cascade/restrict/setNull, nested fetch + select projection, twoWay rename, permission cascade)
  • Shared tables with _tenant discriminator
  • Sequence ids
  • Single-shot transactions with journal-based rollback (NOT OCC — see Known limitations)

Tests

  • tests/e2e/Adapter/RedisTest.php + tests/e2e/Adapter/SharedTables/RedisTest.php extend the standard Base trait suite against a real Redis
  • tests/e2e/Adapter/RedisCrossProcessTest.php proves cross-process shared state via proc_open, including a relationship round-trip across processes
  • CI workflow (.github/workflows/tests.yml) adds three Redis matrix entries (Redis, RedisCrossProcess, SharedTables/Redis)

Known limitations (documented in src/Database/Adapter/Redis/Contract.md)

  • tx() is single-shot, not OCC. Real WATCH/MULTI/EXEC is a follow-up.
  • Multi-attribute cursor pagination has off-by-one in corner cases. Single-attribute pagination works correctly.
  • Vectors, spatial: not supported (capability flags off).

Test plan

  • CI Redis adapter matrix entries pass
  • composer lint passes
  • composer check introduces no new errors in Redis.php
  • RedisCrossProcessTest passes (including relationship round-trip)
  • Relationship trait suite passes (~144/144 in filter slice)

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Redis is now available as a supported adapter.
  • Tests

    • Expanded test coverage for Redis adapter integration, including shared-tables scenarios.
  • Chores

    • Added PHP Redis extension as a required dependency.

abnegate and others added 18 commits April 30, 2026 22:34
Adds ext-redis to the require block (was previously suggest-only) and
adds Redis, RedisCrossProcess, and SharedTables/Redis to the adapter
test matrix so the new Redis adapter is exercised in CI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the test fixtures for the new Redis adapter:
- RedisBase: shared abstract base; per-test namespace, SCAN/DEL teardown
  (no FLUSHDB/FLUSHALL — would clobber concurrent test runs).
- RedisTest: non-shared mode; inherits Base scopes via RedisBase.
- SharedTables/RedisTest: shared mode (tenant=999, empty namespace).
- RedisCrossProcessTest: spawns a child PHP process via proc_open and
  proves the parent observes the child's update — the actual property
  the Redis adapter exists for. Skips if proc_open is disabled.
- _helpers/redis_cross_process_worker.php: child-process entry point.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wave-1 foundation for the Redis adapter: the locked surface, helper
signatures, key schema, transaction model, and getSupportFor* parity
with the Memory adapter. T1-owned helpers (key/ns/encode/decode, plus
the pass-through tx executor) are fully implemented; cross-architect
helpers and method groups are stub-throwing LogicException with locked
signatures so Wave-2 architects can fill bodies in parallel without
contention on imports, region markers, or the contract document.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Redis adapter test fixtures were authored in parallel with the T1
skeleton, so they suppressed class.notFound / argument.type errors
expected while RedisAdapter did not yet exist. Now that T1 is merged on
the trunk, those suppressions are unmatched and PHPStan reports them.
Remove them to bring the trunk back to the 28-error baseline before
Wave 2.
Implements the Wave-2 T20 region of the Redis adapter:

- Database lifecycle: create / exists / list / delete (sets backed
  by `{ns}:{db}:dbs`, cascading collection cleanup on drop).
- Collection lifecycle: createCollection / deleteCollection backed by
  `{ns}:{db}:cols` set + `meta:{col}` hash storing schema, attrs,
  indexes, docCount and sizeBytes JSON fields.
- Stats: analyzeCollection returns false (Redis has no statistics
  subsystem), getSizeOfCollection / getSizeOfCollectionOnDisk sum
  MEMORY USAGE across the meta hash, doc blobs, idx set, and
  permission keys, falling back to STRLEN/HLEN approximations.
- Attribute mutations: createAttribute / createAttributes /
  updateAttribute / deleteAttribute / renameAttribute do
  read-modify-write on the `attrs` JSON field inside `tx()`.
- getSchemaAttributes returns [] (no physical schema), and
  getCountOfAttributes mirrors Memory's count + INTERNAL_ATTRIBUTES.

Removes a now-stale `@phpstan-ignore method.unused` pragma above
the locked `key()` helper since the helper is now exercised — same
mechanical cleanup pattern as commit cfb1a91.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implements all ten T30-region methods on the Redis adapter:
getDocument, createDocument, createDocuments, updateDocument,
updateDocuments, upsertDocuments, getSequences, deleteDocument,
deleteDocuments, increaseDocumentAttribute.

Storage layout follows Contract.md: STRING values under
{ns}:doc:{col}:{id} carry the JSON-encoded document, the per-collection
{ns}:idx:{col} SET tracks every doc id for SCAN/SMEMBERS, and
{ns}:seq:{col} INCR mints monotonic sequences when callers omit them.
All write paths go through tx() so T56 can layer WATCH/MULTI/EXEC
retries later without touching this region; bulk reads inside
upsertDocuments / deleteDocuments / getSequences pipeline GETs to
collapse network round-trips. writePermissions / clearPermissions /
journal calls preserve the locked T50 / T56 contracts.

Drops six stale @phpstan-ignore-next-line method.unused pragmas from
key, ns, encode, decode, writePermissions, clearPermissions —
those helpers now have call sites in T30 so the ignores became real
PHPStan errors. The pragmas on rawDeleteDoc / rawRestoreDoc /
applyPermissionFilter remain since T30 does not call them.

CONTRACT_GAP.md flags the task-spec mismatch with Contract.md
(count, sum, getDocuments listed in the task but owned by T40 /
not abstract); followed Contract.md as the locked truth.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement startTransaction/commitTransaction/rollbackTransaction plus
real bodies for the journal helper set:

- tx() now catches RedisException, retries up to TX_MAX_RETRIES with
  TX_BACKOFF_MS exponential backoff, and re-throws as
  TransactionException on exhaustion.
- journal() pushes {op, payload} entries onto the topmost frame; no-op
  outside a transaction so non-transactional writes pay zero overhead.
- rollbackJournal() pops the topmost frame and replays inverses in LIFO
  order using raw \Redis client commands only, so it never re-enters
  the journal. Dispatches createDoc/deleteDoc/updateDoc/createPerm/
  deletePerm via rawDeleteDoc, rawRestoreDoc, and inline raw
  hSet/sAdd/sRem ops.
- commitJournal() splices the inner frame onto the parent frame when
  nested so an outer rollback still rewinds inner work; discards at
  the outermost level (writes already landed in Redis).

The pass-through tx() body is replaced. Stale phpstan-ignore pragmas on
TX_MAX_RETRIES, TX_BACKOFF_MS, journalStack, key(), and ns() are
dropped now that they are referenced.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Implement T40-owned Redis adapter region: createIndex/deleteIndex/
renameIndex maintain the indexes JSON field on the collection meta
hash; find/sum/count load documents from the id-set, filter and sort
in PHP, and honour cursor/offset/limit/projection. The locked
evaluateQueries helper is wired through the same fetch+filter pipeline.

Stale phpstan-ignore method.unused pragmas on key()/decode()/
applyPermissionFilter() are removed now that T40 invokes them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fill in the bodies of the three private permission helpers locked in T1
and replace the relationship stubs with explicit "not supported" throws.

writePermissions builds an inverted role->letters layout: every action
the document grants for a role is appended to the per-action SET
{ns}:{db}:perm:{col}:{letter}:{role} and the role->csv letters HASH at
{ns}:{db}:perm:doc:{col}:{id}. clearPermissions reads the HASH back and
SREMs every set, then DELs the HASH. applyPermissionFilter unions the
caller's role+action set keys via SUNION, then intersects against the
candidate id list -- O(roles) Redis round trips, no per-doc fetch.

Both write paths emit a journal entry so T56's rollback can replay the
inverse exactly.

Three contract gaps observed and recorded in CONTRACT_GAP.md (action-
letter map needs the 'c' for create, the per-doc HASH stores letters
rather than full action names, and tenant scoping for the perm keys
under shared tables was not pinned in Contract.md). Contract.md is left
untouched per the wave-2 etiquette rule.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
# Conflicts:
#	CONTRACT_GAP.md
#	src/Database/Adapter/Redis.php
Wave-2 architects diverged on the `key()`/`ns()` contract and on the
journal payload shape. The combined effect was a broken end-to-end
adapter — `find()` after `createDocument()` returned empty because
writes went to `utopia:utopia:{ns}:{db}:doc:...` but reads went to
`utopia:{ns}:{db}:doc:...`. This pass picks one convention and sweeps
the file.

C1: `key()` no longer prepends KEY_PREFIX. Call sites compose the
prefix by passing `$this->ns()` (which returns `KEY_PREFIX:{ns}:{db}`)
as the first argument. Audited with `grep -nE 'key\(.*getNamespace|
key\(.*getDatabase'` — returns nothing. A new `nsFor($ns, $db)` helper
covers cross-database operations like `exists()` and `delete()`.

C2: T30 journal entries now use `['collection', 'id', 'payload']`
matching the schema rollback already expected. Drops the legacy
`['col', 'id', 'before']` shape entirely.

C3: `writePermissions` and `clearPermissions` no longer emit bespoke
`'writePermissions'`/`'clearPermissions'` ops the rollback switch
didn't handle. They now emit one granular `'createPerm'`/`'deletePerm'`
entry per (role, letter) pair, dispatching through the existing
rollback cases. The unhandled-op `TransactionException` path is now
truly unreachable in normal operation.

C4: Rollback `'createPerm'`/`'deletePerm'` cases now call
`permKey()`/`permDocKey()` directly so the keys SREM'd on rollback
match the keys SADD'd on writePermissions byte-for-byte under shared
tables (the previous `key($this->ns(), 'perm', ...)` form skipped the
tenant bucket).

C5/C6: `getSupportForTransactionRetries()` returns `false` to mirror
Memory. The current `tx()` body is a network-error retry loop, NOT
a WATCH/MULTI/EXEC OCC implementation. Reporting `true` made the
shared trait's OCC tests run against semantics this adapter doesn't
provide. Real OCC is deferred to a follow-up PR. Documented in both
the class docblock and Contract.md.

C7: `RedisBase::tearDown` and `RedisCrossProcessTest::tearDown` SCAN
patterns now include the `KEY_PREFIX` so cleanup actually matches
the adapter's keys. RedisBase moves the cleanup into
`tearDownAfterClass()` and uses a class-scoped Database instance
(matching MemoryTest) so the inherited Base scope tests, which chain
state across methods, see a stable collection set.

M1: Unique-index pre-flight under shared tables now filters the
inverted-index scan to the active tenant before computing the
duplicate signature, preventing cross-tenant rows from producing
spurious DuplicateException.

M2: `count()` short-circuits to `sCard($idxKey)` when there are no
queries and authorization is off. With queries it still walks the
collection — TODO documented as a known scaling limit.

M4: `getSequences()` now wraps the pipeline in a try/finally that
calls `discard()` on the early-return path so the connection can't
leak in MULTI mode.

M7: `RedisCrossProcessTest` proc_open guard now uses
`markTestIncomplete` instead of `markTestSkipped` so CI sees the
gap rather than silently ignoring it.

M8: `writePermissions` and `clearPermissions` SADD/SREM/HMSET/DEL
loops are now wrapped in `multi(\Redis::PIPELINE)`/`exec()` so a doc
with N (role, action) pairs hits Redis in a single round trip.

M9: `decode()` catches `JsonException` and rethrows as
`DatabaseException('Document decode failed: ...')` with the original
JsonException as `previous`, giving callers a typed error path.

M10: `purgeCollectionKeys()` now also DELs the per-collection sequence
key so collection drops don't leave stale `seq:{col}` keys behind.

Bonus: `getIdAttributeType()` now returns `VAR_INTEGER` (matching
Memory) — Redis sequences come from `INCR`, and the Sequence
validator rejects string-typed sequence values, which broke every
test that read `$sequence` after a write.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Stage-5 re-review fixes:

- C-Re1: count() fast-path used sCard on idx:{collection} which is
  shared across tenants under shared-tables mode. Gate the fast path
  on getSharedTables() === false; otherwise fall through to the
  permission/tenant-aware slow path.
- C-Re3 + M-Re2: tx() retry loop double-journalled side effects on
  retry and could double-INCR sequence keys. Replace with a single-shot
  wrapper that surfaces transient errors as TransactionException.
  Drop now-unused TX_MAX_RETRIES / TX_BACKOFF_MS constants.
- M-Re1: writePermissions / clearPermissions discard() on PIPELINE mode
  is version-dependent across phpredis. Wrap discard() in try/catch
  so the original exception cause propagates.
- M-Re3: document the nested-pipeline constraint on writePermissions /
  clearPermissions / getSequences for future tx() refactors.
- M-Re5: getSequences re-threw raw \\Throwable. Wrap as
  TransactionException with previous exception preserved.

Contract.md updated to reflect the new tx() semantics.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two adapter bugs surfaced by the verifier:

1. Bulk-insert ignored skipDuplicates mode. createDocument() threw
   DuplicateException unconditionally on existing primary keys, so
   createDocuments() under a skipDuplicates() scope failed instead of
   silently skipping duplicates. Mirror the MariaDB INSERT IGNORE /
   Memory adapter contract: when the flag is set, return the document
   with the existing row's sequence so the caller still emits an
   onNext event for it.

2. The inverted permission SETs stored ids with their original case
   while the index SET and document keys lowercased ids. find() with
   authorization on then intersected mismatched casing and silently
   dropped any document whose id contained uppercase letters.
   Lowercase ids in writePermissions()/clearPermissions() so reads
   and writes stay symmetric; permDocKey() lookups already pass
   through these helpers so the per-doc HASH stays consistent too.

Fixes 5 skipDuplicates bulk-insert tests and resolves the underlying
case-sensitivity hazard exposed by mixed-case test fixtures.

Document the deferred multi-attribute cursor pagination corner case
under "Known limitations" in the Redis adapter contract.
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Apr 30, 2026

📝 Walkthrough

Walkthrough

Introduces Redis as a new adapter for end-to-end testing by requiring the ext-redis PHP extension, expanding the test matrix workflow to include Redis and SharedTables/Redis variants, and implementing Redis-backed test classes with keyspace management and cleanup logic.

Changes

Cohort / File(s) Summary
Test Infrastructure & Dependencies
.github/workflows/tests.yml, composer.json
Added Redis and SharedTables/Redis to adapter test matrix; added ext-redis as a required PHP extension.
Redis Adapter Implementation
tests/e2e/Adapter/RedisTest.php, tests/e2e/Adapter/SharedTables/RedisTest.php
New E2E test classes establishing Redis adapter with lazy-initialized client, configurable namespace/tenancy, keyspace SCAN tracking, and state cleanup via teardown. Shared-tables variant enforces tenant configuration and empty namespace.
Test Precision Fixes
tests/e2e/Adapter/Scopes/DocumentTests.php
Added microsecond sleep pauses in timestamp assertion tests to ensure sufficient time separation between document reads and adapter updates, accounting for millisecond-precision timestamp adapters.

Sequence Diagram

sequenceDiagram
    participant Test as Test Client
    participant DB as Database
    participant Redis as Redis Server

    Test->>Redis: Connect (REDIS_HOST:REDIS_PORT)
    Test->>DB: Create with RedisAdapter
    Test->>DB: Set namespace & tenancy config
    
    rect rgba(100, 150, 200, 0.5)
        Note over Test,Redis: Setup Phase
        Test->>Redis: SCAN for existing keys (patterns)
        Redis-->>Test: Key list
        Test->>Redis: DEL matching keys
    end
    
    rect rgba(150, 200, 100, 0.5)
        Note over Test,DB: Test Execution
        Test->>DB: Run adapter tests
        DB->>Redis: JSON store/read operations
    end
    
    rect rgba(200, 150, 100, 0.5)
        Note over Test,Redis: Teardown Phase
        Test->>Redis: SCAN for recorded key patterns
        Redis-->>Test: Key list
        Test->>Redis: DEL all matching keys
        Test->>Test: Clear static state
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Poem

🐰 Redis hops into the test suite with flair,
Adapters multiplied through the matrix air,
Keys scan and delete in a cleanup dance,
Shared tables and tenants all get their chance,
Timestamps now sleep, precision held tight!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Redis adapter' directly and clearly summarizes the main change—the addition of a Redis adapter implementation for the Utopia Database library.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat-redis-adapter

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.1.51)

PHPStan was skipped because the sandbox runner could not parse its output.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Apr 30, 2026

Greptile Summary

This PR introduces a full Redis adapter (~4 200 lines) mirroring the Memory adapter's surface, backed by a journal-based rollback mechanism and a well-documented keyspace layout. The test infrastructure (RedisTest, SharedTables/RedisTest, DocumentTests tweaks) and CI matrix additions are sound.

Several P1 correctness issues flagged in earlier review threads remain unaddressed in the current code: updateDocuments and the upsertDocuments update path both write merged documents without calling enforceUniqueIndexes; deleteDocuments reads all IDs from the adapter-tenant idx set without a tenant filter, enabling cross-tenant deletions under shared tables; matchesFulltextRedis returns true on the first matching token (OR semantics) rather than requiring all tokens to match (AND semantics); and renameDocumentField/dropDocumentField only iterate the adapter-bound tenant's idx set, leaving other tenants' JSON payloads with the old field name after a schema rename. These should be resolved before the adapter is considered production-ready.

Confidence Score: 2/5

Not safe to merge — multiple unfixed P1 data-integrity and cross-tenant correctness issues remain open from prior review rounds.

Multiple P1 issues confirmed present in the current code: bulk-update paths bypass unique-index enforcement, deleteDocuments has a cross-tenant data-deletion path under shared tables, fulltext search uses OR instead of AND semantics (breaking pagination correctness), and field-rename migrations are tenant-incomplete. Any of these individually would cap the score at 4/5; the combination of four open P1s pulls it to 2/5.

src/Database/Adapter/Redis.php — specifically updateDocuments (line 2282), upsertDocuments update path (line 2371), deleteDocuments (line 2518), matchesFulltextRedis (line 3724), and renameDocumentField/dropDocumentField (lines 1187/1231).

Important Files Changed

Filename Overview
src/Database/Adapter/Redis.php New 4242-line Redis adapter. Several P1 correctness issues remain open from previous review threads: updateDocuments and the upsertDocuments update-path both skip unique-index enforcement; deleteDocuments performs a cross-tenant full scan under shared tables; matchesFulltextRedis uses OR token semantics instead of AND; renameDocumentField/dropDocumentField only migrate the adapter-bound tenant's documents. Also contains a stale class docblock entry for a 'uniq:' key type that was never implemented.
tests/e2e/Adapter/RedisTest.php New E2E test class extending Base trait. Uses NoneCacheAdapter to avoid cache-connection issues. Falls back to 'redis-mirror' hostname when REDIS_HOST is unset. Teardown scrubs keys via SCAN+DEL rather than FLUSHDB.
tests/e2e/Adapter/SharedTables/RedisTest.php Thin shared-tables variant that extends RedisTest, enabling shared-tables mode with tenant 999 and empty namespace before database creation. Correctly re-asserts tenancy in setUp().
tests/e2e/Adapter/Scopes/DocumentTests.php Adds two usleep(2000) calls to ensure timestamp differences are observable at millisecond precision — safe, backwards-compatible tweak.
.github/workflows/tests.yml Adds Redis and SharedTables/Redis to the CI matrix. The PR description mentions a third RedisCrossProcess entry but that test file is not present in the changeset, so the matrix correctly reflects only what exists.
composer.json Adds ext-redis as a required PHP extension, consistent with the new adapter's dependency on the phpredis extension.

Reviews (10): Last reviewed commit: "(fix): use document tenant for upsertDoc..." | Re-trigger Greptile

Comment thread src/Database/Adapter/Redis.php
Comment thread tests/e2e/Adapter/RedisBase.php Outdated
Comment thread src/Database/Adapter/Redis.php
Comment thread src/Database/Adapter/Redis.php
Comment thread src/Database/Adapter/Redis.php Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
composer.json (1)

35-46: ⚠️ Potential issue | 🟠 Major

Remove ext-redis from the require section in composer.json.

This library supports multiple database adapters (MariaDB, MySQL, Postgres, SQLite, Mongo, and Redis), and the composer.json file already lists Redis in the suggests section with the note "Needed to support Redis Cache Adapter," correctly indicating it's optional. Having ext-redis in require will break installations for users who only need one of the other adapters. Keep it in suggests only, or move it to a separate Redis-specific package.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@composer.json` around lines 35 - 46, Remove ext-redis from the "require"
block in composer.json and ensure it remains only in the "suggests" section (or
moved to a Redis-specific package) so installations without Redis won't fail;
update the composer.json "require" array to omit "ext-redis" while leaving
"suggests" entry unchanged and keep references to adapter support in
documentation if present.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/Database/Adapter/Redis/Contract.md`:
- Around line 56-60: In the Contract.md table row for the private function
encode(), escape the pipe so the table parser doesn't treat it as a column
separator: update the text for encode() from "JSON_THROW_ON_ERROR |
JSON_UNESCAPED_UNICODE" to an escaped form such as "JSON_THROW_ON_ERROR \|
JSON_UNESCAPED_UNICODE" (or use the HTML entity &#124;), ensuring the encode()
row content remains a single table cell.

In `@tests/e2e/Adapter/SharedTables/RedisTest.php`:
- Around line 13-17: The test sets an empty namespace via
$database->setNamespace('') which prevents RedisBase::tearDownAfterClass() from
finding and cleaning up keys; change the test to use a dedicated non-empty
namespace (e.g., 'utopia_test_redis' or similar) when calling setNamespace in
RedisTest (so cleanup scans the correct prefix), or alternatively modify
RedisBase::tearDownAfterClass() to respect the overridden namespace value when
scanning keys; locate the setNamespace call in RedisTest and the
tearDownAfterClass method in RedisBase to implement the chosen fix.

---

Outside diff comments:
In `@composer.json`:
- Around line 35-46: Remove ext-redis from the "require" block in composer.json
and ensure it remains only in the "suggests" section (or moved to a
Redis-specific package) so installations without Redis won't fail; update the
composer.json "require" array to omit "ext-redis" while leaving "suggests" entry
unchanged and keep references to adapter support in documentation if present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: dcf02e2d-bbd8-4866-8e95-8d7a28672315

📥 Commits

Reviewing files that changed from the base of the PR and between a1bb3e2 and 82ec44d.

⛔ Files ignored due to path filters (1)
  • composer.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • .github/workflows/tests.yml
  • composer.json
  • src/Database/Adapter/Redis.php
  • src/Database/Adapter/Redis/Contract.md
  • tests/e2e/Adapter/RedisBase.php
  • tests/e2e/Adapter/RedisCrossProcessTest.php
  • tests/e2e/Adapter/RedisTest.php
  • tests/e2e/Adapter/SharedTables/RedisTest.php
  • tests/e2e/Adapter/_helpers/redis_cross_process_worker.php

Comment thread src/Database/Adapter/Redis/Contract.md Outdated
Comment thread tests/e2e/Adapter/SharedTables/RedisTest.php
Copy link
Copy Markdown
Contributor

@claude claude Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Multi-dimension review of the Redis adapter PR. Findings split across security, performance, correctness, readability, maintainability, testing. CRITICAL/WARNING items relevant to this push were fixed in 29ef327d; suggestion-tier items are listed below for follow-up.

Fixed in this push

[CRITICAL — security/correctness] Cross-tenant read via getDocument under shared tables. Doc keys are not tenant-bucketed and the single-doc path skipped the tenant comparison loadCollectionDocuments performs after MGET. Tenant A could read tenant B's row by id. Fixed by mirroring the loader's tenant filter on the decoded payload.

[CRITICAL — correctness] purgeCollectionKeys SCAN patterns only covered the non-shared perm:{col}:* layout. Under shared tables, permKey()/permDocKey() emit perm:t:{tenant}:{col}:... and perm:t:{tenant}:doc:{col}:... — neither matched the existing patterns, so dropping a collection (or a database) under shared tables leaked every role-set and per-doc HASH. A recreated collection then inherited stale grants. Added two extra SCAN sweeps for the tenant-bucketed forms.

[CRITICAL — testing] RedisBase::getDatabase called flushAll() on the cache Redis. Contract.md explicitly forbids this because the test runner shares the cache instance across workers — parallel workers wiped each other's state mid-run. Replaced with namespace-scoped SCAN/DEL in tearDownAfterClass.

[CRITICAL — testing] SharedTables/RedisTest called setNamespace('') AFTER getDatabase() had already created the database under 'utopia_test_<uniqid>'. Bootstrap keys leaked under the per-run namespace; subsequent test writes went to the empty-namespace keyspace; teardown's scan pattern matched neither set fully. Refactored to apply shared-tables config and empty namespace via a configureDatabase() hook BEFORE create().

[WARNING — performance] purgeCollectionKeys issued one DEL per doc. Now batches into variadic DEL calls of size SCAN_BATCH_SIZE (500).

[WARNING — readability] Stale class docblock advertised a DSN parser (none exists), WATCH/MULTI/EXEC OCC retry (tx() is a single-shot wrapper), and Wave-2 architects MUST NOT modify language that no longer applies post-merge. Rewrote to match the actual implementation; documented both shared and non-shared perm key layouts.

[WARNING — readability] Magic numbers 500 (SCAN batch) and 512 (json_decode depth) repeated 5+ times. Hoisted to SCAN_BATCH_SIZE and JSON_DECODE_DEPTH private constants.

[WARNING — readability] loadCollectionDocuments had a // visible empty-branch / elseif structure for the tenant filter. Inverted to a single cross-tenant guard expression.

[WARNING — maintainability] ns() and nsFor() duplicated the prefix composition. ns() now delegates.

[WARNING — readability] OrderException and TimeoutException were imported but never used. Removed.

Verified false positives (not fixed)

  • updateDocuments !empty($updates->getUpdatedAt()) vs offsetExists — Memory adapter uses the same !empty() pattern, this matches abstract contract intent.
  • tx() not opening a journal frame — Contract.md documents this as the intended behaviour; frames are opened by startTransaction() only.
  • getSequences line 1816 missing \strtolower on id — verified the call DOES lowercase via \strtolower($doc->getId()).

Remaining suggestions (not in this push's scope)

  • find() / count() / evaluateQueries load the entire collection into PHP for every call. Indexed Redis SETs/ZSETs would let count() use SINTERCARD and equality queries use SINTER. Tracked as a known scaling limit (TODO already present).
  • applyPermissionFilter does SUNION + PHP array_filter; could use SUNIONSTORE + SINTER idx:{col} server-side.
  • deleteDocuments does SMEMBERS idx:{col} + decode-everything to filter by sequence; a seq:{col} HASH (id ↔ sequence) would let it HMGET only the targets.
  • unique-index pre-flight does serial GET per id; could pipeline.
  • Cross-process test is sequential, not concurrent — does not exercise contention on idx:{col}, the unique pre-flight scan vs. concurrent insert, or journal compensation under racing writers.
  • Region markers (=== @architect:Tnn ===) and the 5-blank-line buffers between regions are sprint scaffolding that no longer carries meaning post-merge — left in place to avoid risk of disrupting downstream tooling, but worth a follow-up cleanup.

Address critical findings from PR #872 review:

- getDocument now applies the same tenant filter loadCollectionDocuments
  uses; cross-tenant reads under shared tables previously leaked because
  doc keys are not tenant-bucketed and the single-doc path skipped the
  decoded-payload tenant comparison that the multi-doc loader performs.
- purgeCollectionKeys now sweeps the shared-tables perm layouts
  (perm:t:{tenant}:{col}:* and perm:t:{tenant}:doc:{col}:*); the prior
  pattern only covered the non-shared form, so dropping a collection
  under shared tables left stale role/doc HASH grants behind. Per-doc
  cleanup is also batched into variadic DEL chunks instead of one round
  trip per document.

Test infra:
- RedisBase no longer calls flushAll() on the cache instance — the
  Contract explicitly forbids it because the test runner shares the
  cache across workers. Cache keys are now scrubbed by namespace-scoped
  SCAN/DEL in tearDownAfterClass.
- SharedTables/RedisTest configures shared-tables and the empty
  namespace BEFORE Database::create() runs, via a configureDatabase()
  hook; patching after-the-fact wrote bootstrap keys under the per-run
  namespace and leaked them when teardown only scrubbed the empty-
  namespace pattern.

Cleanup:
- Hoist SCAN_BATCH_SIZE (500) and JSON_DECODE_DEPTH (512) to named
  private constants so the magic numbers live in one place.
- Strip stale class-docblock scaffolding: the Wave-2 architect /
  CONTRACT_GAP escalation language no longer applies post-merge, the
  advertised DSN parser does not exist, and the WATCH/MULTI/EXEC retry
  description contradicts what tx() actually does. Updated the storage
  schema entry to document both shared and non-shared perm layouts.
- Drop unused OrderException / TimeoutException imports.
- Replace the // visible no-op branch in loadCollectionDocuments with
  a single cross-tenant guard expression.
- ns() now delegates to nsFor() to remove the duplicate prefix
  composition.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Claude pushed fixes from: improvement

82ec44d...38b1f03

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 30, 2026

CI fix — SharedTables/Redis operator failures (run 25188037057)

What failed: 24 operator-related test failures on Tests\E2E\Adapter\SharedTables\RedisTest, e.g. testOperatorMultiplyWithNegativeMultiplier, testOperatorIncrementDecrementWithPreciseFloats, testOperatorArrayInsertAtExactBoundaries, testOperatorToggleFromDefaultValue, testSingleUpsertWithOperators, testUpsertOperatorsOnNewDocuments, etc. All assertions showed the operator was a no-op (0 instead of expected, Array () instead of strings, false instead of toggled true).

Root cause: Redis::getSupportForOperators() returns true, but updateDocument, updateDocuments, and upsertDocuments were merging the incoming Document straight into the stored payload without resolving Operator instances. The Operator object survived array_merge and got serialized verbatim, so the stored value was never actually incremented/multiplied/concatenated/etc.

Fix: Resolve operators against the existing row before persisting. Mirrors Memory::applyOperator / Memory::applyOperators so non-SQL adapters share the same arithmetic, bound-clamping, array, string, toggle, and date semantics. Three call sites updated:

  • updateDocument — applies operators against the existing decoded payload, then merges.
  • updateDocuments — applies operators per-row against each document's existing values (operator-derived results cannot be precomputed once and reused, since each row's current value differs).
  • upsertDocuments — both branches: existing rows resolve against the prior payload; new rows resolve against [] so e.g. Operator::increment(10) on a fresh doc yields 10, matching testUpsertOperatorsOnNewDocuments.

The helper trio (applyOperator, applyNumericLimit, preserveNumericType, coerceArray, matchesArrayFilter, shiftDate) sits at the bottom of Redis.php and is line-for-line equivalent to the Memory adapter's implementation, which the failing tests already pass against.

php -l clean. Local container verification was skipped (docker compose build took too long in this runner); CI will re-run the full SharedTables/Redis matrix.

@github-actions
Copy link
Copy Markdown
Contributor

Claude could not apply patches from: healing (merge conflicts with current branch tip). These tasks need manual attention.

@claude
Copy link
Copy Markdown
Contributor

claude Bot commented Apr 30, 2026

CI failure: Redis adapter operator tests (run 25188688688)

What failed

73 operator tests against RedisTest failed (e.g. testArrayInsertAtEnd, testOperatorIncrementExceedsMaxValue, testOperatorPowerWithFractionalExponent, testSingleUpsertWithOperators). The pattern was uniform: post-update reads returned the default value (e.g. 0/0.0/[]) or an array decoded from a serialized Operator payload (e.g. Array () does not match expected type "string").

Root cause

src/Database/Adapter/Redis.php advertises getSupportForOperators() = true, but updateDocument, updateDocuments, and upsertDocuments merged the raw Operator instances into the JSON payload via array_merge and never resolved them against the existing row. The encoded document then contained either a JSON-serialized operator descriptor (later decoded back as a plain array) or, after castingAfter, the type-coerced default value — never the computed result.

A second, smaller issue surfaced once operators were applied: json_encode was called without JSON_PRESERVE_ZERO_FRACTION, so 50.0 round-tripped to 50 and assertIsFloat failed for VAR_FLOAT columns even when the value was numerically correct.

Fix

Ported the in-PHP operator evaluator from Memory::applyOperator (which mirrors MariaDB::getOperatorSQL semantics) into the Redis adapter and invoked it before encoding the merged document in updateDocument, updateDocuments, and both branches of upsertDocuments (so operators applied to a brand-new upserted document still resolve against a null base, matching testUpsertOperatorsOnNewDocuments). Added JSON_PRESERVE_ZERO_FRACTION to the encode() flags.

Verification

Re-ran the originally failing set against RedisTest — all 27 (the operator subset) pass. Full tests/e2e/Adapter/RedisTest.php run goes from Errors: 11, Failures: 73 (baseline before fix) to Errors: 4, Failures: 7. The remaining 11 problems (testPurgeCollectionCache, testUniqueIndexDuplicate, testRenameAttribute, testUpdateAttributeStructure, testUpdateAttributeRename, testUpdateAttributeSize, testDuplicateExceptionMessages, plus 4 unrelated errors) are pre-existing on this branch — confirmed by stashing the fix and re-running with the same baseline. They are not regressions and are unrelated to operator handling.

…fraction

The Redis adapter advertised getSupportForOperators() = true but never
actually applied Operator-typed attributes to stored documents. updateDocument,
updateDocuments, and upsertDocuments merged the raw Operator instances into
the JSON payload, so subsequent reads returned the default value (or the
operator object decoded as an array), breaking 27 operator tests covering
INCREMENT/DECREMENT/MULTIPLY/DIVIDE/MODULO/POWER, STRING_CONCAT/REPLACE,
TOGGLE, ARRAY_*, and DATE_* operators against the Redis adapter.

Port the in-PHP operator evaluator from Memory::applyOperator (matching
MariaDB::getOperatorSQL semantics) and call it before encoding the merged
document. Also pass JSON_PRESERVE_ZERO_FRACTION to encode() so float
attributes survive the JSON round trip — without it, 50.0 was stored as
50 and the schema-declared float type was lost.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Claude pushed fixes from: healing

38b1f03...acdfdd4

abnegate and others added 3 commits May 1, 2026 10:13
Schema-only adapter implementation: relationship attributes registered
in meta.attrs as minimal records; orchestrator handles populate, junction,
and cascade via standard CRUD; null-surfaced on every read path.

- Redis.php: getSupportForRelationships() returns true.
- Contract.md: parity-table row updated with rationale; trailing
  T4-pending language and footnote dropped.
Adds testRelationshipRoundTripAcrossProcesses to RedisCrossProcessTest,
mirroring the existing proc_open harness:

- Parent creates parents/children collections, a one-to-one relationship
  with onDelete=setNull, a parent doc with the relationship key set, and
  a parent doc with the key omitted.
- Child process re-attaches to the same Redis namespace, asserts the
  first parent's relationship round-trips to the expected child id, and
  asserts the second parent surfaces null for the omitted key.

Proves both round-trip identity and null-surfacing work across
independent PHP processes sharing the same Redis backend.
Three correctness fixes to land the remaining relationship-trait failures:

1. Keyspace consistency. createDocument/updateDocument/getDocument and
   the doc-bulk paths previously took `$collection->getId()` raw while
   find/sum/count filtered it via `$this->filter(...)`. Collection names
   carrying symbols (`$`, `.`) hashed into two different keyspaces, so a
   doc written under `idx:\$symbols_coll.ection2` was invisible to a
   read going through `idx:symbols_collection2`. Now every CRUD entry
   point filters the collection name once, matching find()'s contract
   and the per-tenant perm-set keys built by writePermissions.

2. Symbol-bearing attribute resolution. resolveDocumentAttribute split
   on `.` before checking the literal key, so `\$symbols_coll.ection3`
   degraded into a nested-path walk against a non-existent `\$symbols_coll`
   head. Try the literal key first, then the filtered alias, and only
   then fall back to dotted-path traversal — mirrors Memory's
   resolveAttributeValue precedence.

3. Unique-index enforcement on write. The adapter advertised
   getSupportForUniqueIndex()=true and registered indexes in meta.indexes,
   but createDocument/updateDocument never probed for collisions. Add
   enforceUniqueIndexes() (an mGet-pipelined scan that mirrors Memory's
   checkUniqueSignatures, including shared-tables tenant scoping and
   NULL-as-distinct semantics) and call it from both write paths so 1:1
   relationship parents correctly reject reassigning a child that's
   already linked.

Tests: 11 previously-failing relationship-trait tests now pass; 97
already-passing tests remain green; full RedisTest suite shows the
same 9/296 unrelated errors/failures as before this commit.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment thread src/Database/Adapter/Redis.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/Database/Adapter/Redis/Contract.md (1)

9-25: 💤 Low value

Consider adding language specifiers to fenced code blocks.

The storage key schema and DSN format code blocks lack language specifiers. Adding text or plaintext would satisfy linters and improve rendering in some markdown viewers.

-```
+```text
 {ns}                      = getNamespace()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Database/Adapter/Redis/Contract.md` around lines 9 - 25, Update the
fenced code blocks that show the storage key schema and the DSN format to
include a language specifier (e.g., "text" or "plaintext") so linters render
them correctly; specifically change the opening backticks for the block
containing the "{ns}                      = getNamespace()" key schema and the
separate DSN-format block to use ```text (or ```plaintext) instead of plain ```.
tests/e2e/Adapter/RedisCrossProcessTest.php (1)

48-71: 💤 Low value

Consider cleaning cache keys in addition to adapter keys.

The tearDown cleans adapter keys (utopia:{namespace}:*), but unlike RedisBase::tearDownAfterClass(), it doesn't clean cache keys ({namespace}:{database}:*). If the cache adapter writes keys during the test, they would be left behind.

Since each test uses a unique namespace and the cache operations are minimal in these cross-process tests, this is low risk. However, for consistency with RedisBase, you could add the cache pattern:

♻️ Suggested enhancement
             // Adapter keys are prefixed with `KEY_PREFIX:` — without the
             // prefix this SCAN matches nothing and leaks every key.
             $pattern = RedisDbAdapter::KEY_PREFIX . ':' . $this->namespace . ':*';
-            while (($keys = $client->scan($iterator, $pattern, 500)) !== false) {
+            $patterns = [
+                $pattern,
+                $this->namespace . ':utopiaTests:*', // Cache keys
+            ];
+            foreach ($patterns as $scanPattern) {
+              $iterator = null;
+              while (($keys = $client->scan($iterator, $scanPattern, 500)) !== false) {
                 if (\is_array($keys) && \count($keys) > 0) {
                     $client->del($keys);
                 }
                 if ($iterator === 0) {
                     break;
                 }
+              }
             }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Adapter/RedisCrossProcessTest.php` around lines 48 - 71, The
tearDown currently only removes adapter keys using RedisDbAdapter::KEY_PREFIX;
add a second SCAN/DEL loop (mirroring the existing one) to also remove cache
keys following the pattern "{namespace}:{database}:*" so cache entries created
during tests are cleaned too; locate the tearDown method and after the
adapter-key loop add a similar loop that sets $pattern to
"{$this->namespace}:*:*" (or the exact "{namespace}:{database}:*" format used by
your cache) and uses $client->scan(...) and $client->del(...) just like the
existing logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Database/Adapter/Redis/Contract.md`:
- Around line 9-25: Update the fenced code blocks that show the storage key
schema and the DSN format to include a language specifier (e.g., "text" or
"plaintext") so linters render them correctly; specifically change the opening
backticks for the block containing the "{ns}                      =
getNamespace()" key schema and the separate DSN-format block to use ```text (or
```plaintext) instead of plain ```.

In `@tests/e2e/Adapter/RedisCrossProcessTest.php`:
- Around line 48-71: The tearDown currently only removes adapter keys using
RedisDbAdapter::KEY_PREFIX; add a second SCAN/DEL loop (mirroring the existing
one) to also remove cache keys following the pattern "{namespace}:{database}:*"
so cache entries created during tests are cleaned too; locate the tearDown
method and after the adapter-key loop add a similar loop that sets $pattern to
"{$this->namespace}:*:*" (or the exact "{namespace}:{database}:*" format used by
your cache) and uses $client->scan(...) and $client->del(...) just like the
existing logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7931aefd-7d42-4320-82bb-31697736027d

📥 Commits

Reviewing files that changed from the base of the PR and between 82ec44d and 8b5f56f.

📒 Files selected for processing (6)
  • src/Database/Adapter/Redis.php
  • src/Database/Adapter/Redis/Contract.md
  • tests/e2e/Adapter/RedisBase.php
  • tests/e2e/Adapter/RedisCrossProcessTest.php
  • tests/e2e/Adapter/SharedTables/RedisTest.php
  • tests/e2e/Adapter/_helpers/redis_relationship_worker.php

Comment thread src/Database/Adapter/Redis.php
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Introduces a new Utopia\Database\Adapter\Redis to allow multiple processes/containers to share a Redis-backed in-memory database, along with e2e coverage and CI wiring to validate adapter parity (including shared-tables and cross-process behavior).

Changes:

  • Added Redis adapter implementation (src/Database/Adapter/Redis.php) plus a contract/spec doc for storage layout and behavioral guarantees.
  • Added Redis e2e test suites (standard + shared-tables) and cross-process tests using proc_open worker scripts.
  • Updated CI adapter matrix and Composer platform requirements to include ext-redis.

Reviewed changes

Copilot reviewed 10 out of 12 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/Database/Adapter/Redis.php New Redis adapter implementation (schemas, CRUD, queries, permissions, relationships, journaling/transactions).
src/Database/Adapter/Redis/Contract.md Contract documentation for Redis keyspace/layout and adapter expectations.
tests/e2e/Adapter/RedisBase.php Shared test base for Redis adapter suites with scoped SCAN+DEL cleanup.
tests/e2e/Adapter/RedisTest.php Non-shared-tables Redis adapter suite entrypoint.
tests/e2e/Adapter/SharedTables/RedisTest.php Shared-tables Redis adapter suite entrypoint with tenant/namespace configuration.
tests/e2e/Adapter/RedisCrossProcessTest.php Cross-process verification (state sharing + relationship round-trip).
tests/e2e/Adapter/_helpers/redis_cross_process_worker.php Child process worker used by cross-process read/write test.
tests/e2e/Adapter/_helpers/redis_relationship_worker.php Child process worker used by cross-process relationship test.
tests/e2e/Adapter/Scopes/DocumentTests.php Adds small sleeps to avoid timestamp precision flakiness with ms-resolution adapters.
.github/workflows/tests.yml Extends adapter test matrix with Redis variants.
composer.json Adds ext-redis platform requirement.
composer.lock Lockfile update reflecting ext-redis requirement and regenerated metadata.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +775 to +778
public function getSupportForFulltextIndex(): bool
{
return false;
}
Comment on lines +1289 to +1293
{
$name = $this->filter($name);
$dbsKey = $this->key($this->ns(), 'dbs');

$this->tx(fn (RedisClient $client) => $client->sAdd($dbsKey, $name));
Comment on lines +527 to +543
case 'updateDoc':
/** @var string $collection */
$collection = $payload['collection'];
/** @var string $id */
$id = $payload['id'];
/** @var string $beforePayload */
$beforePayload = $payload['payload'];
$this->client->set($this->docKey($collection, $id), $beforePayload);
// If the update changed the id, the new key must be removed
// and the old id restored to the index set.
if (isset($payload['newId']) && \is_string($payload['newId']) && $payload['newId'] !== $id) {
$newId = $payload['newId'];
$this->client->del($this->docKey($collection, $newId));
$idxKey = $this->idxKey($collection);
$this->client->sRem($idxKey, \strtolower($newId));
$this->client->sAdd($idxKey, \strtolower($id));
}
Comment on lines +3803 to +3810
if ($newKey !== null && $newKey !== $key) {
$this->renameAttribute($collection, $key, $newKey);
$this->renameDocumentField($collection, $key, $newKey);
}
if ($twoWay && $newTwoWayKey !== null && $newTwoWayKey !== $twoWayKey) {
$this->renameAttribute($relatedCollection, $twoWayKey, $newTwoWayKey);
$this->renameDocumentField($relatedCollection, $twoWayKey, $newTwoWayKey);
}
Comment thread src/Database/Adapter/Redis/Contract.md Outdated
Comment on lines +21 to +22
{ns}:{db}:perm:{col}:r/w/u/d:{role} | SET | doc IDs by action+role
{ns}:{db}:perm:doc:{col}:{id} | HASH | role -> csv("read,update,delete")
@abnegate
Copy link
Copy Markdown
Member Author

abnegate commented May 1, 2026

@copilot apply changes based on the comments in this thread

Comment thread src/Database/Adapter/Redis.php Outdated
Comment thread src/Database/Adapter/Redis.php Outdated
…ld migration, journal rollback keys, contract doc

Agent-Logs-Url: https://github.com/utopia-php/database/sessions/691d9204-91ef-4be8-88b8-6e7c70fe1fa7

Co-authored-by: abnegate <5857008+abnegate@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

Copilot AI commented May 1, 2026

@copilot apply changes based on the comments in this thread

Applied all four changes in d36c6bd:

  1. DB registry key (Redis.php:1289-1293): Added nsBase() helper that returns KEY_PREFIX:{namespace} (without the database segment). All four dbs key sites (create, exists, list, delete) now use key($this->nsBase(), 'dbs') so the registry SET is shared across all databases in a namespace.

  2. Duplicate field migration in relationship methods (Redis.php:3803-3810): Removed the extra renameDocumentField() calls from updateRelationship() and dropDocumentField() calls from deleteRelationship()renameAttribute() and deleteAttribute() already call these helpers internally, so the relationship methods now delegate entirely to the attribute helpers.

  3. Journal rollback keys (Redis.php:527-543): Every createDoc, deleteDoc, and updateDoc journal entry now records the exact Redis key strings computed at write time (docKey, idxKey, permDocKey, newDocKey). rawDeleteDoc() and rawRestoreDoc() accept optional precomputed keys and the rollback switch passes them through, so rollback always targets the keys that were actually written regardless of the current tenant context.

  4. Contract.md permission key schema (Contract.md:21-22): Updated the key table to reflect actual shapes — namespace-only dbs key, tenant-scoped variants for doc/idx/perm keys, all five action letters (r/c/u/d/w), and explicit (non-shared-tables)/(shared-tables) variants for each key pattern.

Comment thread src/Database/Adapter/Redis.php
Comment thread src/Database/Adapter/Redis.php
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
tests/e2e/Adapter/RedisTest.php (1)

48-49: 💤 Low value

Consider adding a connection timeout to prevent tests from hanging.

If Redis is unreachable, the connect() call will hang indefinitely with the default timeout. Adding a timeout ensures tests fail fast rather than blocking the CI pipeline.

🔧 Optional improvement
 $client = new Redis();
-$client->connect($host, $port);
+$client->connect($host, $port, 5.0); // 5 second timeout
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/e2e/Adapter/RedisTest.php` around lines 48 - 49, The Redis connect call
may hang indefinitely; update the test to pass an explicit connection timeout to
Redis::connect so tests fail fast when Redis is unreachable (e.g., call
Redis::connect($host, $port, $timeout) with a small timeout like 1–2s). Locate
the instantiation and connect call on the Redis client ($client = new Redis();
$client->connect(...)) and add the timeout argument (and optional retry_interval
if desired) to avoid CI hangs.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@tests/e2e/Adapter/RedisTest.php`:
- Around line 48-49: The Redis connect call may hang indefinitely; update the
test to pass an explicit connection timeout to Redis::connect so tests fail fast
when Redis is unreachable (e.g., call Redis::connect($host, $port, $timeout)
with a small timeout like 1–2s). Locate the instantiation and connect call on
the Redis client ($client = new Redis(); $client->connect(...)) and add the
timeout argument (and optional retry_interval if desired) to avoid CI hangs.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9e1743ba-1246-41be-a22a-d42ee2aa8f28

📥 Commits

Reviewing files that changed from the base of the PR and between 8b5f56f and a43e72d.

📒 Files selected for processing (5)
  • .github/workflows/tests.yml
  • src/Database/Adapter/Redis.php
  • tests/e2e/Adapter/RedisTest.php
  • tests/e2e/Adapter/Scopes/DocumentTests.php
  • tests/e2e/Adapter/SharedTables/RedisTest.php
🚧 Files skipped from review as they are similar to previous changes (2)
  • .github/workflows/tests.yml
  • tests/e2e/Adapter/SharedTables/RedisTest.php

@abnegate abnegate merged commit 609ebcd into main May 1, 2026
45 of 46 checks passed
@abnegate abnegate deleted the feat-redis-adapter branch May 1, 2026 01:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants