Skip to content

Commit 438845d

Browse files
author
zth
committed
Add query diagnostics CLI
1 parent 71ca90b commit 438845d

6 files changed

Lines changed: 854 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
# Unreleased
2+
3+
- Add `pgtyped-rescript diagnose` for inspecting named SQL queries with `describe`, `explain`, and `explain analyze`, including support for parameter JSON, parameter files, processed SQL output, query listing, JSON output, and statement timeouts.
4+
- Reduce default CLI generation output to only recompiled files, errors, and concise unchanged-run summaries; pass `--verbose` to restore detailed per-file processing and skipped-file output.
5+
16
# 3.0.1
27

38
- Gracefully skip optional AST-based query analysis when `pgsql-ast-parser` cannot parse valid PostgreSQL syntax, so generation can continue with database-derived types.

package-lock.json

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/cli/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,9 @@
4848
"io-ts-reporters": "^2.0.1",
4949
"nunjucks": "3.2.4",
5050
"pascal-case": "^3.1.1",
51+
"pg": "^8.11.2",
5152
"pgtyped-rescript-query": "^3.0.1",
53+
"pgtyped-rescript-runtime": "^3.0.0",
5254
"piscina": "^4.0.0",
5355
"tinypool": "^0.7.0",
5456
"ts-parse-database-url": "^1.0.3",
@@ -58,6 +60,7 @@
5860
"@types/debug": "4.1.8",
5961
"@types/fs-extra": "11.0.1",
6062
"@types/nunjucks": "^3.1.3",
63+
"@types/pg": "^8.10.2",
6164
"@types/yargs": "17.0.24",
6265
"rescript": "12.2.0"
6366
},
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
import { jest } from '@jest/globals';
2+
import { parseSQLFile, queryASTToIR } from '@pgtyped/parser';
3+
import fs from 'fs';
4+
import {
5+
getParameterLabels,
6+
parseErrorFields,
7+
runDiagnostics,
8+
} from './diagnostics.js';
9+
10+
const sqlFile = '../example/src/books/books.sql';
11+
12+
describe('diagnostics', () => {
13+
let logSpy: ReturnType<typeof jest.spyOn>;
14+
let errorSpy: ReturnType<typeof jest.spyOn>;
15+
16+
beforeEach(() => {
17+
logSpy = jest.spyOn(console, 'log').mockImplementation(() => undefined);
18+
errorSpy = jest.spyOn(console, 'error').mockImplementation(() => undefined);
19+
});
20+
21+
afterEach(() => {
22+
logSpy.mockRestore();
23+
errorSpy.mockRestore();
24+
});
25+
26+
test('lists query names without config', async () => {
27+
await runDiagnostics(undefined, {
28+
file: sqlFile,
29+
list: true,
30+
mode: 'explain',
31+
format: 'text',
32+
});
33+
34+
expect(logSpy).toHaveBeenCalledWith('FindBookById');
35+
expect(logSpy).toHaveBeenCalledWith('InsertBooks');
36+
});
37+
38+
test('prints processed SQL without config', async () => {
39+
await runDiagnostics(undefined, {
40+
file: sqlFile,
41+
queryName: 'FindBookById',
42+
sql: true,
43+
mode: 'explain',
44+
format: 'text',
45+
});
46+
47+
expect(logSpy).toHaveBeenCalledWith('SELECT * FROM books WHERE id = $1');
48+
});
49+
50+
test('keeps scalar parameter labels when params are supplied', () => {
51+
const query = parseSQLFile(`
52+
/* @name FindBookById */
53+
SELECT * FROM books WHERE id = :id;
54+
`).queries[0];
55+
56+
const labels = getParameterLabels(queryASTToIR(query, null), { id: 1 });
57+
58+
expect(labels.get(1)).toBe('id');
59+
});
60+
61+
test('does not require params for unused parameter declarations', async () => {
62+
const file = '/tmp/pgtyped-unused-params.sql';
63+
await fs.promises.writeFile(
64+
file,
65+
`
66+
/*
67+
@name StaticQuery
68+
@param unused -> (id!)
69+
*/
70+
SELECT 1;
71+
`,
72+
);
73+
74+
await runDiagnostics(undefined, {
75+
file,
76+
queryName: 'StaticQuery',
77+
sql: true,
78+
mode: 'explain',
79+
format: 'text',
80+
});
81+
82+
expect(logSpy).toHaveBeenCalledWith('SELECT 1');
83+
});
84+
85+
test('uses SQLSTATE as parse error code', () => {
86+
expect(
87+
parseErrorFields({
88+
C: '42601',
89+
M: 'syntax error at or near "FROM"',
90+
R: 'scanner_yyerror',
91+
}),
92+
).toMatchObject({
93+
errorCode: '42601',
94+
message: 'syntax error at or near "FROM"',
95+
});
96+
});
97+
98+
test('keeps spread-pick labels when params are supplied', () => {
99+
const query = parseSQLFile(`
100+
/*
101+
@name InsertBooks
102+
@param books -> ((rank!, name!)...)
103+
*/
104+
INSERT INTO books (rank, name)
105+
VALUES :books;
106+
`).queries[0];
107+
108+
const labels = getParameterLabels(queryASTToIR(query, null), {
109+
books: [
110+
{ rank: 1, name: 'one' },
111+
{ rank: 2, name: 'two' },
112+
],
113+
});
114+
115+
expect([...labels.values()]).toEqual([
116+
'books.rank',
117+
'books.name',
118+
'books.rank',
119+
'books.name',
120+
]);
121+
});
122+
});

0 commit comments

Comments
 (0)