@query-doctor/core 0.1.0 → 0.1.2

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/dist/package.json DELETED
@@ -1,25 +0,0 @@
1
- {
2
- "name": "@query-doctor/core",
3
- "private": false,
4
- "version": "0.0.3",
5
- "description": "Core logic for Query Doctor",
6
- "license": "",
7
- "author": "Query Doctor",
8
- "type": "module",
9
- "types": "./index.d.ts",
10
- "main": "./index.js",
11
- "exports": {
12
- ".": {
13
- "types": "./index.d.ts",
14
- "import": "./index.js",
15
- "require": "./index.cjs"
16
- }
17
- },
18
- "dependencies": {
19
- "@pgsql/types": "^17.6.1",
20
- "colorette": "^2.0.20",
21
- "dedent": "^1.7.0",
22
- "pgsql-deparser": "^17.11.1",
23
- "zod": "^4.1.12"
24
- }
25
- }
@@ -1,270 +0,0 @@
1
- import { bgMagentaBright, blue, dim, strikethrough, } from "colorette";
2
- import { Walker } from "./walker.js";
3
- export const ignoredIdentifier = "__qd_placeholder";
4
- /**
5
- * Analyzes a query and returns a list of column references that
6
- * should be indexed.
7
- *
8
- * This should be instantiated once per analyzed query.
9
- */
10
- export class Analyzer {
11
- parser;
12
- constructor(parser) {
13
- this.parser = parser;
14
- }
15
- async analyze(query, formattedQuery) {
16
- const ast = (await this.parser(query));
17
- if (!ast.stmts) {
18
- throw new Error("Query did not have any statements. This should probably never happen?");
19
- }
20
- const stmt = ast.stmts[0].stmt;
21
- if (!stmt) {
22
- throw new Error("Query did not have any statements. This should probably never happen?");
23
- }
24
- const walker = new Walker(query);
25
- const { highlights, indexRepresentations, indexesToCheck, shadowedAliases, tempTables, tableMappings, nudges, } = walker.walk(stmt);
26
- const sortedHighlights = highlights.sort((a, b) => b.position.end - a.position.end);
27
- let currQuery = query;
28
- for (const highlight of sortedHighlights) {
29
- // our parts might have
30
- const parts = this.resolveTableAliases(highlight.parts, tableMappings);
31
- if (parts.length === 0) {
32
- console.error(highlight);
33
- throw new Error("Highlight must have at least one part");
34
- }
35
- let color;
36
- let skip = false;
37
- if (highlight.ignored) {
38
- color = (x) => dim(strikethrough(x));
39
- skip = true;
40
- }
41
- else if (parts.length === 2 &&
42
- tempTables.has(parts[0].text) &&
43
- // sometimes temp tables are aliased as existing tables
44
- // we don't want to ignore them if they are
45
- !tableMappings.has(parts[0].text)) {
46
- color = blue;
47
- skip = true;
48
- }
49
- else {
50
- color = bgMagentaBright;
51
- }
52
- const queryRepr = highlight.representation;
53
- const queryBeforeMatch = currQuery.slice(0, highlight.position.start);
54
- const queryAfterToken = currQuery.slice(highlight.position.end);
55
- currQuery = `${queryBeforeMatch}${color(queryRepr)}${this.colorizeKeywords(queryAfterToken, color)}`;
56
- if (indexRepresentations.has(queryRepr)) {
57
- skip = true;
58
- }
59
- if (!skip) {
60
- indexesToCheck.push(highlight);
61
- indexRepresentations.add(queryRepr);
62
- }
63
- }
64
- const referencedTables = [];
65
- for (const value of tableMappings.values()) {
66
- // aliased mappings are not concrete tables
67
- // eg: select * from table t -> t is not a table
68
- if (!value.alias) {
69
- referencedTables.push(value.text);
70
- }
71
- }
72
- const { tags, queryWithoutTags } = this.extractSqlcommenter(query);
73
- const formattedQueryWithoutTags = formattedQuery
74
- ? this.extractSqlcommenter(formattedQuery).queryWithoutTags
75
- : undefined;
76
- return {
77
- indexesToCheck,
78
- ansiHighlightedQuery: currQuery,
79
- referencedTables,
80
- shadowedAliases,
81
- tags,
82
- queryWithoutTags,
83
- formattedQueryWithoutTags,
84
- nudges,
85
- };
86
- }
87
- deriveIndexes(tables, discovered) {
88
- /**
89
- * There are 3 different kinds of parts a col reference can have
90
- * {a} = just a column within context. Find out the table
91
- * {a, b} = a column reference with a table reference. There's still ambiguity here
92
- * with what the schema could be in case there are 2 tables with the same name in different schemas.
93
- * {a, b, c} = a column reference with a table reference and a schema reference.
94
- * This is the best case scenario.
95
- */
96
- const allIndexes = [];
97
- const seenIndexes = new Set();
98
- function addIndex(index) {
99
- const key = `"${index.schema}":"${index.table}":"${index.column}"`;
100
- if (seenIndexes.has(key)) {
101
- return;
102
- }
103
- seenIndexes.add(key);
104
- allIndexes.push(index);
105
- }
106
- for (const colReference of discovered) {
107
- const partsCount = colReference.parts.length;
108
- const columnOnlyReference = partsCount === 1;
109
- const tableReference = partsCount === 2;
110
- const fullReference = partsCount === 3;
111
- if (columnOnlyReference) {
112
- // select c from x
113
- const [column] = colReference.parts;
114
- const referencedColumn = this.normalize(column);
115
- // TODO: this is not a good guess
116
- // we can absolutely infer the schema name
117
- // much better from the surrounding context
118
- // this will lead to problems where we use
119
- // tables like `auth.users` instead of `public.users`
120
- // just because `auth` might have alphabetic priority
121
- const matchingTables = tables.filter((table) => {
122
- return (table.columns?.some((column) => {
123
- return column.columnName === referencedColumn;
124
- }) ?? false);
125
- });
126
- for (const table of matchingTables) {
127
- const index = {
128
- schema: table.schemaName,
129
- table: table.tableName,
130
- column: referencedColumn,
131
- };
132
- if (colReference.sort) {
133
- index.sort = colReference.sort;
134
- }
135
- if (colReference.where) {
136
- index.where = colReference.where;
137
- }
138
- addIndex(index);
139
- }
140
- }
141
- else if (tableReference) {
142
- // select b.c from x
143
- const [table, column] = colReference.parts;
144
- const referencedTable = this.normalize(table);
145
- const referencedColumn = this.normalize(column);
146
- const matchingTable = tables.find((table) => {
147
- const hasMatchingColumn = table.columns?.some((column) => {
148
- return column.columnName === referencedColumn;
149
- }) ?? false;
150
- return table.tableName === referencedTable && hasMatchingColumn;
151
- });
152
- if (matchingTable) {
153
- const index = {
154
- schema: matchingTable.schemaName,
155
- table: referencedTable,
156
- column: referencedColumn,
157
- };
158
- if (colReference.sort) {
159
- index.sort = colReference.sort;
160
- }
161
- if (colReference.where) {
162
- index.where = colReference.where;
163
- }
164
- addIndex(index);
165
- }
166
- }
167
- else if (fullReference) {
168
- // select a.b.c from x
169
- const [schema, table, column] = colReference.parts;
170
- const referencedSchema = this.normalize(schema);
171
- const referencedTable = this.normalize(table);
172
- const referencedColumn = this.normalize(column);
173
- const index = {
174
- schema: referencedSchema,
175
- table: referencedTable,
176
- column: referencedColumn,
177
- };
178
- if (colReference.sort) {
179
- index.sort = colReference.sort;
180
- }
181
- if (colReference.where) {
182
- index.where = colReference.where;
183
- }
184
- addIndex(index);
185
- }
186
- else {
187
- // select huh.a.b.c from x
188
- console.error("Column reference has too many parts. The query is malformed", colReference);
189
- continue;
190
- }
191
- }
192
- return allIndexes;
193
- }
194
- colorizeKeywords(query, color) {
195
- return query
196
- .replace(
197
- // eh? This kinda sucks
198
- /(^\s+)(asc|desc)?(\s+(nulls first|nulls last))?/i, (_, pre, dir, spaceNulls, nulls) => {
199
- return `${pre}${dir ? color(dir) : ""}${nulls ? spaceNulls.replace(nulls, color(nulls)) : ""}`;
200
- })
201
- .replace(/(^\s+)(is (null|not null))/i, (_, pre, nulltest) => {
202
- return `${pre}${color(nulltest)}`;
203
- });
204
- }
205
- /**
206
- * Resolves aliases such as `a.b` to `x.b` if `a` is a known
207
- * alias to a table called x.
208
- *
209
- * Ignores all other combination of parts such as `a.b.c`
210
- */
211
- resolveTableAliases(parts, tableMappings) {
212
- // we don't want to resolve aliases for references such as
213
- // `a.b.c` - this is fully qualified with a schema and can't be an alias
214
- // `c` - because there's no table reference here (as far as we can tell)
215
- if (parts.length !== 2) {
216
- return parts;
217
- }
218
- const tablePart = parts[0];
219
- const mapping = tableMappings.get(tablePart.text);
220
- if (mapping) {
221
- parts[0] = mapping;
222
- }
223
- return parts;
224
- }
225
- normalize(columnReference) {
226
- return columnReference.quoted
227
- ? columnReference.text
228
- : // postgres automatically lowercases column names if not quoted
229
- columnReference.text.toLowerCase();
230
- }
231
- extractSqlcommenter(query) {
232
- const trimmedQuery = query.trimEnd();
233
- const startPosition = trimmedQuery.lastIndexOf("/*");
234
- const endPosition = trimmedQuery.lastIndexOf("*/");
235
- if (startPosition === -1 || endPosition === -1) {
236
- return { tags: [], queryWithoutTags: trimmedQuery };
237
- }
238
- const queryWithoutTags = trimmedQuery.slice(0, startPosition);
239
- const tagString = trimmedQuery.slice(startPosition + 2, endPosition);
240
- if (!tagString || typeof tagString !== "string") {
241
- return { tags: [], queryWithoutTags: queryWithoutTags };
242
- }
243
- const tags = [];
244
- for (const match of tagString.split(",")) {
245
- const [key, value] = match.split("=");
246
- if (!key || !value) {
247
- console.warn(`Invalid sqlcommenter tag: ${match}. Ignoring`);
248
- continue;
249
- }
250
- try {
251
- let sliceStart = 0;
252
- if (value.startsWith("'")) {
253
- sliceStart = 1;
254
- }
255
- let sliceEnd = value.length;
256
- if (value.endsWith("'")) {
257
- sliceEnd -= 1;
258
- }
259
- const decoded = decodeURIComponent(value.slice(sliceStart, sliceEnd));
260
- // should we be trimming here?
261
- tags.push({ key: key.trim(), value: decoded });
262
- }
263
- catch (err) {
264
- // we want to be very conservative with this parser and ignore errors
265
- console.error(err);
266
- }
267
- }
268
- return { tags, queryWithoutTags };
269
- }
270
- }
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=analyzer_test.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"analyzer_test.d.ts","sourceRoot":"","sources":["../../src/sql/analyzer_test.ts"],"names":[],"mappings":""}