Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/design/01-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ These are the core CRUD operations. All must be fully implemented.
- REQ-DATA-012: TransactGetItems must be atomic — consistent snapshot of up to 100 items
- REQ-DATA-013: All data operations must support legacy `Expected` and `ConditionalOperator` parameters (converted internally to expressions)
- REQ-DATA-014: All write operations must trigger stream event capture when streams are enabled on the table
- REQ-DATA-015: `ConsistentRead` parameter must be accepted on GetItem, Query, Scan, and BatchGetItem. When a read replica is configured, `ConsistentRead=false` reads must route to the replica; `ConsistentRead=true` reads must route to the primary. When no replica is configured, all reads are strongly consistent. Capacity calculations must reflect the consistency mode regardless of replica configuration (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent).
- REQ-DATA-015: `ConsistentRead` parameter must be accepted on GetItem, Query, Scan, and BatchGetItem. Capacity calculations must reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent). No in-tree backend routes a read on the parameter. Routing `ConsistentRead=false` reads to a follower is proposed for the PostgreSQL backend (REQ-PG-008) and not implemented.

### 3.2 Control Plane Operations (In Scope)

Expand Down Expand Up @@ -449,7 +449,7 @@ The catalog database stores extenddb metadata: table definitions, indexes, tags,
- REQ-PG-005: Use parameterized queries for all read operations to prevent SQL injection
- REQ-PG-006: Schema migrations managed via embedded migration files
- REQ-PG-007: Support PostgreSQL 14+
- REQ-PG-008: Support optional read replica connection for eventually consistent reads. When `read_replica_url` is configured, `ConsistentRead=false` reads (GetItem, Query, Scan, BatchGetItem) route to the replica pool. All writes and `ConsistentRead=true` reads always use the primary pool.
- REQ-PG-008 (proposed, not implemented): Support an optional read replica connection for eventually consistent reads. When a `read_replica_url` is configured, `ConsistentRead=false` reads (GetItem, Query, Scan, BatchGetItem) would route to the replica pool, and all writes and `ConsistentRead=true` reads would use the primary pool. The backend has no such setting today: `PostgresStorageConfig` (`crates/storage-postgres/src/config.rs`) refuses unknown fields, so a configuration carrying `read_replica_url` stops the server at startup. Every item read and write runs on the data pool (`data_pool`). Design document 04, section 5.3, sketches the proposal with one replica URL; design document 11, Stage 2, plans the same routing with a `replicas` list.

## 9. Expression Engine Requirements

Expand Down
14 changes: 7 additions & 7 deletions docs/design/02-high-level-design.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,18 +265,18 @@ sequenceDiagram
- sqlx provides native async PostgreSQL support
- The storage trait being async allows backends that use network I/O (e.g., a future DynamoDB-backed backend for testing, or a distributed storage backend)

### 4.7 Read Consistency via Optional Read Replica
### 4.7 Read Consistency and the Proposed PostgreSQL Read Replica

**Decision:** All reads are strongly consistent by default. Eventually consistent reads (`ConsistentRead=false`) are supported by routing to an optional PostgreSQL streaming replica.
**Decision:** The PostgreSQL backend serves every item read from `data_pool`, the pool that takes the writes, so its reads are strongly consistent whatever `ConsistentRead` says. The flag is accepted and charged (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent). Routing `ConsistentRead=false` reads to a PostgreSQL streaming replica is a proposal (design document 04, section 5.3; REQ-PG-008; design document 11, Stage 2) and not code that exists.

**Rationale:**
- DynamoDB's eventual consistency is a consequence of reading from storage replicas that haven't received the latest write yet — it's not artificial delays or queues
- Strongly consistent is strictly stronger than eventually consistent — any application that works against eventually consistent DynamoDB also works against a strongly consistent backend
- For migration testing (relational → DynamoDB), teams need to surface bugs where application code assumes strong consistency on default reads. A real read replica provides genuine staleness without synthetic delays
- The read replica is opt-in via `storage.postgres.read_replica_url`. When unset, all reads hit the primary — simple, correct, and matching what DynamoDB Local and other emulators do
- Writes always go to the primary. `ConsistentRead=true` always reads from the primary. Only `ConsistentRead=false` reads route to the replica
- Capacity calculations always reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent) regardless of whether a replica is configured
- This requires zero changes to the storage trait, engine, or server — it's entirely contained in the PostgreSQL backend's pool selection logic
- For migration testing (relational → DynamoDB), teams need to surface bugs where application code assumes strong consistency on default reads. A read replica would return values that lag the primary without synthetic delays
- In the proposal the replica is opt-in; when it is unset, all reads go to the primary, which is what the backend does today
- Writes always go to the primary. `ConsistentRead=true` always reads from the primary. In the proposal only `ConsistentRead=false` reads route to the replica
- Capacity calculations always reflect the consistency mode (0.5 RCU for eventually consistent, 1.0 RCU for strongly consistent) whether or not a replica exists
- The proposal routes on the flag inside the PostgreSQL backend, so it needs the storage trait to carry the flag (design document 11, Stage 1); with that in place it needs no change to the engine or server and is contained in the backend's pool selection

## 5. Data Flow

Expand Down
5 changes: 5 additions & 0 deletions docs/design/04-component-storage.md
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,11 @@ The PostgreSQL backend uses two categories of tables:

### 5.2 Connection Pooling

The sketch below is a proposal for a read replica mode, not code in this repository; section 5.3 states what the
backend does today. It predates the split into a catalog pool and a data pool: its `pool` stands for today's
`data_pool`. `PostgresStorageConfig` refuses unknown fields, so a configuration carrying `read_replica_url` or
`read_replica_pool_size` stops the server at startup.

```rust
use sqlx::postgres::PgPoolOptions;

Expand Down
5 changes: 0 additions & 5 deletions docs/design/08-component-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ Environment variables use double-underscore (`__`) as a nesting separator, prefi
| `server.port` | `EXTENDDB__SERVER__PORT` |
| `server.bind_addr` | `EXTENDDB__SERVER__BIND_ADDR` |
| `storage.postgres.connection_string` | `EXTENDDB__STORAGE__POSTGRES__CONNECTION_STRING` |
| `storage.postgres.read_replica_url` | `EXTENDDB__STORAGE__POSTGRES__READ_REPLICA_URL` |
| `storage.sqlite.path` | `EXTENDDB__STORAGE__SQLITE__PATH` |
| `auth.provider` | `EXTENDDB__AUTH__PROVIDER` |
| `auth.encryption_key` | `EXTENDDB__AUTH__ENCRYPTION_KEY` |
Expand Down Expand Up @@ -111,10 +110,6 @@ backend = "postgres" # "postgres" | future: "sqlite", "mysql"
[storage.postgres]
connection_string = "postgresql://localhost:5432/extenddb" # Set credentials via env var in production
pool_size = 20
read_replica_url = "" # Optional: PostgreSQL streaming replica for eventually consistent reads.
# When set, ConsistentRead=false reads route here. When empty, all reads
# use the primary (strongly consistent). Recommended for migration testing.
read_replica_pool_size = 20 # Pool size for the read replica (defaults to pool_size if unset)
connection_timeout_secs = 5
statement_timeout_secs = 30

Expand Down
2 changes: 1 addition & 1 deletion docs/technical-debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Last updated: 2026-09-16
| # | Item | Location | Priority | Origin |
|---|------|----------|----------|--------|
| F-1 | `AttributesToGet` (legacy API) not supported | `core/types/batch.rs:27` | Low | P6 |
| F-2 | `ConsistentRead=false` not routed to read replica | `core/types/item.rs:166` | Low | P5 |
| F-2 | `ConsistentRead=false` is served the same way as `ConsistentRead=true` on every backend; the PostgreSQL read replica mode that would route it is proposed, not built (REQ-PG-008) | `core/types/item.rs:176` | Low | P5 |
| F-3 | Import/export: full Ion parser not implemented (JSON subset only) | `engine/import_export.rs:339` | Medium | P24 |
| F-4 | Import/export: full Ion writer not implemented (JSON subset only) | `engine/import_export.rs:433` | Medium | P24 |
| F-5 | `PutItem` returns `None` for `Item` field instead of omitting it | `server/lib.rs:235` | Low | P2 |
Expand Down
Loading