@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.
Files changed (3) hide show
  1. package/DESIGN.md +204 -0
  2. package/README.md +2 -0
  3. 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.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.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"