neonctl 2.37.0 → 2.38.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.
Files changed (160) hide show
  1. package/{LICENSE.md → LICENSE} +27 -3
  2. package/README.md +15 -700
  3. package/bin/cli.js +3 -0
  4. package/package.json +8 -75
  5. package/dist/analytics.js +0 -168
  6. package/dist/api.js +0 -720
  7. package/dist/auth.js +0 -125
  8. package/dist/callback.html +0 -51
  9. package/dist/cli.js +0 -9
  10. package/dist/commands/api.js +0 -278
  11. package/dist/commands/auth.js +0 -214
  12. package/dist/commands/bootstrap.js +0 -481
  13. package/dist/commands/branches.js +0 -488
  14. package/dist/commands/bucket.js +0 -549
  15. package/dist/commands/checkout.js +0 -321
  16. package/dist/commands/config.js +0 -626
  17. package/dist/commands/connection_string.js +0 -172
  18. package/dist/commands/data_api.js +0 -284
  19. package/dist/commands/databases.js +0 -82
  20. package/dist/commands/deploy.js +0 -26
  21. package/dist/commands/dev.js +0 -698
  22. package/dist/commands/diff.js +0 -222
  23. package/dist/commands/env.js +0 -189
  24. package/dist/commands/functions.js +0 -373
  25. package/dist/commands/index.js +0 -62
  26. package/dist/commands/init.js +0 -73
  27. package/dist/commands/inspect.js +0 -65
  28. package/dist/commands/ip_allow.js +0 -137
  29. package/dist/commands/link.js +0 -1121
  30. package/dist/commands/neon_auth.js +0 -1028
  31. package/dist/commands/operations.js +0 -28
  32. package/dist/commands/orgs.js +0 -24
  33. package/dist/commands/projects.js +0 -413
  34. package/dist/commands/psql.js +0 -62
  35. package/dist/commands/roles.js +0 -65
  36. package/dist/commands/schema_diff.js +0 -151
  37. package/dist/commands/set_context.js +0 -29
  38. package/dist/commands/snapshots.js +0 -455
  39. package/dist/commands/status.js +0 -40
  40. package/dist/commands/user.js +0 -15
  41. package/dist/commands/vpc_endpoints.js +0 -134
  42. package/dist/config.js +0 -11
  43. package/dist/config_format.js +0 -72
  44. package/dist/context.js +0 -231
  45. package/dist/current_branch_fast_path.js +0 -55
  46. package/dist/dev/env.js +0 -244
  47. package/dist/dev/functions.js +0 -70
  48. package/dist/dev/inputs.js +0 -63
  49. package/dist/dev/runtime.js +0 -146
  50. package/dist/env.js +0 -36
  51. package/dist/env_file.js +0 -159
  52. package/dist/errors.js +0 -80
  53. package/dist/functions_api.js +0 -48
  54. package/dist/help.js +0 -146
  55. package/dist/index.js +0 -240
  56. package/dist/log.js +0 -18
  57. package/dist/parameters.gen.js +0 -480
  58. package/dist/pkg.js +0 -25
  59. package/dist/psql/cli.js +0 -53
  60. package/dist/psql/command/cmd_cond.js +0 -437
  61. package/dist/psql/command/cmd_connect.js +0 -820
  62. package/dist/psql/command/cmd_copy.js +0 -1035
  63. package/dist/psql/command/cmd_describe.js +0 -1815
  64. package/dist/psql/command/cmd_format.js +0 -948
  65. package/dist/psql/command/cmd_io.js +0 -2193
  66. package/dist/psql/command/cmd_lo.js +0 -393
  67. package/dist/psql/command/cmd_meta.js +0 -969
  68. package/dist/psql/command/cmd_misc.js +0 -187
  69. package/dist/psql/command/cmd_pipeline.js +0 -1148
  70. package/dist/psql/command/cmd_restrict.js +0 -171
  71. package/dist/psql/command/cmd_show.js +0 -766
  72. package/dist/psql/command/dispatch.js +0 -343
  73. package/dist/psql/command/inputQueue.js +0 -42
  74. package/dist/psql/command/shared.js +0 -71
  75. package/dist/psql/complete/filenames.js +0 -139
  76. package/dist/psql/complete/index.js +0 -104
  77. package/dist/psql/complete/matcher.js +0 -315
  78. package/dist/psql/complete/psqlVars.js +0 -249
  79. package/dist/psql/complete/queries.js +0 -493
  80. package/dist/psql/complete/rules.js +0 -2424
  81. package/dist/psql/core/common.js +0 -1253
  82. package/dist/psql/core/help.js +0 -576
  83. package/dist/psql/core/mainloop.js +0 -1360
  84. package/dist/psql/core/prompt.js +0 -439
  85. package/dist/psql/core/settings.js +0 -686
  86. package/dist/psql/core/sqlHelp.js +0 -1066
  87. package/dist/psql/core/startup.js +0 -846
  88. package/dist/psql/core/syncVars.js +0 -116
  89. package/dist/psql/core/variables.js +0 -287
  90. package/dist/psql/describe/formatters.js +0 -1330
  91. package/dist/psql/describe/processNamePattern.js +0 -270
  92. package/dist/psql/describe/queries.js +0 -2452
  93. package/dist/psql/describe/versionGate.js +0 -44
  94. package/dist/psql/index.js +0 -2030
  95. package/dist/psql/io/history.js +0 -299
  96. package/dist/psql/io/input.js +0 -120
  97. package/dist/psql/io/lineEditor/buffer.js +0 -325
  98. package/dist/psql/io/lineEditor/complete.js +0 -227
  99. package/dist/psql/io/lineEditor/filename.js +0 -159
  100. package/dist/psql/io/lineEditor/index.js +0 -893
  101. package/dist/psql/io/lineEditor/keymap.js +0 -745
  102. package/dist/psql/io/lineEditor/vt100.js +0 -363
  103. package/dist/psql/io/pgpass.js +0 -202
  104. package/dist/psql/io/pgservice.js +0 -194
  105. package/dist/psql/io/psqlrc.js +0 -422
  106. package/dist/psql/print/aligned.js +0 -1748
  107. package/dist/psql/print/asciidoc.js +0 -230
  108. package/dist/psql/print/crosstab.js +0 -463
  109. package/dist/psql/print/csv.js +0 -76
  110. package/dist/psql/print/html.js +0 -240
  111. package/dist/psql/print/json.js +0 -96
  112. package/dist/psql/print/latex.js +0 -379
  113. package/dist/psql/print/pager.js +0 -267
  114. package/dist/psql/print/troff.js +0 -240
  115. package/dist/psql/print/unaligned.js +0 -99
  116. package/dist/psql/print/units.js +0 -188
  117. package/dist/psql/scanner/slash.js +0 -515
  118. package/dist/psql/scanner/sql.js +0 -914
  119. package/dist/psql/scanner/stringutils.js +0 -394
  120. package/dist/psql/types/backslash.js +0 -1
  121. package/dist/psql/types/connection.js +0 -1
  122. package/dist/psql/types/index.js +0 -7
  123. package/dist/psql/types/printer.js +0 -1
  124. package/dist/psql/types/repl.js +0 -1
  125. package/dist/psql/types/scanner.js +0 -24
  126. package/dist/psql/types/settings.js +0 -1
  127. package/dist/psql/types/variables.js +0 -1
  128. package/dist/psql/wire/connection.js +0 -2857
  129. package/dist/psql/wire/copy.js +0 -108
  130. package/dist/psql/wire/notify.js +0 -59
  131. package/dist/psql/wire/pipeline.js +0 -521
  132. package/dist/psql/wire/protocol.js +0 -466
  133. package/dist/psql/wire/sasl.js +0 -294
  134. package/dist/psql/wire/tls.js +0 -602
  135. package/dist/storage_api.js +0 -147
  136. package/dist/test_utils/fixtures.js +0 -121
  137. package/dist/test_utils/oauth_server.js +0 -9
  138. package/dist/types.js +0 -1
  139. package/dist/utils/ai_gateway_notice.js +0 -180
  140. package/dist/utils/api_enums.js +0 -33
  141. package/dist/utils/auth.js +0 -5
  142. package/dist/utils/branch_notice.js +0 -22
  143. package/dist/utils/branch_picker.js +0 -103
  144. package/dist/utils/compute_units.js +0 -28
  145. package/dist/utils/config_diff.js +0 -185
  146. package/dist/utils/enrichers.js +0 -161
  147. package/dist/utils/esbuild.js +0 -158
  148. package/dist/utils/formats.js +0 -18
  149. package/dist/utils/git_diff.js +0 -90
  150. package/dist/utils/inspect_db.js +0 -153
  151. package/dist/utils/inspect_queries.js +0 -372
  152. package/dist/utils/middlewares.js +0 -20
  153. package/dist/utils/openapi.js +0 -114
  154. package/dist/utils/package_manager.js +0 -68
  155. package/dist/utils/point_in_time.js +0 -56
  156. package/dist/utils/psql.js +0 -120
  157. package/dist/utils/string.js +0 -5
  158. package/dist/utils/ui.js +0 -59
  159. package/dist/utils/zip.js +0 -4
  160. package/dist/writer.js +0 -97
@@ -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
- };
@@ -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
- }