@aws-blocks/bb-distributed-data 0.1.1 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +204 -0
- package/README.md +2 -0
- package/package.json +2 -2
package/DESIGN.md
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# DistributedDatabase — Design
|
|
2
|
+
|
|
3
|
+
Design document for the DistributedDatabase Building Block. For usage, see [README.md](./README.md).
|
|
4
|
+
|
|
5
|
+
**Package:** `@aws-blocks/bb-distributed-data`
|
|
6
|
+
**Type:** Primitive (new infrastructure)
|
|
7
|
+
**AWS Service:** Amazon Aurora DSQL
|
|
8
|
+
|
|
9
|
+
## Architecture
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
data-common (shared abstractions)
|
|
13
|
+
├── DatabaseEngine interface
|
|
14
|
+
├── DatabaseBase class
|
|
15
|
+
├── sql tagged template + SqlQuery
|
|
16
|
+
├── Kysely adapter
|
|
17
|
+
└── splitStatements()
|
|
18
|
+
|
|
19
|
+
bb-distributed-data (this package)
|
|
20
|
+
├── DsqlEngine (AWS — pg.Pool + IAM token auth)
|
|
21
|
+
├── DsqlMockEngine (local — PGlite + validation layer)
|
|
22
|
+
├── Validation layer (DSQL compatibility checks)
|
|
23
|
+
├── TransactionTracker (DDL/DML/row-limit enforcement)
|
|
24
|
+
├── DSQL-specific migration runner
|
|
25
|
+
└── CDK construct (CfnResource + migration CustomResource)
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Why a Separate Block (Not an Engine Flag on Database)
|
|
29
|
+
|
|
30
|
+
1. **Transaction semantics differ** — OCC means callbacks may need retry. Different API contract.
|
|
31
|
+
2. **Feature set is a strict subset** — FK, RLS, triggers, views absent. An engine flag hides this until deploy time.
|
|
32
|
+
3. **Mock parity goes in opposite directions** — PGlite is too permissive for DSQL. A separate block can have a restrictive mock.
|
|
33
|
+
4. **"When to use" guidance is completely different** — customers shouldn't accidentally pick DSQL.
|
|
34
|
+
5. **Multi-region is a first-class capability** — not a bolt-on option.
|
|
35
|
+
|
|
36
|
+
## Engine Implementations
|
|
37
|
+
|
|
38
|
+
### DsqlEngine (AWS Runtime)
|
|
39
|
+
|
|
40
|
+
- `pg.Pool` with IAM token authentication via `@aws-sdk/dsql-signer`
|
|
41
|
+
- Password callback generates fresh tokens per connection (60-min expiry)
|
|
42
|
+
- Translates pg error codes to `DistributedDatabaseErrors` names
|
|
43
|
+
- Pool handles reconnection transparently when connections expire
|
|
44
|
+
|
|
45
|
+
### DsqlMockEngine (Local Dev)
|
|
46
|
+
|
|
47
|
+
- PGlite wrapped with a validation layer
|
|
48
|
+
- `validateStatement()` rejects unsupported SQL before execution
|
|
49
|
+
- `TransactionTracker` enforces DDL/DML separation and 3,000-row limit
|
|
50
|
+
- `simulateConflict()` test helper for OCC testing
|
|
51
|
+
- Error translation matches production behavior
|
|
52
|
+
|
|
53
|
+
## Validation Layer
|
|
54
|
+
|
|
55
|
+
The core insight: PGlite supports everything DSQL doesn't. Without validation, code works locally but breaks in production — the worst failure mode. The mock actively restricts PGlite to match DSQL's subset.
|
|
56
|
+
|
|
57
|
+
### Statement Validation
|
|
58
|
+
|
|
59
|
+
`validateStatement(sql)` strips string literals and comments, then checks regex patterns:
|
|
60
|
+
|
|
61
|
+
| Pattern | Rejects |
|
|
62
|
+
|---------|---------|
|
|
63
|
+
| `FOREIGN KEY` / `REFERENCES` | FK constraints |
|
|
64
|
+
| `CREATE TRIGGER` | Triggers |
|
|
65
|
+
| `CREATE VIEW` | Views |
|
|
66
|
+
| `LANGUAGE plpgsql` | PL/pgSQL functions |
|
|
67
|
+
| `SERIAL` / `BIGSERIAL` | Sequences |
|
|
68
|
+
| `TRUNCATE` | Use DELETE FROM |
|
|
69
|
+
| `LISTEN` / `NOTIFY` | Async notifications |
|
|
70
|
+
| `CREATE EXTENSION` | Extensions |
|
|
71
|
+
| `ADD COLUMN ... DEFAULT` | Column default on ALTER |
|
|
72
|
+
| `ALTER DEFAULT PRIVILEGES` | Not supported by DSQL |
|
|
73
|
+
| `CREATE POLICY` / `ENABLE ROW LEVEL SECURITY` | RLS |
|
|
74
|
+
| `CREATE TEMP TABLE` | Temporary tables |
|
|
75
|
+
| `SET TRANSACTION ISOLATION LEVEL` | Fixed Repeatable Read |
|
|
76
|
+
| `COLLATE` | C collation only |
|
|
77
|
+
|
|
78
|
+
### Transaction Tracking
|
|
79
|
+
|
|
80
|
+
`TransactionTracker` enforces per-transaction constraints:
|
|
81
|
+
|
|
82
|
+
- Max 1 DDL statement per transaction
|
|
83
|
+
- Cannot mix DDL and DML in the same transaction
|
|
84
|
+
- Max 3,000 rows mutated (cumulative across all executes)
|
|
85
|
+
|
|
86
|
+
### Migration Validation
|
|
87
|
+
|
|
88
|
+
`validateMigrations()` checks all files upfront before running any:
|
|
89
|
+
|
|
90
|
+
- Each file: max 1 DDL statement
|
|
91
|
+
- Each file: no DDL + DML mixing
|
|
92
|
+
- All statements pass `validateStatement()`
|
|
93
|
+
|
|
94
|
+
## Migration Runner
|
|
95
|
+
|
|
96
|
+
DSQL's migration runner differs from the generic one in `data-common`:
|
|
97
|
+
|
|
98
|
+
- **DDL files** run as implicit transactions (no explicit BEGIN/COMMIT) — DSQL auto-commits DDL
|
|
99
|
+
- **DML files** run in explicit transactions (atomic)
|
|
100
|
+
- `validateMigrations()` runs upfront — catches errors before any SQL executes
|
|
101
|
+
- Uses `gen_random_uuid()` for `_migrations.id` (no SERIAL)
|
|
102
|
+
|
|
103
|
+
## OCC Retry Logic
|
|
104
|
+
|
|
105
|
+
```typescript
|
|
106
|
+
async transaction<T>(fn, options?) {
|
|
107
|
+
const maxAttempts = options?.retryOnConflict ? (options.maxRetries ?? 3) + 1 : 1;
|
|
108
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
109
|
+
try {
|
|
110
|
+
return await this.base.transaction(fn);
|
|
111
|
+
} catch (e) {
|
|
112
|
+
const isOcc = e.code === '40001' || e.name === 'SerializationFailureException';
|
|
113
|
+
if (isOcc && attempt < maxAttempts) continue;
|
|
114
|
+
throw e;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
Default: no retry (honest, predictable). `retryOnConflict: true` is explicit opt-in with JSDoc warning about side effects.
|
|
121
|
+
|
|
122
|
+
## Error Translation
|
|
123
|
+
|
|
124
|
+
Happens in the engine layer (same pattern as bb-data engines):
|
|
125
|
+
|
|
126
|
+
| pg error code | DistributedDatabaseErrors name |
|
|
127
|
+
|---------------|------------------------|
|
|
128
|
+
| `40001` | `SerializationFailure` |
|
|
129
|
+
| `23505` | `UniqueConstraintViolation` |
|
|
130
|
+
| `08xxx` | `ConnectionFailed` |
|
|
131
|
+
| (other) | `QueryFailed` |
|
|
132
|
+
|
|
133
|
+
The `DistributedDatabase` class does not wrap errors — engines handle translation.
|
|
134
|
+
|
|
135
|
+
## Infrastructure (CDK)
|
|
136
|
+
|
|
137
|
+
| Resource | Purpose |
|
|
138
|
+
|----------|---------|
|
|
139
|
+
| `AWS::DSQL::Cluster` (CfnResource) | DSQL cluster |
|
|
140
|
+
| Migration Lambda (NodejsFunction) | Runs .sql files on deploy |
|
|
141
|
+
| CustomResource + Provider | Triggers migration on deploy |
|
|
142
|
+
| IAM PolicyStatement | `dsql:DbConnect` (app Lambda), `dsql:DbConnectAdmin` (migration Lambda) |
|
|
143
|
+
| CfnOutput | Cluster endpoint |
|
|
144
|
+
|
|
145
|
+
### Deletion Protection
|
|
146
|
+
|
|
147
|
+
`DeletionProtectionEnabled` is computed from `removalPolicy`:
|
|
148
|
+
- `destroy` or `sandboxMode=true` → disabled
|
|
149
|
+
- Otherwise → enabled
|
|
150
|
+
|
|
151
|
+
### Migration Lambda
|
|
152
|
+
|
|
153
|
+
- Connects via `pg.Client` + `DsqlSigner.getDbConnectAdminAuthToken()`
|
|
154
|
+
- Retries with exponential backoff on transient connection errors (cluster may take a moment after creation)
|
|
155
|
+
- `.sql` files bundled into the Lambda package via CDK `commandHooks`
|
|
156
|
+
- `migrationsHash` property triggers re-invocation when files change
|
|
157
|
+
- **Provisions custom DB role** on every deploy (idempotent):
|
|
158
|
+
1. `CREATE ROLE "app_role" WITH LOGIN` (if not exists)
|
|
159
|
+
2. `AWS IAM GRANT "app_role" TO 'arn:aws:iam::...:role/...'` (maps IAM to DB role)
|
|
160
|
+
3. Per-table `GRANT SELECT, INSERT, UPDATE, DELETE` on user-created tables
|
|
161
|
+
|
|
162
|
+
### DSQL Permission Model Limitations
|
|
163
|
+
|
|
164
|
+
- `ALTER DEFAULT PRIVILEGES` — not supported (system entity error)
|
|
165
|
+
- `GRANT ... ON ALL TABLES IN SCHEMA public` — not supported (`public` is a system entity)
|
|
166
|
+
- `GRANT USAGE ON SCHEMA public` — not supported (same reason)
|
|
167
|
+
- Handled by enumerating user tables via `pg_tables` and granting DML individually on each deploy
|
|
168
|
+
|
|
169
|
+
## Mock vs AWS Behavior Differences
|
|
170
|
+
|
|
171
|
+
| Behavior Difference | Impact | Mitigation |
|
|
172
|
+
|------------|--------|------------|
|
|
173
|
+
| No real OCC conflicts | Single-connection PGlite has no concurrency | `simulateConflict()` test helper |
|
|
174
|
+
| PGlite supports JSONB columns | DSQL rejects JSONB as a column type (use JSON instead; JSONB available as runtime cast only) | Validator rejects JSONB in DDL |
|
|
175
|
+
| System collation vs C only | String sorting may differ | Reject explicit COLLATE |
|
|
176
|
+
| No 60-min connection timeout | Dev sessions are short | Document only |
|
|
177
|
+
| No 10 MiB / 5-min tx limits | Impractical to measure locally | Document only |
|
|
178
|
+
| CREATE INDEX ASYNC is synchronous | Index immediately available locally | Log warning |
|
|
179
|
+
|
|
180
|
+
## Connection Management
|
|
181
|
+
|
|
182
|
+
DSQL uses IAM token authentication:
|
|
183
|
+
- **App Lambda**: `DsqlSigner.getDbConnectAuthToken()` (DML only)
|
|
184
|
+
- **Migration Lambda**: `DsqlSigner.getDbConnectAdminAuthToken()` (DDL)
|
|
185
|
+
- `pg.Pool` with password callback (fresh token per connection)
|
|
186
|
+
- 60-min connection timeout — transparent to Lambda (short-lived)
|
|
187
|
+
- No secrets, no VPC, no proxy needed
|
|
188
|
+
|
|
189
|
+
## Relationship to data-common
|
|
190
|
+
|
|
191
|
+
`bb-distributed-data` uses `DatabaseBase` from `data-common` directly (no subclass). Error translation is in the engine. This is the cleaner pattern — `bb-data` subclasses `DatabaseBase` only because it adds RLS support.
|
|
192
|
+
|
|
193
|
+
Shared from `data-common`:
|
|
194
|
+
- `DatabaseEngine` / `DatabaseBase` / `TransactionHandle`
|
|
195
|
+
- `sql` / `SqlQuery` / `unwrapQuery`
|
|
196
|
+
- `createKyselyAdapter`
|
|
197
|
+
- `splitStatements`
|
|
198
|
+
|
|
199
|
+
DSQL-specific (not shared):
|
|
200
|
+
- `validateStatement` / `classifyStatement` / `TransactionTracker`
|
|
201
|
+
- `validateMigrations`
|
|
202
|
+
- `DsqlEngine` / `DsqlMockEngine`
|
|
203
|
+
- `DistributedDatabaseErrors`
|
|
204
|
+
- OCC retry logic
|
package/README.md
CHANGED
|
@@ -6,6 +6,8 @@ Serverless SQL database backed by Amazon Aurora DSQL. Zero-ops, instant provisio
|
|
|
6
6
|
|
|
7
7
|
**When NOT to use:** If you need foreign keys, Row Level Security, triggers, views, or stored procedures — use `Database` (Aurora). If you need transactions that must not fail at commit under contention — use `Database`. If you're connecting to Supabase — use `Database` with `fromExisting()`.
|
|
8
8
|
|
|
9
|
+
> Design & mock parity details: [DESIGN.md](./DESIGN.md)
|
|
10
|
+
|
|
9
11
|
## Quick Start
|
|
10
12
|
|
|
11
13
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-blocks/bb-distributed-data",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"author": "Amazon Web Services",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"dependencies": {
|
|
30
30
|
"@aws-blocks/core": "^0.1.1",
|
|
31
31
|
"@aws-blocks/data-common": "^0.1.1",
|
|
32
|
-
"@aws-blocks/bb-logger": "^0.1.
|
|
32
|
+
"@aws-blocks/bb-logger": "^0.1.2",
|
|
33
33
|
"@aws-sdk/dsql-signer": "^3.700.0",
|
|
34
34
|
"@electric-sql/pglite": "^0.2.0",
|
|
35
35
|
"pg": "^8.13.0"
|