@aws-blocks/bb-distributed-data 0.1.3 → 0.1.6
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 +1 -0
- package/README.md +5 -0
- package/dist/e2e-mock.test.js +4 -0
- package/dist/engines/dsql-mock-engine.d.ts +23 -2
- package/dist/engines/dsql-mock-engine.d.ts.map +1 -1
- package/dist/engines/dsql-mock-engine.js +63 -6
- package/dist/engines/dsql-mock-engine.test.d.ts +2 -0
- package/dist/engines/dsql-mock-engine.test.d.ts.map +1 -0
- package/dist/engines/dsql-mock-engine.test.js +112 -0
- package/dist/index.aws.d.ts.map +1 -1
- package/dist/index.aws.js +2 -1
- package/dist/index.cdk.d.ts +7 -0
- package/dist/index.cdk.d.ts.map +1 -1
- package/dist/index.cdk.js +23 -8
- package/dist/index.cdk.test.js +31 -10
- package/dist/index.mock.d.ts.map +1 -1
- package/dist/index.mock.js +2 -1
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +56 -0
- package/dist/types.d.ts +4 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +1 -0
- package/dist/validation.test.js +26 -0
- package/dist/version.d.ts +3 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +3 -0
- package/package.json +14 -4
package/DESIGN.md
CHANGED
|
@@ -75,6 +75,7 @@ The core insight: PGlite supports everything DSQL doesn't. Without validation, c
|
|
|
75
75
|
| `SET TRANSACTION ISOLATION LEVEL` | Fixed Repeatable Read |
|
|
76
76
|
| `COLLATE` | C collation only |
|
|
77
77
|
| `CREATE INDEX ... ASC/DESC` | Sort direction on index keys (NULLS FIRST/LAST is allowed) |
|
|
78
|
+
| `ALTER TABLE ... DROP [COLUMN]` | Not in DSQL's supported ALTER TABLE subset (`DROP CONSTRAINT` and `ALTER COLUMN ... DROP DEFAULT/NOT NULL/EXPRESSION/IDENTITY` are supported) |
|
|
78
79
|
|
|
79
80
|
### Transaction Tracking
|
|
80
81
|
|
package/README.md
CHANGED
|
@@ -90,6 +90,10 @@ CREATE INDEX ASYNC idx_users_email ON users(email);
|
|
|
90
90
|
|
|
91
91
|
Migrations are validated at dev time — unsupported features (FK, SERIAL, TRUNCATE, etc.) are caught before deploy.
|
|
92
92
|
|
|
93
|
+
> **Set `migrationsPath` to a path relative to your project root** (e.g. `'./aws-blocks/dsql-migrations'`); it's resolved at synth from the directory you run `cdk` / `npm run deploy` in. That's the simplest reliable pattern.
|
|
94
|
+
>
|
|
95
|
+
> You don't need `fileURLToPath(import.meta.url)` for this. Your backend module runs as ESM locally but is bundled to **CommonJS** in Lambda, where `import.meta` is empty. AWS Blocks shims `import.meta.url` / `import.meta.dirname` in the bundle so it won't crash at load — but at runtime those resolve to the **bundled output** location, not your source tree. So don't use `import.meta.url` to read a file relative to your source at request time; inline the data or ship it as a Lambda asset instead.
|
|
96
|
+
|
|
93
97
|
## DSQL Limitations
|
|
94
98
|
|
|
95
99
|
DSQL is a subset of PostgreSQL. The local mock enforces these restrictions so code that works locally also works in production.
|
|
@@ -109,6 +113,7 @@ DSQL is a subset of PostgreSQL. The local mock enforces these restrictions so co
|
|
|
109
113
|
| Extensions | Not available |
|
|
110
114
|
| ADD COLUMN with DEFAULT | Add column without default, handle nulls in app |
|
|
111
115
|
| Index key sort direction (`ASC`/`DESC`) | Omit it; enforce ordering with `ORDER BY` in queries (`NULLS FIRST/LAST` is supported) |
|
|
116
|
+
| `ALTER TABLE DROP [COLUMN]` | Leave the column in place and stop referencing it; or rebuild the table (create new → `INSERT INTO ... SELECT` → `DROP` → `RENAME TO`, one migration file per step) |
|
|
112
117
|
|
|
113
118
|
### Transaction Constraints
|
|
114
119
|
|
package/dist/e2e-mock.test.js
CHANGED
|
@@ -157,6 +157,10 @@ describe('DsqlMockEngine — CREATE INDEX ASYNC parity', () => {
|
|
|
157
157
|
it('rejects CREATE INDEX ASYNC with a DESC sort order on a key', async () => {
|
|
158
158
|
await assert.rejects(() => engine.withDdl(() => db.execute(sql `CREATE INDEX ASYNC idx_users_email_desc ON users (email DESC)`)), /sort order/i);
|
|
159
159
|
});
|
|
160
|
+
it('rejects ALTER TABLE DROP COLUMN', async () => {
|
|
161
|
+
await engine.withDdl(() => db.execute(sql `CREATE TABLE legacy (id TEXT PRIMARY KEY, obsolete TEXT)`));
|
|
162
|
+
await assert.rejects(() => engine.withDdl(() => db.execute(sql `ALTER TABLE legacy DROP COLUMN obsolete`)), /DROP COLUMN/i);
|
|
163
|
+
});
|
|
160
164
|
it('does not strip ASYNC outside of CREATE INDEX (column named "async")', async () => {
|
|
161
165
|
await engine.withDdl(() => db.execute(sql `CREATE TABLE jobs (id TEXT PRIMARY KEY, async BOOLEAN)`));
|
|
162
166
|
await db.execute(sql `INSERT INTO jobs (id, async) VALUES (${'j1'}, ${true})`);
|
|
@@ -1,10 +1,31 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* PGlite engine wrapped with DSQL validation layer for local development.
|
|
3
|
+
*/
|
|
4
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
5
|
+
import { type DatabaseEngine, type TransactionHandle } from '@aws-blocks/data-common';
|
|
2
6
|
export declare class DsqlMockEngine implements DatabaseEngine {
|
|
3
7
|
private db;
|
|
4
8
|
private closed;
|
|
5
9
|
private shouldConflict;
|
|
6
10
|
private _allowDdl;
|
|
7
|
-
|
|
11
|
+
private readonly dataDir;
|
|
12
|
+
private readonly createClient;
|
|
13
|
+
private ready?;
|
|
14
|
+
/**
|
|
15
|
+
* @param dataDir - directory the PGlite mock persists to.
|
|
16
|
+
* @param createClient - factory for the underlying PGlite instance; defaults
|
|
17
|
+
* to a real `PGlite`. Exposed as a seam so tests can inject an instance
|
|
18
|
+
* that simulates a WASM init trap.
|
|
19
|
+
*/
|
|
20
|
+
constructor(dataDir: string, createClient?: (dataDir: string) => PGlite);
|
|
21
|
+
private createDb;
|
|
22
|
+
/**
|
|
23
|
+
* Force PGlite's lazy WASM initialization, retrying past intermittent
|
|
24
|
+
* `_pg_initdb` `unreachable` traps by recreating the instance. Runs once per
|
|
25
|
+
* engine; a permanent failure is not cached, so a later query can retry once
|
|
26
|
+
* transient memory pressure eases.
|
|
27
|
+
*/
|
|
28
|
+
private ensureReady;
|
|
8
29
|
/** Test helper: simulate OCC conflict on next commit. */
|
|
9
30
|
simulateConflict(): void;
|
|
10
31
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dsql-mock-engine.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"dsql-mock-engine.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.ts"],"names":[],"mappings":"AAGA;;GAEG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAG9C,OAAO,EAA6B,KAAK,cAAc,EAAE,KAAK,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoCjH,qBAAa,cAAe,YAAW,cAAc;IACnD,OAAO,CAAC,EAAE,CAAS;IACnB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,cAAc,CAAS;IAC/B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAA8B;IAC3D,OAAO,CAAC,KAAK,CAAC,CAAkB;IAEhC;;;;;OAKG;gBACS,OAAO,EAAE,MAAM,EAAE,YAAY,GAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAiC;IAMjG,OAAO,CAAC,QAAQ;IAMhB;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IA6BnB,yDAAyD;IACzD,gBAAgB,IAAI,IAAI;IAExB;;;OAGG;IACG,OAAO,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC;IAK5C,KAAK,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAMvD,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAMvE,gBAAgB,IAAI,OAAO,CAAC,iBAAiB,CAAC;IAQ9C,iBAAiB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAc3D,mBAAmB,CAAC,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAK7D,kBAAkB,CAAC,CAAC,EAAE,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAO/F,oBAAoB,CAAC,MAAM,EAAE,iBAAiB,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAW/G,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CAK/B"}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import { PGlite } from '@electric-sql/pglite';
|
|
7
7
|
import { existsSync, unlinkSync, mkdirSync } from 'node:fs';
|
|
8
8
|
import { join } from 'node:path';
|
|
9
|
+
import { initializePgliteWithRetry } from '@aws-blocks/data-common';
|
|
9
10
|
import { DistributedDatabaseErrors, PG_SERIALIZATION_FAILURE, translateDsqlError } from '../errors.js';
|
|
10
11
|
import { validateStatement, classifyStatement, TransactionTracker } from '../validation.js';
|
|
11
12
|
function cleanStaleLock(dataDir) {
|
|
@@ -44,10 +45,58 @@ export class DsqlMockEngine {
|
|
|
44
45
|
closed = false;
|
|
45
46
|
shouldConflict = false;
|
|
46
47
|
_allowDdl = false;
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
dataDir;
|
|
49
|
+
createClient;
|
|
50
|
+
ready;
|
|
51
|
+
/**
|
|
52
|
+
* @param dataDir - directory the PGlite mock persists to.
|
|
53
|
+
* @param createClient - factory for the underlying PGlite instance; defaults
|
|
54
|
+
* to a real `PGlite`. Exposed as a seam so tests can inject an instance
|
|
55
|
+
* that simulates a WASM init trap.
|
|
56
|
+
*/
|
|
57
|
+
constructor(dataDir, createClient = (dir) => new PGlite(dir)) {
|
|
58
|
+
this.dataDir = dataDir;
|
|
59
|
+
this.createClient = createClient;
|
|
60
|
+
this.db = this.createDb();
|
|
61
|
+
}
|
|
62
|
+
createDb() {
|
|
63
|
+
cleanStaleLock(this.dataDir);
|
|
64
|
+
mkdirSync(this.dataDir, { recursive: true });
|
|
65
|
+
return this.createClient(this.dataDir);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Force PGlite's lazy WASM initialization, retrying past intermittent
|
|
69
|
+
* `_pg_initdb` `unreachable` traps by recreating the instance. Runs once per
|
|
70
|
+
* engine; a permanent failure is not cached, so a later query can retry once
|
|
71
|
+
* transient memory pressure eases.
|
|
72
|
+
*/
|
|
73
|
+
ensureReady() {
|
|
74
|
+
if (!this.ready) {
|
|
75
|
+
this.ready = initializePgliteWithRetry(this.db, () => (this.db = this.createDb()), {
|
|
76
|
+
onRetry: (attempt, error) => console.warn(`[DsqlMockEngine] PGlite init trap on attempt ${attempt}; recreating instance`, error),
|
|
77
|
+
})
|
|
78
|
+
// Pin this.db to the settled instance explicitly (not just via the recreate closure).
|
|
79
|
+
.then((db) => (this.db = db))
|
|
80
|
+
.catch((error) => {
|
|
81
|
+
// Init retry is exhausted; initializePgliteWithRetry has already closed
|
|
82
|
+
// the last trapped instance, so this.db now points at a dead handle.
|
|
83
|
+
// Reset readiness AND swap in a fresh, un-probed instance. Without the
|
|
84
|
+
// swap, the next call would re-probe the CLOSED handle, whose error is a
|
|
85
|
+
// "closed" error (not an `unreachable` trap) and is therefore classified
|
|
86
|
+
// non-retryable — permanently wedging the engine and defeating the
|
|
87
|
+
// "a later query can retry" recovery documented above. Recreating here
|
|
88
|
+
// is best effort; if it throws we keep propagating the original init error.
|
|
89
|
+
this.ready = undefined;
|
|
90
|
+
try {
|
|
91
|
+
this.db = this.createDb();
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
// Leave the dead handle in place; the original init error is more useful.
|
|
95
|
+
}
|
|
96
|
+
throw error;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return this.ready;
|
|
51
100
|
}
|
|
52
101
|
/** Test helper: simulate OCC conflict on next commit. */
|
|
53
102
|
simulateConflict() { this.shouldConflict = true; }
|
|
@@ -67,6 +116,7 @@ export class DsqlMockEngine {
|
|
|
67
116
|
async query(sql, params) {
|
|
68
117
|
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
69
118
|
try {
|
|
119
|
+
await this.ensureReady();
|
|
70
120
|
return (await this.db.query(normalized, params)).rows;
|
|
71
121
|
}
|
|
72
122
|
catch (e) {
|
|
@@ -76,6 +126,7 @@ export class DsqlMockEngine {
|
|
|
76
126
|
async execute(sql, params) {
|
|
77
127
|
const normalized = preprocessSqlForDsqlMock(sql, { allowDdl: this._allowDdl });
|
|
78
128
|
try {
|
|
129
|
+
await this.ensureReady();
|
|
79
130
|
return { rowCount: (await this.db.query(normalized, params)).affectedRows ?? 0 };
|
|
80
131
|
}
|
|
81
132
|
catch (e) {
|
|
@@ -83,8 +134,14 @@ export class DsqlMockEngine {
|
|
|
83
134
|
}
|
|
84
135
|
}
|
|
85
136
|
async beginTransaction() {
|
|
86
|
-
|
|
87
|
-
|
|
137
|
+
try {
|
|
138
|
+
await this.ensureReady();
|
|
139
|
+
await this.db.query('BEGIN');
|
|
140
|
+
return { active: true, tracker: new TransactionTracker() };
|
|
141
|
+
}
|
|
142
|
+
catch (e) {
|
|
143
|
+
translateDsqlError(e);
|
|
144
|
+
}
|
|
88
145
|
}
|
|
89
146
|
async commitTransaction(handle) {
|
|
90
147
|
if (this.shouldConflict) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dsql-mock-engine.test.d.ts","sourceRoot":"","sources":["../../src/engines/dsql-mock-engine.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
import { test, afterEach } from 'node:test';
|
|
4
|
+
import assert from 'node:assert';
|
|
5
|
+
import { PGlite } from '@electric-sql/pglite';
|
|
6
|
+
import { rmSync } from 'node:fs';
|
|
7
|
+
import { join } from 'node:path';
|
|
8
|
+
import { DsqlMockEngine } from './dsql-mock-engine.js';
|
|
9
|
+
import { DistributedDatabaseErrors } from '../errors.js';
|
|
10
|
+
const TEST_DIR = '.bb-data-dsql-mock-test-' + process.pid;
|
|
11
|
+
let engine;
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
if (engine)
|
|
14
|
+
await engine.destroy().catch(() => { });
|
|
15
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
16
|
+
});
|
|
17
|
+
// --- Regression #188: PGlite WASM `_pg_initdb` `unreachable` init trap ---
|
|
18
|
+
// The DSQL mock wraps PGlite; the same lazy-init WASM trap kills the dev server
|
|
19
|
+
// during runMigrations. A factory injects a first instance that traps on the
|
|
20
|
+
// init probe; recovery falls through to a REAL PGlite so data round-trips.
|
|
21
|
+
test('recovers when the first PGlite instance traps during init', async () => {
|
|
22
|
+
const dir = join(TEST_DIR, 'init-trap-recover');
|
|
23
|
+
let creates = 0;
|
|
24
|
+
const factory = (d) => {
|
|
25
|
+
creates++;
|
|
26
|
+
if (creates === 1) {
|
|
27
|
+
return {
|
|
28
|
+
query: async () => {
|
|
29
|
+
throw new Error('Aborted(). Build with -sASSERTIONS for more info. RuntimeError: unreachable');
|
|
30
|
+
},
|
|
31
|
+
close: async () => { },
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return new PGlite(d);
|
|
35
|
+
};
|
|
36
|
+
engine = new DsqlMockEngine(dir, factory);
|
|
37
|
+
await engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)'));
|
|
38
|
+
await engine.execute("INSERT INTO t (id) VALUES ('ok')");
|
|
39
|
+
const rows = await engine.query('SELECT id FROM t');
|
|
40
|
+
assert.deepStrictEqual(rows, [{ id: 'ok' }]);
|
|
41
|
+
assert.strictEqual(creates, 2, 'engine should recreate the trapped instance exactly once');
|
|
42
|
+
});
|
|
43
|
+
test('surfaces a QueryFailed error when init keeps trapping', async () => {
|
|
44
|
+
const dir = join(TEST_DIR, 'init-trap-persistent');
|
|
45
|
+
const factory = () => ({
|
|
46
|
+
query: async () => {
|
|
47
|
+
throw new Error('RuntimeError: unreachable');
|
|
48
|
+
},
|
|
49
|
+
close: async () => { },
|
|
50
|
+
});
|
|
51
|
+
engine = new DsqlMockEngine(dir, factory);
|
|
52
|
+
await assert.rejects(() => engine.execute("INSERT INTO t (id) VALUES ('x')"), (err) => {
|
|
53
|
+
assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
|
|
54
|
+
return true;
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
// beginTransaction() forces the same lazy WASM init as query()/execute(); an
|
|
58
|
+
// exhausted init-retry at transaction start must also normalize to QueryFailed
|
|
59
|
+
// rather than leaking a raw `RuntimeError: unreachable`.
|
|
60
|
+
test('surfaces a QueryFailed error when init keeps trapping during beginTransaction', async () => {
|
|
61
|
+
const dir = join(TEST_DIR, 'init-trap-begin-transaction');
|
|
62
|
+
const factory = () => ({
|
|
63
|
+
query: async () => {
|
|
64
|
+
throw new Error('RuntimeError: unreachable');
|
|
65
|
+
},
|
|
66
|
+
close: async () => { },
|
|
67
|
+
});
|
|
68
|
+
engine = new DsqlMockEngine(dir, factory);
|
|
69
|
+
await assert.rejects(() => engine.beginTransaction(), (err) => {
|
|
70
|
+
assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
|
|
71
|
+
return true;
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
// --- Regression #188 (recovery): a later call must succeed after the init
|
|
75
|
+
// retry budget is fully exhausted, once the factory stops trapping. The
|
|
76
|
+
// exhausted attempt closes the last instance; keeping that dead handle would
|
|
77
|
+
// make the next call re-probe a CLOSED instance whose error is a "closed"
|
|
78
|
+
// error (NOT `unreachable`) and thus non-retryable, wedging the engine
|
|
79
|
+
// permanently. Trapped instances model a real PGlite: `unreachable` while
|
|
80
|
+
// open, a "closed" error after close().
|
|
81
|
+
test('recovers on a later call after the init-retry budget is exhausted', async () => {
|
|
82
|
+
const dir = join(TEST_DIR, 'init-trap-exhaust-recover');
|
|
83
|
+
let creates = 0;
|
|
84
|
+
const factory = (d) => {
|
|
85
|
+
creates++;
|
|
86
|
+
if (creates <= 3) {
|
|
87
|
+
let closed = false;
|
|
88
|
+
return {
|
|
89
|
+
query: async () => {
|
|
90
|
+
throw new Error(closed ? 'PGlite is closed' : 'Aborted(). RuntimeError: unreachable');
|
|
91
|
+
},
|
|
92
|
+
close: async () => {
|
|
93
|
+
closed = true;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return new PGlite(d);
|
|
98
|
+
};
|
|
99
|
+
engine = new DsqlMockEngine(dir, factory);
|
|
100
|
+
// First call exhausts the 3-attempt retry budget and rejects.
|
|
101
|
+
await assert.rejects(() => engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)')), (err) => {
|
|
102
|
+
assert.strictEqual(err.name, DistributedDatabaseErrors.QueryFailed);
|
|
103
|
+
return true;
|
|
104
|
+
});
|
|
105
|
+
// Once the factory stops trapping, a later call must recover on a fresh
|
|
106
|
+
// instance instead of re-probing the closed handle forever.
|
|
107
|
+
await engine.withDdl(() => engine.execute('CREATE TABLE t (id TEXT PRIMARY KEY)'));
|
|
108
|
+
await engine.execute("INSERT INTO t (id) VALUES ('ok')");
|
|
109
|
+
const rows = await engine.query('SELECT id FROM t');
|
|
110
|
+
assert.deepStrictEqual(rows, [{ id: 'ok' }]);
|
|
111
|
+
assert.ok(creates >= 4, 'engine should build a fresh instance after exhaustion');
|
|
112
|
+
});
|
package/dist/index.aws.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAIxF,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.aws.d.ts","sourceRoot":"","sources":["../src/index.aws.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAIxF,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAGjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,OAAO,CAAC,KAAK,CAA6B;IAE1C,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,0BAA0B;IAQjF,OAAO,KAAK,IAAI,GAiBf;IAED,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/C,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAEvD;;;;;OAKG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC;IAInG,gBAAgB;IAChB,SAAS;CACV;AAED,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.aws.js
CHANGED
|
@@ -11,12 +11,13 @@ import { DsqlEngine } from './engines/dsql-engine.js';
|
|
|
11
11
|
import { transactionWithRetry } from './transaction.js';
|
|
12
12
|
import { ENV_SANITIZE, sanitizeDbRoleName } from './constants.js';
|
|
13
13
|
import { Logger } from '@aws-blocks/bb-logger';
|
|
14
|
+
import { BB_NAME, BB_VERSION } from './version.js';
|
|
14
15
|
export class DistributedDatabase extends Scope {
|
|
15
16
|
_base = null;
|
|
16
17
|
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
17
18
|
log;
|
|
18
19
|
constructor(scope, id, _options) {
|
|
19
|
-
super(id, { parent: scope });
|
|
20
|
+
super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
|
|
20
21
|
this.log = _options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
21
22
|
const envName = this.fullId.replace(ENV_SANITIZE, '_');
|
|
22
23
|
const clusterEndpoint = process.env[`BLOCKS_${envName}_ENDPOINT`] ?? '';
|
package/dist/index.cdk.d.ts
CHANGED
|
@@ -8,6 +8,13 @@ import type { ScopeParent } from '@aws-blocks/core';
|
|
|
8
8
|
import type { DistributedDatabaseOptions } from './types.js';
|
|
9
9
|
export declare class DistributedDatabase extends Scope {
|
|
10
10
|
constructor(scope: ScopeParent, id: string, options?: DistributedDatabaseOptions);
|
|
11
|
+
/**
|
|
12
|
+
* Runtime-only. This is the CDK (synth) build: it defines infrastructure and
|
|
13
|
+
* has no engine — queries run in the app Lambda against the deployed cluster.
|
|
14
|
+
* `createKyselyAdapter()` no longer calls this eagerly, so reaching it means a
|
|
15
|
+
* query ran at synth time (e.g. at module scope).
|
|
16
|
+
*/
|
|
17
|
+
getEngine(): never;
|
|
11
18
|
}
|
|
12
19
|
export { sql, createKyselyAdapter } from '@aws-blocks/data-common';
|
|
13
20
|
export type { SqlQuery, Transaction } from '@aws-blocks/data-common';
|
package/dist/index.cdk.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAA0D,MAAM,sBAAsB,CAAC;AACrG,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAQpD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC;AAG7D,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,0BAA0B;IAgGhF;;;;;OAKG;IACH,SAAS,IAAI,KAAK;CAGnB;AAaD,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,0BAA0B,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.cdk.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* Provisions Aurora DSQL cluster via CloudFormation.
|
|
6
6
|
* Optionally runs migrations via a CustomResource Lambda.
|
|
7
7
|
*/
|
|
8
|
-
import { Scope, DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
|
|
8
|
+
import { Scope, DEFAULT_NODE_RUNTIME, synthGuard, blocksNodejsBundling } from '@aws-blocks/core/cdk';
|
|
9
9
|
import * as cdk from 'aws-cdk-lib';
|
|
10
10
|
import * as iam from 'aws-cdk-lib/aws-iam';
|
|
11
11
|
import * as lambda from 'aws-cdk-lib/aws-lambda-nodejs';
|
|
@@ -18,17 +18,23 @@ export class DistributedDatabase extends Scope {
|
|
|
18
18
|
constructor(scope, id, options) {
|
|
19
19
|
super(id, { parent: scope });
|
|
20
20
|
const stack = cdk.Stack.of(this);
|
|
21
|
-
const isSandbox = stack.node.tryGetContext('sandboxMode') === 'true';
|
|
22
21
|
const envName = this.fullId.replace(ENV_SANITIZE, '_');
|
|
23
22
|
const region = stack.region;
|
|
24
23
|
const dbRole = sanitizeDbRoleName(this.fullId);
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
// Removal policy and deletion protection are resolved independently from the
|
|
25
|
+
// stack-wide `defaults` (per-block `removalPolicy` option wins for that field).
|
|
26
|
+
// Reading `defaults.deletionProtection` directly — rather than deriving it
|
|
27
|
+
// from `removalPolicy` — keeps every adopting block consistent: the same
|
|
28
|
+
// `defaults` object yields the same posture no matter which block reads it.
|
|
29
|
+
const removalPolicy = options?.removalPolicy === 'destroy'
|
|
30
|
+
? cdk.RemovalPolicy.DESTROY
|
|
31
|
+
: options?.removalPolicy === 'retain'
|
|
32
|
+
? cdk.RemovalPolicy.RETAIN
|
|
33
|
+
: this.defaults.removalPolicy;
|
|
28
34
|
const cluster = new cdk.CfnResource(stack, `${this.fullId}DsqlCluster`, {
|
|
29
35
|
type: 'AWS::DSQL::Cluster',
|
|
30
36
|
properties: {
|
|
31
|
-
DeletionProtectionEnabled:
|
|
37
|
+
DeletionProtectionEnabled: this.defaults.deletionProtection,
|
|
32
38
|
},
|
|
33
39
|
});
|
|
34
40
|
cluster.applyRemovalPolicy(removalPolicy);
|
|
@@ -63,7 +69,7 @@ export class DistributedDatabase extends Scope {
|
|
|
63
69
|
APP_ROLE_ARN: appRoleArn,
|
|
64
70
|
DB_ROLE_NAME: dbRole,
|
|
65
71
|
},
|
|
66
|
-
bundling: {
|
|
72
|
+
bundling: blocksNodejsBundling({
|
|
67
73
|
commandHooks: {
|
|
68
74
|
beforeBundling: () => [],
|
|
69
75
|
beforeInstall: () => [],
|
|
@@ -71,7 +77,7 @@ export class DistributedDatabase extends Scope {
|
|
|
71
77
|
? [`cp -r ${resolvedMigrationsPath} ${outputDir}${LAMBDA_MIGRATIONS_DIR.replace('/var/task', '')}`]
|
|
72
78
|
: [],
|
|
73
79
|
},
|
|
74
|
-
},
|
|
80
|
+
}),
|
|
75
81
|
});
|
|
76
82
|
// Migration Lambda needs Admin access (DDL + role management)
|
|
77
83
|
migrationFn.addToRolePolicy(new iam.PolicyStatement({
|
|
@@ -88,6 +94,15 @@ export class DistributedDatabase extends Scope {
|
|
|
88
94
|
// Ensure migrations run after cluster is created
|
|
89
95
|
migrationCR.node.addDependency(cluster);
|
|
90
96
|
}
|
|
97
|
+
/**
|
|
98
|
+
* Runtime-only. This is the CDK (synth) build: it defines infrastructure and
|
|
99
|
+
* has no engine — queries run in the app Lambda against the deployed cluster.
|
|
100
|
+
* `createKyselyAdapter()` no longer calls this eagerly, so reaching it means a
|
|
101
|
+
* query ran at synth time (e.g. at module scope).
|
|
102
|
+
*/
|
|
103
|
+
getEngine() {
|
|
104
|
+
return synthGuard('DistributedDatabase', 'getEngine');
|
|
105
|
+
}
|
|
91
106
|
}
|
|
92
107
|
/** Hash all .sql files in a directory to detect changes. */
|
|
93
108
|
function hashMigrationsDir(dir) {
|
package/dist/index.cdk.test.js
CHANGED
|
@@ -12,7 +12,7 @@ import * as lambda from 'aws-cdk-lib/aws-lambda';
|
|
|
12
12
|
import { Template, Match } from 'aws-cdk-lib/assertions';
|
|
13
13
|
import { mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
14
14
|
import { join } from 'node:path';
|
|
15
|
-
import { DEFAULT_NODE_RUNTIME } from '@aws-blocks/core/cdk';
|
|
15
|
+
import { DEFAULT_NODE_RUNTIME, BlocksPresets } from '@aws-blocks/core/cdk';
|
|
16
16
|
import { DistributedDatabase } from './index.cdk.js';
|
|
17
17
|
const MIGRATIONS_DIR = '.bb-data/__test_cdk_migrations__';
|
|
18
18
|
function synth(build) {
|
|
@@ -52,17 +52,35 @@ test('CDK: cluster has DeletionProtectionEnabled=true by default', () => {
|
|
|
52
52
|
DeletionPolicy: 'Retain',
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
-
test('CDK: removalPolicy
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
55
|
+
test('CDK: per-block removalPolicy is independent of deletion protection', () => {
|
|
56
|
+
// Deletion protection is read from `defaults` independently of removalPolicy
|
|
57
|
+
// (consistent across all adopting blocks). Here: sandbox defaults (protection
|
|
58
|
+
// off) with a per-block `removalPolicy: 'retain'` override → the cluster is
|
|
59
|
+
// RETAINed on stack delete but not deletion-protected.
|
|
60
|
+
const app = new cdk.App();
|
|
61
|
+
const stack = new cdk.Stack(app, 'IndepStack');
|
|
62
|
+
const handler = new lambda.Function(stack, 'Handler', {
|
|
63
|
+
runtime: DEFAULT_NODE_RUNTIME,
|
|
64
|
+
handler: 'index.handler',
|
|
65
|
+
code: lambda.Code.fromInline('exports.handler = async () => {};'),
|
|
62
66
|
});
|
|
67
|
+
stack.handler = handler;
|
|
68
|
+
stack.defaults = BlocksPresets.sandbox;
|
|
69
|
+
globalThis.CURRENT_BLOCKS_STACK = stack;
|
|
70
|
+
try {
|
|
71
|
+
new DistributedDatabase(scope(stack), 'mydsql', { removalPolicy: 'retain' });
|
|
72
|
+
const template = Template.fromStack(stack);
|
|
73
|
+
template.hasResource('AWS::DSQL::Cluster', {
|
|
74
|
+
Properties: { DeletionProtectionEnabled: false },
|
|
75
|
+
DeletionPolicy: 'Retain',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
delete globalThis.CURRENT_BLOCKS_STACK;
|
|
80
|
+
}
|
|
63
81
|
});
|
|
64
|
-
test('CDK:
|
|
65
|
-
const app = new cdk.App(
|
|
82
|
+
test('CDK: sandbox defaults disable deletion protection', () => {
|
|
83
|
+
const app = new cdk.App();
|
|
66
84
|
const stack = new cdk.Stack(app, 'SandboxStack');
|
|
67
85
|
const handler = new lambda.Function(stack, 'Handler', {
|
|
68
86
|
runtime: DEFAULT_NODE_RUNTIME,
|
|
@@ -70,6 +88,9 @@ test('CDK: sandboxMode=true disables deletion protection', () => {
|
|
|
70
88
|
code: lambda.Code.fromInline('exports.handler = async () => {};'),
|
|
71
89
|
});
|
|
72
90
|
stack.handler = handler;
|
|
91
|
+
// The cluster's removal/protection follows the stack-wide defaults (resolved
|
|
92
|
+
// via the globalThis fallback here), not the sandboxMode context.
|
|
93
|
+
stack.defaults = BlocksPresets.sandbox;
|
|
73
94
|
globalThis.CURRENT_BLOCKS_STACK = stack;
|
|
74
95
|
try {
|
|
75
96
|
new DistributedDatabase(scope(stack), 'mydsql');
|
package/dist/index.mock.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAIxF,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.mock.d.ts","sourceRoot":"","sources":["../src/index.mock.ts"],"names":[],"mappings":"AAGA;;;GAGG;AAEH,OAAO,EAAE,KAAK,EAA0B,MAAM,kBAAkB,CAAC;AACjE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAgB,KAAK,QAAQ,EAAE,KAAK,WAAW,EAAE,MAAM,yBAAyB,CAAC;AAIxF,OAAO,KAAK,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAEjF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAGzD,qBAAa,mBAAoB,SAAQ,KAAK;IAC5C,OAAO,CAAC,IAAI,CAAe;IAC3B,OAAO,CAAC,UAAU,CAAiB;IACnC,OAAO,CAAC,aAAa,CAA8B;IAEnD,2FAA2F;IAC3F,SAAS,CAAC,GAAG,EAAE,WAAW,CAAC;gBAEf,KAAK,EAAE,WAAW,EAAE,EAAE,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,0BAA0B;YAelE,KAAK;IAEnB,KAAK,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IACvC,QAAQ,CAAC,CAAC,EAAE,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC,CAAC,GAAG,IAAI,CAAC;IAC/C,OAAO,CAAC,KAAK,EAAE,QAAQ,GAAG,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAA;KAAE,CAAC;IAEvD;;;;;OAKG;IACG,WAAW,CAAC,CAAC,EAAE,EAAE,EAAE,CAAC,EAAE,EAAE,WAAW,KAAK,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,CAAC,CAAC;IAKnG,yDAAyD;IACzD,gBAAgB,IAAI,IAAI;IAExB,gBAAgB;IAChB,SAAS;CACV;AAED,OAAO,EAAE,GAAG,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AACnE,YAAY,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACrE,OAAO,EAAE,yBAAyB,EAAE,MAAM,aAAa,CAAC;AACxD,YAAY,EAAE,0BAA0B,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC"}
|
package/dist/index.mock.js
CHANGED
|
@@ -10,6 +10,7 @@ import { DsqlMockEngine } from './engines/dsql-mock-engine.js';
|
|
|
10
10
|
import { runMigrations, loadMigrationsFromDir } from './migrations.js';
|
|
11
11
|
import { transactionWithRetry } from './transaction.js';
|
|
12
12
|
import { Logger } from '@aws-blocks/bb-logger';
|
|
13
|
+
import { BB_NAME, BB_VERSION } from './version.js';
|
|
13
14
|
export class DistributedDatabase extends Scope {
|
|
14
15
|
base;
|
|
15
16
|
mockEngine;
|
|
@@ -17,7 +18,7 @@ export class DistributedDatabase extends Scope {
|
|
|
17
18
|
/** @internal Logger for internal operations. Defaults to error-level when not provided. */
|
|
18
19
|
log;
|
|
19
20
|
constructor(scope, id, options) {
|
|
20
|
-
super(id, { parent: scope });
|
|
21
|
+
super(id, { parent: scope, bbName: BB_NAME, bbVersion: BB_VERSION });
|
|
21
22
|
this.log = options?.logger ?? new Logger(this, 'logger', { level: 'error' });
|
|
22
23
|
this.mockEngine = new DsqlMockEngine(`.bb-data/${this.fullId}`);
|
|
23
24
|
this.base = new DatabaseBase(this.mockEngine);
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
|
|
2
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
3
|
+
/**
|
|
4
|
+
* Telemetry-registration tests for DistributedDatabase.
|
|
5
|
+
*
|
|
6
|
+
* `Scope.getRegisteredBlocks()` only names a block whose `bbName` is in
|
|
7
|
+
* OFFICIAL_BB_NAMES, and that set is generated from the umbrella's
|
|
8
|
+
* `aws-blocks.vendorize` map. These tests pin the three coupled artifacts to
|
|
9
|
+
* each other: the block's generated BB_NAME, its vendorize entry, and the
|
|
10
|
+
* generated name set. A block that omits `bbMeta` still constructs fine and
|
|
11
|
+
* every other test still passes, so that gap is only visible here.
|
|
12
|
+
*
|
|
13
|
+
* The package ships a distinct mock implementation (the default entry does not
|
|
14
|
+
* re-export the AWS class), so both `index.aws.ts` and `index.mock.ts` carry
|
|
15
|
+
* the `super()` change. Registration happens in `super()`, identical for both;
|
|
16
|
+
* these tests exercise the AWS class because its constructor is side-effect
|
|
17
|
+
* free (the mock eagerly spins up PGlite on disk), keeping the suite fast and
|
|
18
|
+
* deterministic.
|
|
19
|
+
*/
|
|
20
|
+
import { test, describe, beforeEach } from 'node:test';
|
|
21
|
+
import assert from 'node:assert';
|
|
22
|
+
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { dirname, join } from 'node:path';
|
|
24
|
+
import { fileURLToPath } from 'node:url';
|
|
25
|
+
import { Scope } from '@aws-blocks/core';
|
|
26
|
+
import { DistributedDatabase } from './index.aws.js';
|
|
27
|
+
import { BB_NAME, BB_VERSION } from './version.js';
|
|
28
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
29
|
+
let counter = 0;
|
|
30
|
+
function makeDb() {
|
|
31
|
+
const scope = new Scope(`distdata-telemetry-${++counter}`);
|
|
32
|
+
return new DistributedDatabase(scope, 'db');
|
|
33
|
+
}
|
|
34
|
+
describe('DistributedDatabase telemetry registration', () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
Scope._resetRegistry();
|
|
37
|
+
});
|
|
38
|
+
test('BB_NAME is the name the vendorize map and OFFICIAL_BB_NAMES carry', () => {
|
|
39
|
+
assert.strictEqual(BB_NAME, 'DistributedDatabase');
|
|
40
|
+
});
|
|
41
|
+
test('BB_VERSION tracks the package version', () => {
|
|
42
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf-8'));
|
|
43
|
+
assert.strictEqual(BB_VERSION, pkg.version);
|
|
44
|
+
});
|
|
45
|
+
test('an instance carries bbName and bbVersion', () => {
|
|
46
|
+
const db = makeDb();
|
|
47
|
+
assert.strictEqual(db.bbName, BB_NAME);
|
|
48
|
+
assert.strictEqual(db.bbVersion, BB_VERSION);
|
|
49
|
+
});
|
|
50
|
+
test('registers as an official block, so telemetry is allowed to name it', () => {
|
|
51
|
+
makeDb();
|
|
52
|
+
const { blocks, customBlocksCount } = Scope.getRegisteredBlocks();
|
|
53
|
+
assert.deepStrictEqual(blocks.filter(b => b.name === BB_NAME), [{ name: BB_NAME, version: BB_VERSION }]);
|
|
54
|
+
assert.strictEqual(customBlocksCount, 0, 'must not be filtered out as an unnamed custom block');
|
|
55
|
+
});
|
|
56
|
+
});
|
package/dist/types.d.ts
CHANGED
|
@@ -6,8 +6,10 @@ export interface DistributedDatabaseOptions {
|
|
|
6
6
|
/** Path to directory containing numbered .sql migration files. */
|
|
7
7
|
migrationsPath?: string;
|
|
8
8
|
/**
|
|
9
|
-
* CloudFormation removal policy.
|
|
10
|
-
*
|
|
9
|
+
* CloudFormation removal policy for the DSQL cluster. When omitted, the
|
|
10
|
+
* stack-wide `defaults` apply (`production` → RETAIN, `sandbox` → DESTROY).
|
|
11
|
+
* Pass `'destroy'`/`'retain'` to override for this one database. Deletion
|
|
12
|
+
* protection follows: on unless the cluster is being destroyed.
|
|
11
13
|
*/
|
|
12
14
|
removalPolicy?: 'destroy' | 'retain';
|
|
13
15
|
/** Optional logger for internal operations. When omitted, a default Logger at error level is created. */
|
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAGA;;GAEG;AACH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACzD,MAAM,WAAW,0BAA0B;IACzC,kEAAkE;IAClE,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;OAKG;IACH,aAAa,CAAC,EAAE,SAAS,GAAG,QAAQ,CAAC;IACtC,yGAAyG;IACzG,MAAM,CAAC,EAAE,WAAW,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB"}
|
package/dist/validation.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,eAAe,EAAuB,MAAM,yBAAyB,CAAC;AAU/E,mEAAmE;AACnE,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA6C5D;
|
|
1
|
+
{"version":3,"file":"validation.d.ts","sourceRoot":"","sources":["../src/validation.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,eAAe,EAAuB,MAAM,yBAAyB,CAAC;AAU/E,mEAAmE;AACnE,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CA6C5D;AA8BD,uFAAuF;AACvF,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAenD;AAED,kDAAkD;AAClD,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,GAAG,KAAK,GAAG,KAAK,GAAG,OAAO,CAKtE;AAID,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,QAAQ,CAAK;IAErB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAalC,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,IAAI;IAUnC,KAAK,IAAI,IAAI;CACd;AAID,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAiB3E;AAED,OAAO,EAAE,eAAe,EAAE,CAAC"}
|
package/dist/validation.js
CHANGED
|
@@ -73,6 +73,7 @@ const RULES = [
|
|
|
73
73
|
{ pattern: /\b(LISTEN|NOTIFY)\b/i, message: 'DSQL does not support LISTEN/NOTIFY.', severity: 'error' },
|
|
74
74
|
{ pattern: /\bCREATE\s+EXTENSION\b/i, message: 'DSQL does not support extensions.', severity: 'error' },
|
|
75
75
|
{ pattern: /\bALTER\s+TABLE\b[\s\S]*\bADD\s+COLUMN\b[\s\S]*\bDEFAULT\b/i, message: 'DSQL does not support ADD COLUMN with DEFAULT.', severity: 'error' },
|
|
76
|
+
{ pattern: /\bALTER\s+TABLE\b[^;]*\bDROP\s+(?:COLUMN\b|IF\s+EXISTS\b|(?!(?:DEFAULT|NOT|EXPRESSION|IDENTITY|CONSTRAINT)\b)[\w"])/i, message: 'DSQL does not support ALTER TABLE DROP COLUMN. Leave the column in place and stop referencing it, or rebuild the table (create a new table → INSERT INTO ... SELECT → DROP → RENAME TO). (DSQL: "unsupported ALTER TABLE DROP COLUMN statement")', severity: 'error' },
|
|
76
77
|
{ pattern: /\bALTER\s+DEFAULT\s+PRIVILEGES\b/i, message: 'DSQL does not support ALTER DEFAULT PRIVILEGES.', severity: 'error' },
|
|
77
78
|
{ pattern: /\b(CREATE\s+POLICY|ENABLE\s+ROW\s+LEVEL\s+SECURITY)\b/i, message: 'DSQL does not support Row Level Security.', severity: 'error' },
|
|
78
79
|
{ pattern: /\bCREATE\s+(TEMP|TEMPORARY)\s+TABLE\b/i, message: 'DSQL does not support temporary tables.', severity: 'error' },
|
package/dist/validation.test.js
CHANGED
|
@@ -45,12 +45,38 @@ describe('validateStatement', () => {
|
|
|
45
45
|
['ISOLATION LEVEL', 'SET TRANSACTION ISOLATION LEVEL SERIALIZABLE'],
|
|
46
46
|
// DSQL only supports C collation — locale-aware sorting is not available
|
|
47
47
|
['COLLATE', 'SELECT * FROM t ORDER BY name COLLATE "en_US"'],
|
|
48
|
+
// DROP COLUMN is not in DSQL's supported ALTER TABLE subset — rebuild the table instead
|
|
49
|
+
['DROP COLUMN', 'ALTER TABLE t DROP COLUMN x'],
|
|
50
|
+
// Postgres allows omitting the COLUMN keyword — same DROP COLUMN action, same rejection
|
|
51
|
+
['DROP COLUMN shorthand', 'ALTER TABLE t DROP x'],
|
|
52
|
+
['DROP COLUMN IF EXISTS shorthand', 'ALTER TABLE t DROP IF EXISTS x'],
|
|
53
|
+
['DROP COLUMN quoted shorthand', 'ALTER TABLE t DROP "identity"'],
|
|
48
54
|
];
|
|
49
55
|
for (const [label, sql] of rejects) {
|
|
50
56
|
it(`rejects ${label}`, () => {
|
|
51
57
|
assert.throws(() => validateStatement(sql), { name: 'DsqlValidationError' });
|
|
52
58
|
});
|
|
53
59
|
}
|
|
60
|
+
it('allows supported ALTER TABLE forms that contain DROP', () => {
|
|
61
|
+
// Per the DSQL ALTER TABLE grammar, the ALTER COLUMN ... DROP actions and
|
|
62
|
+
// DROP CONSTRAINT are supported — must not be confused with the
|
|
63
|
+
// unsupported DROP [COLUMN].
|
|
64
|
+
// https://docs.aws.amazon.com/aurora-dsql/latest/userguide/alter-table-syntax-support.html
|
|
65
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t ALTER COLUMN c DROP IDENTITY'));
|
|
66
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t ALTER COLUMN c DROP DEFAULT'));
|
|
67
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t ALTER COLUMN c DROP NOT NULL'));
|
|
68
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t ALTER COLUMN c DROP EXPRESSION'));
|
|
69
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t DROP CONSTRAINT c'));
|
|
70
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t DROP CONSTRAINT IF EXISTS c CASCADE'));
|
|
71
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t RENAME TO t2'));
|
|
72
|
+
assert.doesNotThrow(() => validateStatement('DROP TABLE t'));
|
|
73
|
+
});
|
|
74
|
+
it('allows a supported ALTER TABLE followed by an unrelated statement in one batch', () => {
|
|
75
|
+
// The DROP COLUMN rule must not match across a statement boundary: the
|
|
76
|
+
// ALTER TABLE here is a supported DROP DEFAULT, and the DROP TABLE that
|
|
77
|
+
// follows the semicolon belongs to a different statement.
|
|
78
|
+
assert.doesNotThrow(() => validateStatement('ALTER TABLE t ALTER COLUMN c DROP DEFAULT; DROP TABLE archived'));
|
|
79
|
+
});
|
|
54
80
|
});
|
|
55
81
|
describe('classifyStatement', () => {
|
|
56
82
|
it('classifies DDL/DML/other', () => {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,OAAO,wBAAwB,CAAC;AAC7C,eAAO,MAAM,UAAU,UAAU,CAAC"}
|
package/dist/version.js
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aws-blocks/bb-distributed-data",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/aws-devtools-labs/aws-blocks.git",
|
|
7
|
+
"directory": "packages/bb-distributed-data"
|
|
8
|
+
},
|
|
9
|
+
"homepage": "https://github.com/aws-devtools-labs/aws-blocks/tree/main/packages/bb-distributed-data#readme",
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/aws-devtools-labs/aws-blocks/issues"
|
|
12
|
+
},
|
|
4
13
|
"author": "Amazon Web Services",
|
|
5
14
|
"license": "Apache-2.0",
|
|
6
15
|
"type": "module",
|
|
@@ -23,13 +32,14 @@
|
|
|
23
32
|
}
|
|
24
33
|
},
|
|
25
34
|
"scripts": {
|
|
35
|
+
"prebuild": "node ../../scripts/generate-version.mjs DistributedDatabase",
|
|
26
36
|
"build": "tsc --build",
|
|
27
37
|
"test": "node --test 'dist/**/*.test.js'"
|
|
28
38
|
},
|
|
29
39
|
"dependencies": {
|
|
30
|
-
"@aws-blocks/core": "^0.
|
|
31
|
-
"@aws-blocks/data-common": "^0.1.
|
|
32
|
-
"@aws-blocks/bb-logger": "^0.1.
|
|
40
|
+
"@aws-blocks/core": "^0.2.0",
|
|
41
|
+
"@aws-blocks/data-common": "^0.1.4",
|
|
42
|
+
"@aws-blocks/bb-logger": "^0.1.4",
|
|
33
43
|
"@aws-sdk/dsql-signer": "^3.700.0",
|
|
34
44
|
"@electric-sql/pglite": "^0.2.0",
|
|
35
45
|
"pg": "^8.13.0"
|