@coradb/coradb-sqlsyntaxchecker-wasm 0.3.3
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 +55 -0
- package/LICENSE +21 -0
- package/NOTICE +14 -0
- package/README-schema.md +362 -0
- package/README.md +318 -0
- package/build-info.json +11 -0
- package/coradb-sqlsyntaxchecker.wasm +0 -0
- package/index.d.ts +234 -0
- package/index.js +529 -0
- package/package.json +60 -0
- package/schema-check.js +715 -0
- package/schema.json +127 -0
- package/tools/coradbSchema.ts +483 -0
package/schema.json
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "coradb-schema-v1.json",
|
|
4
|
+
"title": "CoraDB schema document, version 1",
|
|
5
|
+
"description": "A CoraDB database's schema, read from its catalog: objects, their columns, keys, indexes and comments, stored procedures and functions, serials and synonyms. Nothing from the tables' data. Any optional part may be missing: a missing list or field means 'not known', never 'there are none'.",
|
|
6
|
+
"type": "object",
|
|
7
|
+
"required": ["format", "version", "objects"],
|
|
8
|
+
"properties": {
|
|
9
|
+
"format": { "const": "coradb-schema" },
|
|
10
|
+
"version": { "const": 1 },
|
|
11
|
+
"database": { "type": "string" },
|
|
12
|
+
"objects": { "type": "array", "items": { "$ref": "#/$defs/object" } },
|
|
13
|
+
"procedures": { "type": "array", "items": { "$ref": "#/$defs/procedure" } },
|
|
14
|
+
"serials": { "type": "array", "items": { "$ref": "#/$defs/serial" } },
|
|
15
|
+
"synonyms": { "type": "array", "items": { "$ref": "#/$defs/synonym" } }
|
|
16
|
+
},
|
|
17
|
+
"$defs": {
|
|
18
|
+
"comment": { "type": ["string", "null"] },
|
|
19
|
+
"names": { "type": "array", "items": { "type": "string" } },
|
|
20
|
+
"object": {
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["name", "kind"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"name": { "type": "string" },
|
|
25
|
+
"owner": { "type": "string" },
|
|
26
|
+
"kind": { "enum": ["table", "view", "node", "edge"] },
|
|
27
|
+
"comment": { "$ref": "#/$defs/comment" },
|
|
28
|
+
"under": { "$ref": "#/$defs/names", "description": "Parent classes (CREATE ... UNDER), as owner.name or name; their columns are inherited." },
|
|
29
|
+
"columns": { "type": "array", "items": { "$ref": "#/$defs/column" }, "description": "In table order." },
|
|
30
|
+
"primaryKey": { "$ref": "#/$defs/names" },
|
|
31
|
+
"unique": {
|
|
32
|
+
"type": "array",
|
|
33
|
+
"items": {
|
|
34
|
+
"type": "object",
|
|
35
|
+
"required": ["name", "columns"],
|
|
36
|
+
"properties": { "name": { "type": "string" }, "columns": { "$ref": "#/$defs/names" } }
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"foreignKeys": { "type": "array", "items": { "$ref": "#/$defs/foreignKey" } },
|
|
40
|
+
"indexes": { "type": "array", "items": { "$ref": "#/$defs/index" } },
|
|
41
|
+
"definition": { "type": "string", "description": "Views only: the SELECT that defines it." }
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"column": {
|
|
45
|
+
"type": "object",
|
|
46
|
+
"required": ["name", "type"],
|
|
47
|
+
"properties": {
|
|
48
|
+
"name": { "type": "string" },
|
|
49
|
+
"type": { "type": "string", "description": "As CoraDB names it: INTEGER, VARCHAR, NUMERIC, DATETIME, ENUM, SET..." },
|
|
50
|
+
"length": { "type": "integer" },
|
|
51
|
+
"precision": { "type": "integer" },
|
|
52
|
+
"scale": { "type": "integer" },
|
|
53
|
+
"values": { "$ref": "#/$defs/names", "description": "ENUM: the values it allows." },
|
|
54
|
+
"elementType": { "type": "string", "description": "SET, MULTISET, LIST: the type of the elements." },
|
|
55
|
+
"nullable": { "type": "boolean" },
|
|
56
|
+
"default": { "type": ["string", "null"], "description": "The default as SQL text, e.g. 'RO' or SYS_DATETIME." },
|
|
57
|
+
"autoIncrement": { "type": "boolean" },
|
|
58
|
+
"comment": { "$ref": "#/$defs/comment" }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
"foreignKey": {
|
|
62
|
+
"type": "object",
|
|
63
|
+
"required": ["name", "columns", "references", "refColumns"],
|
|
64
|
+
"properties": {
|
|
65
|
+
"name": { "type": "string" },
|
|
66
|
+
"columns": { "$ref": "#/$defs/names" },
|
|
67
|
+
"references": { "type": "string", "description": "owner.name, or name when the owner is not known." },
|
|
68
|
+
"refColumns": { "$ref": "#/$defs/names" },
|
|
69
|
+
"onDelete": { "type": "string" },
|
|
70
|
+
"onUpdate": { "type": "string" }
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
"index": {
|
|
74
|
+
"type": "object",
|
|
75
|
+
"required": ["name", "columns", "unique"],
|
|
76
|
+
"properties": {
|
|
77
|
+
"name": { "type": "string" },
|
|
78
|
+
"columns": { "$ref": "#/$defs/names" },
|
|
79
|
+
"unique": { "type": "boolean" },
|
|
80
|
+
"comment": { "$ref": "#/$defs/comment" }
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"procedure": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"required": ["name", "kind"],
|
|
86
|
+
"properties": {
|
|
87
|
+
"name": { "type": "string" },
|
|
88
|
+
"owner": { "type": "string" },
|
|
89
|
+
"kind": { "enum": ["procedure", "function"] },
|
|
90
|
+
"returns": { "type": "string", "description": "Functions only." },
|
|
91
|
+
"comment": { "$ref": "#/$defs/comment" },
|
|
92
|
+
"params": {
|
|
93
|
+
"type": "array",
|
|
94
|
+
"items": {
|
|
95
|
+
"type": "object",
|
|
96
|
+
"required": ["name", "type", "mode"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"name": { "type": "string" },
|
|
99
|
+
"type": { "type": "string" },
|
|
100
|
+
"mode": { "enum": ["IN", "OUT", "INOUT"] },
|
|
101
|
+
"comment": { "$ref": "#/$defs/comment" }
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
"serial": {
|
|
108
|
+
"type": "object",
|
|
109
|
+
"required": ["name"],
|
|
110
|
+
"properties": {
|
|
111
|
+
"name": { "type": "string" },
|
|
112
|
+
"owner": { "type": "string" },
|
|
113
|
+
"comment": { "$ref": "#/$defs/comment" }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"synonym": {
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["name", "target"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"name": { "type": "string" },
|
|
121
|
+
"owner": { "type": "string" },
|
|
122
|
+
"target": { "type": "string", "description": "owner.name of what it stands for." },
|
|
123
|
+
"comment": { "$ref": "#/$defs/comment" }
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read a CoraDB database's schema from its catalog, as a CoraDB schema
|
|
3
|
+
* document (`"format": "coradb-schema"`, version 1): its tables, views, node
|
|
4
|
+
* and edge classes with their columns, keys, indexes and comments, its stored
|
|
5
|
+
* procedures and functions, serials and synonyms. Nothing is read from the
|
|
6
|
+
* tables themselves: no rows, no counts, no sample values.
|
|
7
|
+
*
|
|
8
|
+
* Self-contained, to be copied into other projects: it needs only a way to run
|
|
9
|
+
* a query and get its rows back (`QueryRows`), so it works with any driver.
|
|
10
|
+
*
|
|
11
|
+
* const schema = await readCoraDbSchema(async (sql) => rowsOf(await conn.query(sql)));
|
|
12
|
+
*
|
|
13
|
+
* Catalogs differ between engine builds — a column one has, another lacks —
|
|
14
|
+
* so every read tries the fullest form first and falls back to plainer ones,
|
|
15
|
+
* and a part of the catalog that cannot be read is left out of the document
|
|
16
|
+
* rather than failing it: a missing list means "not known", never "none".
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// --- the format ------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface CoraDbSchema {
|
|
22
|
+
format: 'coradb-schema';
|
|
23
|
+
version: 1;
|
|
24
|
+
database?: string;
|
|
25
|
+
objects: SchemaObject[];
|
|
26
|
+
procedures?: SchemaProcedure[];
|
|
27
|
+
serials?: SchemaSerial[];
|
|
28
|
+
synonyms?: SchemaSynonym[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type SchemaObjectKind = 'table' | 'view' | 'node' | 'edge';
|
|
32
|
+
|
|
33
|
+
export interface SchemaObject {
|
|
34
|
+
name: string;
|
|
35
|
+
owner?: string;
|
|
36
|
+
kind: SchemaObjectKind;
|
|
37
|
+
comment?: string | null;
|
|
38
|
+
/** Parent classes (CREATE … UNDER), as `owner.name` or `name`. */
|
|
39
|
+
under?: string[];
|
|
40
|
+
/** In table order. Missing when the catalog could not say. */
|
|
41
|
+
columns?: SchemaColumn[];
|
|
42
|
+
primaryKey?: string[];
|
|
43
|
+
unique?: { name: string; columns: string[] }[];
|
|
44
|
+
foreignKeys?: SchemaForeignKey[];
|
|
45
|
+
/** Secondary indexes, unique ones included; not the primary or foreign keys. */
|
|
46
|
+
indexes?: SchemaIndex[];
|
|
47
|
+
/** Views only: the SELECT that defines it. */
|
|
48
|
+
definition?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface SchemaColumn {
|
|
52
|
+
name: string;
|
|
53
|
+
type: string;
|
|
54
|
+
length?: number;
|
|
55
|
+
precision?: number;
|
|
56
|
+
scale?: number;
|
|
57
|
+
values?: string[];
|
|
58
|
+
elementType?: string;
|
|
59
|
+
nullable?: boolean;
|
|
60
|
+
default?: string | null;
|
|
61
|
+
autoIncrement?: boolean;
|
|
62
|
+
comment?: string | null;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface SchemaForeignKey {
|
|
66
|
+
name: string;
|
|
67
|
+
columns: string[];
|
|
68
|
+
references: string;
|
|
69
|
+
refColumns: string[];
|
|
70
|
+
onDelete?: string;
|
|
71
|
+
onUpdate?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface SchemaIndex {
|
|
75
|
+
name: string;
|
|
76
|
+
columns: string[];
|
|
77
|
+
unique: boolean;
|
|
78
|
+
comment?: string | null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface SchemaProcedure {
|
|
82
|
+
name: string;
|
|
83
|
+
owner?: string;
|
|
84
|
+
kind: 'procedure' | 'function';
|
|
85
|
+
returns?: string;
|
|
86
|
+
comment?: string | null;
|
|
87
|
+
params?: SchemaParam[];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface SchemaParam {
|
|
91
|
+
name: string;
|
|
92
|
+
type: string;
|
|
93
|
+
mode: 'IN' | 'OUT' | 'INOUT';
|
|
94
|
+
comment?: string | null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface SchemaSerial {
|
|
98
|
+
name: string;
|
|
99
|
+
owner?: string;
|
|
100
|
+
comment?: string | null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SchemaSynonym {
|
|
104
|
+
name: string;
|
|
105
|
+
owner?: string;
|
|
106
|
+
target: string;
|
|
107
|
+
comment?: string | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// --- reading it ----------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
/** Run one query and return its rows, keyed by lower-cased column name. */
|
|
113
|
+
export type QueryRows = (sql: string) => Promise<Record<string, unknown>[]>;
|
|
114
|
+
|
|
115
|
+
const str = (v: unknown): string => (v === null || v === undefined ? '' : String(v)).trim();
|
|
116
|
+
const yes = (v: unknown): boolean => str(v).toUpperCase() === 'YES';
|
|
117
|
+
const opt = (v: unknown): string | null => (str(v) === '' ? null : str(v));
|
|
118
|
+
const num = (v: unknown): number | undefined => {
|
|
119
|
+
const n = Number(v);
|
|
120
|
+
return Number.isFinite(n) ? n : undefined;
|
|
121
|
+
};
|
|
122
|
+
/** `[name]`, for a statement the catalog is asked with. */
|
|
123
|
+
const bracket = (name: string): string => `[${name.replace(/]/g, ']]')}]`;
|
|
124
|
+
const lower = (s: string): string => s.toLowerCase();
|
|
125
|
+
/** The key one class's rows are filed under: owner (when known) and name. */
|
|
126
|
+
const keyOf = (owner: string, name: string): string => `${lower(owner)}\u0000${lower(name)}`;
|
|
127
|
+
|
|
128
|
+
/** The first of these queries the catalog accepts; null when it accepts none. */
|
|
129
|
+
async function firstOf(query: QueryRows, sqls: string[]): Promise<Record<string, unknown>[] | null> {
|
|
130
|
+
for (const sql of sqls) {
|
|
131
|
+
try {
|
|
132
|
+
return await query(sql);
|
|
133
|
+
} catch {
|
|
134
|
+
// A column this build does not have: try the next, plainer form.
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** The engine's largest string: a VARCHAR declared without a length has it. */
|
|
141
|
+
const UNBOUNDED = 1073741823;
|
|
142
|
+
|
|
143
|
+
/** A column's type as CoraDB names it, with the sizes that matter for it. */
|
|
144
|
+
function columnType(
|
|
145
|
+
dataType: string,
|
|
146
|
+
prec: number | undefined,
|
|
147
|
+
scale: number | undefined,
|
|
148
|
+
): Pick<SchemaColumn, 'type' | 'length' | 'precision' | 'scale'> {
|
|
149
|
+
const t = dataType.toUpperCase();
|
|
150
|
+
const type = t === 'STRING' ? 'VARCHAR' : t === 'SEQUENCE' ? 'LIST' : t;
|
|
151
|
+
if (/^(NUMERIC|DECIMAL)$/.test(type)) {
|
|
152
|
+
return { type, ...(prec ? { precision: prec } : {}), ...(scale !== undefined ? { scale } : {}) };
|
|
153
|
+
}
|
|
154
|
+
if (/CHAR|BIT/.test(type) && prec && prec > 0 && prec < UNBOUNDED) return { type, length: prec };
|
|
155
|
+
return { type };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** ENUM('a','b') → ['a', 'b']. */
|
|
159
|
+
function enumValues(type: string): string[] | undefined {
|
|
160
|
+
const m = /^\s*ENUM\s*\(([\s\S]*)\)\s*$/i.exec(type);
|
|
161
|
+
if (!m) return undefined;
|
|
162
|
+
return [...m[1].matchAll(/'((?:[^']|'')*)'/g)].map((v) => v[1].replace(/''/g, "'"));
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const IDENT = String.raw`(?:\[[^\]]+\]|"[^"]+"|` + '`[^`]+`' + String.raw`|[A-Za-z_#][\w#$]*)`;
|
|
166
|
+
const unquote = (s: string): string => (/^[[`"]/.test(s) ? s.slice(1, -1) : s);
|
|
167
|
+
const nameOf = (s: string): string => [...s.matchAll(new RegExp(IDENT, 'g'))].map((m) => unquote(m[0])).join('.');
|
|
168
|
+
|
|
169
|
+
/** The FOREIGN KEY clauses of a CREATE TABLE statement. */
|
|
170
|
+
export function parseForeignKeys(ddl: string): SchemaForeignKey[] {
|
|
171
|
+
const out: SchemaForeignKey[] = [];
|
|
172
|
+
const re = new RegExp(
|
|
173
|
+
String.raw`(?:CONSTRAINT\s+(${IDENT})\s+)?FOREIGN\s+KEY\s*(?:${IDENT}\s*)?\(([^)]*)\)\s*` +
|
|
174
|
+
String.raw`REFERENCES\s+(${IDENT}(?:\s*\.\s*${IDENT})?)\s*\(([^)]*)\)` +
|
|
175
|
+
String.raw`((?:\s+ON\s+(?:DELETE|UPDATE)\s+(?:NO\s+ACTION|SET\s+NULL|CASCADE|RESTRICT))*)`,
|
|
176
|
+
'gi',
|
|
177
|
+
);
|
|
178
|
+
const list = (s: string) =>
|
|
179
|
+
s
|
|
180
|
+
.split(',')
|
|
181
|
+
.map((c) => unquote(c.trim().replace(/\s+(asc|desc)$/i, '')))
|
|
182
|
+
.filter(Boolean);
|
|
183
|
+
let m: RegExpExecArray | null;
|
|
184
|
+
while ((m = re.exec(ddl)) !== null) {
|
|
185
|
+
const actions = m[5] ?? '';
|
|
186
|
+
const onDelete = /ON\s+DELETE\s+(NO\s+ACTION|SET\s+NULL|CASCADE|RESTRICT)/i.exec(actions)?.[1];
|
|
187
|
+
const onUpdate = /ON\s+UPDATE\s+(NO\s+ACTION|SET\s+NULL|CASCADE|RESTRICT)/i.exec(actions)?.[1];
|
|
188
|
+
out.push({
|
|
189
|
+
name: m[1] ? unquote(m[1]) : `fk_${out.length + 1}`,
|
|
190
|
+
columns: list(m[2]),
|
|
191
|
+
references: nameOf(m[3]),
|
|
192
|
+
refColumns: list(m[4]),
|
|
193
|
+
...(onDelete ? { onDelete: onDelete.toUpperCase().replace(/\s+/g, ' ') } : {}),
|
|
194
|
+
...(onUpdate ? { onUpdate: onUpdate.toUpperCase().replace(/\s+/g, ' ') } : {}),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const KINDS: Record<string, SchemaObjectKind> = { CLASS: 'table', VCLASS: 'view', VERTEX: 'node', EDGE: 'edge' };
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The whole schema of the database the query runs against: every object that
|
|
204
|
+
* is not part of the engine's own catalog, as the connection's user can see it.
|
|
205
|
+
*/
|
|
206
|
+
export async function readCoraDbSchema(query: QueryRows): Promise<CoraDbSchema> {
|
|
207
|
+
const schema: CoraDbSchema = { format: 'coradb-schema', version: 1, objects: [] };
|
|
208
|
+
|
|
209
|
+
const db = await firstOf(query, ['select database() as name from db_root']);
|
|
210
|
+
if (db?.[0] && str(db[0].name)) schema.database = str(db[0].name);
|
|
211
|
+
|
|
212
|
+
// --- objects ---
|
|
213
|
+
const classes =
|
|
214
|
+
(await firstOf(query, [
|
|
215
|
+
"select class_name, owner_name, class_type, comment from db_class where is_system_class = 'NO' order by class_name",
|
|
216
|
+
"select class_name, owner_name, class_type from db_class where is_system_class = 'NO' order by class_name",
|
|
217
|
+
"select class_name, class_type from db_class where is_system_class = 'NO' order by class_name",
|
|
218
|
+
])) ?? [];
|
|
219
|
+
// A partitioned table's partitions are classes of their own; they are the table.
|
|
220
|
+
const partitions = new Set(
|
|
221
|
+
((await firstOf(query, ['select partition_class_name from db_partition'])) ?? []).map((r) =>
|
|
222
|
+
lower(str(r.partition_class_name)),
|
|
223
|
+
),
|
|
224
|
+
);
|
|
225
|
+
const byKey = new Map<string, SchemaObject>();
|
|
226
|
+
const byName = new Map<string, SchemaObject[]>();
|
|
227
|
+
for (const r of classes) {
|
|
228
|
+
const name = str(r.class_name);
|
|
229
|
+
const kind = KINDS[str(r.class_type).toUpperCase()];
|
|
230
|
+
if (!name || !kind || partitions.has(lower(name))) continue;
|
|
231
|
+
const owner = str(r.owner_name);
|
|
232
|
+
const o: SchemaObject = { name, ...(owner ? { owner } : {}), kind, columns: [] as SchemaColumn[] };
|
|
233
|
+
if ('comment' in r) o.comment = opt(r.comment);
|
|
234
|
+
schema.objects.push(o);
|
|
235
|
+
byKey.set(keyOf(owner, name), o);
|
|
236
|
+
byName.set(lower(name), [...(byName.get(lower(name)) ?? []), o]);
|
|
237
|
+
}
|
|
238
|
+
/** The object a catalog row is about: by owner and name where the row has an owner. */
|
|
239
|
+
const objectFor = (r: Record<string, unknown>, nameCol = 'class_name', ownerCol = 'owner_name') => {
|
|
240
|
+
const name = str(r[nameCol]);
|
|
241
|
+
if (ownerCol in r && str(r[ownerCol])) return byKey.get(keyOf(str(r[ownerCol]), name)) ?? null;
|
|
242
|
+
const all = byName.get(lower(name)) ?? [];
|
|
243
|
+
return all.length > 0 ? all : null;
|
|
244
|
+
};
|
|
245
|
+
const eachObject = (found: SchemaObject | SchemaObject[] | null, fn: (o: SchemaObject) => void) => {
|
|
246
|
+
if (!found) return;
|
|
247
|
+
for (const o of Array.isArray(found) ? found : [found]) fn(o);
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
// --- columns ---
|
|
251
|
+
const attrs = await firstOf(query, [
|
|
252
|
+
'select class_name, owner_name, attr_name, attr_type, data_type, prec, scale, is_nullable, default_value, comment, def_order from db_attribute order by class_name, def_order',
|
|
253
|
+
'select class_name, attr_name, attr_type, data_type, prec, scale, is_nullable, default_value, comment, def_order from db_attribute order by class_name, def_order',
|
|
254
|
+
'select class_name, attr_name, attr_type, data_type, prec, scale, is_nullable, default_value, def_order from db_attribute order by class_name, def_order',
|
|
255
|
+
'select class_name, attr_name, data_type, prec, scale, def_order from db_attribute order by class_name, def_order',
|
|
256
|
+
]);
|
|
257
|
+
const enumTables = new Set<SchemaObject>();
|
|
258
|
+
const collections: { o: SchemaObject; c: SchemaColumn }[] = [];
|
|
259
|
+
for (const r of attrs ?? []) {
|
|
260
|
+
// A CLASS attribute belongs to the class, not to its rows.
|
|
261
|
+
if (str(r.attr_type).toUpperCase() === 'CLASS') continue;
|
|
262
|
+
eachObject(objectFor(r), (o) => {
|
|
263
|
+
const c: SchemaColumn = { name: str(r.attr_name), ...columnType(str(r.data_type), num(r.prec), num(r.scale)) };
|
|
264
|
+
if ('is_nullable' in r) c.nullable = yes(r.is_nullable);
|
|
265
|
+
if ('default_value' in r) c.default = opt(r.default_value);
|
|
266
|
+
if ('comment' in r) c.comment = opt(r.comment);
|
|
267
|
+
if (c.type === 'ENUM') enumTables.add(o);
|
|
268
|
+
if (/^(SET|MULTISET|LIST)$/.test(c.type)) collections.push({ o, c });
|
|
269
|
+
o.columns?.push(c);
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (attrs === null) {
|
|
273
|
+
// Not even the plainest form: columns not known for any object.
|
|
274
|
+
for (const o of schema.objects) delete o.columns;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The values an ENUM allows are only in the column's full type.
|
|
278
|
+
for (const o of enumTables) {
|
|
279
|
+
// With the owner where there is one: two owners may have a table of the same name.
|
|
280
|
+
const rows = await firstOf(query, [
|
|
281
|
+
...(o.owner ? [`show columns from ${bracket(o.owner)}.${bracket(o.name)}`] : []),
|
|
282
|
+
`show columns from ${bracket(o.name)}`,
|
|
283
|
+
]);
|
|
284
|
+
for (const r of rows ?? []) {
|
|
285
|
+
const c = o.columns?.find((x) => lower(x.name) === lower(str(r.field)));
|
|
286
|
+
const values = c ? enumValues(str(r.type)) : undefined;
|
|
287
|
+
if (c && values) c.values = values;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
// What a SET, MULTISET or LIST holds.
|
|
291
|
+
if (collections.length > 0) {
|
|
292
|
+
const elems = await firstOf(query, [
|
|
293
|
+
'select class_name, owner_name, attr_name, data_type from db_attr_setdomain_elm',
|
|
294
|
+
'select class_name, attr_name, data_type from db_attr_setdomain_elm',
|
|
295
|
+
]);
|
|
296
|
+
for (const r of elems ?? []) {
|
|
297
|
+
// By owner too where the row has one: two owners may have a table of the same name.
|
|
298
|
+
const owner = str(r.owner_name);
|
|
299
|
+
for (const { o, c } of collections) {
|
|
300
|
+
if (owner && o.owner && lower(o.owner) !== lower(owner)) continue;
|
|
301
|
+
if (lower(o.name) === lower(str(r.class_name)) && lower(c.name) === lower(str(r.attr_name))) {
|
|
302
|
+
c.elementType = columnType(str(r.data_type), undefined, undefined).type;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// --- inheritance ---
|
|
309
|
+
const supers = await firstOf(query, [
|
|
310
|
+
'select class_name, owner_name, super_class_name, super_owner_name from db_direct_super_class',
|
|
311
|
+
'select class_name, super_class_name from db_direct_super_class',
|
|
312
|
+
]);
|
|
313
|
+
for (const r of supers ?? []) {
|
|
314
|
+
const parent = str(r.super_owner_name) ? `${str(r.super_owner_name)}.${str(r.super_class_name)}` : str(r.super_class_name);
|
|
315
|
+
eachObject(objectFor(r), (o) => {
|
|
316
|
+
o.under = [...(o.under ?? []), parent];
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// --- keys and indexes ---
|
|
321
|
+
const keyRows = await firstOf(query, [
|
|
322
|
+
'select i.class_name, i.owner_name, i.index_name, i.is_unique, i.is_primary_key, i.is_foreign_key, i.comment, k.key_attr_name, k.key_order ' +
|
|
323
|
+
'from db_index i, db_index_key k where i.index_name = k.index_name and i.class_name = k.class_name and i.owner_name = k.owner_name ' +
|
|
324
|
+
'order by i.class_name, i.index_name, k.key_order',
|
|
325
|
+
'select i.class_name, i.index_name, i.is_unique, i.is_primary_key, i.is_foreign_key, i.comment, k.key_attr_name, k.key_order ' +
|
|
326
|
+
'from db_index i, db_index_key k where i.index_name = k.index_name and i.class_name = k.class_name ' +
|
|
327
|
+
'order by i.class_name, i.index_name, k.key_order',
|
|
328
|
+
'select i.class_name, i.index_name, i.is_unique, i.is_primary_key, i.is_foreign_key, k.key_attr_name, k.key_order ' +
|
|
329
|
+
'from db_index i, db_index_key k where i.index_name = k.index_name and i.class_name = k.class_name ' +
|
|
330
|
+
'order by i.class_name, i.index_name, k.key_order',
|
|
331
|
+
]);
|
|
332
|
+
if (keyRows) {
|
|
333
|
+
const idx = new Map<SchemaObject, Map<string, { pk: boolean; fk: boolean; unique: boolean; comment: string | null; columns: string[] }>>();
|
|
334
|
+
for (const r of keyRows) {
|
|
335
|
+
eachObject(objectFor(r), (o) => {
|
|
336
|
+
const forObject = idx.get(o) ?? new Map();
|
|
337
|
+
idx.set(o, forObject);
|
|
338
|
+
const name = str(r.index_name);
|
|
339
|
+
const entry = forObject.get(name) ?? {
|
|
340
|
+
pk: yes(r.is_primary_key),
|
|
341
|
+
fk: yes(r.is_foreign_key),
|
|
342
|
+
unique: yes(r.is_unique),
|
|
343
|
+
comment: 'comment' in r ? opt(r.comment) : null,
|
|
344
|
+
columns: [],
|
|
345
|
+
};
|
|
346
|
+
forObject.set(name, entry);
|
|
347
|
+
// An index on an expression has no column name: its columns are not listed.
|
|
348
|
+
if (str(r.key_attr_name)) entry.columns.push(str(r.key_attr_name));
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
for (const o of schema.objects) {
|
|
352
|
+
if (o.kind === 'view') continue;
|
|
353
|
+
const forObject = idx.get(o);
|
|
354
|
+
o.primaryKey = [];
|
|
355
|
+
o.unique = [];
|
|
356
|
+
o.indexes = [];
|
|
357
|
+
if (!forObject) continue;
|
|
358
|
+
for (const [name, e] of forObject) {
|
|
359
|
+
if (e.pk) o.primaryKey = e.columns;
|
|
360
|
+
else if (!e.fk) {
|
|
361
|
+
if (e.unique) o.unique.push({ name, columns: e.columns });
|
|
362
|
+
o.indexes.push({ name, columns: e.columns, unique: e.unique, comment: e.comment });
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Where a foreign key points is only in the table's DDL.
|
|
368
|
+
for (const o of schema.objects) {
|
|
369
|
+
const forObject = idx.get(o);
|
|
370
|
+
const fks = forObject ? [...forObject].filter(([, e]) => e.fk) : [];
|
|
371
|
+
o.foreignKeys = [];
|
|
372
|
+
if (fks.length === 0) continue;
|
|
373
|
+
const target = o.owner ? `${bracket(o.owner)}.${bracket(o.name)}` : bracket(o.name);
|
|
374
|
+
const ddlRows =
|
|
375
|
+
(await firstOf(query, [`show create table ${target}`])) ?? (await firstOf(query, [`show create table ${bracket(o.name)}`]));
|
|
376
|
+
const row = ddlRows?.[0];
|
|
377
|
+
const ddl = row ? str(Object.values(row)[Object.values(row).length - 1]) : '';
|
|
378
|
+
const parsed = ddl ? parseForeignKeys(ddl) : [];
|
|
379
|
+
for (const [name, e] of fks) {
|
|
380
|
+
const same = (a: string[], b: string[]) => a.length === b.length && a.every((c, i) => lower(c) === lower(b[i] ?? ''));
|
|
381
|
+
const p = parsed.find((f) => lower(f.name) === lower(name)) ?? parsed.find((f) => same(f.columns, e.columns));
|
|
382
|
+
o.foreignKeys.push(
|
|
383
|
+
p ? { ...p, name, columns: e.columns } : { name, columns: e.columns, references: '', refColumns: [] },
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// --- views ---
|
|
390
|
+
const views = await firstOf(query, [
|
|
391
|
+
'select vclass_name, owner_name, vclass_def from db_vclass',
|
|
392
|
+
'select vclass_name, vclass_def from db_vclass',
|
|
393
|
+
]);
|
|
394
|
+
for (const r of views ?? []) {
|
|
395
|
+
eachObject(objectFor(r, 'vclass_name'), (o) => {
|
|
396
|
+
if (o.kind === 'view') o.definition = str(r.vclass_def);
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// --- serials, and which columns are AUTO_INCREMENT ---
|
|
401
|
+
const serials = await firstOf(query, [
|
|
402
|
+
'select name, owner.name as owner_name, class_name, att_name, comment from db_serial order by name',
|
|
403
|
+
'select name, class_name, att_name, comment from db_serial order by name',
|
|
404
|
+
'select name, class_name, att_name from db_serial order by name',
|
|
405
|
+
'select name from db_serial order by name',
|
|
406
|
+
]);
|
|
407
|
+
if (serials) {
|
|
408
|
+
schema.serials = [];
|
|
409
|
+
for (const r of serials) {
|
|
410
|
+
const cls = str(r.class_name);
|
|
411
|
+
if (cls && str(r.att_name)) {
|
|
412
|
+
// An AUTO_INCREMENT column's own serial.
|
|
413
|
+
for (const o of byName.get(lower(cls)) ?? []) {
|
|
414
|
+
const c = o.columns?.find((x) => lower(x.name) === lower(str(r.att_name)));
|
|
415
|
+
if (c) c.autoIncrement = true;
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const s: SchemaSerial = { name: str(r.name) };
|
|
420
|
+
if (str(r.owner_name)) s.owner = str(r.owner_name);
|
|
421
|
+
if ('comment' in r) s.comment = opt(r.comment);
|
|
422
|
+
if (s.name) schema.serials.push(s);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// --- stored procedures and functions ---
|
|
427
|
+
const procs = await firstOf(query, [
|
|
428
|
+
'select sp_name, owner, sp_type, return_type, comment from db_stored_procedure order by sp_name',
|
|
429
|
+
'select sp_name, sp_type, return_type, comment from db_stored_procedure order by sp_name',
|
|
430
|
+
'select sp_name, sp_type, return_type from db_stored_procedure order by sp_name',
|
|
431
|
+
]);
|
|
432
|
+
if (procs) {
|
|
433
|
+
const args = await firstOf(query, [
|
|
434
|
+
'select sp_name, index_of, arg_name, data_type, mode, comment from db_stored_procedure_args order by sp_name, index_of',
|
|
435
|
+
'select sp_name, index_of, arg_name, data_type, mode from db_stored_procedure_args order by sp_name, index_of',
|
|
436
|
+
]);
|
|
437
|
+
schema.procedures = procs
|
|
438
|
+
.filter((r) => str(r.sp_name))
|
|
439
|
+
.map((r) => {
|
|
440
|
+
const isFunction = str(r.sp_type).toUpperCase() === 'FUNCTION';
|
|
441
|
+
const p: SchemaProcedure = { name: str(r.sp_name), kind: isFunction ? 'function' : 'procedure' };
|
|
442
|
+
if (str(r.owner)) p.owner = str(r.owner);
|
|
443
|
+
if (isFunction && str(r.return_type)) p.returns = str(r.return_type);
|
|
444
|
+
if ('comment' in r) p.comment = opt(r.comment);
|
|
445
|
+
if (args) {
|
|
446
|
+
p.params = args
|
|
447
|
+
.filter((a) => lower(str(a.sp_name)) === lower(p.name))
|
|
448
|
+
.map((a) => {
|
|
449
|
+
const mode = str(a.mode).toUpperCase();
|
|
450
|
+
const param: SchemaParam = {
|
|
451
|
+
name: str(a.arg_name),
|
|
452
|
+
type: str(a.data_type),
|
|
453
|
+
mode: mode === 'OUT' ? 'OUT' : mode === 'INOUT' ? 'INOUT' : 'IN',
|
|
454
|
+
};
|
|
455
|
+
if ('comment' in a) param.comment = opt(a.comment);
|
|
456
|
+
return param;
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
return p;
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
// --- synonyms ---
|
|
464
|
+
const synonyms = await firstOf(query, [
|
|
465
|
+
'select synonym_name, synonym_owner_name, target_name, target_owner_name, comment from db_synonym order by synonym_name',
|
|
466
|
+
'select synonym_name, synonym_owner_name, target_name, target_owner_name from db_synonym order by synonym_name',
|
|
467
|
+
]);
|
|
468
|
+
if (synonyms) {
|
|
469
|
+
schema.synonyms = synonyms
|
|
470
|
+
.filter((r) => str(r.synonym_name))
|
|
471
|
+
.map((r) => {
|
|
472
|
+
const s: SchemaSynonym = {
|
|
473
|
+
name: str(r.synonym_name),
|
|
474
|
+
target: str(r.target_owner_name) ? `${str(r.target_owner_name)}.${str(r.target_name)}` : str(r.target_name),
|
|
475
|
+
};
|
|
476
|
+
if (str(r.synonym_owner_name)) s.owner = str(r.synonym_owner_name);
|
|
477
|
+
if ('comment' in r) s.comment = opt(r.comment);
|
|
478
|
+
return s;
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
return schema;
|
|
483
|
+
}
|