Skip to content

Commit 3578008

Browse files
committed
feat(pgsql-types): add new package for narrowed PostgreSQL AST types
This package generates narrowed TypeScript type definitions for PostgreSQL AST Node fields by parsing SQL fixtures and inferring which specific node types actually appear in each Node-typed field. Features: - Inference script that parses all SQL fixtures from kitchen-sink and postgres directories - Generates field-metadata.json with inferred type information - Generates narrowed TypeScript types with wrapped unions like { Integer: Integer } | { Float: Float } - Type aliases for each Node-typed field to avoid bloating interfaces Example: DefElem.arg is now typed as DefElem_arg which is a union of { A_Const: A_Const } | { Boolean: Boolean } | { Float: Float } | { Integer: Integer } | ... instead of the generic Node type. Addresses: constructive-io/libpg-query-node#143
1 parent 529f487 commit 3578008

File tree

9 files changed

+8671
-5553
lines changed

9 files changed

+8671
-5553
lines changed

packages/pgsql-types/package.json

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "pgsql-types",
3+
"version": "17.0.0",
4+
"author": "Constructive <[email protected]>",
5+
"description": "Narrowed PostgreSQL AST type definitions with specific Node unions",
6+
"main": "index.js",
7+
"module": "esm/index.js",
8+
"types": "index.d.ts",
9+
"homepage": "https://github.com/constructive-io/pgsql-parser",
10+
"license": "MIT",
11+
"publishConfig": {
12+
"access": "public",
13+
"directory": "dist"
14+
},
15+
"repository": {
16+
"type": "git",
17+
"url": "https://github.com/constructive-io/pgsql-parser"
18+
},
19+
"bugs": {
20+
"url": "https://github.com/constructive-io/pgsql-parser/issues"
21+
},
22+
"scripts": {
23+
"copy": "makage assets",
24+
"clean": "makage clean dist",
25+
"prepublishOnly": "npm run build",
26+
"build": "npm run infer && npm run clean && tsc && tsc -p tsconfig.esm.json && npm run copy",
27+
"build:dev": "npm run clean && tsc --declarationMap && tsc -p tsconfig.esm.json && npm run copy",
28+
"infer": "ts-node scripts/infer-field-metadata.ts",
29+
"generate": "ts-node scripts/generate-types.ts",
30+
"lint": "eslint . --fix",
31+
"test": "jest",
32+
"test:watch": "jest --watch"
33+
},
34+
"devDependencies": {
35+
"makage": "^0.1.8",
36+
"libpg-query": "17.7.3"
37+
},
38+
"dependencies": {
39+
"@pgsql/types": "^17.6.2",
40+
"@pgsql/enums": "^17.6.2",
41+
"@pgsql/utils": "^17.8.9"
42+
},
43+
"keywords": [
44+
"sql",
45+
"postgres",
46+
"postgresql",
47+
"pg",
48+
"ast",
49+
"types",
50+
"typescript"
51+
]
52+
}
Lines changed: 182 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import { runtimeSchema, NodeSpec, FieldSpec } from '../../utils/src/runtime-schema';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
5+
interface FieldMetadata {
6+
nullable: boolean;
7+
tags: string[];
8+
isArray: boolean;
9+
}
10+
11+
interface NodeFieldMetadata {
12+
[fieldName: string]: FieldMetadata;
13+
}
14+
15+
interface AllFieldMetadata {
16+
[nodeName: string]: NodeFieldMetadata;
17+
}
18+
19+
const schemaMap = new Map<string, NodeSpec>(
20+
runtimeSchema.map((spec: NodeSpec) => [spec.name, spec])
21+
);
22+
23+
const primitiveTypeMap: Record<string, string> = {
24+
'string': 'string',
25+
'bool': 'boolean',
26+
'int32': 'number',
27+
'int64': 'number',
28+
'uint32': 'number',
29+
'uint64': 'number',
30+
'float': 'number',
31+
'double': 'number',
32+
'bytes': 'Uint8Array',
33+
};
34+
35+
function isPrimitiveType(type: string): boolean {
36+
return type in primitiveTypeMap;
37+
}
38+
39+
function getTsType(type: string): string {
40+
return primitiveTypeMap[type] || type;
41+
}
42+
43+
function generateWrappedUnion(tags: string[]): string {
44+
if (tags.length === 0) {
45+
return 'Node';
46+
}
47+
48+
const sortedTags = [...tags].sort();
49+
return sortedTags.map(tag => `{ ${tag}: ${tag} }`).join(' | ');
50+
}
51+
52+
function generateTypeAlias(nodeName: string, fieldName: string, tags: string[]): string {
53+
const aliasName = `${nodeName}_${fieldName}`;
54+
const union = generateWrappedUnion(tags);
55+
return `export type ${aliasName} = ${union};`;
56+
}
57+
58+
function generateInterface(
59+
nodeSpec: NodeSpec,
60+
fieldMetadata: NodeFieldMetadata | undefined
61+
): string {
62+
const lines: string[] = [];
63+
lines.push(`export interface ${nodeSpec.name} {`);
64+
65+
for (const field of nodeSpec.fields) {
66+
const tsType = getFieldType(nodeSpec.name, field, fieldMetadata);
67+
const optional = field.optional ? '?' : '';
68+
lines.push(` ${field.name}${optional}: ${tsType};`);
69+
}
70+
71+
lines.push('}');
72+
return lines.join('\n');
73+
}
74+
75+
function getFieldType(
76+
nodeName: string,
77+
field: FieldSpec,
78+
fieldMetadata: NodeFieldMetadata | undefined
79+
): string {
80+
let baseType: string;
81+
82+
if (field.type === 'Node') {
83+
const meta = fieldMetadata?.[field.name];
84+
if (meta && meta.tags.length > 0) {
85+
baseType = `${nodeName}_${field.name}`;
86+
} else {
87+
baseType = 'Node';
88+
}
89+
} else if (isPrimitiveType(field.type)) {
90+
baseType = getTsType(field.type);
91+
} else {
92+
if (schemaMap.has(field.type)) {
93+
baseType = `{ ${field.type}: ${field.type} }`;
94+
} else {
95+
baseType = field.type;
96+
}
97+
}
98+
99+
if (field.isArray) {
100+
if (baseType.includes('|') || baseType.includes('{')) {
101+
return `(${baseType})[]`;
102+
}
103+
return `${baseType}[]`;
104+
}
105+
106+
return baseType;
107+
}
108+
109+
function generateTypes(metadata: AllFieldMetadata): string {
110+
const lines: string[] = [];
111+
112+
lines.push('/**');
113+
lines.push(' * This file was automatically generated by pgsql-types.');
114+
lines.push(' * DO NOT MODIFY IT BY HAND.');
115+
lines.push(' * ');
116+
lines.push(' * These types provide narrowed Node unions based on actual usage');
117+
lines.push(' * patterns discovered by parsing SQL fixtures.');
118+
lines.push(' */');
119+
lines.push('');
120+
121+
lines.push("import type { Node } from '@pgsql/types';");
122+
lines.push("export type { Node } from '@pgsql/types';");
123+
lines.push("export * from '@pgsql/enums';");
124+
lines.push('');
125+
126+
const typeAliases: string[] = [];
127+
for (const nodeName of Object.keys(metadata).sort()) {
128+
const nodeMetadata = metadata[nodeName];
129+
for (const fieldName of Object.keys(nodeMetadata).sort()) {
130+
const fieldMeta = nodeMetadata[fieldName];
131+
if (fieldMeta.tags.length > 0) {
132+
typeAliases.push(generateTypeAlias(nodeName, fieldName, fieldMeta.tags));
133+
}
134+
}
135+
}
136+
137+
if (typeAliases.length > 0) {
138+
lines.push('// Narrowed type aliases for Node-typed fields');
139+
lines.push(typeAliases.join('\n'));
140+
lines.push('');
141+
}
142+
143+
lines.push('// Interfaces with narrowed Node types');
144+
for (const nodeSpec of runtimeSchema) {
145+
const nodeMetadata = metadata[nodeSpec.name];
146+
lines.push(generateInterface(nodeSpec, nodeMetadata));
147+
lines.push('');
148+
}
149+
150+
return lines.join('\n');
151+
}
152+
153+
async function main() {
154+
const metadataPath = path.resolve(__dirname, '../src/field-metadata.json');
155+
const outputPath = path.resolve(__dirname, '../src/types.ts');
156+
157+
if (!fs.existsSync(metadataPath)) {
158+
console.error('Field metadata not found. Run "npm run infer" first.');
159+
process.exit(1);
160+
}
161+
162+
const metadata: AllFieldMetadata = JSON.parse(fs.readFileSync(metadataPath, 'utf-8'));
163+
164+
console.log('Generating narrowed types...');
165+
const typesContent = generateTypes(metadata);
166+
167+
fs.writeFileSync(outputPath, typesContent);
168+
console.log(`Wrote narrowed types to ${outputPath}`);
169+
170+
let totalAliases = 0;
171+
for (const nodeName of Object.keys(metadata)) {
172+
for (const fieldName of Object.keys(metadata[nodeName])) {
173+
if (metadata[nodeName][fieldName].tags.length > 0) {
174+
totalAliases++;
175+
}
176+
}
177+
}
178+
179+
console.log(`Generated ${totalAliases} narrowed type aliases`);
180+
}
181+
182+
main().catch(console.error);

0 commit comments

Comments
 (0)