neonctl 2.37.1 → 2.38.1
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/README.md +15 -700
- package/bin/cli.js +3 -0
- package/package.json +8 -75
- package/dist/analytics.js +0 -168
- package/dist/api.js +0 -720
- package/dist/auth.js +0 -125
- package/dist/callback.html +0 -60
- package/dist/cli.js +0 -9
- package/dist/commands/api.js +0 -278
- package/dist/commands/auth.js +0 -214
- package/dist/commands/bootstrap.js +0 -481
- package/dist/commands/branches.js +0 -488
- package/dist/commands/bucket.js +0 -549
- package/dist/commands/checkout.js +0 -321
- package/dist/commands/config.js +0 -626
- package/dist/commands/connection_string.js +0 -172
- package/dist/commands/data_api.js +0 -284
- package/dist/commands/databases.js +0 -82
- package/dist/commands/deploy.js +0 -26
- package/dist/commands/dev.js +0 -698
- package/dist/commands/diff.js +0 -222
- package/dist/commands/env.js +0 -189
- package/dist/commands/functions.js +0 -373
- package/dist/commands/index.js +0 -62
- package/dist/commands/init.js +0 -73
- package/dist/commands/inspect.js +0 -65
- package/dist/commands/ip_allow.js +0 -137
- package/dist/commands/link.js +0 -1121
- package/dist/commands/neon_auth.js +0 -1028
- package/dist/commands/operations.js +0 -28
- package/dist/commands/orgs.js +0 -24
- package/dist/commands/projects.js +0 -413
- package/dist/commands/psql.js +0 -62
- package/dist/commands/roles.js +0 -65
- package/dist/commands/schema_diff.js +0 -151
- package/dist/commands/set_context.js +0 -29
- package/dist/commands/snapshots.js +0 -455
- package/dist/commands/status.js +0 -40
- package/dist/commands/user.js +0 -15
- package/dist/commands/vpc_endpoints.js +0 -134
- package/dist/config.js +0 -11
- package/dist/config_format.js +0 -72
- package/dist/context.js +0 -231
- package/dist/current_branch_fast_path.js +0 -55
- package/dist/dev/env.js +0 -244
- package/dist/dev/functions.js +0 -70
- package/dist/dev/inputs.js +0 -63
- package/dist/dev/runtime.js +0 -146
- package/dist/env.js +0 -36
- package/dist/env_file.js +0 -159
- package/dist/errors.js +0 -80
- package/dist/functions_api.js +0 -48
- package/dist/help.js +0 -146
- package/dist/index.js +0 -240
- package/dist/log.js +0 -18
- package/dist/parameters.gen.js +0 -480
- package/dist/pkg.js +0 -25
- package/dist/psql/cli.js +0 -53
- package/dist/psql/command/cmd_cond.js +0 -437
- package/dist/psql/command/cmd_connect.js +0 -820
- package/dist/psql/command/cmd_copy.js +0 -1035
- package/dist/psql/command/cmd_describe.js +0 -1815
- package/dist/psql/command/cmd_format.js +0 -948
- package/dist/psql/command/cmd_io.js +0 -2193
- package/dist/psql/command/cmd_lo.js +0 -393
- package/dist/psql/command/cmd_meta.js +0 -969
- package/dist/psql/command/cmd_misc.js +0 -187
- package/dist/psql/command/cmd_pipeline.js +0 -1148
- package/dist/psql/command/cmd_restrict.js +0 -171
- package/dist/psql/command/cmd_show.js +0 -766
- package/dist/psql/command/dispatch.js +0 -343
- package/dist/psql/command/inputQueue.js +0 -42
- package/dist/psql/command/shared.js +0 -71
- package/dist/psql/complete/filenames.js +0 -139
- package/dist/psql/complete/index.js +0 -104
- package/dist/psql/complete/matcher.js +0 -315
- package/dist/psql/complete/psqlVars.js +0 -249
- package/dist/psql/complete/queries.js +0 -493
- package/dist/psql/complete/rules.js +0 -2424
- package/dist/psql/core/common.js +0 -1253
- package/dist/psql/core/help.js +0 -576
- package/dist/psql/core/mainloop.js +0 -1360
- package/dist/psql/core/prompt.js +0 -439
- package/dist/psql/core/settings.js +0 -686
- package/dist/psql/core/sqlHelp.js +0 -1066
- package/dist/psql/core/startup.js +0 -846
- package/dist/psql/core/syncVars.js +0 -116
- package/dist/psql/core/variables.js +0 -287
- package/dist/psql/describe/formatters.js +0 -1330
- package/dist/psql/describe/processNamePattern.js +0 -270
- package/dist/psql/describe/queries.js +0 -2452
- package/dist/psql/describe/versionGate.js +0 -44
- package/dist/psql/index.js +0 -2030
- package/dist/psql/io/history.js +0 -299
- package/dist/psql/io/input.js +0 -120
- package/dist/psql/io/lineEditor/buffer.js +0 -325
- package/dist/psql/io/lineEditor/complete.js +0 -227
- package/dist/psql/io/lineEditor/filename.js +0 -159
- package/dist/psql/io/lineEditor/index.js +0 -893
- package/dist/psql/io/lineEditor/keymap.js +0 -745
- package/dist/psql/io/lineEditor/vt100.js +0 -363
- package/dist/psql/io/pgpass.js +0 -202
- package/dist/psql/io/pgservice.js +0 -194
- package/dist/psql/io/psqlrc.js +0 -422
- package/dist/psql/print/aligned.js +0 -1748
- package/dist/psql/print/asciidoc.js +0 -230
- package/dist/psql/print/crosstab.js +0 -463
- package/dist/psql/print/csv.js +0 -76
- package/dist/psql/print/html.js +0 -240
- package/dist/psql/print/json.js +0 -96
- package/dist/psql/print/latex.js +0 -379
- package/dist/psql/print/pager.js +0 -267
- package/dist/psql/print/troff.js +0 -240
- package/dist/psql/print/unaligned.js +0 -99
- package/dist/psql/print/units.js +0 -188
- package/dist/psql/scanner/slash.js +0 -515
- package/dist/psql/scanner/sql.js +0 -914
- package/dist/psql/scanner/stringutils.js +0 -394
- package/dist/psql/types/backslash.js +0 -1
- package/dist/psql/types/connection.js +0 -1
- package/dist/psql/types/index.js +0 -7
- package/dist/psql/types/printer.js +0 -1
- package/dist/psql/types/repl.js +0 -1
- package/dist/psql/types/scanner.js +0 -24
- package/dist/psql/types/settings.js +0 -1
- package/dist/psql/types/variables.js +0 -1
- package/dist/psql/wire/connection.js +0 -2857
- package/dist/psql/wire/copy.js +0 -108
- package/dist/psql/wire/notify.js +0 -59
- package/dist/psql/wire/pipeline.js +0 -521
- package/dist/psql/wire/protocol.js +0 -466
- package/dist/psql/wire/sasl.js +0 -294
- package/dist/psql/wire/tls.js +0 -602
- package/dist/storage_api.js +0 -147
- package/dist/test_utils/fixtures.js +0 -121
- package/dist/test_utils/oauth_server.js +0 -9
- package/dist/types.js +0 -1
- package/dist/utils/ai_gateway_notice.js +0 -180
- package/dist/utils/api_enums.js +0 -33
- package/dist/utils/auth.js +0 -5
- package/dist/utils/branch_notice.js +0 -22
- package/dist/utils/branch_picker.js +0 -103
- package/dist/utils/compute_units.js +0 -28
- package/dist/utils/config_diff.js +0 -185
- package/dist/utils/enrichers.js +0 -161
- package/dist/utils/esbuild.js +0 -158
- package/dist/utils/formats.js +0 -18
- package/dist/utils/git_diff.js +0 -90
- package/dist/utils/inspect_db.js +0 -153
- package/dist/utils/inspect_queries.js +0 -372
- package/dist/utils/middlewares.js +0 -20
- package/dist/utils/openapi.js +0 -114
- package/dist/utils/package_manager.js +0 -68
- package/dist/utils/point_in_time.js +0 -56
- package/dist/utils/psql.js +0 -120
- package/dist/utils/string.js +0 -5
- package/dist/utils/ui.js +0 -59
- package/dist/utils/zip.js +0 -4
- package/dist/writer.js +0 -97
package/dist/utils/inspect_db.js
DELETED
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
import { parseConnectionUri } from "../psql/index.js";
|
|
2
|
-
import { PgConnection } from "../psql/wire/connection.js";
|
|
3
|
-
import { EndpointType } from "./api_enums.js";
|
|
4
|
-
import { branchIdFromProps } from "./enrichers.js";
|
|
5
|
-
import { parsePITBranch } from "./point_in_time.js";
|
|
6
|
-
export const SSL_MODES = [
|
|
7
|
-
"require",
|
|
8
|
-
"verify-ca",
|
|
9
|
-
"verify-full",
|
|
10
|
-
"omit",
|
|
11
|
-
];
|
|
12
|
-
/**
|
|
13
|
-
* Resolve a branch's live Postgres connection details via the Neon API
|
|
14
|
-
* (endpoint → role → password → database → URL, honoring point-in-time,
|
|
15
|
-
* pooling, prisma tuning, and SSL mode). Mirrors how `connection-string`
|
|
16
|
-
* resolves a branch so `inspect db` connects the same way.
|
|
17
|
-
*/
|
|
18
|
-
export const resolveConnectionUri = async (props) => {
|
|
19
|
-
const projectId = props.projectId;
|
|
20
|
-
const parsedPIT = props.branch
|
|
21
|
-
? parsePITBranch(props.branch)
|
|
22
|
-
: { tag: "head", branch: "" };
|
|
23
|
-
if (props.branch) {
|
|
24
|
-
props.branch = parsedPIT.branch;
|
|
25
|
-
}
|
|
26
|
-
const branchId = await branchIdFromProps(props);
|
|
27
|
-
const { data: { endpoints }, } = await props.apiClient.listProjectBranchEndpoints(projectId, branchId);
|
|
28
|
-
const matchEndpointType = props.endpointType ?? EndpointType.ReadWrite;
|
|
29
|
-
let endpoint = endpoints.find((e) => e.type === matchEndpointType);
|
|
30
|
-
if (!endpoint && props.endpointType == null) {
|
|
31
|
-
endpoint = endpoints[0];
|
|
32
|
-
}
|
|
33
|
-
if (!endpoint) {
|
|
34
|
-
throw new Error(`No ${props.endpointType ?? ""} endpoint found for the branch: ${branchId}`);
|
|
35
|
-
}
|
|
36
|
-
const role = props.roleName ||
|
|
37
|
-
(await props.apiClient
|
|
38
|
-
.listProjectBranchRoles(projectId, branchId)
|
|
39
|
-
.then(({ data }) => {
|
|
40
|
-
if (data.roles.length === 0) {
|
|
41
|
-
throw new Error(`No roles found for the branch: ${branchId}`);
|
|
42
|
-
}
|
|
43
|
-
if (data.roles.length === 1) {
|
|
44
|
-
return data.roles[0].name;
|
|
45
|
-
}
|
|
46
|
-
throw new Error(`Multiple roles found for the branch, please provide one with the --role-name option: ${data.roles
|
|
47
|
-
.map((r) => r.name)
|
|
48
|
-
.join(", ")}`);
|
|
49
|
-
}));
|
|
50
|
-
const { data: { databases: branchDatabases }, } = await props.apiClient.listProjectBranchDatabases(projectId, branchId);
|
|
51
|
-
const database = props.databaseName ||
|
|
52
|
-
(() => {
|
|
53
|
-
if (branchDatabases.length === 0) {
|
|
54
|
-
throw new Error(`No databases found for the branch: ${branchId}`);
|
|
55
|
-
}
|
|
56
|
-
if (branchDatabases.length === 1) {
|
|
57
|
-
return branchDatabases[0].name;
|
|
58
|
-
}
|
|
59
|
-
throw new Error(`Multiple databases found for the branch, please provide one with the --database-name option: ${branchDatabases
|
|
60
|
-
.map((d) => d.name)
|
|
61
|
-
.join(", ")}`);
|
|
62
|
-
})();
|
|
63
|
-
if (!branchDatabases.find((d) => d.name === database)) {
|
|
64
|
-
throw new Error(`Database not found: ${database}`);
|
|
65
|
-
}
|
|
66
|
-
const { data: { password }, } = await props.apiClient.getProjectBranchRolePassword(props.projectId, endpoint.branch_id, role);
|
|
67
|
-
let host = props.pooled
|
|
68
|
-
? endpoint.host.replace(endpoint.id, `${endpoint.id}-pooler`)
|
|
69
|
-
: endpoint.host;
|
|
70
|
-
if (parsedPIT.tag !== "head") {
|
|
71
|
-
host = endpoint.host.replace(endpoint.id, endpoint.branch_id);
|
|
72
|
-
}
|
|
73
|
-
const connectionString = new URL(`postgresql://${host}`);
|
|
74
|
-
connectionString.pathname = database;
|
|
75
|
-
connectionString.username = role;
|
|
76
|
-
connectionString.password = password;
|
|
77
|
-
if (props.prisma) {
|
|
78
|
-
connectionString.searchParams.set("connect_timeout", "30");
|
|
79
|
-
if (props.pooled) {
|
|
80
|
-
connectionString.searchParams.set("pool_timeout", "30");
|
|
81
|
-
connectionString.searchParams.set("pgbouncer", "true");
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
const ssl = props.ssl ?? "require";
|
|
85
|
-
if (ssl !== "omit") {
|
|
86
|
-
connectionString.searchParams.set("sslmode", ssl);
|
|
87
|
-
connectionString.searchParams.set("channel_binding", "require");
|
|
88
|
-
}
|
|
89
|
-
if (parsedPIT.tag === "lsn") {
|
|
90
|
-
connectionString.searchParams.set("options", `neon_lsn:${parsedPIT.lsn}`);
|
|
91
|
-
}
|
|
92
|
-
else if (parsedPIT.tag === "timestamp") {
|
|
93
|
-
connectionString.searchParams.set("options", `neon_timestamp:${parsedPIT.timestamp}`);
|
|
94
|
-
}
|
|
95
|
-
return {
|
|
96
|
-
connectionUri: connectionString.toString(),
|
|
97
|
-
host,
|
|
98
|
-
role,
|
|
99
|
-
password,
|
|
100
|
-
database,
|
|
101
|
-
options: connectionString.searchParams.toString(),
|
|
102
|
-
};
|
|
103
|
-
};
|
|
104
|
-
const rowsFrom = (result) => {
|
|
105
|
-
const names = result.fields.map((f) => f.name);
|
|
106
|
-
return result.rows.map((row) => {
|
|
107
|
-
const obj = {};
|
|
108
|
-
names.forEach((name, i) => {
|
|
109
|
-
obj[name] = row[i];
|
|
110
|
-
});
|
|
111
|
-
return obj;
|
|
112
|
-
});
|
|
113
|
-
};
|
|
114
|
-
/**
|
|
115
|
-
* Connect to `connectionUri` with the embedded wire client, run a single
|
|
116
|
-
* read-only `sql`, and return the rows as objects keyed by column name (so the
|
|
117
|
-
* shared `writer` can render them by field). The connection is always closed,
|
|
118
|
-
* even on error.
|
|
119
|
-
*/
|
|
120
|
-
export const runInspectQuery = async (connectionUri, sql, options = {}) => {
|
|
121
|
-
const opts = parseConnectionUri(connectionUri);
|
|
122
|
-
let connection;
|
|
123
|
-
try {
|
|
124
|
-
connection = await PgConnection.connect(opts);
|
|
125
|
-
}
|
|
126
|
-
catch (err) {
|
|
127
|
-
// Give the failure a Postgres-specific message. The raw socket error
|
|
128
|
-
// (e.g. ECONNREFUSED) would otherwise be mislabeled by the top-level
|
|
129
|
-
// `isNetworkError` handler as a "Could not reach the Neon API" hint —
|
|
130
|
-
// misleading for `inspect db`, which talks to Postgres, not the API.
|
|
131
|
-
// We intentionally do not chain `cause` so that classifier can't match
|
|
132
|
-
// the socket `code`; the underlying reason is preserved in the message.
|
|
133
|
-
const reason = err instanceof Error ? err.message : String(err);
|
|
134
|
-
throw new Error(`Could not connect to Postgres at ${opts.host}:${opts.port}: ${reason}`);
|
|
135
|
-
}
|
|
136
|
-
try {
|
|
137
|
-
const ext = options.requiresExtension;
|
|
138
|
-
if (ext) {
|
|
139
|
-
// `ext` is an internal constant from INSPECT_QUERIES, never user input,
|
|
140
|
-
// so interpolating it into the literal is safe.
|
|
141
|
-
const check = await connection.query(`SELECT 1 FROM pg_extension WHERE extname = '${ext}';`);
|
|
142
|
-
if (check.rows.length === 0) {
|
|
143
|
-
throw new Error(`This query needs the "${ext}" extension, which is not installed. ` +
|
|
144
|
-
`Enable it with: CREATE EXTENSION ${ext};`);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
const result = await connection.query(sql);
|
|
148
|
-
return rowsFrom(result);
|
|
149
|
-
}
|
|
150
|
-
finally {
|
|
151
|
-
await connection.close();
|
|
152
|
-
}
|
|
153
|
-
};
|
|
@@ -1,372 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Diagnostic SQL for `neon inspect db`. Each entry is a read-only query against
|
|
3
|
-
* Postgres' own statistics/catalog views plus the output columns (in display
|
|
4
|
-
* order) the table/json/yaml formatter should show.
|
|
5
|
-
*
|
|
6
|
-
* Most queries need no extra extension. The few that do (currently `outliers`
|
|
7
|
-
* and `calls`, which read `pg_stat_statements`) declare it via
|
|
8
|
-
* `requiresExtension`; the runner checks for it and fails with an actionable
|
|
9
|
-
* hint when it's missing.
|
|
10
|
-
*/
|
|
11
|
-
export const INSPECT_QUERIES = {
|
|
12
|
-
"table-sizes": {
|
|
13
|
-
describe: "Size of each table (including TOAST), largest first (pg_table_size)",
|
|
14
|
-
fields: ["schema", "name", "size"],
|
|
15
|
-
emptyMessage: "No user tables found.",
|
|
16
|
-
sql: /* sql */ `
|
|
17
|
-
SELECT
|
|
18
|
-
n.nspname AS schema,
|
|
19
|
-
c.relname AS name,
|
|
20
|
-
pg_size_pretty(pg_table_size(c.oid)) AS size
|
|
21
|
-
FROM pg_class c
|
|
22
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
23
|
-
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
24
|
-
AND n.nspname !~ '^pg_toast'
|
|
25
|
-
AND c.relkind = 'r'
|
|
26
|
-
ORDER BY pg_table_size(c.oid) DESC;
|
|
27
|
-
`,
|
|
28
|
-
},
|
|
29
|
-
"index-sizes": {
|
|
30
|
-
describe: "Size of each index, largest first (pg_relation_size)",
|
|
31
|
-
fields: ["schema", "name", "size"],
|
|
32
|
-
emptyMessage: "No indexes found.",
|
|
33
|
-
sql: /* sql */ `
|
|
34
|
-
SELECT
|
|
35
|
-
n.nspname AS schema,
|
|
36
|
-
c.relname AS name,
|
|
37
|
-
pg_size_pretty(pg_relation_size(c.oid)) AS size
|
|
38
|
-
FROM pg_class c
|
|
39
|
-
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
40
|
-
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
41
|
-
AND n.nspname !~ '^pg_toast'
|
|
42
|
-
AND c.relkind = 'i'
|
|
43
|
-
ORDER BY pg_relation_size(c.oid) DESC;
|
|
44
|
-
`,
|
|
45
|
-
},
|
|
46
|
-
"unused-indexes": {
|
|
47
|
-
describe: "Non-unique indexes with few scans — candidates for removal (pg_stat_user_indexes)",
|
|
48
|
-
fields: ["table", "index", "index_size", "index_scans"],
|
|
49
|
-
emptyMessage: "No unused indexes detected.",
|
|
50
|
-
sql: /* sql */ `
|
|
51
|
-
SELECT
|
|
52
|
-
s.schemaname || '.' || s.relname AS "table",
|
|
53
|
-
s.indexrelname AS index,
|
|
54
|
-
pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
|
|
55
|
-
s.idx_scan AS index_scans
|
|
56
|
-
FROM pg_stat_user_indexes s
|
|
57
|
-
JOIN pg_index i ON i.indexrelid = s.indexrelid
|
|
58
|
-
WHERE NOT i.indisunique
|
|
59
|
-
AND NOT i.indisprimary
|
|
60
|
-
AND s.idx_scan < 50
|
|
61
|
-
ORDER BY pg_relation_size(s.indexrelid) DESC, s.idx_scan ASC;
|
|
62
|
-
`,
|
|
63
|
-
},
|
|
64
|
-
"seq-scans": {
|
|
65
|
-
describe: "Number of sequential scans recorded against each table (pg_stat_user_tables)",
|
|
66
|
-
fields: ["schema", "name", "count"],
|
|
67
|
-
emptyMessage: "No user tables found.",
|
|
68
|
-
sql: /* sql */ `
|
|
69
|
-
SELECT
|
|
70
|
-
schemaname AS schema,
|
|
71
|
-
relname AS name,
|
|
72
|
-
seq_scan AS count
|
|
73
|
-
FROM pg_stat_user_tables
|
|
74
|
-
ORDER BY seq_scan DESC;
|
|
75
|
-
`,
|
|
76
|
-
},
|
|
77
|
-
"long-running-queries": {
|
|
78
|
-
describe: "Queries running longer than 5 minutes (pg_stat_activity)",
|
|
79
|
-
fields: ["pid", "duration", "state", "query"],
|
|
80
|
-
emptyMessage: "No long-running queries.",
|
|
81
|
-
sql: /* sql */ `
|
|
82
|
-
SELECT
|
|
83
|
-
pid,
|
|
84
|
-
(now() - query_start)::text AS duration,
|
|
85
|
-
state,
|
|
86
|
-
query
|
|
87
|
-
FROM pg_stat_activity
|
|
88
|
-
WHERE state <> 'idle'
|
|
89
|
-
AND query NOT ILIKE '%pg_stat_activity%'
|
|
90
|
-
AND now() - query_start > interval '5 minutes'
|
|
91
|
-
ORDER BY now() - query_start DESC;
|
|
92
|
-
`,
|
|
93
|
-
},
|
|
94
|
-
locks: {
|
|
95
|
-
describe: "Locks held with the acquiring query and its age (pg_locks + pg_stat_activity)",
|
|
96
|
-
fields: [
|
|
97
|
-
"pid",
|
|
98
|
-
"relname",
|
|
99
|
-
"mode",
|
|
100
|
-
"locktype",
|
|
101
|
-
"granted",
|
|
102
|
-
"age",
|
|
103
|
-
"query",
|
|
104
|
-
],
|
|
105
|
-
emptyMessage: "No locks held.",
|
|
106
|
-
sql: /* sql */ `
|
|
107
|
-
SELECT
|
|
108
|
-
a.pid,
|
|
109
|
-
c.relname,
|
|
110
|
-
l.mode,
|
|
111
|
-
l.locktype,
|
|
112
|
-
l.granted,
|
|
113
|
-
(now() - a.query_start)::text AS age,
|
|
114
|
-
a.query
|
|
115
|
-
FROM pg_locks l
|
|
116
|
-
JOIN pg_stat_activity a ON a.pid = l.pid
|
|
117
|
-
LEFT JOIN pg_class c ON c.oid = l.relation
|
|
118
|
-
WHERE a.query <> '<insufficient privilege>'
|
|
119
|
-
AND l.pid <> pg_backend_pid()
|
|
120
|
-
ORDER BY a.query_start;
|
|
121
|
-
`,
|
|
122
|
-
},
|
|
123
|
-
outliers: {
|
|
124
|
-
describe: "Queries taking the most cumulative execution time (needs pg_stat_statements)",
|
|
125
|
-
fields: ["total_exec_time", "prop_exec_time", "ncalls", "query"],
|
|
126
|
-
emptyMessage: "No statements recorded yet.",
|
|
127
|
-
requiresExtension: "pg_stat_statements",
|
|
128
|
-
sql: /* sql */ `
|
|
129
|
-
SELECT
|
|
130
|
-
(interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
|
|
131
|
-
to_char(
|
|
132
|
-
(total_exec_time / nullif(sum(total_exec_time) OVER (), 0)) * 100,
|
|
133
|
-
'FM990D0'
|
|
134
|
-
) || '%' AS prop_exec_time,
|
|
135
|
-
calls AS ncalls,
|
|
136
|
-
query
|
|
137
|
-
FROM pg_stat_statements s
|
|
138
|
-
JOIN pg_database d ON d.oid = s.dbid
|
|
139
|
-
WHERE d.datname = current_database()
|
|
140
|
-
ORDER BY total_exec_time DESC
|
|
141
|
-
LIMIT 25;
|
|
142
|
-
`,
|
|
143
|
-
},
|
|
144
|
-
calls: {
|
|
145
|
-
describe: "Most frequently called queries (needs pg_stat_statements)",
|
|
146
|
-
fields: ["ncalls", "total_exec_time", "prop_exec_time", "query"],
|
|
147
|
-
emptyMessage: "No statements recorded yet.",
|
|
148
|
-
requiresExtension: "pg_stat_statements",
|
|
149
|
-
sql: /* sql */ `
|
|
150
|
-
SELECT
|
|
151
|
-
calls AS ncalls,
|
|
152
|
-
(interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
|
|
153
|
-
to_char(
|
|
154
|
-
(calls / nullif(sum(calls) OVER (), 0)::float) * 100,
|
|
155
|
-
'FM990D0'
|
|
156
|
-
) || '%' AS prop_exec_time,
|
|
157
|
-
query
|
|
158
|
-
FROM pg_stat_statements s
|
|
159
|
-
JOIN pg_database d ON d.oid = s.dbid
|
|
160
|
-
WHERE d.datname = current_database()
|
|
161
|
-
ORDER BY calls DESC
|
|
162
|
-
LIMIT 25;
|
|
163
|
-
`,
|
|
164
|
-
},
|
|
165
|
-
"lfc-hit-rate": {
|
|
166
|
-
describe: "Local File Cache hit rate (needs neon extension)",
|
|
167
|
-
fields: ["name", "ratio"],
|
|
168
|
-
emptyMessage: "No LFC stats available.",
|
|
169
|
-
requiresExtension: "neon",
|
|
170
|
-
// Read the hit/miss counters from neon_get_lfc_stats() rather than the
|
|
171
|
-
// neon_lfc_stats / neon_stat_file_cache views: those views are owned by
|
|
172
|
-
// cloud_admin and are not always readable by end-user roles, whereas the
|
|
173
|
-
// function is EXECUTE-able by the default role.
|
|
174
|
-
sql: /* sql */ `
|
|
175
|
-
WITH lfc AS (
|
|
176
|
-
SELECT lfc_key AS k, lfc_value AS v
|
|
177
|
-
FROM neon_get_lfc_stats() t(lfc_key text, lfc_value bigint)
|
|
178
|
-
)
|
|
179
|
-
SELECT
|
|
180
|
-
'lfc hit rate' AS name,
|
|
181
|
-
max(v) FILTER (WHERE k = 'file_cache_hits')::float
|
|
182
|
-
/ nullif(
|
|
183
|
-
max(v) FILTER (WHERE k = 'file_cache_hits')
|
|
184
|
-
+ max(v) FILTER (WHERE k = 'file_cache_misses'),
|
|
185
|
-
0
|
|
186
|
-
) AS ratio
|
|
187
|
-
FROM lfc;
|
|
188
|
-
`,
|
|
189
|
-
},
|
|
190
|
-
"working-set": {
|
|
191
|
-
describe: "Estimated working set vs LFC size (needs neon extension)",
|
|
192
|
-
fields: ["window", "working_set", "lfc_size", "exceeds_lfc"],
|
|
193
|
-
emptyMessage: "No working-set estimate available.",
|
|
194
|
-
requiresExtension: "neon",
|
|
195
|
-
sql: /* sql */ `
|
|
196
|
-
SELECT
|
|
197
|
-
w.window,
|
|
198
|
-
pg_size_pretty(w.working_set_bytes) AS working_set,
|
|
199
|
-
pg_size_pretty(pg_size_bytes(current_setting('neon.file_cache_size_limit'))) AS lfc_size,
|
|
200
|
-
CASE
|
|
201
|
-
WHEN w.working_set_bytes
|
|
202
|
-
> pg_size_bytes(current_setting('neon.file_cache_size_limit'))
|
|
203
|
-
THEN 'yes' ELSE 'no'
|
|
204
|
-
END AS exceeds_lfc
|
|
205
|
-
FROM (
|
|
206
|
-
SELECT
|
|
207
|
-
x AS window,
|
|
208
|
-
approximate_working_set_size_seconds(
|
|
209
|
-
extract('epoch' from x::interval)::int
|
|
210
|
-
)::bigint * 8192 AS working_set_bytes
|
|
211
|
-
FROM (VALUES ('1m'), ('5m'), ('15m'), ('1h')) AS t(x)
|
|
212
|
-
) w
|
|
213
|
-
ORDER BY working_set_bytes;
|
|
214
|
-
`,
|
|
215
|
-
},
|
|
216
|
-
"vacuum-stats": {
|
|
217
|
-
describe: "Autovacuum status per table: last (auto)vacuum, dead tuples, threshold",
|
|
218
|
-
fields: [
|
|
219
|
-
"schema",
|
|
220
|
-
"table",
|
|
221
|
-
"last_vacuum",
|
|
222
|
-
"last_autovacuum",
|
|
223
|
-
"rowcount",
|
|
224
|
-
"dead_rowcount",
|
|
225
|
-
"expect_autovacuum",
|
|
226
|
-
],
|
|
227
|
-
emptyMessage: "No user tables found.",
|
|
228
|
-
sql: /* sql */ `
|
|
229
|
-
SELECT
|
|
230
|
-
n.nspname AS schema,
|
|
231
|
-
c.relname AS "table",
|
|
232
|
-
to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI') AS last_vacuum,
|
|
233
|
-
to_char(psut.last_autovacuum, 'YYYY-MM-DD HH24:MI') AS last_autovacuum,
|
|
234
|
-
c.reltuples::bigint AS rowcount,
|
|
235
|
-
psut.n_dead_tup AS dead_rowcount,
|
|
236
|
-
CASE
|
|
237
|
-
WHEN current_setting('autovacuum_vacuum_threshold')::bigint
|
|
238
|
-
+ current_setting('autovacuum_vacuum_scale_factor')::numeric
|
|
239
|
-
* c.reltuples
|
|
240
|
-
< psut.n_dead_tup
|
|
241
|
-
THEN 'yes'
|
|
242
|
-
ELSE 'no'
|
|
243
|
-
END AS expect_autovacuum
|
|
244
|
-
FROM pg_stat_user_tables psut
|
|
245
|
-
JOIN pg_class c ON psut.relid = c.oid
|
|
246
|
-
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
247
|
-
ORDER BY psut.n_dead_tup DESC;
|
|
248
|
-
`,
|
|
249
|
-
},
|
|
250
|
-
bloat: {
|
|
251
|
-
describe: "Estimated table/index bloat (statistical estimate, no extension needed)",
|
|
252
|
-
fields: ["type", "schema", "object_name", "bloat", "waste"],
|
|
253
|
-
emptyMessage: "No bloat estimate available.",
|
|
254
|
-
sql: /* sql */ `
|
|
255
|
-
WITH constants AS (
|
|
256
|
-
SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 8 AS ma
|
|
257
|
-
),
|
|
258
|
-
bloat_info AS (
|
|
259
|
-
SELECT
|
|
260
|
-
ma, bs, schemaname, tablename,
|
|
261
|
-
(datawidth + (hdr + ma - (CASE WHEN hdr % ma = 0 THEN ma ELSE hdr % ma END)))::numeric AS datahdr,
|
|
262
|
-
(maxfracsum * (nullhdr + ma - (CASE WHEN nullhdr % ma = 0 THEN ma ELSE nullhdr % ma END))) AS nullhdr2
|
|
263
|
-
FROM (
|
|
264
|
-
SELECT
|
|
265
|
-
schemaname, tablename, hdr, ma, bs,
|
|
266
|
-
SUM((1 - null_frac) * avg_width) AS datawidth,
|
|
267
|
-
MAX(null_frac) AS maxfracsum,
|
|
268
|
-
hdr + (
|
|
269
|
-
SELECT 1 + count(*) / 8
|
|
270
|
-
FROM pg_stats s2
|
|
271
|
-
WHERE null_frac <> 0
|
|
272
|
-
AND s2.schemaname = s.schemaname
|
|
273
|
-
AND s2.tablename = s.tablename
|
|
274
|
-
) AS nullhdr
|
|
275
|
-
FROM pg_stats s, constants
|
|
276
|
-
GROUP BY 1, 2, 3, 4, 5
|
|
277
|
-
) AS foo
|
|
278
|
-
),
|
|
279
|
-
table_bloat AS (
|
|
280
|
-
SELECT
|
|
281
|
-
schemaname, tablename, cc.relpages, bs,
|
|
282
|
-
CEIL((cc.reltuples * ((datahdr + ma
|
|
283
|
-
- (CASE WHEN datahdr % ma = 0 THEN ma ELSE datahdr % ma END))
|
|
284
|
-
+ nullhdr2 + 4)) / (bs - 20::float)) AS otta
|
|
285
|
-
FROM bloat_info
|
|
286
|
-
JOIN pg_class cc ON cc.relname = bloat_info.tablename
|
|
287
|
-
JOIN pg_namespace nn ON cc.relnamespace = nn.oid
|
|
288
|
-
AND nn.nspname = bloat_info.schemaname
|
|
289
|
-
AND nn.nspname NOT IN ('information_schema', 'pg_catalog')
|
|
290
|
-
WHERE cc.relkind = 'r'
|
|
291
|
-
)
|
|
292
|
-
SELECT
|
|
293
|
-
'table' AS type,
|
|
294
|
-
schemaname AS schema,
|
|
295
|
-
tablename AS object_name,
|
|
296
|
-
round(CASE WHEN otta = 0 THEN 0.0 ELSE relpages::numeric / otta::numeric END, 1) AS bloat,
|
|
297
|
-
pg_size_pretty(
|
|
298
|
-
(CASE WHEN relpages < otta THEN 0 ELSE (bs * (relpages - otta))::bigint END)
|
|
299
|
-
) AS waste
|
|
300
|
-
FROM table_bloat
|
|
301
|
-
ORDER BY (CASE WHEN relpages < otta THEN 0 ELSE bs * (relpages - otta) END) DESC
|
|
302
|
-
LIMIT 25;
|
|
303
|
-
`,
|
|
304
|
-
},
|
|
305
|
-
"replication-slots": {
|
|
306
|
-
describe: "Replication slots: kind, status, client, restart/confirmed-flush LSNs, and lag (pg_replication_slots + pg_stat_replication)",
|
|
307
|
-
fields: [
|
|
308
|
-
"slot_name",
|
|
309
|
-
"slot_type",
|
|
310
|
-
"slot_kind",
|
|
311
|
-
"status",
|
|
312
|
-
"client_addr",
|
|
313
|
-
"restart_lsn",
|
|
314
|
-
"confirmed_flush_lsn",
|
|
315
|
-
"replication_lag",
|
|
316
|
-
],
|
|
317
|
-
emptyMessage: "No replication slots found.",
|
|
318
|
-
sql: /* sql */ `
|
|
319
|
-
SELECT
|
|
320
|
-
s.slot_name,
|
|
321
|
-
s.slot_type,
|
|
322
|
-
CASE
|
|
323
|
-
WHEN s.slot_type = 'physical' THEN 'physical'
|
|
324
|
-
WHEN s.slot_name LIKE 'pg_%_sync_%' THEN 'initial-sync (tablesync)'
|
|
325
|
-
WHEN s.slot_type = 'logical' THEN 'CDC (apply)'
|
|
326
|
-
ELSE 'other'
|
|
327
|
-
END AS slot_kind,
|
|
328
|
-
CASE
|
|
329
|
-
WHEN r.state IS NOT NULL THEN r.state
|
|
330
|
-
WHEN s.active THEN 'active (no walsender)'
|
|
331
|
-
ELSE 'inactive'
|
|
332
|
-
END AS status,
|
|
333
|
-
r.client_addr,
|
|
334
|
-
s.restart_lsn,
|
|
335
|
-
s.confirmed_flush_lsn,
|
|
336
|
-
pg_size_pretty(
|
|
337
|
-
pg_wal_lsn_diff(
|
|
338
|
-
CASE
|
|
339
|
-
WHEN pg_is_in_recovery() THEN pg_last_wal_receive_lsn()
|
|
340
|
-
ELSE pg_current_wal_lsn()
|
|
341
|
-
END,
|
|
342
|
-
COALESCE(s.confirmed_flush_lsn, s.restart_lsn)
|
|
343
|
-
)
|
|
344
|
-
) AS replication_lag
|
|
345
|
-
FROM pg_replication_slots s
|
|
346
|
-
LEFT JOIN pg_stat_replication r ON r.pid = s.active_pid
|
|
347
|
-
ORDER BY s.slot_name;
|
|
348
|
-
`,
|
|
349
|
-
},
|
|
350
|
-
subscriptions: {
|
|
351
|
-
describe: "Per-table logical replication progress on this subscriber (pg_subscription_rel)",
|
|
352
|
-
fields: ["subscription", "table_name", "status", "lsn"],
|
|
353
|
-
emptyMessage: "No subscriptions found on this database.",
|
|
354
|
-
sql: /* sql */ `
|
|
355
|
-
SELECT
|
|
356
|
-
sub.subname AS subscription,
|
|
357
|
-
sr.srrelid::regclass::text AS table_name,
|
|
358
|
-
CASE sr.srsubstate
|
|
359
|
-
WHEN 'i' THEN 'initialize'
|
|
360
|
-
WHEN 'd' THEN 'data being copied'
|
|
361
|
-
WHEN 'f' THEN 'finished table copy'
|
|
362
|
-
WHEN 's' THEN 'synchronized'
|
|
363
|
-
WHEN 'r' THEN 'ready'
|
|
364
|
-
ELSE sr.srsubstate::text
|
|
365
|
-
END AS status,
|
|
366
|
-
sr.srsublsn AS lsn
|
|
367
|
-
FROM pg_subscription_rel sr
|
|
368
|
-
JOIN pg_subscription sub ON sub.oid = sr.srsubid
|
|
369
|
-
ORDER BY sub.subname, table_name;
|
|
370
|
-
`,
|
|
371
|
-
},
|
|
372
|
-
};
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* This middleware is needed to fill in the args for nested objects,
|
|
3
|
-
* so that required arguments would work
|
|
4
|
-
* otherwise yargs just throws an error
|
|
5
|
-
*/
|
|
6
|
-
export const fillInArgs = (args, currentArgs = args, acc = []) => {
|
|
7
|
-
Object.entries(currentArgs).forEach(([k, v]) => {
|
|
8
|
-
if (k === "_" || k === "--") {
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
// check if the value is an Object
|
|
12
|
-
if (typeof v === "object" && v !== null) {
|
|
13
|
-
fillInArgs(args, v, [...acc, k]);
|
|
14
|
-
}
|
|
15
|
-
else if (acc.length > 0) {
|
|
16
|
-
// if it's not an object, and we have a path, fill it in
|
|
17
|
-
args[acc.join(".") + "." + k] = v;
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
};
|
package/dist/utils/openapi.js
DELETED
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
// Lightweight loader for the Neon OpenAPI spec, used by `neon api --list` to
|
|
2
|
-
// enumerate the available routes. The `neon api <path>` request path does NOT
|
|
3
|
-
// depend on this module — it is a pure passthrough — so a stale or unreachable
|
|
4
|
-
// spec never blocks a real API call. Listing degrades gracefully: fresh cache →
|
|
5
|
-
// live fetch → stale cache → clear error.
|
|
6
|
-
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { dirname, join } from "node:path";
|
|
8
|
-
import { log } from "../log.js";
|
|
9
|
-
/** Public URL of the released Neon OpenAPI v2 spec. */
|
|
10
|
-
export const DEFAULT_SPEC_URL = "https://neon.com/api_spec/release/v2.json";
|
|
11
|
-
const CACHE_FILE = "openapi-spec.json";
|
|
12
|
-
const CACHE_TTL_MS = 24 * 60 * 60 * 1000;
|
|
13
|
-
const FETCH_TIMEOUT_MS = 10000;
|
|
14
|
-
const HTTP_METHODS = new Set([
|
|
15
|
-
"get",
|
|
16
|
-
"post",
|
|
17
|
-
"put",
|
|
18
|
-
"patch",
|
|
19
|
-
"delete",
|
|
20
|
-
"head",
|
|
21
|
-
"options",
|
|
22
|
-
]);
|
|
23
|
-
async function fetchSpec(url) {
|
|
24
|
-
const controller = new AbortController();
|
|
25
|
-
const timer = setTimeout(() => {
|
|
26
|
-
controller.abort();
|
|
27
|
-
}, FETCH_TIMEOUT_MS);
|
|
28
|
-
try {
|
|
29
|
-
const res = await fetch(url, {
|
|
30
|
-
signal: controller.signal,
|
|
31
|
-
headers: { Accept: "application/json" },
|
|
32
|
-
});
|
|
33
|
-
if (!res.ok) {
|
|
34
|
-
throw new Error(`Failed to fetch OpenAPI spec (${res.status} ${res.statusText})`);
|
|
35
|
-
}
|
|
36
|
-
return (await res.json());
|
|
37
|
-
}
|
|
38
|
-
finally {
|
|
39
|
-
clearTimeout(timer);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
function readCache(cachePath) {
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(readFileSync(cachePath, "utf8"));
|
|
45
|
-
}
|
|
46
|
-
catch {
|
|
47
|
-
return null;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
function writeCache(cachePath, data) {
|
|
51
|
-
try {
|
|
52
|
-
mkdirSync(dirname(cachePath), { recursive: true });
|
|
53
|
-
writeFileSync(cachePath, JSON.stringify(data));
|
|
54
|
-
}
|
|
55
|
-
catch (err) {
|
|
56
|
-
log.debug("Failed to cache OpenAPI spec: %s", err);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Load the Neon OpenAPI spec, preferring a fresh on-disk cache, then a live
|
|
61
|
-
* fetch (which refreshes the cache), then a stale cache as a last resort.
|
|
62
|
-
* Returns `null` when no spec can be obtained.
|
|
63
|
-
*/
|
|
64
|
-
export async function loadSpec(opts) {
|
|
65
|
-
const { configDir, specUrl, refresh } = opts;
|
|
66
|
-
const cachePath = join(configDir, CACHE_FILE);
|
|
67
|
-
if (!refresh) {
|
|
68
|
-
const cached = readCache(cachePath);
|
|
69
|
-
if (cached &&
|
|
70
|
-
cached.specUrl === specUrl &&
|
|
71
|
-
Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
|
|
72
|
-
log.debug("Using cached OpenAPI spec from %s", cachePath);
|
|
73
|
-
return cached.spec;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
try {
|
|
77
|
-
log.debug("Fetching OpenAPI spec from %s", specUrl);
|
|
78
|
-
const spec = await fetchSpec(specUrl);
|
|
79
|
-
writeCache(cachePath, { fetchedAt: Date.now(), specUrl, spec });
|
|
80
|
-
return spec;
|
|
81
|
-
}
|
|
82
|
-
catch (err) {
|
|
83
|
-
log.debug("Failed to fetch OpenAPI spec: %s", err);
|
|
84
|
-
const stale = readCache(cachePath);
|
|
85
|
-
if (stale && stale.specUrl === specUrl) {
|
|
86
|
-
log.debug("Falling back to stale cached OpenAPI spec");
|
|
87
|
-
return stale.spec;
|
|
88
|
-
}
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
/** Flatten a spec into a sorted list of routes (by path, then method). */
|
|
93
|
-
export function getEndpoints(spec) {
|
|
94
|
-
const endpoints = [];
|
|
95
|
-
for (const [path, item] of Object.entries(spec.paths ?? {})) {
|
|
96
|
-
for (const [method, op] of Object.entries(item)) {
|
|
97
|
-
if (!HTTP_METHODS.has(method.toLowerCase())) {
|
|
98
|
-
continue;
|
|
99
|
-
}
|
|
100
|
-
const operation = (op ?? {});
|
|
101
|
-
endpoints.push({
|
|
102
|
-
method: method.toUpperCase(),
|
|
103
|
-
path,
|
|
104
|
-
summary: operation.summary,
|
|
105
|
-
operationId: operation.operationId,
|
|
106
|
-
tags: operation.tags ?? [],
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
endpoints.sort((a, b) => a.path === b.path
|
|
111
|
-
? a.method.localeCompare(b.method)
|
|
112
|
-
: a.path.localeCompare(b.path));
|
|
113
|
-
return endpoints;
|
|
114
|
-
}
|