@motherbase/cli 0.2.4 → 0.2.6
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/LICENSE +21 -0
- package/README.md +131 -18
- package/bin/motherbase.js +10 -10
- package/package.json +35 -26
- package/src/api.js +233 -229
- package/src/commands/_util.js +106 -50
- package/src/commands/backups.js +16 -0
- package/src/commands/branches.js +113 -0
- package/src/commands/completion.js +90 -0
- package/src/commands/config.js +47 -47
- package/src/commands/cron.js +65 -65
- package/src/commands/db-catalog.js +326 -0
- package/src/commands/db.js +103 -92
- package/src/commands/domains.js +124 -0
- package/src/commands/functions.js +63 -33
- package/src/commands/gen.js +63 -17
- package/src/commands/import.js +55 -35
- package/src/commands/index.js +85 -64
- package/src/commands/inspect.js +178 -41
- package/src/commands/link.js +35 -35
- package/src/commands/local.js +23 -2
- package/src/commands/login.js +39 -39
- package/src/commands/logout.js +16 -16
- package/src/commands/mcp.js +27 -9
- package/src/commands/migrate.js +207 -0
- package/src/commands/migration.js +108 -33
- package/src/commands/network.js +83 -0
- package/src/commands/orgs.js +40 -0
- package/src/commands/postgres-config.js +113 -0
- package/src/commands/projects.js +130 -125
- package/src/commands/queues.js +133 -0
- package/src/commands/realtime.js +35 -35
- package/src/commands/rls.js +176 -176
- package/src/commands/secrets.js +66 -45
- package/src/commands/signing.js +130 -0
- package/src/commands/snippets.js +44 -0
- package/src/commands/status.js +42 -42
- package/src/commands/storage.js +261 -113
- package/src/commands/test.js +9 -0
- package/src/commands/unlink.js +15 -15
- package/src/commands/users.js +142 -113
- package/src/commands/vault.js +82 -0
- package/src/commands/webhooks.js +62 -62
- package/src/commands/whoami.js +20 -20
- package/src/commands/wrappers.js +150 -0
- package/src/config.js +98 -98
- package/src/context.js +106 -102
- package/src/docs.js +68 -0
- package/src/edge-templates.js +194 -0
- package/src/format.js +92 -0
- package/src/inspect/csv.js +22 -0
- package/src/inspect/queries.js +562 -0
- package/src/main.js +224 -160
- package/src/mcp/platform-tools.js +233 -0
- package/src/mcp/protocol.js +4 -1
- package/src/mcp/storage-tools.js +93 -0
- package/src/mcp/tools.js +318 -6
- package/src/migration/commands.js +375 -0
- package/src/migration/files.js +245 -0
- package/src/prompt.js +91 -91
- package/src/storage/remote.js +62 -0
- package/src/ui.js +111 -111
- package/src/commands/migrations.js +0 -85
package/src/format.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Machine output formats for `-o/--output`, matching the Supabase CLI's set
|
|
4
|
+
// (minus toml). `pretty` is each command's own human renderer; the rest are
|
|
5
|
+
// rendered here from the command's data, so every command gets them for free.
|
|
6
|
+
|
|
7
|
+
const { CliError } = require('./context');
|
|
8
|
+
|
|
9
|
+
const FORMATS = ['pretty', 'json', 'yaml', 'env'];
|
|
10
|
+
|
|
11
|
+
// Resolved once in main (`-o json` also sets `json`), but commands and tests
|
|
12
|
+
// that only pass `json: true` still land on the right format.
|
|
13
|
+
function outputFormat(flags = {}) {
|
|
14
|
+
return flags.format || (flags.json ? 'json' : 'pretty');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ── YAML ─────────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const YAML_RESERVED = /^(true|false|yes|no|on|off|null|~)$/i;
|
|
20
|
+
|
|
21
|
+
// A string stays plain only when YAML would read it back as that same string;
|
|
22
|
+
// anything else is double-quoted, and JSON's quoting is valid YAML.
|
|
23
|
+
function yamlScalar(value) {
|
|
24
|
+
if (value === null || value === undefined) return 'null';
|
|
25
|
+
if (typeof value !== 'string') return String(value);
|
|
26
|
+
const plain =
|
|
27
|
+
/^[A-Za-z0-9_./@-][A-Za-z0-9_ ./@:-]*$/.test(value) &&
|
|
28
|
+
!/(: |:$| $)/.test(value) &&
|
|
29
|
+
!YAML_RESERVED.test(value) &&
|
|
30
|
+
Number.isNaN(Number(value)) &&
|
|
31
|
+
!/^\d{4}-\d\d-\d\d/.test(value);
|
|
32
|
+
return plain ? value : JSON.stringify(value);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function isNonEmptyCollection(value) {
|
|
36
|
+
return value !== null && typeof value === 'object' && Object.keys(value).length > 0;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function yamlLines(value) {
|
|
40
|
+
if (Array.isArray(value)) {
|
|
41
|
+
if (!value.length) return ['[]'];
|
|
42
|
+
return value.flatMap(item => {
|
|
43
|
+
const lines = yamlLines(item);
|
|
44
|
+
return [`- ${lines[0]}`, ...lines.slice(1).map(l => ` ${l}`)];
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (value !== null && typeof value === 'object') {
|
|
48
|
+
const keys = Object.keys(value).filter(k => value[k] !== undefined);
|
|
49
|
+
if (!keys.length) return ['{}'];
|
|
50
|
+
return keys.flatMap(key => {
|
|
51
|
+
const child = value[key];
|
|
52
|
+
const lines = yamlLines(child);
|
|
53
|
+
return isNonEmptyCollection(child)
|
|
54
|
+
? [`${yamlScalar(key)}:`, ...lines.map(l => ` ${l}`)]
|
|
55
|
+
: [`${yamlScalar(key)}: ${lines[0]}`];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return [yamlScalar(value)];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function toYaml(value) {
|
|
62
|
+
return yamlLines(value).join('\n') + '\n';
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ── env ──────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
// `apiUrl` → `API_URL`, nested keys joined with `_`, as `supabase status -o env`.
|
|
68
|
+
function envKey(key) {
|
|
69
|
+
return String(key)
|
|
70
|
+
.replace(/([a-z0-9])([A-Z])/g, '$1_$2')
|
|
71
|
+
.replace(/[^A-Za-z0-9]+/g, '_')
|
|
72
|
+
.toUpperCase();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function flattenEnv(value, prefix, out) {
|
|
76
|
+
for (const [key, child] of Object.entries(value)) {
|
|
77
|
+
if (child === undefined) continue;
|
|
78
|
+
const name = prefix ? `${prefix}_${envKey(key)}` : envKey(key);
|
|
79
|
+
if (child !== null && typeof child === 'object') flattenEnv(child, name, out);
|
|
80
|
+
else out.push(`${name}=${JSON.stringify(child === null ? '' : String(child))}`);
|
|
81
|
+
}
|
|
82
|
+
return out;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function toEnv(value) {
|
|
86
|
+
if (value === null || typeof value !== 'object' || Array.isArray(value)) {
|
|
87
|
+
throw new CliError('The env output format needs a single object; this command returns a list. Use -o json or -o yaml.');
|
|
88
|
+
}
|
|
89
|
+
return flattenEnv(value, '', []).join('\n') + '\n';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = { FORMATS, outputFormat, toYaml, toEnv };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// CSV for `inspect report`, written the way Postgres' `COPY ... TO STDOUT WITH
|
|
4
|
+
// CSV HEADER` writes it (which is how the Supabase CLI produces its report), so
|
|
5
|
+
// the files read the same whichever tool made them: NULL is an empty field, an
|
|
6
|
+
// empty string is `""`, booleans are `t`/`f`.
|
|
7
|
+
|
|
8
|
+
function field(value) {
|
|
9
|
+
if (value === null || value === undefined) return '';
|
|
10
|
+
if (typeof value === 'boolean') return value ? 't' : 'f';
|
|
11
|
+
const text = typeof value === 'object' ? JSON.stringify(value) : String(value);
|
|
12
|
+
return text === '' || /[",\r\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** One CSV document: a header of column keys, then one line per row. */
|
|
16
|
+
function toCsv(columns, rows) {
|
|
17
|
+
const lines = [columns.map(c => field(c.key)).join(',')];
|
|
18
|
+
for (const row of rows) lines.push(columns.map(c => field(row[c.key])).join(','));
|
|
19
|
+
return lines.join('\n') + '\n';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = { toCsv };
|
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// The `inspect db` query set, ported from the Supabase CLI
|
|
4
|
+
// (apps/cli/src/commands/inspect/db/*/*.query.ts) so the same subcommand
|
|
5
|
+
// answers the same question with the same columns.
|
|
6
|
+
//
|
|
7
|
+
// Every query runs through the admin API's POST /sql/readonly, which executes it
|
|
8
|
+
// in a READ ONLY transaction and cannot bind parameters. The deltas from
|
|
9
|
+
// upstream all follow from that, or from running on a plain Postgres rather than
|
|
10
|
+
// the supabase/postgres image:
|
|
11
|
+
//
|
|
12
|
+
// - `$1` (the internal schemas to skip) is inlined as a text[] literal, and
|
|
13
|
+
// `$2` (the database name) is `current_database()`.
|
|
14
|
+
// - pg_stat_statements is referenced unqualified: `CREATE EXTENSION` puts it on
|
|
15
|
+
// the search_path, not in an `extensions` schema. It is preloaded by our
|
|
16
|
+
// Postgres image but may not be created in a given database, so the queries
|
|
17
|
+
// that need it are flagged `needsStatStatements` for a clearer error.
|
|
18
|
+
// - numeric results are cast to text. The readonly endpoint serialises a
|
|
19
|
+
// Prisma Decimal as its internal `{ s, e, d }` fields, not as a number.
|
|
20
|
+
// - vacuum-stats prints `No stats` in SQL, where upstream post-processes the
|
|
21
|
+
// rendered cell.
|
|
22
|
+
|
|
23
|
+
// Schemas the queries exclude, verbatim from upstream's inspect-schemas.ts.
|
|
24
|
+
const INTERNAL_SCHEMAS = [
|
|
25
|
+
'information_schema',
|
|
26
|
+
'pg_*',
|
|
27
|
+
'_analytics', '_realtime', '_supavisor', 'auth', 'etl', 'extensions', 'pgbouncer',
|
|
28
|
+
'realtime', 'storage', 'supabase_functions', 'supabase_migrations',
|
|
29
|
+
'cron', 'dbdev', 'graphql', 'graphql_public', 'net', 'pgmq', 'pgsodium',
|
|
30
|
+
'pgsodium_masks', 'pgtle', 'repack', 'tiger', 'tiger_data', 'timescaledb_*',
|
|
31
|
+
'_timescaledb_*', 'topology', 'vault',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// A schema name as a LIKE pattern: `_` and `\` are literal, `*` is the wildcard.
|
|
35
|
+
function likePattern(schema) {
|
|
36
|
+
return schema.replace(/[\\_*]/g, char => (char === '*' ? '%' : `\\${char}`));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const IGNORED_SCHEMAS = `'{${INTERNAL_SCHEMAS.map(likePattern).join(',')}}'::text[]`;
|
|
40
|
+
|
|
41
|
+
// Column shorthand: `[key, label]` pairs, in display order.
|
|
42
|
+
const cols = pairs => pairs.map(([key, label]) => ({ key, label }));
|
|
43
|
+
|
|
44
|
+
// pg_stat_statements' timing columns were renamed in PG17; to_jsonb reads
|
|
45
|
+
// whichever this server has. Shared by `calls` and `outliers`.
|
|
46
|
+
const SYNC_IO_TIME = `(
|
|
47
|
+
interval '1 millisecond' * (
|
|
48
|
+
COALESCE((to_jsonb(s) ->> 'shared_blk_read_time')::double precision, (to_jsonb(s) ->> 'blk_read_time')::double precision, 0)
|
|
49
|
+
+ COALESCE((to_jsonb(s) ->> 'shared_blk_write_time')::double precision, (to_jsonb(s) ->> 'blk_write_time')::double precision, 0)
|
|
50
|
+
)
|
|
51
|
+
)::text AS sync_io_time`;
|
|
52
|
+
|
|
53
|
+
const QUERIES = {
|
|
54
|
+
bloat: {
|
|
55
|
+
summary: 'Estimate space wasted by dead tuples in tables and indexes',
|
|
56
|
+
columns: cols([['type', 'Type'], ['name', 'Name'], ['bloat', 'Bloat'], ['waste', 'Waste']]),
|
|
57
|
+
sql: `WITH constants AS (
|
|
58
|
+
SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 4 AS ma
|
|
59
|
+
), bloat_info AS (
|
|
60
|
+
SELECT
|
|
61
|
+
ma,bs,schemaname,tablename,
|
|
62
|
+
(datawidth+(hdr+ma-(case when hdr%ma=0 THEN ma ELSE hdr%ma END)))::numeric AS datahdr,
|
|
63
|
+
(maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma ELSE nullhdr%ma END))) AS nullhdr2
|
|
64
|
+
FROM (
|
|
65
|
+
SELECT
|
|
66
|
+
schemaname, tablename, hdr, ma, bs,
|
|
67
|
+
SUM((1-null_frac)*avg_width) AS datawidth,
|
|
68
|
+
MAX(null_frac) AS maxfracsum,
|
|
69
|
+
hdr+(
|
|
70
|
+
SELECT 1+count(*)/8
|
|
71
|
+
FROM pg_stats s2
|
|
72
|
+
WHERE null_frac<>0 AND s2.schemaname = s.schemaname AND s2.tablename = s.tablename
|
|
73
|
+
) AS nullhdr
|
|
74
|
+
FROM pg_stats s, constants
|
|
75
|
+
GROUP BY 1,2,3,4,5
|
|
76
|
+
) AS foo
|
|
77
|
+
), table_bloat AS (
|
|
78
|
+
SELECT
|
|
79
|
+
schemaname, tablename, cc.relpages, bs,
|
|
80
|
+
CEIL((cc.reltuples*((datahdr+ma-
|
|
81
|
+
(CASE WHEN datahdr%ma=0 THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)) AS otta
|
|
82
|
+
FROM bloat_info
|
|
83
|
+
JOIN pg_class cc ON cc.relname = bloat_info.tablename
|
|
84
|
+
JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname
|
|
85
|
+
WHERE NOT nn.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
86
|
+
), index_bloat AS (
|
|
87
|
+
SELECT
|
|
88
|
+
schemaname, tablename, bs,
|
|
89
|
+
COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) AS ituples, COALESCE(c2.relpages,0) AS ipages,
|
|
90
|
+
COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) AS iotta
|
|
91
|
+
FROM bloat_info
|
|
92
|
+
JOIN pg_class cc ON cc.relname = bloat_info.tablename
|
|
93
|
+
JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname
|
|
94
|
+
JOIN pg_index i ON indrelid = cc.oid
|
|
95
|
+
JOIN pg_class c2 ON c2.oid = i.indexrelid
|
|
96
|
+
WHERE NOT nn.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
97
|
+
), bloat_summary AS (
|
|
98
|
+
SELECT
|
|
99
|
+
'table' as type,
|
|
100
|
+
FORMAT('%I.%I', schemaname, tablename) AS name,
|
|
101
|
+
ROUND(CASE WHEN otta=0 THEN 0.0 ELSE table_bloat.relpages/otta::numeric END,1) AS bloat,
|
|
102
|
+
CASE WHEN relpages < otta THEN '0' ELSE (bs*(table_bloat.relpages-otta)::bigint)::bigint END AS raw_waste
|
|
103
|
+
FROM table_bloat
|
|
104
|
+
UNION
|
|
105
|
+
SELECT
|
|
106
|
+
'index' as type,
|
|
107
|
+
FORMAT('%I.%I::%I', schemaname, tablename, iname) AS name,
|
|
108
|
+
ROUND(CASE WHEN iotta=0 OR ipages=0 THEN 0.0 ELSE ipages/iotta::numeric END,1) AS bloat,
|
|
109
|
+
CASE WHEN ipages < iotta THEN '0' ELSE (bs*(ipages-iotta))::bigint END AS raw_waste
|
|
110
|
+
FROM index_bloat
|
|
111
|
+
)
|
|
112
|
+
SELECT type, name, bloat::text AS bloat, pg_size_pretty(raw_waste) as waste
|
|
113
|
+
FROM bloat_summary
|
|
114
|
+
ORDER BY raw_waste DESC, bloat_summary.bloat DESC`,
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
blocking: {
|
|
118
|
+
summary: 'Queries holding locks and the queries waiting on them',
|
|
119
|
+
columns: cols([
|
|
120
|
+
['blocked_pid', 'blocked pid'], ['blocking_statement', 'blocking statement'],
|
|
121
|
+
['blocking_duration', 'blocking duration'], ['blocking_pid', 'blocking pid'],
|
|
122
|
+
['blocked_statement', 'blocked statement'], ['blocked_duration', 'blocked duration'],
|
|
123
|
+
]),
|
|
124
|
+
sql: `SELECT
|
|
125
|
+
bl.pid AS blocked_pid,
|
|
126
|
+
ka.query AS blocking_statement,
|
|
127
|
+
age(now(), ka.query_start)::text AS blocking_duration,
|
|
128
|
+
kl.pid AS blocking_pid,
|
|
129
|
+
a.query AS blocked_statement,
|
|
130
|
+
age(now(), a.query_start)::text AS blocked_duration
|
|
131
|
+
FROM pg_catalog.pg_locks bl
|
|
132
|
+
JOIN pg_catalog.pg_stat_activity a
|
|
133
|
+
ON bl.pid = a.pid
|
|
134
|
+
JOIN pg_catalog.pg_locks kl
|
|
135
|
+
JOIN pg_catalog.pg_stat_activity ka
|
|
136
|
+
ON kl.pid = ka.pid
|
|
137
|
+
ON bl.transactionid = kl.transactionid AND bl.pid != kl.pid
|
|
138
|
+
WHERE NOT bl.granted`,
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
calls: {
|
|
142
|
+
summary: 'Queries ordered by number of calls (pg_stat_statements)',
|
|
143
|
+
needsStatStatements: true,
|
|
144
|
+
columns: cols([
|
|
145
|
+
['query', 'Query'], ['total_exec_time', 'Total Execution Time'],
|
|
146
|
+
['prop_exec_time', 'Proportion of total exec time'], ['ncalls', 'Number Calls'],
|
|
147
|
+
['sync_io_time', 'Sync IO time'],
|
|
148
|
+
]),
|
|
149
|
+
sql: `SELECT
|
|
150
|
+
query,
|
|
151
|
+
(interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
|
|
152
|
+
to_char((total_exec_time/sum(total_exec_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time,
|
|
153
|
+
to_char(calls, 'FM999G999G999G999G990') AS ncalls,
|
|
154
|
+
${SYNC_IO_TIME}
|
|
155
|
+
FROM pg_stat_statements s
|
|
156
|
+
ORDER BY calls DESC
|
|
157
|
+
LIMIT 10`,
|
|
158
|
+
},
|
|
159
|
+
|
|
160
|
+
'db-stats': {
|
|
161
|
+
summary: 'Database size, cache hit rates and WAL size',
|
|
162
|
+
columns: cols([
|
|
163
|
+
['name', 'Name'], ['database_size', 'Database Size'], ['total_index_size', 'Total Index Size'],
|
|
164
|
+
['total_table_size', 'Total Table Size'], ['total_toast_size', 'Total Toast Size'],
|
|
165
|
+
['time_since_stats_reset', 'Time Since Stats Reset'], ['index_hit_rate', 'Index Hit Rate'],
|
|
166
|
+
['table_hit_rate', 'Table Hit Rate'], ['wal_size', 'WAL Size'],
|
|
167
|
+
]),
|
|
168
|
+
// Upstream reads pg_stat_statements_info unconditionally, which would fail
|
|
169
|
+
// the whole summary on a database without the extension. query_to_xml
|
|
170
|
+
// defers the reference to run time, and the CASE never reaches it when the
|
|
171
|
+
// view does not exist.
|
|
172
|
+
sql: `WITH total_objects AS (
|
|
173
|
+
SELECT c.relkind, pg_size_pretty(SUM(pg_relation_size(c.oid))) AS size
|
|
174
|
+
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
175
|
+
WHERE c.relkind IN ('i', 'r', 't') AND NOT n.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
176
|
+
GROUP BY c.relkind
|
|
177
|
+
), cache_hit AS (
|
|
178
|
+
SELECT
|
|
179
|
+
'i' AS relkind,
|
|
180
|
+
ROUND(SUM(idx_blks_hit)::numeric / nullif(SUM(idx_blks_hit + idx_blks_read), 0), 2) AS ratio
|
|
181
|
+
FROM pg_statio_user_indexes
|
|
182
|
+
WHERE NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
183
|
+
UNION
|
|
184
|
+
SELECT
|
|
185
|
+
't' AS relkind,
|
|
186
|
+
ROUND(
|
|
187
|
+
SUM(COALESCE((to_jsonb(s) ->> 'rel_blks_hit')::bigint, (to_jsonb(s) ->> 'heap_blks_hit')::bigint, 0))::numeric
|
|
188
|
+
/ nullif(SUM(
|
|
189
|
+
COALESCE((to_jsonb(s) ->> 'rel_blks_hit')::bigint, (to_jsonb(s) ->> 'heap_blks_hit')::bigint, 0)
|
|
190
|
+
+ COALESCE((to_jsonb(s) ->> 'rel_blks_read')::bigint, (to_jsonb(s) ->> 'heap_blks_read')::bigint, 0)
|
|
191
|
+
), 0),
|
|
192
|
+
2
|
|
193
|
+
) AS ratio
|
|
194
|
+
FROM pg_statio_user_tables s
|
|
195
|
+
WHERE NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
196
|
+
)
|
|
197
|
+
SELECT
|
|
198
|
+
current_database() AS name,
|
|
199
|
+
pg_size_pretty(pg_database_size(current_database())) AS database_size,
|
|
200
|
+
COALESCE((SELECT size FROM total_objects WHERE relkind = 'i'), '0 bytes') AS total_index_size,
|
|
201
|
+
COALESCE((SELECT size FROM total_objects WHERE relkind = 'r'), '0 bytes') AS total_table_size,
|
|
202
|
+
COALESCE((SELECT size FROM total_objects WHERE relkind = 't'), '0 bytes') AS total_toast_size,
|
|
203
|
+
CASE WHEN to_regclass('pg_stat_statements_info') IS NULL THEN 'N/A' ELSE COALESCE((xpath(
|
|
204
|
+
'/row/age/text()',
|
|
205
|
+
query_to_xml('SELECT (now() - stats_reset)::text AS age FROM pg_stat_statements_info', false, true, '')
|
|
206
|
+
))[1]::text, 'N/A') END AS time_since_stats_reset,
|
|
207
|
+
(SELECT COALESCE(ratio::text, 'N/A') FROM cache_hit WHERE relkind = 'i') AS index_hit_rate,
|
|
208
|
+
(SELECT COALESCE(ratio::text, 'N/A') FROM cache_hit WHERE relkind = 't') AS table_hit_rate,
|
|
209
|
+
COALESCE((SELECT pg_size_pretty(SUM(size)) FROM pg_ls_waldir()), '0 bytes') AS wal_size`,
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
'index-stats': {
|
|
213
|
+
summary: 'Index size, usage, scan counts and unused indexes',
|
|
214
|
+
columns: cols([
|
|
215
|
+
['name', 'Name'], ['table', 'Table'], ['columns', 'Columns'], ['size', 'Size'],
|
|
216
|
+
['percent_used', 'Percent used'], ['index_scans', 'Index scans'], ['seq_scans', 'Seq scans'],
|
|
217
|
+
['unused', 'Unused'],
|
|
218
|
+
]),
|
|
219
|
+
sql: `WITH idx_sizes AS (
|
|
220
|
+
SELECT
|
|
221
|
+
i.indexrelid AS oid,
|
|
222
|
+
FORMAT('%I.%I', n.nspname, c.relname) AS name,
|
|
223
|
+
FORMAT('%I.%I', tn.nspname, tc.relname) AS table_name,
|
|
224
|
+
(
|
|
225
|
+
SELECT STRING_AGG(pg_get_indexdef(i.indexrelid, ord::int, false), ',' ORDER BY ord)
|
|
226
|
+
FROM unnest(i.indkey::int[]) WITH ORDINALITY AS k(attnum, ord)
|
|
227
|
+
) AS columns,
|
|
228
|
+
pg_relation_size(i.indexrelid) AS index_size_bytes
|
|
229
|
+
FROM pg_stat_user_indexes ui
|
|
230
|
+
JOIN pg_index i ON ui.indexrelid = i.indexrelid
|
|
231
|
+
JOIN pg_class c ON ui.indexrelid = c.oid
|
|
232
|
+
JOIN pg_namespace n ON c.relnamespace = n.oid
|
|
233
|
+
JOIN pg_class tc ON tc.oid = i.indrelid
|
|
234
|
+
JOIN pg_namespace tn ON tn.oid = tc.relnamespace
|
|
235
|
+
WHERE NOT n.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
236
|
+
),
|
|
237
|
+
idx_usage AS (
|
|
238
|
+
SELECT indexrelid AS oid, idx_scan::bigint AS idx_scans
|
|
239
|
+
FROM pg_stat_user_indexes ui
|
|
240
|
+
WHERE NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
241
|
+
),
|
|
242
|
+
seq_usage AS (
|
|
243
|
+
SELECT relid AS oid, seq_scan::bigint AS seq_scans
|
|
244
|
+
FROM pg_stat_user_tables
|
|
245
|
+
WHERE NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
246
|
+
),
|
|
247
|
+
usage_pct AS (
|
|
248
|
+
SELECT
|
|
249
|
+
u.oid,
|
|
250
|
+
CASE
|
|
251
|
+
WHEN u.idx_scans IS NULL OR u.idx_scans = 0 THEN 0
|
|
252
|
+
WHEN s.seq_scans IS NULL THEN 100
|
|
253
|
+
ELSE ROUND(100.0 * u.idx_scans / (s.seq_scans + u.idx_scans), 1)
|
|
254
|
+
END AS percent_used
|
|
255
|
+
FROM idx_usage u
|
|
256
|
+
LEFT JOIN seq_usage s ON s.oid = u.oid
|
|
257
|
+
)
|
|
258
|
+
SELECT
|
|
259
|
+
s.name,
|
|
260
|
+
s.table_name AS "table",
|
|
261
|
+
s.columns,
|
|
262
|
+
pg_size_pretty(s.index_size_bytes) AS size,
|
|
263
|
+
COALESCE(up.percent_used, 0)::text || '%' AS percent_used,
|
|
264
|
+
COALESCE(u.idx_scans, 0) AS index_scans,
|
|
265
|
+
COALESCE(sq.seq_scans, 0) AS seq_scans,
|
|
266
|
+
CASE WHEN COALESCE(u.idx_scans, 0) = 0 THEN true ELSE false END AS unused
|
|
267
|
+
FROM idx_sizes s
|
|
268
|
+
LEFT JOIN idx_usage u ON u.oid = s.oid
|
|
269
|
+
LEFT JOIN seq_usage sq ON sq.oid = s.oid
|
|
270
|
+
LEFT JOIN usage_pct up ON up.oid = s.oid
|
|
271
|
+
ORDER BY s.index_size_bytes DESC`,
|
|
272
|
+
},
|
|
273
|
+
|
|
274
|
+
locks: {
|
|
275
|
+
summary: 'Queries holding an exclusive lock on a relation',
|
|
276
|
+
columns: cols([
|
|
277
|
+
['pid', 'pid'], ['relname', 'relname'], ['transactionid', 'transaction id'],
|
|
278
|
+
['granted', 'granted'], ['stmt', 'stmt'], ['age', 'age'],
|
|
279
|
+
]),
|
|
280
|
+
sql: `SELECT
|
|
281
|
+
pg_stat_activity.pid,
|
|
282
|
+
COALESCE(pg_class.relname, 'null') AS relname,
|
|
283
|
+
COALESCE(pg_locks.transactionid::text, 'null') AS transactionid,
|
|
284
|
+
pg_locks.granted,
|
|
285
|
+
pg_stat_activity.query AS stmt,
|
|
286
|
+
age(now(), pg_stat_activity.query_start)::text AS age
|
|
287
|
+
FROM pg_stat_activity, pg_locks LEFT OUTER JOIN pg_class ON (pg_locks.relation = pg_class.oid)
|
|
288
|
+
WHERE pg_stat_activity.query <> '<insufficient privilege>'
|
|
289
|
+
AND pg_locks.pid = pg_stat_activity.pid
|
|
290
|
+
AND pg_locks.mode = 'ExclusiveLock'
|
|
291
|
+
ORDER BY query_start`,
|
|
292
|
+
},
|
|
293
|
+
|
|
294
|
+
'long-running-queries': {
|
|
295
|
+
summary: 'Queries running for longer than five minutes',
|
|
296
|
+
columns: cols([['pid', 'pid'], ['duration', 'Duration'], ['query', 'Query']]),
|
|
297
|
+
sql: `SELECT
|
|
298
|
+
pid,
|
|
299
|
+
age(now(), pg_stat_activity.query_start)::text AS duration,
|
|
300
|
+
query AS query
|
|
301
|
+
FROM pg_stat_activity
|
|
302
|
+
WHERE pg_stat_activity.query <> ''::text
|
|
303
|
+
AND state <> 'idle'
|
|
304
|
+
AND age(now(), pg_stat_activity.query_start) > interval '5 minutes'
|
|
305
|
+
ORDER BY age(now(), pg_stat_activity.query_start) DESC`,
|
|
306
|
+
},
|
|
307
|
+
|
|
308
|
+
outliers: {
|
|
309
|
+
summary: 'Queries ordered by total execution time (pg_stat_statements)',
|
|
310
|
+
needsStatStatements: true,
|
|
311
|
+
columns: cols([
|
|
312
|
+
['query', 'Query'], ['total_exec_time', 'Execution Time'],
|
|
313
|
+
['prop_exec_time', 'Proportion of exec time'], ['ncalls', 'Number Calls'],
|
|
314
|
+
['sync_io_time', 'Sync IO time'],
|
|
315
|
+
]),
|
|
316
|
+
sql: `SELECT
|
|
317
|
+
(interval '1 millisecond' * total_exec_time)::text AS total_exec_time,
|
|
318
|
+
to_char((total_exec_time/sum(total_exec_time) OVER()) * 100, 'FM90D0') || '%' AS prop_exec_time,
|
|
319
|
+
to_char(calls, 'FM999G999G999G990') AS ncalls,
|
|
320
|
+
${SYNC_IO_TIME},
|
|
321
|
+
query
|
|
322
|
+
FROM pg_stat_statements s WHERE userid = (SELECT usesysid FROM pg_user WHERE usename = current_user LIMIT 1)
|
|
323
|
+
ORDER BY total_exec_time DESC
|
|
324
|
+
LIMIT 10`,
|
|
325
|
+
},
|
|
326
|
+
|
|
327
|
+
'replication-slots': {
|
|
328
|
+
summary: 'Replication slots and their lag',
|
|
329
|
+
columns: cols([
|
|
330
|
+
['slot_name', 'Name'], ['active', 'Active'], ['state', 'State'],
|
|
331
|
+
['replication_client_address', 'Replication Client Address'],
|
|
332
|
+
['replication_lag_gb', 'Replication Lag GB'],
|
|
333
|
+
]),
|
|
334
|
+
sql: `SELECT
|
|
335
|
+
s.slot_name,
|
|
336
|
+
s.active,
|
|
337
|
+
COALESCE(r.state, 'N/A') as state,
|
|
338
|
+
CASE WHEN r.client_addr IS NULL
|
|
339
|
+
THEN 'N/A'
|
|
340
|
+
ELSE r.client_addr::text
|
|
341
|
+
END replication_client_address,
|
|
342
|
+
GREATEST(0, ROUND((redo_lsn-restart_lsn)/1024/1024/1024, 2))::text as replication_lag_gb
|
|
343
|
+
FROM pg_control_checkpoint(), pg_replication_slots s
|
|
344
|
+
LEFT JOIN pg_stat_replication r ON (r.pid = s.active_pid)`,
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
'role-stats': {
|
|
348
|
+
summary: 'Roles with their connection counts, limits and custom config',
|
|
349
|
+
columns: cols([
|
|
350
|
+
['role_name', 'Role name'], ['active_connections', 'Active connections'],
|
|
351
|
+
['connection_limit', 'Connection limit'], ['custom_config', 'Custom config'],
|
|
352
|
+
]),
|
|
353
|
+
sql: `SELECT
|
|
354
|
+
rolname as role_name,
|
|
355
|
+
(SELECT count(*) FROM pg_stat_activity WHERE pg_roles.rolname = pg_stat_activity.usename) AS active_connections,
|
|
356
|
+
CASE WHEN rolconnlimit = -1
|
|
357
|
+
THEN current_setting('max_connections')::int8
|
|
358
|
+
ELSE rolconnlimit
|
|
359
|
+
END AS connection_limit,
|
|
360
|
+
array_to_string(rolconfig, ',', '*') as custom_config
|
|
361
|
+
FROM pg_roles
|
|
362
|
+
ORDER BY 1 DESC`,
|
|
363
|
+
},
|
|
364
|
+
|
|
365
|
+
'table-stats': {
|
|
366
|
+
summary: 'Table size, index size and estimated row count',
|
|
367
|
+
columns: cols([
|
|
368
|
+
['name', 'Name'], ['table_size', 'Table size'], ['index_size', 'Index size'],
|
|
369
|
+
['total_size', 'Total size'], ['estimated_row_count', 'Estimated row count'],
|
|
370
|
+
['seq_scans', 'Seq scans'],
|
|
371
|
+
]),
|
|
372
|
+
sql: `SELECT
|
|
373
|
+
ts.name,
|
|
374
|
+
pg_size_pretty(ts.table_size_bytes) AS table_size,
|
|
375
|
+
pg_size_pretty(ts.index_size_bytes) AS index_size,
|
|
376
|
+
pg_size_pretty(ts.total_size_bytes) AS total_size,
|
|
377
|
+
COALESCE(rc.estimated_row_count, 0) AS estimated_row_count,
|
|
378
|
+
COALESCE(rc.seq_scans, 0) AS seq_scans
|
|
379
|
+
FROM (
|
|
380
|
+
SELECT
|
|
381
|
+
FORMAT('%I.%I', n.nspname, c.relname) AS name,
|
|
382
|
+
pg_table_size(c.oid) AS table_size_bytes,
|
|
383
|
+
pg_indexes_size(c.oid) AS index_size_bytes,
|
|
384
|
+
pg_total_relation_size(c.oid) AS total_size_bytes
|
|
385
|
+
FROM pg_class c
|
|
386
|
+
LEFT JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
387
|
+
WHERE NOT n.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
388
|
+
AND c.relkind = 'r'
|
|
389
|
+
) ts
|
|
390
|
+
LEFT JOIN (
|
|
391
|
+
SELECT
|
|
392
|
+
FORMAT('%I.%I', schemaname, relname) AS name,
|
|
393
|
+
n_live_tup AS estimated_row_count,
|
|
394
|
+
seq_scan AS seq_scans
|
|
395
|
+
FROM pg_stat_user_tables
|
|
396
|
+
WHERE NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
397
|
+
) rc ON rc.name = ts.name
|
|
398
|
+
ORDER BY ts.total_size_bytes DESC`,
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
'traffic-profile': {
|
|
402
|
+
summary: 'Read/write balance of each table',
|
|
403
|
+
columns: cols([
|
|
404
|
+
['schemaname', 'Schema'], ['table_name', 'Table'], ['blocks_read', 'Blocks Read'],
|
|
405
|
+
['write_tuples', 'Write Tuples'], ['blocks_write', 'Blocks Write'], ['activity_ratio', 'Activity Ratio'],
|
|
406
|
+
]),
|
|
407
|
+
// Adapted upstream from Crunchy Data's "Is Postgres Read Heavy or Write Heavy?".
|
|
408
|
+
sql: `WITH
|
|
409
|
+
ratio_target AS (SELECT 5 AS ratio),
|
|
410
|
+
table_list AS (SELECT
|
|
411
|
+
s.schemaname,
|
|
412
|
+
s.relname AS table_name,
|
|
413
|
+
si.heap_blks_read + si.idx_blks_read AS blocks_read,
|
|
414
|
+
s.n_tup_ins + s.n_tup_upd + s.n_tup_del AS write_tuples,
|
|
415
|
+
relpages * (s.n_tup_ins + s.n_tup_upd + s.n_tup_del ) / (case when reltuples = 0 then 1 else reltuples end) as blocks_write
|
|
416
|
+
FROM pg_stat_user_tables AS s
|
|
417
|
+
JOIN pg_statio_user_tables AS si ON s.relid = si.relid
|
|
418
|
+
JOIN pg_class c ON c.oid = s.relid
|
|
419
|
+
WHERE (s.n_tup_ins + s.n_tup_upd + s.n_tup_del) > 0
|
|
420
|
+
AND (si.heap_blks_read + si.idx_blks_read) > 0
|
|
421
|
+
)
|
|
422
|
+
SELECT
|
|
423
|
+
schemaname,
|
|
424
|
+
table_name,
|
|
425
|
+
blocks_read,
|
|
426
|
+
write_tuples,
|
|
427
|
+
to_char(blocks_write, 'FM999999999990.0') AS blocks_write,
|
|
428
|
+
CASE
|
|
429
|
+
WHEN blocks_read = 0 and blocks_write = 0 THEN 'No Activity'
|
|
430
|
+
WHEN blocks_write * ratio > blocks_read THEN
|
|
431
|
+
CASE
|
|
432
|
+
WHEN blocks_read = 0 THEN 'Write-Only'
|
|
433
|
+
ELSE ROUND(blocks_write :: numeric / blocks_read :: numeric, 1)::text || ':1 (Write-Heavy)'
|
|
434
|
+
END
|
|
435
|
+
WHEN blocks_read > blocks_write * ratio THEN
|
|
436
|
+
CASE
|
|
437
|
+
WHEN blocks_write = 0 THEN 'Read-Only'
|
|
438
|
+
ELSE '1:' || ROUND(blocks_read::numeric / blocks_write :: numeric, 1)::text || ' (Read-Heavy)'
|
|
439
|
+
END
|
|
440
|
+
ELSE '1:1 (Balanced)'
|
|
441
|
+
END AS activity_ratio
|
|
442
|
+
FROM table_list, ratio_target
|
|
443
|
+
ORDER BY (blocks_read + table_list.blocks_write) DESC`,
|
|
444
|
+
},
|
|
445
|
+
|
|
446
|
+
'vacuum-stats': {
|
|
447
|
+
summary: 'Vacuum and analyze history, and whether autovacuum is due',
|
|
448
|
+
columns: cols([
|
|
449
|
+
['name', 'Table'], ['last_vacuum', 'Last Vacuum'], ['last_autovacuum', 'Last Auto Vacuum'],
|
|
450
|
+
['last_analyze', 'Last Analyze'], ['last_autoanalyze', 'Last Auto Analyze'],
|
|
451
|
+
['rowcount', 'Row count'], ['dead_rowcount', 'Dead row count'],
|
|
452
|
+
['expect_autovacuum', 'Expect autovacuum?'], ['expect_autoanalyze', 'Expect autoanalyze?'],
|
|
453
|
+
]),
|
|
454
|
+
sql: `WITH table_opts AS (
|
|
455
|
+
SELECT pg_class.oid, relname, nspname, array_to_string(reloptions, '') AS relopts
|
|
456
|
+
FROM pg_class INNER JOIN pg_namespace ns ON relnamespace = ns.oid
|
|
457
|
+
), vacuum_settings AS (
|
|
458
|
+
SELECT
|
|
459
|
+
oid, relname, nspname,
|
|
460
|
+
CASE
|
|
461
|
+
WHEN relopts LIKE '%autovacuum_vacuum_threshold%'
|
|
462
|
+
THEN substring(relopts, '.*autovacuum_vacuum_threshold=([0-9.]+).*')::integer
|
|
463
|
+
ELSE current_setting('autovacuum_vacuum_threshold')::integer
|
|
464
|
+
END AS autovacuum_vacuum_threshold,
|
|
465
|
+
CASE
|
|
466
|
+
WHEN relopts LIKE '%autovacuum_vacuum_scale_factor%'
|
|
467
|
+
THEN substring(relopts, '.*autovacuum_vacuum_scale_factor=([0-9.]+).*')::real
|
|
468
|
+
ELSE current_setting('autovacuum_vacuum_scale_factor')::real
|
|
469
|
+
END AS autovacuum_vacuum_scale_factor,
|
|
470
|
+
CASE
|
|
471
|
+
WHEN relopts LIKE '%autovacuum_analyze_threshold%'
|
|
472
|
+
THEN substring(relopts, '.*autovacuum_analyze_threshold=([0-9.]+).*')::integer
|
|
473
|
+
ELSE current_setting('autovacuum_analyze_threshold')::integer
|
|
474
|
+
END AS autovacuum_analyze_threshold,
|
|
475
|
+
CASE
|
|
476
|
+
WHEN relopts LIKE '%autovacuum_analyze_scale_factor%'
|
|
477
|
+
THEN substring(relopts, '.*autovacuum_analyze_scale_factor=([0-9.]+).*')::real
|
|
478
|
+
ELSE current_setting('autovacuum_analyze_scale_factor')::real
|
|
479
|
+
END AS autovacuum_analyze_scale_factor
|
|
480
|
+
FROM table_opts
|
|
481
|
+
)
|
|
482
|
+
SELECT
|
|
483
|
+
FORMAT('%I.%I', vacuum_settings.nspname, vacuum_settings.relname) AS name,
|
|
484
|
+
coalesce(to_char(psut.last_vacuum, 'YYYY-MM-DD HH24:MI'), '') AS last_vacuum,
|
|
485
|
+
coalesce(to_char(psut.last_autovacuum, 'YYYY-MM-DD HH24:MI'), '') AS last_autovacuum,
|
|
486
|
+
coalesce(to_char(psut.last_analyze, 'YYYY-MM-DD HH24:MI'), '') AS last_analyze,
|
|
487
|
+
coalesce(to_char(psut.last_autoanalyze, 'YYYY-MM-DD HH24:MI'), '') AS last_autoanalyze,
|
|
488
|
+
CASE WHEN pg_class.reltuples = -1 THEN 'No stats' ELSE to_char(pg_class.reltuples, '9G999G999G999') END AS rowcount,
|
|
489
|
+
to_char(psut.n_dead_tup, '9G999G999G999') AS dead_rowcount,
|
|
490
|
+
to_char(autovacuum_vacuum_threshold
|
|
491
|
+
+ (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples), '9G999G999G999') AS autovacuum_threshold,
|
|
492
|
+
CASE
|
|
493
|
+
WHEN autovacuum_vacuum_threshold + (autovacuum_vacuum_scale_factor::numeric * pg_class.reltuples) < psut.n_dead_tup
|
|
494
|
+
THEN 'yes'
|
|
495
|
+
ELSE 'no'
|
|
496
|
+
END AS expect_autovacuum,
|
|
497
|
+
to_char(autovacuum_analyze_threshold
|
|
498
|
+
+ (autovacuum_analyze_scale_factor::numeric * pg_class.reltuples), '9G999G999G999') AS autoanalyze_threshold,
|
|
499
|
+
CASE
|
|
500
|
+
WHEN autovacuum_analyze_threshold + (autovacuum_analyze_scale_factor::numeric * pg_class.reltuples) < psut.n_dead_tup
|
|
501
|
+
THEN 'yes'
|
|
502
|
+
ELSE 'no'
|
|
503
|
+
END AS expect_autoanalyze
|
|
504
|
+
FROM pg_stat_user_tables psut INNER JOIN pg_class ON psut.relid = pg_class.oid
|
|
505
|
+
INNER JOIN vacuum_settings ON pg_class.oid = vacuum_settings.oid
|
|
506
|
+
WHERE NOT vacuum_settings.nspname LIKE ANY(${IGNORED_SCHEMAS})
|
|
507
|
+
ORDER BY
|
|
508
|
+
case when pg_class.reltuples = -1 then 1 else 0 end,
|
|
509
|
+
1`,
|
|
510
|
+
},
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Names Supabase has folded into the queries above and still accepts, each
|
|
514
|
+
// routed to the query that replaced it (upstream's deprecated handlers).
|
|
515
|
+
const DEPRECATED = {
|
|
516
|
+
'cache-hit': 'db-stats',
|
|
517
|
+
'index-sizes': 'index-stats',
|
|
518
|
+
'index-usage': 'index-stats',
|
|
519
|
+
'seq-scans': 'index-stats',
|
|
520
|
+
'table-record-counts': 'index-stats',
|
|
521
|
+
'total-index-size': 'index-stats',
|
|
522
|
+
'unused-indexes': 'index-stats',
|
|
523
|
+
'role-configs': 'role-stats',
|
|
524
|
+
'role-connections': 'role-stats',
|
|
525
|
+
'table-index-sizes': 'table-stats',
|
|
526
|
+
'table-sizes': 'table-stats',
|
|
527
|
+
'total-table-sizes': 'table-stats',
|
|
528
|
+
};
|
|
529
|
+
|
|
530
|
+
// `inspect report` has no query of its own for unused indexes (the subcommand
|
|
531
|
+
// is a deprecated alias), so it carries upstream's standalone one.
|
|
532
|
+
const UNUSED_INDEXES = {
|
|
533
|
+
columns: cols([['name', 'name'], ['index', 'index'], ['index_size', 'index_size'], ['index_scans', 'index_scans']]),
|
|
534
|
+
sql: `SELECT
|
|
535
|
+
FORMAT('%I.%I', schemaname, relname) AS name,
|
|
536
|
+
indexrelname AS index,
|
|
537
|
+
pg_size_pretty(pg_relation_size(i.indexrelid)) AS index_size,
|
|
538
|
+
idx_scan as index_scans
|
|
539
|
+
FROM pg_stat_user_indexes ui
|
|
540
|
+
JOIN pg_index i ON ui.indexrelid = i.indexrelid
|
|
541
|
+
WHERE
|
|
542
|
+
NOT indisunique AND idx_scan < 50 AND pg_relation_size(relid) > 5 * 8192
|
|
543
|
+
AND NOT schemaname LIKE ANY(${IGNORED_SCHEMAS})
|
|
544
|
+
ORDER BY
|
|
545
|
+
pg_relation_size(i.indexrelid) / nullif(idx_scan, 0) DESC NULLS FIRST,
|
|
546
|
+
pg_relation_size(i.indexrelid) DESC`,
|
|
547
|
+
};
|
|
548
|
+
|
|
549
|
+
// The report's files, named like upstream's CSVs (`db_stats.csv`), in its order.
|
|
550
|
+
const REPORT = [
|
|
551
|
+
...Object.keys(QUERIES).map(name => ({ file: name.replace(/-/g, '_'), name, query: QUERIES[name] })),
|
|
552
|
+
{ file: 'unused_indexes', name: 'unused-indexes', query: UNUSED_INDEXES },
|
|
553
|
+
].sort((a, b) => a.file.localeCompare(b.file));
|
|
554
|
+
|
|
555
|
+
/** The query a subcommand name runs, following deprecated names to their replacement. */
|
|
556
|
+
function resolveQuery(name) {
|
|
557
|
+
const canonical = DEPRECATED[name] || name;
|
|
558
|
+
const query = QUERIES[canonical];
|
|
559
|
+
return query ? { name: canonical, query } : undefined;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
module.exports = { QUERIES, DEPRECATED, REPORT, INTERNAL_SCHEMAS, resolveQuery, likePattern };
|