@volter/twin-planetscale 0.1.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/LICENSE +202 -0
- package/README.md +473 -0
- package/api/src/fetch.ts +50 -0
- package/api/src/generated/surface.gen.json +1 -0
- package/api/src/generated/ui.gen.json +1 -0
- package/api/src/index.ts +19 -0
- package/api/src/manifest.ts +136 -0
- package/api/src/screens/deploy-request.tsx +111 -0
- package/api/src/screens/service-tokens.tsx +141 -0
- package/api/src/screens/session.tsx +117 -0
- package/api/src/semantics/audit.ts +82 -0
- package/api/src/semantics/backups.ts +258 -0
- package/api/src/semantics/branches.ts +201 -0
- package/api/src/semantics/deploy-requests.ts +493 -0
- package/api/src/semantics/index.ts +371 -0
- package/api/src/semantics/shared.ts +141 -0
- package/api/src/semantics/time.ts +77 -0
- package/api/src/token-gate.ts +96 -0
- package/dist/api/src/fetch.d.ts +8 -0
- package/dist/api/src/fetch.js +51 -0
- package/dist/api/src/fetch.ts +50 -0
- package/dist/api/src/generated/surface.gen.json +1 -0
- package/dist/api/src/generated/ui.gen.json +1 -0
- package/dist/api/src/index.ts +19 -0
- package/dist/api/src/manifest.d.ts +2 -0
- package/dist/api/src/manifest.js +113 -0
- package/dist/api/src/manifest.ts +136 -0
- package/dist/api/src/screens/deploy-request.d.ts +7 -0
- package/dist/api/src/screens/deploy-request.js +106 -0
- package/dist/api/src/screens/deploy-request.tsx +111 -0
- package/dist/api/src/screens/service-tokens.d.ts +3 -0
- package/dist/api/src/screens/service-tokens.js +134 -0
- package/dist/api/src/screens/service-tokens.tsx +141 -0
- package/dist/api/src/screens/session.d.ts +11 -0
- package/dist/api/src/screens/session.js +108 -0
- package/dist/api/src/screens/session.tsx +117 -0
- package/dist/api/src/semantics/audit.d.ts +31 -0
- package/dist/api/src/semantics/audit.js +80 -0
- package/dist/api/src/semantics/audit.ts +82 -0
- package/dist/api/src/semantics/backups.d.ts +37 -0
- package/dist/api/src/semantics/backups.js +264 -0
- package/dist/api/src/semantics/backups.ts +258 -0
- package/dist/api/src/semantics/branches.d.ts +53 -0
- package/dist/api/src/semantics/branches.js +197 -0
- package/dist/api/src/semantics/branches.ts +201 -0
- package/dist/api/src/semantics/deploy-requests.d.ts +47 -0
- package/dist/api/src/semantics/deploy-requests.js +491 -0
- package/dist/api/src/semantics/deploy-requests.ts +493 -0
- package/dist/api/src/semantics/index.d.ts +20 -0
- package/dist/api/src/semantics/index.js +381 -0
- package/dist/api/src/semantics/index.ts +371 -0
- package/dist/api/src/semantics/shared.d.ts +36 -0
- package/dist/api/src/semantics/shared.js +132 -0
- package/dist/api/src/semantics/shared.ts +141 -0
- package/dist/api/src/semantics/time.d.ts +2 -0
- package/dist/api/src/semantics/time.js +81 -0
- package/dist/api/src/semantics/time.ts +77 -0
- package/dist/api/src/token-gate.d.ts +9 -0
- package/dist/api/src/token-gate.js +97 -0
- package/dist/api/src/token-gate.ts +96 -0
- package/dist/src/cli.d.ts +2 -0
- package/dist/src/cli.js +61 -0
- package/dist/src/generated/surface.gen.json +1 -0
- package/dist/src/generated/ui.gen.json +1 -0
- package/dist/src/index.d.ts +26 -0
- package/dist/src/index.js +156 -0
- package/dist/src/manifest.d.ts +2 -0
- package/dist/src/manifest.js +41 -0
- package/dist/src/planetscale-budget.d.ts +78 -0
- package/dist/src/planetscale-budget.js +305 -0
- package/dist/src/planetscale-capabilities.d.ts +10 -0
- package/dist/src/planetscale-capabilities.js +3977 -0
- package/dist/src/planetscale-collation-weights.gen.d.ts +4 -0
- package/dist/src/planetscale-collation-weights.gen.js +12 -0
- package/dist/src/planetscale-collation.d.ts +70 -0
- package/dist/src/planetscale-collation.js +391 -0
- package/dist/src/planetscale-conformance.d.ts +8 -0
- package/dist/src/planetscale-conformance.js +213 -0
- package/dist/src/planetscale-connector.d.ts +150 -0
- package/dist/src/planetscale-connector.js +532 -0
- package/dist/src/planetscale-deploy.d.ts +26 -0
- package/dist/src/planetscale-deploy.js +235 -0
- package/dist/src/planetscale-information-schema.d.ts +32 -0
- package/dist/src/planetscale-information-schema.js +299 -0
- package/dist/src/planetscale-mysql.d.ts +33 -0
- package/dist/src/planetscale-mysql.js +547 -0
- package/dist/src/planetscale-roles.d.ts +11 -0
- package/dist/src/planetscale-roles.js +60 -0
- package/dist/src/planetscale-row.d.ts +12 -0
- package/dist/src/planetscale-row.js +39 -0
- package/dist/src/planetscale-server.d.ts +42 -0
- package/dist/src/planetscale-server.js +137 -0
- package/dist/src/planetscale-sql.d.ts +701 -0
- package/dist/src/planetscale-sql.js +7167 -0
- package/dist/src/planetscale-store.d.ts +126 -0
- package/dist/src/planetscale-store.js +827 -0
- package/dist/src/planetscale-twin.d.ts +48 -0
- package/dist/src/planetscale-twin.js +290 -0
- package/dist/src/planetscale-values.d.ts +139 -0
- package/dist/src/planetscale-values.js +719 -0
- package/dist/src/planetscale-wire.d.ts +110 -0
- package/dist/src/planetscale-wire.js +188 -0
- package/dist/src/semantics/psdb.d.ts +18 -0
- package/dist/src/semantics/psdb.js +30 -0
- package/package.json +58 -0
- package/src/cli.ts +58 -0
- package/src/generated/surface.gen.json +1 -0
- package/src/generated/ui.gen.json +1 -0
- package/src/index.ts +267 -0
- package/src/manifest.ts +60 -0
- package/src/planetscale-budget.ts +347 -0
- package/src/planetscale-capabilities.ts +3862 -0
- package/src/planetscale-collation-weights.gen.ts +13 -0
- package/src/planetscale-collation.ts +378 -0
- package/src/planetscale-conformance.ts +237 -0
- package/src/planetscale-connector.ts +571 -0
- package/src/planetscale-deploy.ts +197 -0
- package/src/planetscale-information-schema.ts +322 -0
- package/src/planetscale-mysql.ts +339 -0
- package/src/planetscale-roles.ts +71 -0
- package/src/planetscale-row.ts +43 -0
- package/src/planetscale-server.ts +162 -0
- package/src/planetscale-sql.ts +5957 -0
- package/src/planetscale-store.ts +869 -0
- package/src/planetscale-twin.ts +338 -0
- package/src/planetscale-values.ts +572 -0
- package/src/planetscale-wire.ts +274 -0
- package/src/semantics/psdb.ts +57 -0
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
import { keyedRowId, keyedRowIdentity, quoteIdent, quoteSqlLiteral } from './planetscale-row.ts';
|
|
2
|
+
import { tableKey, unsupported, isNumericVitessType, vitessTypeFor, type TableDef, type Cell, type Database, type RowRec, type Statement, type Write } from './planetscale-sql.ts';
|
|
3
|
+
|
|
4
|
+
export type RowChange = {
|
|
5
|
+
table: string;
|
|
6
|
+
primaryKey: string[];
|
|
7
|
+
binary: string[];
|
|
8
|
+
numeric?: string[];
|
|
9
|
+
before?: Record<string, Cell>;
|
|
10
|
+
after?: Record<string, Cell>;
|
|
11
|
+
};
|
|
12
|
+
export type SqlPlan = { version: 1; statements: string[]; changes: RowChange[]; refusals: string[] };
|
|
13
|
+
export const emptyPlan = (): SqlPlan => ({ version: 1, statements: [], changes: [], refusals: [] });
|
|
14
|
+
export function combinePlans(plans: SqlPlan[]): SqlPlan {
|
|
15
|
+
return { version: 1, statements: plans.flatMap(p => p.statements), changes: plans.flatMap(p => p.changes), refusals: plans.flatMap(p => p.refusals) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Only deploy keys whose captured spelling is also their stored vendor identity. Other
|
|
19
|
+
* conversions remain local SQL support; they must not be reported as deployed then duplicated. */
|
|
20
|
+
function keyRefusals(table: TableDef, cells: Record<string, Cell>): string[] {
|
|
21
|
+
return table.primaryKey.flatMap(key => {
|
|
22
|
+
const c = table.columns.find(c => c.name.toLowerCase() === key.toLowerCase())!;
|
|
23
|
+
const value = cells[c.name];
|
|
24
|
+
const refuse = () => [`Deployment cannot preserve primary-key representation: ${table.name}.${c.name}`];
|
|
25
|
+
if (typeof value !== 'string') return refuse();
|
|
26
|
+
if ((c.autoIncrement && value === '0') || /\bzerofill\b/i.test(c.columnType)) return refuse();
|
|
27
|
+
const bits = ({ TINYINT: 8, SMALLINT: 16, MEDIUMINT: 24, INT: 32, INTEGER: 32, BIGINT: 64 } as Record<string, number>)[c.dataType];
|
|
28
|
+
if (bits) return integerKeyKept(value, bits, c.unsigned === true) ? [] : refuse();
|
|
29
|
+
if (!['VARCHAR', 'CHAR', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT'].includes(c.dataType)) return binaryKeyKept(c, value) ? [] : refuse();
|
|
30
|
+
return (c.length !== null && [...value].length > c.length) || (c.dataType === 'CHAR' && value.endsWith(' ')) ? refuse() : [];
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** An integer key keeps its spelling when it is canonical decimal within its column's range. */
|
|
35
|
+
function integerKeyKept(value: string, bits: number, unsigned: boolean): boolean {
|
|
36
|
+
if (!/^(?:0|[1-9][0-9]*|-[1-9][0-9]*)$/.test(value)) return false;
|
|
37
|
+
const n = BigInt(value); const width = BigInt(bits);
|
|
38
|
+
return !(n < (unsigned ? 0n : -(1n << (width - 1n))) || n >= (1n << (unsigned ? width : width - 1n)));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A binary key keeps its bytes when they fit (BINARY exactly); a key of any other type is not kept. */
|
|
42
|
+
function binaryKeyKept(c: TableDef['columns'][number], value: string): boolean {
|
|
43
|
+
if (!['VARBINARY', 'BINARY'].includes(c.dataType)) return false;
|
|
44
|
+
return !(c.length !== null && (c.dataType === 'BINARY' ? value.length !== c.length : value.length > c.length));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A row plan may not depend on an unmodeled storage conversion between its statements.
|
|
48
|
+
* Reject rounding/padding and unsupported types up front rather than conflict with our own write. */
|
|
49
|
+
function rowRefusals(table: TableDef, cells: Record<string, Cell>): string[] {
|
|
50
|
+
return table.columns.flatMap(c => {
|
|
51
|
+
const value = cells[c.name]; if (value === null) return [];
|
|
52
|
+
const refuse = () => [`Unsupported stored representation for deployment: ${table.name}.${c.name} (${c.columnType})`];
|
|
53
|
+
if (typeof value !== 'string') return refuse();
|
|
54
|
+
const bits = ({ TINYINT: 8, SMALLINT: 16, MEDIUMINT: 24, INT: 32, INTEGER: 32, BIGINT: 64 } as Record<string, number>)[c.dataType];
|
|
55
|
+
if (bits) {
|
|
56
|
+
if (!/^[+-]?[0-9]+(?:\.0+)?$/.test(value)) return refuse();
|
|
57
|
+
const n = BigInt(value.replace(/\.0+$/, '')); const width = BigInt(bits);
|
|
58
|
+
if (c.autoIncrement && n === 0n) return refuse();
|
|
59
|
+
return n < (c.unsigned ? 0n : -(1n << (width - 1n))) || n >= (1n << (c.unsigned ? width : width - 1n)) ? refuse() : [];
|
|
60
|
+
}
|
|
61
|
+
if (c.dataType === 'DECIMAL' || c.dataType === 'NUMERIC') {
|
|
62
|
+
const parsed = /^([+-]?)([0-9]+)(?:\.([0-9]+))?$/.exec(value);
|
|
63
|
+
if (!parsed) return refuse();
|
|
64
|
+
const declared = /(?:decimal|numeric)(?:\s*\(\s*([0-9]+)(?:\s*,\s*([0-9]+))?\s*\))?/i.exec(c.columnType);
|
|
65
|
+
const precision = Number(declared?.[1] ?? 10); const scale = Number(declared?.[2] ?? 0);
|
|
66
|
+
const integral = parsed[2]!.replace(/^0+/, ''); const fractional = (parsed[3] ?? '').replace(/0+$/, '');
|
|
67
|
+
return integral.length > precision - scale || fractional.length > scale || (c.unsigned && parsed[1] === '-' && /[1-9]/.test(value)) ? refuse() : [];
|
|
68
|
+
}
|
|
69
|
+
if (['VARCHAR', 'CHAR', 'TEXT', 'TINYTEXT', 'MEDIUMTEXT', 'LONGTEXT'].includes(c.dataType)) return (c.length !== null && [...value].length > c.length) || (c.dataType === 'CHAR' && value.endsWith(' ')) ? refuse() : [];
|
|
70
|
+
if (['VARBINARY', 'BINARY', 'BLOB', 'TINYBLOB', 'MEDIUMBLOB', 'LONGBLOB'].includes(c.dataType)) return c.length !== null && (c.dataType === 'BINARY' ? value.length !== c.length : value.length > c.length) ? refuse() : [];
|
|
71
|
+
// Floating-point, temporal and JSON storage normalization require a vendor-faithful codec.
|
|
72
|
+
return refuse();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// A stored row's primary-key identity, remembered on the row record (records are shared by every image and never
|
|
77
|
+
// changed; a write stores a new one), under the table name and key it was computed for: a RENAME or an ALTER of the key recomputes it.
|
|
78
|
+
const identityMemo = new WeakMap<RowRec, { pk: string; id: number; key: string }>();
|
|
79
|
+
function rowIdentity(table: TableDef, row: RowRec): { id: number; key: string } {
|
|
80
|
+
const pk = [tableKey(table.name), ...table.primaryKey].join('\u0000');
|
|
81
|
+
const held = identityMemo.get(row);
|
|
82
|
+
if (held && held.pk === pk) return held;
|
|
83
|
+
const made = { pk, id: keyedRowId(table, row.cells), key: JSON.stringify(keyedRowIdentity(table, row.cells)) };
|
|
84
|
+
identityMemo.set(row, made);
|
|
85
|
+
return made;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Capture the statement's beforeimages now, including its session overlay. Projection writes
|
|
89
|
+
* use the same primary-key identity as refresh; deployment retains the original change order. */
|
|
90
|
+
export function prepareSql(db: Database, writes: Write[], kind: Statement['kind'], sql: string): { writes: Write[]; plan: SqlPlan } {
|
|
91
|
+
const plan = emptyPlan();
|
|
92
|
+
plan.statements.push(sql);
|
|
93
|
+
if (!['insert', 'update', 'update-multi', 'delete'].includes(kind)) plan.refusals.push(`Schema deployment is unsupported: ${kind}`);
|
|
94
|
+
// the beforeimages and primary-key identities of the tables the statement writes, built when it first writes each
|
|
95
|
+
// (building them for every table made each write's cost grow with the whole database)
|
|
96
|
+
const rows = new Map<string, Map<number, RowRec>>();
|
|
97
|
+
const identities = new Map<string, Map<number, string>>();
|
|
98
|
+
const identitiesOf = (table: TableDef): Map<number, string> => {
|
|
99
|
+
const name = tableKey(table.name);
|
|
100
|
+
let held = identities.get(name);
|
|
101
|
+
if (held) return held;
|
|
102
|
+
held = new Map<number, string>();
|
|
103
|
+
for (const row of db.rows.get(name) ?? []) {
|
|
104
|
+
const { id, key } = rowIdentity(table, row);
|
|
105
|
+
if (held.has(id) && held.get(id) !== key) throw unsupported(`row identity hash collision in ${table.name}`);
|
|
106
|
+
held.set(id, key);
|
|
107
|
+
}
|
|
108
|
+
identities.set(name, held);
|
|
109
|
+
return held;
|
|
110
|
+
};
|
|
111
|
+
const projected: Write[] = [];
|
|
112
|
+
// a table the statement itself creates (CREATE TABLE … SELECT) holds the rows written after it
|
|
113
|
+
const tables = new Map(db.tables);
|
|
114
|
+
for (const w of writes) {
|
|
115
|
+
if (w.kind === 'table') tables.set(tableKey(w.table.name), w.table);
|
|
116
|
+
if (w.kind !== 'row' && w.kind !== 'delete-row') { projected.push(w); continue; }
|
|
117
|
+
const table = tables.get(tableKey(w.table));
|
|
118
|
+
if (!table) throw new Error(`Missing schema for ${w.table}`);
|
|
119
|
+
const bucket = rows.get(tableKey(w.table)) ?? new Map((db.rows.get(tableKey(w.table)) ?? []).map(r => [r.rowid, r]));
|
|
120
|
+
rows.set(tableKey(w.table), bucket);
|
|
121
|
+
const rid = w.kind === 'row' ? w.row.rowid : w.rowid;
|
|
122
|
+
const before = bucket.get(rid);
|
|
123
|
+
const after = w.kind === 'row' ? w.row : undefined;
|
|
124
|
+
if (!table.primaryKey.length) plan.refusals.push(`Deployment requires a primary key: ${table.name}`);
|
|
125
|
+
if (!before && !after) throw new Error(`Missing beforeimage for ${table.name}:${rid}`);
|
|
126
|
+
const oldId = before && table.primaryKey.length ? keyedRowId(table, before.cells) : rid;
|
|
127
|
+
const newId = after && table.primaryKey.length ? keyedRowId(table, after.cells) : rid;
|
|
128
|
+
if (table.primaryKey.length) {
|
|
129
|
+
const held = identitiesOf(table);
|
|
130
|
+
if (after) {
|
|
131
|
+
const key = JSON.stringify(keyedRowIdentity(table, after.cells));
|
|
132
|
+
if (held.has(newId) && held.get(newId) !== key) throw unsupported(`row identity hash collision in ${table.name}`);
|
|
133
|
+
plan.refusals.push(...keyRefusals(table, after.cells));
|
|
134
|
+
if (before) plan.refusals.push(...keyRefusals(table, before.cells));
|
|
135
|
+
if (before && oldId !== newId) held.delete(oldId);
|
|
136
|
+
held.set(newId, key);
|
|
137
|
+
} else { held.delete(oldId); if (before) plan.refusals.push(...keyRefusals(table, before.cells)); }
|
|
138
|
+
}
|
|
139
|
+
if (before && (!after || oldId !== newId)) projected.push({ kind: 'delete-row', table: table.name, rowid: oldId });
|
|
140
|
+
if (after) {
|
|
141
|
+
projected.push({ kind: 'row', table: table.name, row: { rowid: newId, cells: after.cells } });
|
|
142
|
+
bucket.set(rid, after);
|
|
143
|
+
} else bucket.delete(rid);
|
|
144
|
+
if (before) plan.refusals.push(...rowRefusals(table, before.cells));
|
|
145
|
+
if (after) plan.refusals.push(...rowRefusals(table, after.cells));
|
|
146
|
+
const changed = !before || !after || table.columns.some(c => before.cells[c.name] !== after.cells[c.name]);
|
|
147
|
+
if (changed) plan.changes.push({
|
|
148
|
+
table: table.name, primaryKey: table.primaryKey.map(k => table.columns.find(c => c.name.toLowerCase() === k.toLowerCase())!.name),
|
|
149
|
+
numeric: table.columns.filter(c => isNumericVitessType(vitessTypeFor(c))).map(c => c.name),
|
|
150
|
+
binary: table.columns.filter(c => /^(?:VARBINARY|BINARY|.*BLOB)$/.test(c.dataType)).map(c => c.name),
|
|
151
|
+
...(before ? { before: { ...before.cells } } : {}), ...(after ? { after: { ...after.cells } } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return { writes: projected, plan };
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Validate the entire plan before starting a vendor transaction. Never infer intent from a
|
|
158
|
+
* row's final fields or accept arbitrary SQL supplied on an old entry. */
|
|
159
|
+
export function deploymentStatements(value: unknown): string[] {
|
|
160
|
+
const plan = value as SqlPlan | undefined;
|
|
161
|
+
if (!plan || plan.version !== 1 || !Array.isArray(plan.changes) || !Array.isArray(plan.refusals)) throw new Error('PlanetScale action has no captured row plan');
|
|
162
|
+
if (plan.refusals.length) throw new Error(String(plan.refusals[0]));
|
|
163
|
+
// A successful no-op can still advance a local AUTO_INCREMENT watermark.
|
|
164
|
+
// Its captured empty row plan has no vendor effect to execute.
|
|
165
|
+
return plan.changes.map(change => {
|
|
166
|
+
if (!change || typeof change.table !== 'string' || !change.table || !Array.isArray(change.primaryKey) || !change.primaryKey.length || !change.primaryKey.every(k => typeof k === 'string' && k.length > 0) || !Array.isArray(change.binary) || !change.binary.every(k => typeof k === 'string')) throw new Error('Malformed PlanetScale row plan');
|
|
167
|
+
const cells = (value: unknown): Record<string, Cell> | undefined => {
|
|
168
|
+
if (value === undefined) return undefined;
|
|
169
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || !Object.keys(value).length || !Object.values(value).every(v => v === null || typeof v === 'string')) throw new Error('Malformed PlanetScale row image');
|
|
170
|
+
const row = value as Record<string, Cell>;
|
|
171
|
+
if (!change.primaryKey.every(k => typeof row[k] === 'string')) throw new Error('PlanetScale row image lacks its primary key');
|
|
172
|
+
return row;
|
|
173
|
+
};
|
|
174
|
+
if (change.numeric !== undefined && (!Array.isArray(change.numeric) || !change.numeric.every(k => typeof k === 'string'))) throw new Error('Malformed PlanetScale numeric columns');
|
|
175
|
+
const before = cells(change.before); const after = cells(change.after);
|
|
176
|
+
if (!before && !after) throw new Error('PlanetScale row plan has no images');
|
|
177
|
+
const literal = (key: string, value: Cell): string => value !== null && change.binary.includes(key)
|
|
178
|
+
? `X'${Buffer.from(value, 'latin1').toString('hex')}'` : quoteSqlLiteral(value);
|
|
179
|
+
const table = quoteIdent(change.table);
|
|
180
|
+
if (!before) {
|
|
181
|
+
const keys = Object.keys(after!);
|
|
182
|
+
return `INSERT INTO ${table} (${keys.map(quoteIdent).join(', ')}) VALUES (${keys.map(k => literal(k, after![k]!)).join(', ')})`;
|
|
183
|
+
}
|
|
184
|
+
const where = Object.entries(before).map(([k, v]) => {
|
|
185
|
+
if (v === null) return `${quoteIdent(k)} IS NULL`;
|
|
186
|
+
if (change.numeric?.includes(k)) {
|
|
187
|
+
// Quoted numeric strings compare through DOUBLE in MySQL; that can equate adjacent
|
|
188
|
+
// BIGINT values. Validated, unquoted decimal literals preserve exact numeric comparison.
|
|
189
|
+
if (!/^[+-]?[0-9]+(?:\.[0-9]+)?$/.test(v)) throw new Error(`Unsupported numeric beforeimage: ${change.table}.${k}`);
|
|
190
|
+
return `${quoteIdent(k)} = ${v}`;
|
|
191
|
+
}
|
|
192
|
+
return `CAST(${quoteIdent(k)} AS BINARY) = CAST(${literal(k, v)} AS BINARY)`;
|
|
193
|
+
}).join(' AND ');
|
|
194
|
+
if (!after) return `DELETE FROM ${table} WHERE ${where} LIMIT 1`;
|
|
195
|
+
return `UPDATE ${table} SET ${Object.entries(after).map(([k, v]) => `${quoteIdent(k)} = ${literal(k, v)}`).join(', ')} WHERE ${where} LIMIT 1`;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
// INFORMATION_SCHEMA — MySQL 8's catalog views, DERIVED on every read from the engine's own
|
|
2
|
+
// catalog (`Database.tables`). Nothing here is stored: each call folds the current image into
|
|
3
|
+
// virtual rows, so the views cannot drift from the tables they describe.
|
|
4
|
+
//
|
|
5
|
+
// Reference: https://dev.mysql.com/doc/refman/8.0/en/information-schema-table-reference.html
|
|
6
|
+
// (column lists, types and value conventions per view). The views and columns Prisma's MySQL
|
|
7
|
+
// schema describer reads (sql-schema-describer/src/mysql.rs) are the ones whose values are
|
|
8
|
+
// modeled in full; every other column carries MySQL's value for a fresh InnoDB table or NULL.
|
|
9
|
+
//
|
|
10
|
+
// What the engine cannot hold is answered truthfully as EMPTY: it has no views, routines,
|
|
11
|
+
// triggers, CHECK constraints or foreign keys, so VIEWS / ROUTINES / TRIGGERS /
|
|
12
|
+
// CHECK_CONSTRAINTS / REFERENTIAL_CONSTRAINTS have no rows. That is the real answer for a
|
|
13
|
+
// schema without those objects, not a placeholder.
|
|
14
|
+
import type { Cell, ColumnDef, Database, IndexDef, TableDef } from './planetscale-sql.ts';
|
|
15
|
+
import { DEFAULT_COLLATION_OF, modelledCollations } from './planetscale-collation.ts';
|
|
16
|
+
|
|
17
|
+
export const INFORMATION_SCHEMA = 'information_schema';
|
|
18
|
+
|
|
19
|
+
/** MySQL 8's defaults for a table declared without a charset/collation. */
|
|
20
|
+
export const DEFAULT_CHARSET = 'utf8mb4';
|
|
21
|
+
export const DEFAULT_COLLATION = 'utf8mb4_0900_ai_ci';
|
|
22
|
+
|
|
23
|
+
/** Identifier columns: the names of schemas, tables, columns, indexes, constraints and routines. */
|
|
24
|
+
const IDENTIFIER = /(_SCHEMA|_NAME)$/;
|
|
25
|
+
const NOT_IDENTIFIER = new Set(['CHARACTER_SET_NAME', 'COLLATION_NAME', 'DEFAULT_CHARACTER_SET_NAME', 'DEFAULT_COLLATION_NAME', 'CATALOG_NAME', 'EXTERNAL_NAME']);
|
|
26
|
+
|
|
27
|
+
/** A catalog column, declared the way MySQL 8 declares it (`SHOW CREATE TABLE information_schema.X`).
|
|
28
|
+
* Its text columns are utf8mb3. An identifier column compares the way this engine resolves the
|
|
29
|
+
* identifier it holds: case-insensitively, as MySQL does with lower_case_table_names = 2 (names
|
|
30
|
+
* keep their declared lettercase and are looked up in lowercase; the catalog's name columns then
|
|
31
|
+
* compare under utf8mb3_tolower_ci, modelled here by utf8mb3_general_ci). So `FROM EV` and
|
|
32
|
+
* `WHERE TABLE_NAME = 'EV'` agree about a table declared `ev`. Other text compares byte for byte. */
|
|
33
|
+
function column(name: string, type: string, nullable = true): ColumnDef {
|
|
34
|
+
const m = /^(\w+)(?:\((.*)\))?( unsigned)?$/.exec(type)!;
|
|
35
|
+
const base = m[1]!.toUpperCase();
|
|
36
|
+
const first = m[2] !== undefined && /^\d+/.test(m[2]) ? Number(/^\d+/.exec(m[2])![0]) : null;
|
|
37
|
+
const text = ['CHAR', 'VARCHAR', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'ENUM'].includes(base);
|
|
38
|
+
const identifier = IDENTIFIER.test(name) && !NOT_IDENTIFIER.has(name);
|
|
39
|
+
return {
|
|
40
|
+
name, dataType: base, columnType: type, length: first, unsigned: m[3] !== undefined, nullable,
|
|
41
|
+
autoIncrement: false, primaryKey: false, unique: false,
|
|
42
|
+
...(text ? { charset: 'utf8mb3', collation: identifier ? 'utf8mb3_general_ci' : 'utf8mb3_bin' } : {}),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
const view = (name: string, columns: ColumnDef[]): TableDef => ({ name, columns, primaryKey: [], uniques: [], autoIncrement: 0 });
|
|
46
|
+
|
|
47
|
+
const TABLES = view('TABLES', [
|
|
48
|
+
column('TABLE_CATALOG', 'varchar(64)', false), column('TABLE_SCHEMA', 'varchar(64)', false), column('TABLE_NAME', 'varchar(64)', false),
|
|
49
|
+
column("TABLE_TYPE", "enum('BASE TABLE','VIEW','SYSTEM VIEW')", false), column('ENGINE', 'varchar(64)'), column('VERSION', 'int'),
|
|
50
|
+
column("ROW_FORMAT", "enum('Fixed','Dynamic','Compressed','Redundant','Compact','Paged')"), column('TABLE_ROWS', 'bigint unsigned'),
|
|
51
|
+
column('AVG_ROW_LENGTH', 'bigint unsigned'), column('DATA_LENGTH', 'bigint unsigned'), column('MAX_DATA_LENGTH', 'bigint unsigned'),
|
|
52
|
+
column('INDEX_LENGTH', 'bigint unsigned'), column('DATA_FREE', 'bigint unsigned'), column('AUTO_INCREMENT', 'bigint unsigned'),
|
|
53
|
+
column('CREATE_TIME', 'timestamp'), column('UPDATE_TIME', 'datetime'), column('CHECK_TIME', 'datetime'),
|
|
54
|
+
column('TABLE_COLLATION', 'varchar(64)'), column('CHECKSUM', 'bigint'), column('CREATE_OPTIONS', 'varchar(256)'), column('TABLE_COMMENT', 'text'),
|
|
55
|
+
]);
|
|
56
|
+
const COLUMNS = view('COLUMNS', [
|
|
57
|
+
column('TABLE_CATALOG', 'varchar(64)', false), column('TABLE_SCHEMA', 'varchar(64)', false), column('TABLE_NAME', 'varchar(64)', false),
|
|
58
|
+
column('COLUMN_NAME', 'varchar(64)'), column('ORDINAL_POSITION', 'int unsigned', false), column('COLUMN_DEFAULT', 'text'),
|
|
59
|
+
column('IS_NULLABLE', 'varchar(3)', false), column('DATA_TYPE', 'longtext'), column('CHARACTER_MAXIMUM_LENGTH', 'bigint'),
|
|
60
|
+
column('CHARACTER_OCTET_LENGTH', 'bigint'), column('NUMERIC_PRECISION', 'bigint unsigned'), column('NUMERIC_SCALE', 'bigint unsigned'),
|
|
61
|
+
column('DATETIME_PRECISION', 'int unsigned'), column('CHARACTER_SET_NAME', 'varchar(64)'), column('COLLATION_NAME', 'varchar(64)'),
|
|
62
|
+
column('COLUMN_TYPE', 'mediumtext', false), column("COLUMN_KEY", "enum('','PRI','UNI','MUL')", false), column('EXTRA', 'varchar(256)'),
|
|
63
|
+
column('PRIVILEGES', 'varchar(154)'), column('COLUMN_COMMENT', 'text', false), column('GENERATION_EXPRESSION', 'longtext', false),
|
|
64
|
+
column('SRS_ID', 'int unsigned'),
|
|
65
|
+
]);
|
|
66
|
+
const STATISTICS = view('STATISTICS', [
|
|
67
|
+
column('TABLE_CATALOG', 'varchar(64)', false), column('TABLE_SCHEMA', 'varchar(64)', false), column('TABLE_NAME', 'varchar(64)', false),
|
|
68
|
+
column('NON_UNIQUE', 'int', false), column('INDEX_SCHEMA', 'varchar(64)', false), column('INDEX_NAME', 'varchar(64)'),
|
|
69
|
+
column('SEQ_IN_INDEX', 'int unsigned', false), column('COLUMN_NAME', 'varchar(64)'), column('COLLATION', 'varchar(1)'),
|
|
70
|
+
column('CARDINALITY', 'bigint'), column('SUB_PART', 'bigint'), column('PACKED', 'binary(0)'), column('NULLABLE', 'varchar(3)', false),
|
|
71
|
+
column('INDEX_TYPE', 'varchar(11)', false), column('COMMENT', 'varchar(8)', false), column('INDEX_COMMENT', 'varchar(2048)', false),
|
|
72
|
+
column('IS_VISIBLE', 'varchar(3)', false), column('EXPRESSION', 'longtext'),
|
|
73
|
+
]);
|
|
74
|
+
const TABLE_CONSTRAINTS = view('TABLE_CONSTRAINTS', [
|
|
75
|
+
column('CONSTRAINT_CATALOG', 'varchar(64)'), column('CONSTRAINT_SCHEMA', 'varchar(64)'), column('CONSTRAINT_NAME', 'varchar(64)'),
|
|
76
|
+
column('TABLE_SCHEMA', 'varchar(64)'), column('TABLE_NAME', 'varchar(64)'), column('CONSTRAINT_TYPE', 'varchar(11)', false),
|
|
77
|
+
column('ENFORCED', 'varchar(3)', false),
|
|
78
|
+
]);
|
|
79
|
+
const KEY_COLUMN_USAGE = view('KEY_COLUMN_USAGE', [
|
|
80
|
+
column('CONSTRAINT_CATALOG', 'varchar(64)'), column('CONSTRAINT_SCHEMA', 'varchar(64)'), column('CONSTRAINT_NAME', 'varchar(64)'),
|
|
81
|
+
column('TABLE_CATALOG', 'varchar(64)'), column('TABLE_SCHEMA', 'varchar(64)'), column('TABLE_NAME', 'varchar(64)'),
|
|
82
|
+
column('COLUMN_NAME', 'varchar(64)'), column('ORDINAL_POSITION', 'int unsigned', false), column('POSITION_IN_UNIQUE_CONSTRAINT', 'int unsigned'),
|
|
83
|
+
column('REFERENCED_TABLE_SCHEMA', 'varchar(64)'), column('REFERENCED_TABLE_NAME', 'varchar(64)'), column('REFERENCED_COLUMN_NAME', 'varchar(64)'),
|
|
84
|
+
]);
|
|
85
|
+
const REFERENTIAL_CONSTRAINTS = view('REFERENTIAL_CONSTRAINTS', [
|
|
86
|
+
column('CONSTRAINT_CATALOG', 'varchar(64)'), column('CONSTRAINT_SCHEMA', 'varchar(64)'), column('CONSTRAINT_NAME', 'varchar(64)'),
|
|
87
|
+
column('UNIQUE_CONSTRAINT_CATALOG', 'varchar(64)'), column('UNIQUE_CONSTRAINT_SCHEMA', 'varchar(64)'), column('UNIQUE_CONSTRAINT_NAME', 'varchar(64)'),
|
|
88
|
+
column("MATCH_OPTION", "enum('NONE','PARTIAL','FULL')", false), column("UPDATE_RULE", "enum('NO ACTION','RESTRICT','CASCADE','SET NULL','SET DEFAULT')", false),
|
|
89
|
+
column("DELETE_RULE", "enum('NO ACTION','RESTRICT','CASCADE','SET NULL','SET DEFAULT')", false), column('TABLE_NAME', 'varchar(64)'),
|
|
90
|
+
column('REFERENCED_TABLE_NAME', 'varchar(64)'),
|
|
91
|
+
]);
|
|
92
|
+
const CHECK_CONSTRAINTS = view('CHECK_CONSTRAINTS', [
|
|
93
|
+
column('CONSTRAINT_CATALOG', 'varchar(64)'), column('CONSTRAINT_SCHEMA', 'varchar(64)'), column('CONSTRAINT_NAME', 'varchar(64)'),
|
|
94
|
+
column('CHECK_CLAUSE', 'longtext', false),
|
|
95
|
+
]);
|
|
96
|
+
// INFORMATION_SCHEMA.COLLATIONS (manual 28.3.6): one row per collation the server supports. The twin's are the ones its
|
|
97
|
+
// engine models (planetscale-collation.ts), a subset of MySQL's (the twin's decision: a collation it lists is one it can
|
|
98
|
+
// compare by); SORTLEN is MySQL's for each.
|
|
99
|
+
const COLLATIONS = view('COLLATIONS', [
|
|
100
|
+
column('COLLATION_NAME', 'varchar(64)', false), column('CHARACTER_SET_NAME', 'varchar(64)', false), column('ID', 'bigint unsigned', false),
|
|
101
|
+
column('IS_DEFAULT', 'varchar(3)', false), column('IS_COMPILED', 'varchar(3)', false), column('SORTLEN', 'int unsigned', false),
|
|
102
|
+
column("PAD_ATTRIBUTE", "enum('PAD SPACE','NO PAD')", false),
|
|
103
|
+
]);
|
|
104
|
+
const SORTLEN: Record<string, number> = { binary: 1, utf8mb4_0900_ai_ci: 0, utf8mb4_0900_as_ci: 0, utf8mb4_0900_as_cs: 0, utf8mb4_0900_bin: 1, utf8mb4_bin: 1,
|
|
105
|
+
utf8mb4_general_ci: 1, utf8mb4_unicode_ci: 8, utf8mb3_general_ci: 1, utf8mb3_bin: 1, utf8mb3_unicode_ci: 8 };
|
|
106
|
+
const VIEWS = view('VIEWS', [
|
|
107
|
+
column('TABLE_CATALOG', 'varchar(64)'), column('TABLE_SCHEMA', 'varchar(64)'), column('TABLE_NAME', 'varchar(64)'),
|
|
108
|
+
column('VIEW_DEFINITION', 'longtext'), column("CHECK_OPTION", "enum('NONE','LOCAL','CASCADED')"), column("IS_UPDATABLE", "enum('NO','YES')"),
|
|
109
|
+
column('DEFINER', 'varchar(288)'), column("SECURITY_TYPE", "varchar(7)"), column('CHARACTER_SET_CLIENT', 'varchar(64)', false),
|
|
110
|
+
column('COLLATION_CONNECTION', 'varchar(64)', false),
|
|
111
|
+
]);
|
|
112
|
+
const ROUTINES = view('ROUTINES', [
|
|
113
|
+
column('SPECIFIC_NAME', 'varchar(64)', false), column('ROUTINE_CATALOG', 'varchar(64)'), column('ROUTINE_SCHEMA', 'varchar(64)'),
|
|
114
|
+
column('ROUTINE_NAME', 'varchar(64)', false), column("ROUTINE_TYPE", "enum('FUNCTION','PROCEDURE')", false), column('DATA_TYPE', 'longtext'),
|
|
115
|
+
column('ROUTINE_BODY', 'varchar(8)', false), column('ROUTINE_DEFINITION', 'longtext'), column('EXTERNAL_NAME', 'varbinary(0)'),
|
|
116
|
+
column('IS_DETERMINISTIC', 'varchar(3)', false), column('SQL_DATA_ACCESS', 'varchar(17)', false), column('SECURITY_TYPE', 'varchar(7)', false),
|
|
117
|
+
column('CREATED', 'timestamp', false), column('LAST_ALTERED', 'timestamp', false), column('SQL_MODE', 'varchar(520)', false),
|
|
118
|
+
column('ROUTINE_COMMENT', 'text', false), column('DEFINER', 'varchar(288)', false),
|
|
119
|
+
]);
|
|
120
|
+
const TRIGGERS = view('TRIGGERS', [
|
|
121
|
+
column('TRIGGER_CATALOG', 'varchar(64)'), column('TRIGGER_SCHEMA', 'varchar(64)'), column('TRIGGER_NAME', 'varchar(64)', false),
|
|
122
|
+
column("EVENT_MANIPULATION", "enum('INSERT','UPDATE','DELETE')", false), column('EVENT_OBJECT_SCHEMA', 'varchar(64)'),
|
|
123
|
+
column('EVENT_OBJECT_TABLE', 'varchar(64)'), column('ACTION_STATEMENT', 'longtext', false),
|
|
124
|
+
]);
|
|
125
|
+
const SCHEMATA = view('SCHEMATA', [
|
|
126
|
+
column('CATALOG_NAME', 'varchar(64)'), column('SCHEMA_NAME', 'varchar(64)'), column('DEFAULT_CHARACTER_SET_NAME', 'varchar(64)', false),
|
|
127
|
+
column('DEFAULT_COLLATION_NAME', 'varchar(64)', false), column('SQL_PATH', 'binary(0)'), column("DEFAULT_ENCRYPTION", "enum('NO','YES')", false),
|
|
128
|
+
]);
|
|
129
|
+
|
|
130
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
131
|
+
// COLUMN METADATA — MySQL 8's rendering of a declared column
|
|
132
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
const INTEGERS: Record<string, { type: string; precision: number }> = {
|
|
135
|
+
TINYINT: { type: 'tinyint', precision: 3 }, SMALLINT: { type: 'smallint', precision: 5 }, MEDIUMINT: { type: 'mediumint', precision: 7 },
|
|
136
|
+
INT: { type: 'int', precision: 10 }, INTEGER: { type: 'int', precision: 10 }, BIGINT: { type: 'bigint', precision: 19 },
|
|
137
|
+
};
|
|
138
|
+
const TEXT_LENGTH: Record<string, number> = { TINYTEXT: 255, TEXT: 65535, MEDIUMTEXT: 16777215, LONGTEXT: 4294967295, TINYBLOB: 255, BLOB: 65535, MEDIUMBLOB: 16777215, LONGBLOB: 4294967295 };
|
|
139
|
+
const CHARACTER_TYPES = new Set(['CHAR', 'VARCHAR', 'TINYTEXT', 'TEXT', 'MEDIUMTEXT', 'LONGTEXT', 'ENUM', 'SET']);
|
|
140
|
+
|
|
141
|
+
/** The parenthesised arguments of a declared type (`decimal(5,2)` → ['5','2']; enum members verbatim). */
|
|
142
|
+
function typeArguments(col: ColumnDef): string | undefined {
|
|
143
|
+
const open = col.columnType.indexOf('(');
|
|
144
|
+
return open < 0 ? undefined : col.columnType.slice(open + 1, col.columnType.lastIndexOf(')'));
|
|
145
|
+
}
|
|
146
|
+
const numbers = (args: string | undefined): number[] => (args ?? '').split(',').map((s) => s.trim()).filter((s) => /^\d+$/.test(s)).map(Number);
|
|
147
|
+
/** ENUM/SET members, unquoted (MySQL doubles an embedded quote in COLUMN_TYPE). */
|
|
148
|
+
function members(args: string | undefined): string[] {
|
|
149
|
+
return [...(args ?? '').matchAll(/'((?:[^']|'')*)'/g)].map((m) => m[1]!.replace(/''/g, "'"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export type ColumnMetadata = {
|
|
153
|
+
dataType: string; columnType: string; characterMaximumLength: number | null; characterOctetLength: number | null;
|
|
154
|
+
numericPrecision: number | null; numericScale: number | null; datetimePrecision: number | null;
|
|
155
|
+
characterSet: string | null; collation: string | null;
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
/** MySQL 8.0.19+'s rendering: integer display widths are dropped (except `tinyint(1)`), BOOLEAN is
|
|
159
|
+
* `tinyint(1)`, a zero fractional-seconds precision is not printed, DECIMAL defaults to (10,0). */
|
|
160
|
+
export function columnMetadata(col: ColumnDef, table: TableDef): ColumnMetadata {
|
|
161
|
+
const args = typeArguments(col); const nums = numbers(args);
|
|
162
|
+
const unsigned = col.unsigned ? ' unsigned' : '';
|
|
163
|
+
// A character column's own resolved collation (planetscale-sql.ts resolves it at CREATE TABLE),
|
|
164
|
+
// falling back to the table's declared default for a catalog written before columns carried one.
|
|
165
|
+
const collation = col.collation ?? table.collation ?? DEFAULT_COLLATION;
|
|
166
|
+
const charset = col.charset ?? (col.collation !== undefined ? col.collation.split('_')[0]! : table.charset ?? DEFAULT_CHARSET);
|
|
167
|
+
const none = { characterMaximumLength: null, characterOctetLength: null, numericPrecision: null, numericScale: null, datetimePrecision: null, characterSet: null, collation: null };
|
|
168
|
+
const base = col.dataType;
|
|
169
|
+
if (base === 'BOOL' || base === 'BOOLEAN') return { ...none, dataType: 'tinyint', columnType: 'tinyint(1)', numericPrecision: 3, numericScale: 0 };
|
|
170
|
+
const integer = INTEGERS[base];
|
|
171
|
+
if (integer) {
|
|
172
|
+
const width = base === 'TINYINT' && nums[0] === 1 ? '(1)' : '';
|
|
173
|
+
return { ...none, dataType: integer.type, columnType: `${integer.type}${width}${unsigned}`, numericPrecision: base === 'BIGINT' && col.unsigned ? 20 : integer.precision, numericScale: 0 };
|
|
174
|
+
}
|
|
175
|
+
if (base === 'DECIMAL' || base === 'NUMERIC' || base === 'DEC' || base === 'FIXED') {
|
|
176
|
+
const p = nums[0] ?? 10; const s = nums[1] ?? 0;
|
|
177
|
+
return { ...none, dataType: 'decimal', columnType: `decimal(${p},${s})${unsigned}`, numericPrecision: p, numericScale: s };
|
|
178
|
+
}
|
|
179
|
+
if (base === 'FLOAT' || base === 'DOUBLE' || base === 'REAL') {
|
|
180
|
+
const type = base === 'FLOAT' ? 'float' : 'double';
|
|
181
|
+
const scaled = nums.length === 2;
|
|
182
|
+
return { ...none, dataType: type, columnType: `${type}${scaled ? `(${nums[0]},${nums[1]})` : ''}${unsigned}`, numericPrecision: scaled ? nums[0]! : type === 'float' ? 12 : 22, numericScale: scaled ? nums[1]! : null };
|
|
183
|
+
}
|
|
184
|
+
if (base === 'DATETIME' || base === 'TIMESTAMP' || base === 'TIME') {
|
|
185
|
+
const fsp = nums[0] ?? 0; const type = base.toLowerCase();
|
|
186
|
+
return { ...none, dataType: type, columnType: fsp > 0 ? `${type}(${fsp})` : type, datetimePrecision: fsp };
|
|
187
|
+
}
|
|
188
|
+
if (base === 'DATE' || base === 'YEAR' || base === 'JSON') return { ...none, dataType: base.toLowerCase(), columnType: base.toLowerCase() };
|
|
189
|
+
if (base === 'BIT') { const n = nums[0] ?? 1; return { ...none, dataType: 'bit', columnType: `bit(${n})`, numericPrecision: n }; }
|
|
190
|
+
if (base === 'CHAR' || base === 'VARCHAR') {
|
|
191
|
+
const n = nums[0] ?? 1; const type = base.toLowerCase();
|
|
192
|
+
return { ...none, dataType: type, columnType: `${type}(${n})`, characterMaximumLength: n, characterOctetLength: n * 4, characterSet: charset, collation };
|
|
193
|
+
}
|
|
194
|
+
if (base === 'BINARY' || base === 'VARBINARY') {
|
|
195
|
+
const n = nums[0] ?? 1; const type = base.toLowerCase();
|
|
196
|
+
return { ...none, dataType: type, columnType: `${type}(${n})`, characterMaximumLength: n, characterOctetLength: n };
|
|
197
|
+
}
|
|
198
|
+
if (TEXT_LENGTH[base] !== undefined) {
|
|
199
|
+
const type = base.toLowerCase(); const n = TEXT_LENGTH[base]!;
|
|
200
|
+
const text = CHARACTER_TYPES.has(base);
|
|
201
|
+
return { ...none, dataType: type, columnType: type, characterMaximumLength: n, characterOctetLength: n,
|
|
202
|
+
...(text ? { characterSet: charset, collation } : {}) };
|
|
203
|
+
}
|
|
204
|
+
if (base === 'ENUM' || base === 'SET') {
|
|
205
|
+
const values = members(args); const type = base.toLowerCase();
|
|
206
|
+
const longest = base === 'ENUM' ? Math.max(0, ...values.map((v) => [...v].length)) : values.reduce((n, v) => n + [...v].length, 0) + Math.max(0, values.length - 1);
|
|
207
|
+
return { ...none, dataType: type, columnType: `${type}(${values.map((v) => `'${v.replace(/'/g, "''")}'`).join(',')})`, characterMaximumLength: longest, characterOctetLength: longest * 4, characterSet: charset, collation };
|
|
208
|
+
}
|
|
209
|
+
return { ...none, dataType: base.toLowerCase(), columnType: col.columnType };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** Every index of a table in MySQL's order: PRIMARY, then UNIQUE, then the others, each in declaration order. */
|
|
213
|
+
export function tableIndexes(table: TableDef): Array<IndexDef & { unique: boolean }> {
|
|
214
|
+
const part = (name: string) => ({ name });
|
|
215
|
+
const out: Array<IndexDef & { unique: boolean }> = [];
|
|
216
|
+
if (table.primaryKey.length) out.push({ name: 'PRIMARY', columns: table.primaryKey.map(part), type: 'BTREE', unique: true });
|
|
217
|
+
for (const u of table.uniques) out.push({ name: u.name, columns: u.parts ?? u.columns.map(part), type: 'BTREE', unique: true });
|
|
218
|
+
for (const col of table.columns) {
|
|
219
|
+
if (col.unique && !out.some((i) => i.unique && i.columns.length === 1 && i.columns[0]!.name.toLowerCase() === col.name.toLowerCase())) {
|
|
220
|
+
out.push({ name: col.name, columns: [part(col.name)], type: 'BTREE', unique: true });
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const i of table.indexes ?? []) out.push({ ...i, unique: false });
|
|
224
|
+
return out;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** COLUMN_KEY: PRI for a primary-key column, UNI for the column of a single-column UNIQUE index,
|
|
228
|
+
* MUL for the first column of any other index, otherwise empty (MySQL's documented precedence). */
|
|
229
|
+
function columnKey(col: ColumnDef, indexes: Array<IndexDef & { unique: boolean }>): string {
|
|
230
|
+
const is = (i: IndexDef, n = 0) => i.columns[n]?.name.toLowerCase() === col.name.toLowerCase();
|
|
231
|
+
if (indexes.some((i) => i.name === 'PRIMARY' && i.columns.some((_, n) => is(i, n)))) return 'PRI';
|
|
232
|
+
if (indexes.some((i) => i.unique && i.columns.length === 1 && is(i))) return 'UNI';
|
|
233
|
+
if (indexes.some((i) => is(i))) return 'MUL';
|
|
234
|
+
return '';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** COLUMN_DEFAULT as MySQL 8 reports it: the literal's text unquoted, or the expression text. */
|
|
238
|
+
function columnDefault(col: ColumnDef): Cell {
|
|
239
|
+
if (col.defaultCurrentTimestamp !== undefined) return col.defaultCurrentTimestamp > 0 ? `CURRENT_TIMESTAMP(${col.defaultCurrentTimestamp})` : 'CURRENT_TIMESTAMP';
|
|
240
|
+
if (col.defaultExpression !== undefined) return col.defaultExpression;
|
|
241
|
+
if (col.defaultValue === undefined || col.defaultValue === null) return null;
|
|
242
|
+
if (col.dataType === 'BIT' || col.dataType === 'BINARY' || col.dataType === 'VARBINARY') {
|
|
243
|
+
return `0x${[...col.defaultValue].map((c) => c.charCodeAt(0).toString(16).padStart(2, '0')).join('')}`;
|
|
244
|
+
}
|
|
245
|
+
return col.defaultValue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
249
|
+
// THE VIEWS
|
|
250
|
+
// ─────────────────────────────────────────────────────────────────────────────────────────────
|
|
251
|
+
|
|
252
|
+
type Rows = Array<Record<string, Cell>>;
|
|
253
|
+
const row = (view: TableDef, values: Cell[]): Record<string, Cell> => Object.fromEntries(view.columns.map((c, i) => [c.name, values[i] ?? null]));
|
|
254
|
+
const n = (v: number | null): Cell => (v === null ? null : String(v));
|
|
255
|
+
|
|
256
|
+
function sortedTables(db: Database): TableDef[] {
|
|
257
|
+
return [...db.tables.values()].sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const DERIVE: Record<string, (db: Database) => Rows> = {
|
|
261
|
+
COLLATIONS: () => [...modelledCollations()].sort((a, b) => a.id - b.id).map((c) => row(COLLATIONS, [c.name, c.charset, String(c.id),
|
|
262
|
+
DEFAULT_COLLATION_OF[c.charset] === c.name ? 'Yes' : '', 'Yes', String(SORTLEN[c.name] ?? 1), c.pad ? 'PAD SPACE' : 'NO PAD'])),
|
|
263
|
+
TABLES: (db) => sortedTables(db).map((t) => {
|
|
264
|
+
const rows = db.rows.get(t.name.toLowerCase())?.length ?? 0;
|
|
265
|
+
const secondary = tableIndexes(t).filter((i) => i.name !== 'PRIMARY').length;
|
|
266
|
+
return row(TABLES, ['def', db.name, t.name, 'BASE TABLE', 'InnoDB', '10', 'Dynamic', String(rows), '0',
|
|
267
|
+
// InnoDB's size for a table this small: one 16 KiB page for the clustered index, one per secondary index.
|
|
268
|
+
'16384', '0', String(16384 * secondary), '0',
|
|
269
|
+
t.columns.some((c) => c.autoIncrement) ? String(t.autoIncrement) : null,
|
|
270
|
+
null, null, null, t.collation ?? DEFAULT_COLLATION, null, '', '']);
|
|
271
|
+
}),
|
|
272
|
+
COLUMNS: (db) => sortedTables(db).flatMap((t) => {
|
|
273
|
+
const indexes = tableIndexes(t);
|
|
274
|
+
return t.columns.map((c, i) => {
|
|
275
|
+
const m = columnMetadata(c, t);
|
|
276
|
+
const generated = c.defaultCurrentTimestamp !== undefined || c.defaultExpression !== undefined;
|
|
277
|
+
const extra = c.autoIncrement ? 'auto_increment' : generated ? 'DEFAULT_GENERATED' : '';
|
|
278
|
+
return row(COLUMNS, ['def', db.name, t.name, c.name, String(i + 1), columnDefault(c), c.nullable ? 'YES' : 'NO', m.dataType,
|
|
279
|
+
n(m.characterMaximumLength), n(m.characterOctetLength), n(m.numericPrecision), n(m.numericScale), n(m.datetimePrecision),
|
|
280
|
+
m.characterSet, m.collation, m.columnType, columnKey(c, indexes), extra, 'select,insert,update,references', '', '', null]);
|
|
281
|
+
});
|
|
282
|
+
}),
|
|
283
|
+
STATISTICS: (db) => sortedTables(db).flatMap((t) => {
|
|
284
|
+
const stored = db.rows.get(t.name.toLowerCase()) ?? [];
|
|
285
|
+
return tableIndexes(t).flatMap((index) => index.columns.map((part, seq) => {
|
|
286
|
+
const col = t.columns.find((c) => c.name.toLowerCase() === part.name.toLowerCase());
|
|
287
|
+
const prefix = index.columns.slice(0, seq + 1).map((p) => t.columns.find((c) => c.name.toLowerCase() === p.name.toLowerCase())?.name ?? p.name);
|
|
288
|
+
const cardinality = new Set(stored.map((r) => JSON.stringify(prefix.map((k) => r.cells[k] ?? null)))).size;
|
|
289
|
+
const fulltext = index.type === 'FULLTEXT';
|
|
290
|
+
return row(STATISTICS, ['def', db.name, t.name, index.unique ? '0' : '1', db.name, index.name, String(seq + 1), col?.name ?? part.name,
|
|
291
|
+
fulltext ? null : part.desc ? 'D' : 'A', String(cardinality), part.length === undefined ? null : String(part.length), null,
|
|
292
|
+
col?.nullable ? 'YES' : '', index.type, '', '', 'YES', null]);
|
|
293
|
+
}));
|
|
294
|
+
}),
|
|
295
|
+
TABLE_CONSTRAINTS: (db) => sortedTables(db).flatMap((t) => tableIndexes(t).filter((i) => i.unique).map((i) =>
|
|
296
|
+
row(TABLE_CONSTRAINTS, ['def', db.name, i.name, db.name, t.name, i.name === 'PRIMARY' ? 'PRIMARY KEY' : 'UNIQUE', 'YES']))),
|
|
297
|
+
KEY_COLUMN_USAGE: (db) => sortedTables(db).flatMap((t) => tableIndexes(t).filter((i) => i.unique).flatMap((i) => i.columns.map((part, seq) =>
|
|
298
|
+
row(KEY_COLUMN_USAGE, ['def', db.name, i.name, 'def', db.name, t.name,
|
|
299
|
+
t.columns.find((c) => c.name.toLowerCase() === part.name.toLowerCase())?.name ?? part.name, String(seq + 1), null, null, null, null])))),
|
|
300
|
+
SCHEMATA: (db) => [row(SCHEMATA, ['def', INFORMATION_SCHEMA, 'utf8mb3', 'utf8mb3_general_ci', null, 'NO']),
|
|
301
|
+
row(SCHEMATA, ['def', db.name, DEFAULT_CHARSET, DEFAULT_COLLATION, null, 'NO'])],
|
|
302
|
+
REFERENTIAL_CONSTRAINTS: () => [],
|
|
303
|
+
CHECK_CONSTRAINTS: () => [],
|
|
304
|
+
VIEWS: () => [],
|
|
305
|
+
ROUTINES: () => [],
|
|
306
|
+
TRIGGERS: () => [],
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
const VIEW_DEFS: Record<string, TableDef> = {
|
|
310
|
+
TABLES, COLUMNS, COLLATIONS, STATISTICS, TABLE_CONSTRAINTS, KEY_COLUMN_USAGE, REFERENTIAL_CONSTRAINTS, CHECK_CONSTRAINTS, VIEWS, ROUTINES, TRIGGERS, SCHEMATA,
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
export const isInformationSchema = (schema: string | undefined): boolean => schema?.toLowerCase() === INFORMATION_SCHEMA;
|
|
314
|
+
|
|
315
|
+
/** One catalog view, derived from `db` now: its definition and its rows. MySQL's own errno 1109
|
|
316
|
+
* for a view it does not have (information_schema is a read-only system schema). */
|
|
317
|
+
export function informationSchemaView(db: Database, name: string): { table: TableDef; rows: Rows } | undefined {
|
|
318
|
+
const key = name.toUpperCase();
|
|
319
|
+
const table = VIEW_DEFS[key];
|
|
320
|
+
if (table === undefined) return undefined;
|
|
321
|
+
return { table, rows: DERIVE[key]!(db) };
|
|
322
|
+
}
|