@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 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
 
@@ -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
- import type { DatabaseEngine, TransactionHandle } from '@aws-blocks/data-common';
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
- constructor(dataDir: string);
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":"AAUA,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,yBAAyB,CAAC;AAoCjF,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;gBAEd,OAAO,EAAE,MAAM;IAM3B,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;IAK9C,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"}
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
- constructor(dataDir) {
48
- cleanStaleLock(dataDir);
49
- mkdirSync(dataDir, { recursive: true });
50
- this.db = new PGlite(dataDir);
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
- await this.db.query('BEGIN');
87
- return { active: true, tracker: new TransactionTracker() };
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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=dsql-mock-engine.test.d.ts.map
@@ -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
+ });
@@ -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;AAEzD,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"}
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`] ?? '';
@@ -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';
@@ -1 +1 @@
1
- {"version":3,"file":"index.cdk.d.ts","sourceRoot":"","sources":["../src/index.cdk.ts"],"names":[],"mappings":"AAGA;;;;GAIG;AAEH,OAAO,EAAE,KAAK,EAAwB,MAAM,sBAAsB,CAAC;AACnE,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;CAwFjF;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"}
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
- // DSQL Cluster respect explicit removalPolicy; default to DESTROY in sandbox, RETAIN otherwise.
26
- const shouldDestroy = options?.removalPolicy === 'destroy' || (!options?.removalPolicy && isSandbox);
27
- const removalPolicy = shouldDestroy ? cdk.RemovalPolicy.DESTROY : cdk.RemovalPolicy.RETAIN;
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: removalPolicy !== cdk.RemovalPolicy.DESTROY,
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) {
@@ -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=destroy disables deletion protection', () => {
56
- const template = synth((stack) => {
57
- new DistributedDatabase(scope(stack), 'mydsql', { removalPolicy: 'destroy' });
58
- });
59
- template.hasResource('AWS::DSQL::Cluster', {
60
- Properties: { DeletionProtectionEnabled: false },
61
- DeletionPolicy: 'Delete',
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: sandboxMode=true disables deletion protection', () => {
65
- const app = new cdk.App({ context: { sandboxMode: 'true' } });
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');
@@ -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;AAEzD,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"}
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"}
@@ -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,2 @@
1
+ export {};
2
+ //# sourceMappingURL=index.test.d.ts.map
@@ -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
- * @default 'retain'
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. */
@@ -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;;;OAGG;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"}
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"}
@@ -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;AA6BD,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"}
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"}
@@ -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' },
@@ -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,3 @@
1
+ export declare const BB_NAME = "DistributedDatabase";
2
+ export declare const BB_VERSION = "0.1.6";
3
+ //# sourceMappingURL=version.d.ts.map
@@ -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"}
@@ -0,0 +1,3 @@
1
+ // Auto-generated by scripts/generate-version.mjs — do not edit manually
2
+ export const BB_NAME = 'DistributedDatabase';
3
+ export const BB_VERSION = '0.1.6';
package/package.json CHANGED
@@ -1,6 +1,15 @@
1
1
  {
2
2
  "name": "@aws-blocks/bb-distributed-data",
3
- "version": "0.1.3",
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.1.1",
31
- "@aws-blocks/data-common": "^0.1.1",
32
- "@aws-blocks/bb-logger": "^0.1.2",
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"