@proteinjs/db-driver-spanner 1.24.0 → 1.26.0
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/CHANGELOG.md +23 -0
- package/dist/generated/test/index.d.ts.map +1 -1
- package/dist/generated/test/index.js +3 -1
- package/dist/generated/test/index.js.map +1 -1
- package/dist/src/SpannerDriver.d.ts.map +1 -1
- package/dist/src/SpannerDriver.js +6 -0
- package/dist/src/SpannerDriver.js.map +1 -1
- package/dist/test/QueryAggregates.test.d.ts +2 -0
- package/dist/test/QueryAggregates.test.d.ts.map +1 -0
- package/dist/test/QueryAggregates.test.js +292 -0
- package/dist/test/QueryAggregates.test.js.map +1 -0
- package/dist/test/ReferenceColumnAdoptWidth.test.d.ts +2 -0
- package/dist/test/ReferenceColumnAdoptWidth.test.d.ts.map +1 -0
- package/dist/test/ReferenceColumnAdoptWidth.test.js +244 -0
- package/dist/test/ReferenceColumnAdoptWidth.test.js.map +1 -0
- package/dist/test/util/referenceAdoptWidthTestTables.d.ts +24 -0
- package/dist/test/util/referenceAdoptWidthTestTables.d.ts.map +1 -0
- package/dist/test/util/referenceAdoptWidthTestTables.js +35 -0
- package/dist/test/util/referenceAdoptWidthTestTables.js.map +1 -0
- package/generated/test/index.ts +3 -1
- package/package.json +6 -6
- package/src/SpannerDriver.ts +5 -0
- package/test/QueryAggregates.test.ts +215 -0
- package/test/ReferenceColumnAdoptWidth.test.ts +155 -0
- package/test/util/referenceAdoptWidthTestTables.ts +23 -0
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { SpannerDriver } from '@proteinjs/db-driver-spanner';
|
|
2
|
+
import { Db, Record, Reference, ReferenceColumn, StringColumn, Table, withRecordColumns } from '@proteinjs/db';
|
|
3
|
+
import { TransactionContext } from '@proteinjs/db-transaction-context';
|
|
4
|
+
import { getDropTestTable } from './util/getDropTestTable';
|
|
5
|
+
import { SpannerEmulatorProvisioner } from './util/SpannerEmulatorProvisioner';
|
|
6
|
+
import {
|
|
7
|
+
AdoptWidthRecord,
|
|
8
|
+
ADOPT_WIDTH_TABLE_NAME,
|
|
9
|
+
ADOPT_WIDTH_TARGET_TABLE_NAME,
|
|
10
|
+
ReferenceAdoptWidthTestTable,
|
|
11
|
+
} from './util/referenceAdoptWidthTestTables';
|
|
12
|
+
import '../generated/test/index';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `ReferenceColumn`'s width-adoption option (`maxLength`), which exists so an EXISTING string-uuid
|
|
16
|
+
* column can be retyped to a reference IN PLACE — same storage type, same bytes, zero DDL.
|
|
17
|
+
*
|
|
18
|
+
* The default reference width is STRING(36), but a column that predates the reference type was
|
|
19
|
+
* created at StringColumn's default STRING(255); Spanner cannot narrow a STRING in place, so
|
|
20
|
+
* without adopting the existing width the schema sync doesn't just emit DDL — it throws and
|
|
21
|
+
* bricks boot. Contracts, as outcomes against the live emulator schema:
|
|
22
|
+
* 1. Adopting the existing width means the sync sees NO changes for the retype (no DDL at all).
|
|
23
|
+
* 2. Storage is byte-identical both directions: rows written by the string era read back as
|
|
24
|
+
* references to the same id, and reference writes store the raw id string.
|
|
25
|
+
* 3. The stock (36) shape against a 255-width column is a narrowing: reported as a type change
|
|
26
|
+
* and refused by the alter pass — the documented reason the adoption option must be passed.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
const spannerDriver = new SpannerDriver({
|
|
30
|
+
projectId: 'proteinjs-test',
|
|
31
|
+
instanceName: 'proteinjs-test',
|
|
32
|
+
databaseName: 'test',
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
const TABLE_NAME = ADOPT_WIDTH_TABLE_NAME;
|
|
36
|
+
const TARGET_TABLE_NAME = ADOPT_WIDTH_TARGET_TABLE_NAME;
|
|
37
|
+
|
|
38
|
+
interface LegacyRecord extends Record {
|
|
39
|
+
invitedBy?: string | null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The string era: how the column was originally declared (and lives in deployed schemas). */
|
|
43
|
+
const legacyTable = (): Table<LegacyRecord> => {
|
|
44
|
+
return new (class extends Table<LegacyRecord> {
|
|
45
|
+
name = TABLE_NAME;
|
|
46
|
+
columns = withRecordColumns<LegacyRecord>({
|
|
47
|
+
invitedBy: new StringColumn('invited_by'),
|
|
48
|
+
});
|
|
49
|
+
})();
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** The retype under test: same physical column, adopted at its existing width (registered — Db
|
|
53
|
+
* statement generation resolves tables by name through the reflection registry). */
|
|
54
|
+
const adoptedTable = (): Table<AdoptWidthRecord> => new ReferenceAdoptWidthTestTable();
|
|
55
|
+
|
|
56
|
+
/** The stock shape (36): what the retype would be WITHOUT width adoption. */
|
|
57
|
+
const stockWidthTable = (): Table<AdoptWidthRecord> => {
|
|
58
|
+
return new (class extends Table<AdoptWidthRecord> {
|
|
59
|
+
name = TABLE_NAME;
|
|
60
|
+
columns = withRecordColumns<AdoptWidthRecord>({
|
|
61
|
+
invitedBy: new ReferenceColumn<Record>('invited_by', TARGET_TABLE_NAME, false),
|
|
62
|
+
});
|
|
63
|
+
})();
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type TableManagerInternals = {
|
|
67
|
+
getTableChanges(table: Table<any>): Promise<{
|
|
68
|
+
columnsToCreate: string[];
|
|
69
|
+
columnsToAlter: string[];
|
|
70
|
+
columnTypeChanges: { name: string; newType: string; oldType: string }[];
|
|
71
|
+
}>;
|
|
72
|
+
shouldAlterTable(tableChanges: unknown): boolean;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
describe('ReferenceColumn width adoption (string uuid column retyped in place)', () => {
|
|
76
|
+
const dropTable = getDropTestTable(spannerDriver);
|
|
77
|
+
const tableManager = spannerDriver.getTableManager();
|
|
78
|
+
const tableManagerInternals = tableManager as unknown as TableManagerInternals;
|
|
79
|
+
|
|
80
|
+
const STRING_ERA_UUID = '3f2a9c04-6b1d-4e8a-9c37-5d20f4a81b6e';
|
|
81
|
+
const REFERENCE_ERA_UUID = '9b7c1e52-0d4f-4a63-8e91-2c85d6f03a47';
|
|
82
|
+
|
|
83
|
+
beforeAll(async () => {
|
|
84
|
+
await SpannerEmulatorProvisioner.ensureProvisioned({
|
|
85
|
+
projectId: 'proteinjs-test',
|
|
86
|
+
instanceName: 'proteinjs-test',
|
|
87
|
+
databaseName: 'test',
|
|
88
|
+
});
|
|
89
|
+
await dropTable(legacyTable());
|
|
90
|
+
}, 30000);
|
|
91
|
+
|
|
92
|
+
afterAll(async () => {
|
|
93
|
+
await dropTable(legacyTable());
|
|
94
|
+
await SpannerEmulatorProvisioner.release();
|
|
95
|
+
}, 30000);
|
|
96
|
+
|
|
97
|
+
test('retype with the adopted width is invisible to the schema sync — zero DDL', async () => {
|
|
98
|
+
// The string era: the column exists as STRING(255) and holds a raw uuid.
|
|
99
|
+
const legacy = legacyTable();
|
|
100
|
+
await tableManager.loadTable(legacy);
|
|
101
|
+
expect((await tableManager.schemaMetadata.getColumnMetadata(legacy))['invited_by'].type).toBe('STRING(255)');
|
|
102
|
+
await spannerDriver.runDml(() => ({
|
|
103
|
+
sql: `INSERT INTO ${TABLE_NAME} (id, invited_by) VALUES (@id, @invitedBy)`,
|
|
104
|
+
namedParams: {
|
|
105
|
+
params: { id: 'string-era-row', invitedBy: STRING_ERA_UUID },
|
|
106
|
+
types: { id: 'string', invitedBy: 'string' },
|
|
107
|
+
},
|
|
108
|
+
}));
|
|
109
|
+
|
|
110
|
+
// The retype: the sync must see NOTHING to do.
|
|
111
|
+
const adopted = adoptedTable();
|
|
112
|
+
const tableChanges = await tableManagerInternals.getTableChanges(adopted);
|
|
113
|
+
expect(tableChanges.columnTypeChanges).toEqual([]);
|
|
114
|
+
expect(tableChanges.columnsToAlter).toEqual([]);
|
|
115
|
+
expect(tableChanges.columnsToCreate).toEqual([]);
|
|
116
|
+
expect(tableManagerInternals.shouldAlterTable(tableChanges)).toBe(false);
|
|
117
|
+
|
|
118
|
+
// And a full load pass leaves the live column untouched.
|
|
119
|
+
await tableManager.loadTable(adopted);
|
|
120
|
+
expect((await tableManager.schemaMetadata.getColumnMetadata(adopted))['invited_by'].type).toBe('STRING(255)');
|
|
121
|
+
}, 60000);
|
|
122
|
+
|
|
123
|
+
test('storage stays byte-identical: string-era rows read as references; reference writes store the raw id', async () => {
|
|
124
|
+
const adopted = adoptedTable();
|
|
125
|
+
// As-system, like the server-side writers of adopted columns (TableAuth is not under test).
|
|
126
|
+
const db = new Db(spannerDriver, () => adopted, new TransactionContext(), true);
|
|
127
|
+
|
|
128
|
+
// The row written by the string era reads back through the retyped column as a reference
|
|
129
|
+
// to the exact same id.
|
|
130
|
+
const stringEraRow = await db.get(adopted, { id: 'string-era-row' });
|
|
131
|
+
expect(stringEraRow.invitedBy).toBeInstanceOf(Reference);
|
|
132
|
+
expect(stringEraRow.invitedBy?._id).toBe(STRING_ERA_UUID);
|
|
133
|
+
expect(stringEraRow.invitedBy?._table).toBe(TARGET_TABLE_NAME);
|
|
134
|
+
|
|
135
|
+
// A reference written through the retyped column stores the raw id string — the same bytes
|
|
136
|
+
// the string era would have written.
|
|
137
|
+
const inserted = await db.insert(adopted, {
|
|
138
|
+
invitedBy: new Reference<Record>(TARGET_TABLE_NAME, REFERENCE_ERA_UUID),
|
|
139
|
+
});
|
|
140
|
+
const rows = await spannerDriver.runQuery(() => ({
|
|
141
|
+
sql: `SELECT invited_by FROM ${TABLE_NAME} WHERE id = @id`,
|
|
142
|
+
namedParams: { params: { id: inserted.id }, types: { id: 'string' } },
|
|
143
|
+
}));
|
|
144
|
+
expect(rows[0].invited_by).toBe(REFERENCE_ERA_UUID);
|
|
145
|
+
}, 60000);
|
|
146
|
+
|
|
147
|
+
test('the stock 36 width against a 255 column is a narrowing: reported as a type change and refused', async () => {
|
|
148
|
+
const stock = stockWidthTable();
|
|
149
|
+
const tableChanges = await tableManagerInternals.getTableChanges(stock);
|
|
150
|
+
expect(tableChanges.columnTypeChanges).toEqual([
|
|
151
|
+
{ name: 'invited_by', oldType: 'STRING(255)', newType: 'STRING(36)' },
|
|
152
|
+
]);
|
|
153
|
+
await expect(tableManager.loadTable(stock)).rejects.toThrow(/Unable to change column types in Spanner/);
|
|
154
|
+
}, 60000);
|
|
155
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Record, Reference, ReferenceColumn, Table, withRecordColumns } from '@proteinjs/db';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The registered face of the width-adoption suite's table (ReferenceColumnAdoptWidth.test.ts):
|
|
5
|
+
* the retyped declaration — a reference adopting the column's pre-existing STRING(255) width.
|
|
6
|
+
* Declared here (not inline in the suite) because Db statement generation resolves tables by
|
|
7
|
+
* NAME through the reflection registry; the suite's string-era and stock-width variants of the
|
|
8
|
+
* same physical table stay inline there, reaching only instance-passed TableManager APIs.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
export interface AdoptWidthRecord extends Record {
|
|
12
|
+
invitedBy?: Reference<Record> | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const ADOPT_WIDTH_TABLE_NAME = 'db_test_reference_adopt_width';
|
|
16
|
+
export const ADOPT_WIDTH_TARGET_TABLE_NAME = 'db_test_reference_adopt_target';
|
|
17
|
+
|
|
18
|
+
export class ReferenceAdoptWidthTestTable extends Table<AdoptWidthRecord> {
|
|
19
|
+
name = ADOPT_WIDTH_TABLE_NAME;
|
|
20
|
+
columns = withRecordColumns<AdoptWidthRecord>({
|
|
21
|
+
invitedBy: new ReferenceColumn<Record>('invited_by', ADOPT_WIDTH_TARGET_TABLE_NAME, false, { maxLength: 255 }),
|
|
22
|
+
});
|
|
23
|
+
}
|