neon 3.3.0 → 3.4.0
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.
|
@@ -106,7 +106,7 @@ const handler = async (props) => {
|
|
|
106
106
|
let host = props.pooled ? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`) : endpoint.host;
|
|
107
107
|
if (parsedPIT.tag !== "head") host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
|
|
108
108
|
const connectionString = new URL(`postgresql://${host}`);
|
|
109
|
-
connectionString.pathname = database
|
|
109
|
+
connectionString.pathname = `/${encodeURIComponent(database)}`;
|
|
110
110
|
connectionString.username = role;
|
|
111
111
|
connectionString.password = password;
|
|
112
112
|
if (props.prisma) {
|
package/dist/commands/inspect.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { t as __exportAll } from "../_chunks/rolldown-runtime-8H4AJuhK.js";
|
|
2
2
|
import { writer } from "../writer.js";
|
|
3
3
|
import { fillSingleProject } from "../utils/enrichers.js";
|
|
4
|
-
import {
|
|
4
|
+
import { formatInspectQueryError, resolveInspectTargets, runInspectQuery } from "../utils/inspect_db.js";
|
|
5
5
|
import { INSPECT_QUERIES } from "../utils/inspect_queries.js";
|
|
6
6
|
//#region src/commands/inspect.ts
|
|
7
7
|
var inspect_exports = /* @__PURE__ */ __exportAll({
|
|
@@ -12,7 +12,7 @@ var inspect_exports = /* @__PURE__ */ __exportAll({
|
|
|
12
12
|
handler: () => handler
|
|
13
13
|
});
|
|
14
14
|
const command = "inspect";
|
|
15
|
-
const describe = "Inspect a
|
|
15
|
+
const describe = "Inspect a branch's Postgres health and configuration";
|
|
16
16
|
const aliases = ["inspection"];
|
|
17
17
|
/**
|
|
18
18
|
* `fillSingleProject` hits the API to auto-resolve a project. When the user
|
|
@@ -24,11 +24,36 @@ const fillSingleProjectUnlessDbUrl = async (props) => {
|
|
|
24
24
|
};
|
|
25
25
|
const runSubcommand = async (name, props) => {
|
|
26
26
|
const query = INSPECT_QUERIES[name];
|
|
27
|
-
const
|
|
28
|
-
const rows =
|
|
27
|
+
const { targets, includeDatabaseColumn, branchDatabaseCount } = await resolveInspectTargets(props, query.scope);
|
|
28
|
+
const rows = [];
|
|
29
|
+
for (const target of targets) {
|
|
30
|
+
let batch;
|
|
31
|
+
try {
|
|
32
|
+
batch = await runInspectQuery(target.connectionUri, query.sql, { requiresExtension: query.requiresExtension });
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
35
|
+
const wrapped = formatInspectQueryError({
|
|
36
|
+
reason,
|
|
37
|
+
database: target.database,
|
|
38
|
+
dbUrl: props.dbUrl,
|
|
39
|
+
databaseName: props.databaseName,
|
|
40
|
+
offerDatabaseNameHint: branchDatabaseCount > 1,
|
|
41
|
+
scope: query.scope,
|
|
42
|
+
requiresExtension: query.requiresExtension
|
|
43
|
+
});
|
|
44
|
+
if (wrapped === void 0) throw err instanceof Error ? err : new Error(reason);
|
|
45
|
+
throw new Error(wrapped);
|
|
46
|
+
}
|
|
47
|
+
if (includeDatabaseColumn) rows.push(...batch.map((row) => ({
|
|
48
|
+
database: target.database,
|
|
49
|
+
...row
|
|
50
|
+
})));
|
|
51
|
+
else rows.push(...batch);
|
|
52
|
+
}
|
|
53
|
+
const fields = includeDatabaseColumn ? ["database", ...query.fields] : query.fields;
|
|
29
54
|
writer(props).end(rows, {
|
|
30
|
-
fields
|
|
31
|
-
emptyMessage: query.emptyMessage
|
|
55
|
+
fields,
|
|
56
|
+
emptyMessage: includeDatabaseColumn ? query.emptyMessageAll ?? query.emptyMessage : query.emptyMessage
|
|
32
57
|
});
|
|
33
58
|
};
|
|
34
59
|
const dbBuilder = (argv) => {
|
|
@@ -42,7 +67,7 @@ const dbBuilder = (argv) => {
|
|
|
42
67
|
type: "string"
|
|
43
68
|
},
|
|
44
69
|
"database-name": {
|
|
45
|
-
describe: "Database
|
|
70
|
+
describe: "Database to inspect. Omit to cover every database the API lists for the branch. Ranking and row limits stay per database. One failing database fails the whole run. Compute-wide checks run once against the first listed database. Ignored with --db-url.",
|
|
46
71
|
type: "string"
|
|
47
72
|
},
|
|
48
73
|
"role-name": {
|
package/dist/utils/inspect_db.js
CHANGED
|
@@ -11,6 +11,60 @@ const SSL_MODES = [
|
|
|
11
11
|
"omit"
|
|
12
12
|
];
|
|
13
13
|
/**
|
|
14
|
+
* Keep the database column on one-database branches so adding another database
|
|
15
|
+
* does not change the output schema. Compute-wide views run once because every
|
|
16
|
+
* database returns the same rows.
|
|
17
|
+
*/
|
|
18
|
+
const selectInspectTargets = (input) => {
|
|
19
|
+
if (input.dbUrlDatabase !== void 0) return {
|
|
20
|
+
databases: [input.dbUrlDatabase],
|
|
21
|
+
includeDatabaseColumn: false
|
|
22
|
+
};
|
|
23
|
+
if (input.databaseName !== void 0) {
|
|
24
|
+
if (input.databaseName === "") throw new Error("--database-name cannot be empty. Omit the flag to cover every database.");
|
|
25
|
+
return {
|
|
26
|
+
databases: [input.databaseName],
|
|
27
|
+
includeDatabaseColumn: false
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
if (input.branchDatabases.length === 0) throw new Error("No databases found for the branch");
|
|
31
|
+
if (input.scope === "compute") return {
|
|
32
|
+
databases: [input.branchDatabases[0]],
|
|
33
|
+
includeDatabaseColumn: false
|
|
34
|
+
};
|
|
35
|
+
return {
|
|
36
|
+
databases: [...input.branchDatabases].sort((a, b) => a.localeCompare(b, "en")),
|
|
37
|
+
includeDatabaseColumn: true
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
const formatInspectQueryError = (input) => {
|
|
41
|
+
if (input.dbUrl !== void 0 || input.databaseName !== void 0) return;
|
|
42
|
+
const missingExtension = input.requiresExtension !== void 0 && input.reason.includes(`"${input.requiresExtension}"`);
|
|
43
|
+
const hint = !input.offerDatabaseNameHint ? "" : missingExtension ? `. Pass --database-name to try a database that already has the "${input.requiresExtension}" extension.` : input.scope === "compute" ? ". Pass --database-name to connect through a different database." : ". Pass --database-name to inspect one database.";
|
|
44
|
+
return `${input.reason} (database ${input.database})${hint}`;
|
|
45
|
+
};
|
|
46
|
+
const connectionUriForDatabase = (connectionUri, database) => {
|
|
47
|
+
const url = new URL(connectionUri);
|
|
48
|
+
url.pathname = `/${encodeURIComponent(database)}`;
|
|
49
|
+
return url.toString();
|
|
50
|
+
};
|
|
51
|
+
const listBranchDatabases = async (props) => {
|
|
52
|
+
const projectId = props.projectId;
|
|
53
|
+
const parsedPIT = props.branch ? parsePITBranch(props.branch) : {
|
|
54
|
+
tag: "head",
|
|
55
|
+
branch: ""
|
|
56
|
+
};
|
|
57
|
+
const branchId = await branchIdFromProps({
|
|
58
|
+
...props,
|
|
59
|
+
...props.branch ? { branch: parsedPIT.branch } : {}
|
|
60
|
+
});
|
|
61
|
+
const { data: { databases } } = await props.apiClient.listProjectBranchDatabases(projectId, branchId);
|
|
62
|
+
return {
|
|
63
|
+
branchId,
|
|
64
|
+
names: databases.map((d) => d.name)
|
|
65
|
+
};
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
14
68
|
* Resolve a branch's live Postgres connection details via the Neon API
|
|
15
69
|
* (endpoint → role → password → database → URL, honoring point-in-time,
|
|
16
70
|
* pooling, prisma tuning, and SSL mode). Mirrors how `connection-string`
|
|
@@ -45,7 +99,7 @@ const resolveConnectionUri = async (props) => {
|
|
|
45
99
|
let host = props.pooled ? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`) : endpoint.host;
|
|
46
100
|
if (parsedPIT.tag !== "head") host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
|
|
47
101
|
const connectionString = new URL(`postgresql://${host}`);
|
|
48
|
-
connectionString.pathname = database
|
|
102
|
+
connectionString.pathname = `/${encodeURIComponent(database)}`;
|
|
49
103
|
connectionString.username = role;
|
|
50
104
|
connectionString.password = password;
|
|
51
105
|
if (props.prisma) {
|
|
@@ -71,6 +125,58 @@ const resolveConnectionUri = async (props) => {
|
|
|
71
125
|
options: connectionString.searchParams.toString()
|
|
72
126
|
};
|
|
73
127
|
};
|
|
128
|
+
const resolveInspectTargets = async (props, scope) => {
|
|
129
|
+
if (props.dbUrl) {
|
|
130
|
+
const parsed = parseConnectionUri(props.dbUrl);
|
|
131
|
+
const selection = selectInspectTargets({
|
|
132
|
+
dbUrlDatabase: parsed.database,
|
|
133
|
+
branchDatabases: [],
|
|
134
|
+
scope
|
|
135
|
+
});
|
|
136
|
+
return {
|
|
137
|
+
targets: [{
|
|
138
|
+
database: selection.databases[0],
|
|
139
|
+
connectionUri: props.dbUrl
|
|
140
|
+
}],
|
|
141
|
+
includeDatabaseColumn: selection.includeDatabaseColumn,
|
|
142
|
+
branchDatabaseCount: 1
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
if (props.databaseName !== void 0) {
|
|
146
|
+
selectInspectTargets({
|
|
147
|
+
databaseName: props.databaseName,
|
|
148
|
+
branchDatabases: [],
|
|
149
|
+
scope
|
|
150
|
+
});
|
|
151
|
+
const resolved = await resolveConnectionUri(props);
|
|
152
|
+
return {
|
|
153
|
+
targets: [{
|
|
154
|
+
database: resolved.database,
|
|
155
|
+
connectionUri: resolved.connectionUri
|
|
156
|
+
}],
|
|
157
|
+
includeDatabaseColumn: false,
|
|
158
|
+
branchDatabaseCount: 1
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const { branchId, names } = await listBranchDatabases(props);
|
|
162
|
+
if (names.length === 0) throw new Error(`No databases found for the branch: ${branchId}`);
|
|
163
|
+
const selection = selectInspectTargets({
|
|
164
|
+
branchDatabases: names,
|
|
165
|
+
scope
|
|
166
|
+
});
|
|
167
|
+
const first = await resolveConnectionUri({
|
|
168
|
+
...props,
|
|
169
|
+
databaseName: selection.databases[0]
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
targets: selection.databases.map((database) => ({
|
|
173
|
+
database,
|
|
174
|
+
connectionUri: connectionUriForDatabase(first.connectionUri, database)
|
|
175
|
+
})),
|
|
176
|
+
includeDatabaseColumn: selection.includeDatabaseColumn,
|
|
177
|
+
branchDatabaseCount: names.length
|
|
178
|
+
};
|
|
179
|
+
};
|
|
74
180
|
const rowsFrom = (result) => {
|
|
75
181
|
const names = result.fields.map((f) => f.name);
|
|
76
182
|
return result.rows.map((row) => {
|
|
@@ -108,4 +214,4 @@ const runInspectQuery = async (connectionUri, sql, options = {}) => {
|
|
|
108
214
|
}
|
|
109
215
|
};
|
|
110
216
|
//#endregion
|
|
111
|
-
export { SSL_MODES, resolveConnectionUri, runInspectQuery };
|
|
217
|
+
export { SSL_MODES, connectionUriForDatabase, formatInspectQueryError, resolveConnectionUri, resolveInspectTargets, runInspectQuery, selectInspectTargets };
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
const INSPECT_QUERIES = {
|
|
3
3
|
"table-sizes": {
|
|
4
4
|
describe: "Size of each table (including TOAST), largest first (pg_table_size)",
|
|
5
|
+
scope: "database",
|
|
5
6
|
fields: [
|
|
6
7
|
"schema",
|
|
7
8
|
"name",
|
|
@@ -23,6 +24,7 @@ const INSPECT_QUERIES = {
|
|
|
23
24
|
},
|
|
24
25
|
"index-sizes": {
|
|
25
26
|
describe: "Size of each index, largest first (pg_relation_size)",
|
|
27
|
+
scope: "database",
|
|
26
28
|
fields: [
|
|
27
29
|
"schema",
|
|
28
30
|
"name",
|
|
@@ -44,6 +46,7 @@ const INSPECT_QUERIES = {
|
|
|
44
46
|
},
|
|
45
47
|
"unused-indexes": {
|
|
46
48
|
describe: "Non-unique indexes with few scans — candidates for removal (pg_stat_user_indexes)",
|
|
49
|
+
scope: "database",
|
|
47
50
|
fields: [
|
|
48
51
|
"table",
|
|
49
52
|
"index",
|
|
@@ -67,6 +70,7 @@ const INSPECT_QUERIES = {
|
|
|
67
70
|
},
|
|
68
71
|
"seq-scans": {
|
|
69
72
|
describe: "Number of sequential scans recorded against each table (pg_stat_user_tables)",
|
|
73
|
+
scope: "database",
|
|
70
74
|
fields: [
|
|
71
75
|
"schema",
|
|
72
76
|
"name",
|
|
@@ -84,13 +88,15 @@ const INSPECT_QUERIES = {
|
|
|
84
88
|
},
|
|
85
89
|
"long-running-queries": {
|
|
86
90
|
describe: "Queries running longer than 5 minutes (pg_stat_activity)",
|
|
91
|
+
scope: "database",
|
|
87
92
|
fields: [
|
|
88
93
|
"pid",
|
|
89
94
|
"duration",
|
|
90
95
|
"state",
|
|
91
96
|
"query"
|
|
92
97
|
],
|
|
93
|
-
emptyMessage: "No long-running queries.",
|
|
98
|
+
emptyMessage: "No long-running queries in this database.",
|
|
99
|
+
emptyMessageAll: "No long-running queries in any database.",
|
|
94
100
|
sql: `
|
|
95
101
|
SELECT
|
|
96
102
|
pid,
|
|
@@ -98,7 +104,8 @@ const INSPECT_QUERIES = {
|
|
|
98
104
|
state,
|
|
99
105
|
query
|
|
100
106
|
FROM pg_stat_activity
|
|
101
|
-
WHERE
|
|
107
|
+
WHERE datname = current_database()
|
|
108
|
+
AND state <> 'idle'
|
|
102
109
|
AND query NOT ILIKE '%pg_stat_activity%'
|
|
103
110
|
AND now() - query_start > interval '5 minutes'
|
|
104
111
|
ORDER BY now() - query_start DESC;
|
|
@@ -106,6 +113,7 @@ const INSPECT_QUERIES = {
|
|
|
106
113
|
},
|
|
107
114
|
locks: {
|
|
108
115
|
describe: "Locks held with the acquiring query and its age (pg_locks + pg_stat_activity)",
|
|
116
|
+
scope: "database",
|
|
109
117
|
fields: [
|
|
110
118
|
"pid",
|
|
111
119
|
"relname",
|
|
@@ -115,7 +123,8 @@ const INSPECT_QUERIES = {
|
|
|
115
123
|
"age",
|
|
116
124
|
"query"
|
|
117
125
|
],
|
|
118
|
-
emptyMessage: "No locks held.",
|
|
126
|
+
emptyMessage: "No locks held in this database.",
|
|
127
|
+
emptyMessageAll: "No locks held in any database.",
|
|
119
128
|
sql: `
|
|
120
129
|
SELECT
|
|
121
130
|
a.pid,
|
|
@@ -128,13 +137,15 @@ const INSPECT_QUERIES = {
|
|
|
128
137
|
FROM pg_locks l
|
|
129
138
|
JOIN pg_stat_activity a ON a.pid = l.pid
|
|
130
139
|
LEFT JOIN pg_class c ON c.oid = l.relation
|
|
131
|
-
WHERE a.
|
|
140
|
+
WHERE a.datname = current_database()
|
|
141
|
+
AND a.query <> '<insufficient privilege>'
|
|
132
142
|
AND l.pid <> pg_backend_pid()
|
|
133
143
|
ORDER BY a.query_start;
|
|
134
144
|
`
|
|
135
145
|
},
|
|
136
146
|
outliers: {
|
|
137
147
|
describe: "Queries taking the most cumulative execution time (needs pg_stat_statements)",
|
|
148
|
+
scope: "database",
|
|
138
149
|
fields: [
|
|
139
150
|
"total_exec_time",
|
|
140
151
|
"prop_exec_time",
|
|
@@ -161,6 +172,7 @@ const INSPECT_QUERIES = {
|
|
|
161
172
|
},
|
|
162
173
|
calls: {
|
|
163
174
|
describe: "Most frequently called queries (needs pg_stat_statements)",
|
|
175
|
+
scope: "database",
|
|
164
176
|
fields: [
|
|
165
177
|
"ncalls",
|
|
166
178
|
"total_exec_time",
|
|
@@ -186,7 +198,8 @@ const INSPECT_QUERIES = {
|
|
|
186
198
|
`
|
|
187
199
|
},
|
|
188
200
|
"lfc-hit-rate": {
|
|
189
|
-
describe: "Local File Cache hit rate (needs neon extension)",
|
|
201
|
+
describe: "Local File Cache hit rate (compute-wide, needs neon extension)",
|
|
202
|
+
scope: "compute",
|
|
190
203
|
fields: ["name", "ratio"],
|
|
191
204
|
emptyMessage: "No LFC stats available.",
|
|
192
205
|
requiresExtension: "neon",
|
|
@@ -207,7 +220,8 @@ const INSPECT_QUERIES = {
|
|
|
207
220
|
`
|
|
208
221
|
},
|
|
209
222
|
"working-set": {
|
|
210
|
-
describe: "Estimated working set vs LFC size (needs neon extension)",
|
|
223
|
+
describe: "Estimated working set vs LFC size (compute-wide, needs neon extension)",
|
|
224
|
+
scope: "compute",
|
|
211
225
|
fields: [
|
|
212
226
|
"window",
|
|
213
227
|
"working_set",
|
|
@@ -239,6 +253,7 @@ const INSPECT_QUERIES = {
|
|
|
239
253
|
},
|
|
240
254
|
"vacuum-stats": {
|
|
241
255
|
describe: "Autovacuum status per table: last (auto)vacuum, dead tuples, threshold",
|
|
256
|
+
scope: "database",
|
|
242
257
|
fields: [
|
|
243
258
|
"schema",
|
|
244
259
|
"table",
|
|
@@ -273,6 +288,7 @@ const INSPECT_QUERIES = {
|
|
|
273
288
|
},
|
|
274
289
|
bloat: {
|
|
275
290
|
describe: "Estimated table/index bloat (statistical estimate, no extension needed)",
|
|
291
|
+
scope: "database",
|
|
276
292
|
fields: [
|
|
277
293
|
"type",
|
|
278
294
|
"schema",
|
|
@@ -333,7 +349,8 @@ const INSPECT_QUERIES = {
|
|
|
333
349
|
`
|
|
334
350
|
},
|
|
335
351
|
"replication-slots": {
|
|
336
|
-
describe: "Replication slots: kind, status, client, restart/confirmed-flush LSNs, and lag (pg_replication_slots + pg_stat_replication)",
|
|
352
|
+
describe: "Replication slots (compute-wide): kind, status, client, restart/confirmed-flush LSNs, and lag (pg_replication_slots + pg_stat_replication)",
|
|
353
|
+
scope: "compute",
|
|
337
354
|
fields: [
|
|
338
355
|
"slot_name",
|
|
339
356
|
"slot_type",
|
|
@@ -379,6 +396,7 @@ const INSPECT_QUERIES = {
|
|
|
379
396
|
},
|
|
380
397
|
subscriptions: {
|
|
381
398
|
describe: "Per-table logical replication progress on this subscriber (pg_subscription_rel)",
|
|
399
|
+
scope: "database",
|
|
382
400
|
fields: [
|
|
383
401
|
"subscription",
|
|
384
402
|
"table_name",
|
|
@@ -386,6 +404,7 @@ const INSPECT_QUERIES = {
|
|
|
386
404
|
"lsn"
|
|
387
405
|
],
|
|
388
406
|
emptyMessage: "No subscriptions found on this database.",
|
|
407
|
+
emptyMessageAll: "No subscriptions found on any database.",
|
|
389
408
|
sql: `
|
|
390
409
|
SELECT
|
|
391
410
|
sub.subname AS subscription,
|