Skip to content
This repository was archived by the owner on Jun 26, 2021. It is now read-only.

Commit 5ea25d5

Browse files
committed
Use incredible introspection query thanks to graphile-engine
1 parent 150134d commit 5ea25d5

File tree

6 files changed

+800
-35
lines changed

6 files changed

+800
-35
lines changed

src/__tests__/test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
schemaToTableObj,
66
tableObjToTSCode,
77
generateTSCodeForAllSchema as generateTSCodeForAllSchemas,
8+
getPostgresVersion,
89
} from '../generator';
910
import Path from 'path';
1011
import { compileTypeScriptCode } from './tsCompiler';
@@ -121,6 +122,10 @@ describe('integration tests', () => {
121122
afterEach(drop);
122123
afterAll(() => pgp.end());
123124

125+
it('reads postgres version', async () => {
126+
expect(await getPostgresVersion()).toBeGreaterThanOrEqual(120000);
127+
});
128+
124129
it('correctly reads schema for table user', async () => {
125130
const result = await getTablesSchemas();
126131

src/generator/index.ts

Lines changed: 303 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,321 @@
1-
import { db, sql } from './db';
2-
import { ColumnSchema, TableName, TableSchema } from './types';
3-
import Path from 'path';
1+
import { db } from './db';
2+
import { TableSchema, ColumnType } from './types';
43
import { Table } from '..';
54
import Prettier from 'prettier';
6-
import { defaults } from 'lodash';
5+
import { defaults, flatMap } from 'lodash';
6+
import { makeIntrospectionQuery } from './introspectionQuery';
7+
import { xByY, xByYAndZ, parseTags } from './utils';
78

89
// @ts-ignore
910
// eslint-disable-next-line @typescript-eslint/no-unused-vars
1011
class NotSupportedError extends Error {
1112
message = 'NotSupportedError';
1213
}
1314

14-
function getTableNames(): Promise<TableName[]> {
15-
return db.manyOrNone(sql(Path.join(__dirname, 'select_table_name.sql')));
16-
}
15+
// Ref: https://github.com/graphile/postgraphile/tree/master/src/postgres/introspection/object
16+
17+
type PgNamespace = {
18+
kind: 'namespace';
19+
id: string;
20+
name: string;
21+
description?: string;
22+
comment?: string;
23+
tags: Record<string, string | boolean | (string | boolean)[]>;
24+
};
25+
26+
type PgProc = {
27+
kind: 'procedure';
28+
id: string;
29+
name: string;
30+
description?: string;
31+
comment?: string;
32+
namespaceId: string;
33+
namespaceName: string;
34+
isStrict: boolean;
35+
returnsSet: boolean;
36+
isStable: boolean;
37+
returnTypeId: string;
38+
argTypeIds: Array<string>;
39+
argNames: Array<string>;
40+
argModes: Array<'i' | 'o' | 'b' | 'v' | 't'>;
41+
inputArgsCount: number;
42+
argDefaultsNum: number;
43+
namespace: PgNamespace;
44+
tags: Record<string, string | boolean | (string | boolean)[]>;
45+
cost: number;
46+
aclExecutable: boolean;
47+
language: string;
48+
};
49+
50+
type PgClass = {
51+
kind: 'class';
52+
id: string;
53+
name: string;
54+
description?: string;
55+
comment?: string;
56+
classKind: string;
57+
namespaceId: string;
58+
namespaceName: string;
59+
typeId: string;
60+
isSelectable: boolean;
61+
isInsertable: boolean;
62+
isUpdatable: boolean;
63+
isDeletable: boolean;
64+
isExtensionConfigurationTable: boolean;
65+
namespace: PgNamespace;
66+
type: PgType;
67+
tags: Record<string, string | boolean | (string | boolean)[]>;
68+
attributes: Array<PgAttribute>;
69+
constraints: Array<PgConstraint>;
70+
foreignConstraints: Array<PgConstraint>;
71+
primaryKeyConstraint?: PgConstraint;
72+
aclSelectable: boolean;
73+
aclInsertable: boolean;
74+
aclUpdatable: boolean;
75+
aclDeletable: boolean;
76+
canUseAsterisk: boolean;
77+
};
78+
79+
type PgType = {
80+
kind: 'type';
81+
id: string;
82+
name: ColumnType;
83+
description?: string;
84+
comment?: string;
85+
namespaceId: string;
86+
namespaceName: string;
87+
type: string;
88+
category: string;
89+
domainIsNotNull: boolean;
90+
arrayItemTypeId?: string;
91+
arrayItemType?: PgType;
92+
arrayType?: PgType;
93+
typeLength?: number;
94+
isPgArray: boolean;
95+
classId?: string;
96+
class?: PgClass;
97+
domainBaseTypeId?: string;
98+
domainBaseType?: PgType;
99+
domainTypeModifier?: number;
100+
tags: Record<string, string | boolean | (string | boolean)[]>;
101+
};
102+
103+
type PgAttribute = {
104+
kind: 'attribute';
105+
classId: string;
106+
num: number;
107+
name: string;
108+
description?: string;
109+
comment?: string;
110+
typeId: string;
111+
typeModifier: number;
112+
isNotNull: boolean;
113+
hasDefault: boolean;
114+
identity: '' | 'a' | 'd';
115+
class: PgClass;
116+
type: PgType;
117+
namespace: PgNamespace;
118+
tags: Record<string, string | boolean | (string | boolean)[]>;
119+
aclSelectable: boolean;
120+
aclInsertable: boolean;
121+
aclUpdatable: boolean;
122+
isIndexed?: boolean;
123+
isUnique?: boolean;
124+
columnLevelSelectGrant: boolean;
125+
};
126+
127+
type PgConstraint = {
128+
kind: 'constraint';
129+
id: string;
130+
name: string;
131+
type: string;
132+
classId: string;
133+
class: PgClass;
134+
foreignClassId?: string;
135+
foreignClass?: PgClass;
136+
description?: string;
137+
comment?: string;
138+
keyAttributeNums: Array<number>;
139+
keyAttributes: Array<PgAttribute>;
140+
foreignKeyAttributeNums: Array<number>;
141+
foreignKeyAttributes: Array<PgAttribute>;
142+
namespace: PgNamespace;
143+
isIndexed?: boolean;
144+
tags: Record<string, string | boolean | (string | boolean)[]>;
145+
};
146+
147+
type PgExtension = {
148+
kind: 'extension';
149+
id: string;
150+
name: string;
151+
namespaceId: string;
152+
namespaceName: string;
153+
relocatable: boolean;
154+
version: string;
155+
configurationClassIds?: Array<string>;
156+
description?: string;
157+
comment?: string;
158+
tags: Record<string, string | boolean | (string | boolean)[]>;
159+
};
17160

18-
function getColumnSchemas(tableName: string): Promise<ColumnSchema[]> {
19-
return db.manyOrNone(sql(Path.join(__dirname, 'select_table_schema.sql')), [tableName]);
161+
type PgIndex = {
162+
kind: 'index';
163+
id: string;
164+
name: string;
165+
namespaceName: string;
166+
classId: string;
167+
numberOfAttributes: number;
168+
indexType: string;
169+
isUnique: boolean;
170+
isPrimary: boolean;
171+
/*
172+
Though these exist, we don't want to officially
173+
support them yet.
174+
isImmediate: boolean,
175+
isReplicaIdentity: boolean,
176+
isValid: boolean,
177+
*/
178+
isPartial: boolean;
179+
attributeNums: Array<number>;
180+
attributePropertiesAsc?: Array<boolean>;
181+
attributePropertiesNullsFirst?: Array<boolean>;
182+
description?: string;
183+
comment?: string;
184+
tags: Record<string, string | boolean | (string | boolean)[]>;
185+
};
186+
187+
type PgEntity =
188+
| PgNamespace
189+
| PgProc
190+
| PgClass
191+
| PgType
192+
| PgAttribute
193+
| PgConstraint
194+
| PgExtension
195+
| PgIndex;
196+
197+
type PgIntrospectionOriginalResultsByKind = {
198+
__pgVersion: number;
199+
attribute: PgAttribute[];
200+
class: PgClass[];
201+
constraint: PgConstraint[];
202+
extension: PgExtension[];
203+
index: PgIndex[];
204+
namespace: PgNamespace[];
205+
procedure: PgProc[];
206+
type: PgType[];
207+
};
208+
209+
type PgIntrospectionResultsByKind = PgIntrospectionOriginalResultsByKind & {
210+
attributeByClassIdAndNum: {
211+
[classId: string]: { [num: string]: PgAttribute };
212+
};
213+
classById: { [classId: string]: PgClass };
214+
extensionById: { [extId: string]: PgExtension };
215+
namespaceById: { [namespaceId: string]: PgNamespace };
216+
typeById: { [typeId: string]: PgType };
217+
};
218+
219+
export async function getPostgresVersion(): Promise<number> {
220+
const versionResult = await db.one('show server_version_num;');
221+
return Number.parseInt(versionResult.server_version_num, 10);
20222
}
21223

22-
export async function getTablesSchemas(): Promise<Array<TableSchema>> {
23-
const tableNames = await getTableNames();
224+
export async function runIntrospectionQuery(): Promise<PgIntrospectionResultsByKind> {
225+
const version = await getPostgresVersion();
226+
const sql = makeIntrospectionQuery(version);
227+
const kinds = [
228+
'namespace',
229+
'class',
230+
'attribute',
231+
'type',
232+
'constraint',
233+
'procedure',
234+
'extension',
235+
'index',
236+
] as const;
237+
type Kinds = PgEntity['kind'];
238+
type Row = {
239+
object: PgEntity;
240+
};
241+
const rows: Row[] = await db.many(sql, [['public'], false]);
242+
243+
const originalResultsByType: PgIntrospectionOriginalResultsByKind = {
244+
__pgVersion: version,
245+
namespace: [],
246+
class: [],
247+
attribute: [],
248+
type: [],
249+
constraint: [],
250+
procedure: [],
251+
extension: [],
252+
index: [],
253+
};
254+
255+
for (const { object } of rows) {
256+
originalResultsByType[object.kind].push(object as any);
257+
}
258+
259+
// Parse tags from comments
260+
kinds.forEach((kind) => {
261+
originalResultsByType[kind].forEach((object: PgIntrospectionResultsByKind[Kinds][number]) => {
262+
// Keep a copy of the raw comment
263+
object.comment = object.description;
264+
// https://www.graphile.org/postgraphile/smart-comments/
265+
if (object.description) {
266+
const parsed = parseTags(object.description);
267+
object.tags = parsed.tags;
268+
object.description = parsed.text;
269+
} else {
270+
object.tags = {};
271+
}
272+
});
273+
});
24274

25-
const result = await Promise.all(
26-
tableNames.map(async (i) => {
27-
return { tableName: i.table_name, schema: await getColumnSchemas(i.table_name) };
28-
}),
275+
const extensionConfigurationClassIds = flatMap(
276+
originalResultsByType.extension,
277+
(e) => e.configurationClassIds,
29278
);
279+
originalResultsByType.class.forEach((pgClass) => {
280+
pgClass.isExtensionConfigurationTable = extensionConfigurationClassIds.includes(pgClass.id);
281+
});
282+
283+
kinds.forEach((k) => {
284+
originalResultsByType[k].forEach(Object.freeze);
285+
});
286+
287+
const result = originalResultsByType as PgIntrospectionResultsByKind;
288+
result.namespaceById = xByY(originalResultsByType.namespace, 'id');
289+
result.classById = xByY(originalResultsByType.class, 'id');
290+
result.typeById = xByY(originalResultsByType.type, 'id');
291+
result.attributeByClassIdAndNum = xByYAndZ(originalResultsByType.attribute, 'classId', 'num');
292+
result.extensionById = xByY(originalResultsByType.extension, 'id');
30293

31-
return result;
294+
return Object.freeze(result);
295+
}
296+
297+
export async function introspectSchemas() {
298+
const intro = await runIntrospectionQuery();
299+
const namespaceIds = intro.namespace.map((n) => n.id);
300+
const classes = intro.class.filter((c) => namespaceIds.includes(c.namespaceId));
301+
return { classes, intro };
302+
}
303+
304+
export async function getTablesSchemas(): Promise<Array<TableSchema>> {
305+
const { classes, intro } = await introspectSchemas();
306+
307+
return classes.map((klass) => {
308+
return {
309+
tableName: klass.name,
310+
schema: Object.values(intro.attributeByClassIdAndNum[klass.id]).map((attribute) => {
311+
return {
312+
column_name: attribute.name,
313+
udt_name: intro.typeById[attribute.typeId].name,
314+
is_nullable: attribute.isNotNull ? 'NO' : 'YES',
315+
};
316+
}),
317+
};
318+
});
32319
}
33320

34321
export function schemaToTableObj(schema: TableSchema): Table {

0 commit comments

Comments
 (0)