Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add inspect report subcommand and embed queries as SQL scripts #2246

Merged
merged 39 commits into from
May 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d6f1396
initial work to split out queries into standalone scripts
encima May 5, 2024
b22a81d
add report command to export all command output to CSV files and embed
encima May 6, 2024
258b54a
add progress printing to report command
encima May 6, 2024
14f3926
add output flag to specify directory
encima May 7, 2024
7bb3427
Update internal/inspect/utils.go
encima May 7, 2024
a77b7b8
use pgx for csv output and rename flag for output path
encima May 7, 2024
effcac0
chore(deps): bump tar from 7.0.1 to 7.1.0 (#2240)
dependabot[bot] May 6, 2024
6bcac8b
chore(deps): bump github.com/golangci/golangci-lint from 1.57.2 to 1.…
dependabot[bot] May 6, 2024
c8f423c
chore(deps): bump golang.org/x/term from 0.19.0 to 0.20.0 (#2243)
dependabot[bot] May 6, 2024
6ca372b
chore(deps): bump golang.org/x/oauth2 from 0.19.0 to 0.20.0 (#2242)
dependabot[bot] May 6, 2024
542fd37
fix: bump edge-runtime to 1.47.0
laktek May 6, 2024
93dc400
fix: bump studio version (#2245)
saltcod May 6, 2024
2e4c445
chore(deps): bump github.com/gin-gonic/gin from 1.9.1 to 1.10.0 (#2249)
dependabot[bot] May 7, 2024
076a244
fix: bump edge-runtime to 1.48.0
laktek May 7, 2024
aa1df11
chore(deps): bump golangci/golangci-lint-action from 5 to 6 (#2248)
dependabot[bot] May 8, 2024
93c28f0
chore: clean up db pull implementation (#2175)
sweatybridge May 8, 2024
1d3b5f4
feat: add hook secrets
J0 Apr 6, 2024
6d8041f
Apply suggestions from code review
sweatybridge May 8, 2024
fa06642
fix: add send sms hook as test
J0 May 8, 2024
c098332
chore: typo in variable name
sweatybridge May 8, 2024
4db76cc
fix: index out of bound when applying migrations
sweatybridge May 8, 2024
e483d14
chore(deps): bump github.com/golangci/golangci-lint from 1.58.0 to 1.…
dependabot[bot] May 9, 2024
9ed1b41
chore(deps): bump github.com/charmbracelet/bubbletea from 0.26.1 to 0…
dependabot[bot] May 9, 2024
420dbf6
chore: support struct return type for pgmock
sweatybridge May 9, 2024
98cda14
fix: add unit test for cache hit
sweatybridge May 9, 2024
6083413
fix: create migrations dir before saving diff (#2260)
sweatybridge May 10, 2024
063fa38
chore(deps): bump github.com/docker/cli from 26.1.1+incompatible to 2…
dependabot[bot] May 10, 2024
3b0ce33
chore(deps): bump github.com/containers/common from 0.58.2 to 0.58.3 …
dependabot[bot] May 10, 2024
a3f8e67
chore(deps): bump github.com/docker/docker from 26.1.1+incompatible t…
dependabot[bot] May 10, 2024
9e597e7
fix: bump edge-runtime to 1.49.0
nyannyacha May 10, 2024
7dfd87f
initial work to split out queries into standalone scripts
encima May 5, 2024
4b63d29
add test cases for inspect commands
encima May 13, 2024
bd592c1
Merge branch 'develop' into feat/extract-sql
encima May 14, 2024
153b65e
chore: use pg schemas in unit tests
sweatybridge May 20, 2024
132d102
Merge branch 'develop' into feat/extract-sql
sweatybridge May 20, 2024
21c7c81
chore: test report command
sweatybridge May 21, 2024
708605d
chore: verify connection is closed
sweatybridge May 21, 2024
18f408b
chore: embed queries within each command package
sweatybridge May 21, 2024
27d3d6f
chore: move query reading to file walker
sweatybridge May 21, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
package cmd

import (
"fmt"
"os"
"os/signal"
"path/filepath"

"github.com/spf13/afero"
"github.com/spf13/cobra"
"github.com/supabase/cli/internal/inspect/bloat"
"github.com/supabase/cli/internal/inspect/blocking"
"github.com/supabase/cli/internal/inspect/cache"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/flags"

"github.com/supabase/cli/internal/inspect"
"github.com/supabase/cli/internal/inspect/calls"
"github.com/supabase/cli/internal/inspect/index_sizes"
"github.com/supabase/cli/internal/inspect/index_usage"
Expand Down Expand Up @@ -197,15 +201,31 @@ var (
return role_connections.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

outputDir string

reportCmd = &cobra.Command{
Use: "report",
Short: "Generate a CSV output for all inspect commands",
RunE: func(cmd *cobra.Command, args []string) error {
if len(outputDir) == 0 {
defaultPath := filepath.Join(utils.CurrentDirAbs, "report")
title := fmt.Sprintf("Enter a directory to save output files (or leave blank to use %s): ", utils.Bold(defaultPath))
if outputDir = utils.NewConsole().PromptText(title); len(outputDir) == 0 {
outputDir = defaultPath
}
}
return inspect.Report(cmd.Context(), outputDir, flags.DbConfig, afero.NewOsFs())
},
}
)

func init() {
inspectFlags := inspectDBCmd.PersistentFlags()
inspectFlags := inspectCmd.PersistentFlags()
inspectFlags.String("db-url", "", "Inspect the database specified by the connection string (must be percent-encoded).")
inspectFlags.Bool("linked", true, "Inspect the linked project.")
inspectFlags.Bool("local", false, "Inspect the local database.")
inspectDBCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
inspectCmd.AddCommand(inspectDBCmd)
inspectCmd.MarkFlagsMutuallyExclusive("db-url", "linked", "local")
inspectDBCmd.AddCommand(inspectCacheHitCmd)
inspectDBCmd.AddCommand(inspectReplicationSlotsCmd)
inspectDBCmd.AddCommand(inspectIndexUsageCmd)
Expand All @@ -225,5 +245,8 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectRoleConnectionsCmd)
inspectCmd.AddCommand(inspectDBCmd)
reportCmd.Flags().StringVar(&outputDir, "output-dir", "", "Path to save CSV files in")
inspectCmd.AddCommand(reportCmd)
rootCmd.AddCommand(inspectCmd)
}
8 changes: 6 additions & 2 deletions internal/inspect/bloat/bloat.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ package bloat

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/db/reset"
"github.com/supabase/cli/internal/inspect"
"github.com/supabase/cli/internal/migration/list"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/pgxv5"
)

//go:embed bloat.sql
var BloatQuery string

type Result struct {
Type string
Schemaname string
Expand All @@ -28,7 +31,8 @@ func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...fu
if err != nil {
return err
}
rows, err := conn.Query(ctx, inspect.BLOAT_QUERY, reset.LikeEscapeSchema(utils.InternalSchemas))
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, BloatQuery, reset.LikeEscapeSchema(utils.InternalSchemas))
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
Expand Down
61 changes: 61 additions & 0 deletions internal/inspect/bloat/bloat.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
WITH constants AS (
SELECT current_setting('block_size')::numeric AS bs, 23 AS hdr, 4 AS ma
), bloat_info AS (
SELECT
ma,bs,schemaname,tablename,
(datawidth+(hdr+ma-(case when hdr%ma=0 THEN ma ELSE hdr%ma END)))::numeric AS datahdr,
(maxfracsum*(nullhdr+ma-(case when nullhdr%ma=0 THEN ma ELSE nullhdr%ma END))) AS nullhdr2
FROM (
SELECT
schemaname, tablename, hdr, ma, bs,
SUM((1-null_frac)*avg_width) AS datawidth,
MAX(null_frac) AS maxfracsum,
hdr+(
SELECT 1+count(*)/8
FROM pg_stats s2
WHERE null_frac<>0 AND s2.schemaname = s.schemaname AND s2.tablename = s.tablename
) AS nullhdr
FROM pg_stats s, constants
GROUP BY 1,2,3,4,5
) AS foo
), table_bloat AS (
SELECT
schemaname, tablename, cc.relpages, bs,
CEIL((cc.reltuples*((datahdr+ma-
(CASE WHEN datahdr%ma=0 THEN ma ELSE datahdr%ma END))+nullhdr2+4))/(bs-20::float)) AS otta
FROM bloat_info
JOIN pg_class cc ON cc.relname = bloat_info.tablename
JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname AND nn.nspname <> 'information_schema'
), index_bloat AS (
SELECT
schemaname, tablename, bs,
COALESCE(c2.relname,'?') AS iname, COALESCE(c2.reltuples,0) AS ituples, COALESCE(c2.relpages,0) AS ipages,
COALESCE(CEIL((c2.reltuples*(datahdr-12))/(bs-20::float)),0) AS iotta -- very rough approximation, assumes all cols
FROM bloat_info
JOIN pg_class cc ON cc.relname = bloat_info.tablename
JOIN pg_namespace nn ON cc.relnamespace = nn.oid AND nn.nspname = bloat_info.schemaname AND nn.nspname <> 'information_schema'
JOIN pg_index i ON indrelid = cc.oid
JOIN pg_class c2 ON c2.oid = i.indexrelid
)
SELECT
type, schemaname, object_name, bloat, pg_size_pretty(raw_waste) as waste
FROM
(SELECT
'table' as type,
schemaname,
tablename as object_name,
ROUND(CASE WHEN otta=0 THEN 0.0 ELSE table_bloat.relpages/otta::numeric END,1) AS bloat,
CASE WHEN relpages < otta THEN '0' ELSE (bs*(table_bloat.relpages-otta)::bigint)::bigint END AS raw_waste
FROM
table_bloat
UNION
SELECT
'index' as type,
schemaname,
tablename || '::' || iname as object_name,
ROUND(CASE WHEN iotta=0 OR ipages=0 THEN 0.0 ELSE ipages/iotta::numeric END,1) AS bloat,
CASE WHEN ipages < iotta THEN '0' ELSE (bs*(ipages-iotta))::bigint END AS raw_waste
FROM
index_bloat) bloat_summary
WHERE NOT schemaname LIKE ANY($1)
ORDER BY raw_waste DESC, bloat DESC
43 changes: 43 additions & 0 deletions internal/inspect/bloat/bloat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package bloat

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/internal/db/reset"
"github.com/supabase/cli/internal/testing/pgtest"
"github.com/supabase/cli/internal/utils"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestBloat(t *testing.T) {
t.Run("inspects bloat", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(BloatQuery, reset.LikeEscapeSchema(utils.InternalSchemas)).
Reply("SELECT 1", Result{
Type: "index hit rate",
Schemaname: "public",
Object_name: "table",
Bloat: "0.9",
Waste: "0.1",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
9 changes: 7 additions & 2 deletions internal/inspect/blocking/blocking.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,22 @@ package blocking

import (
"context"
_ "embed"
"fmt"
"regexp"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/inspect"
"github.com/supabase/cli/internal/migration/list"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/pgxv5"
)

//go:embed blocking.sql
var BlockingQuery string

type Result struct {
Blocked_pid int
Blocking_statement string
Expand All @@ -29,7 +32,9 @@ func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...fu
if err != nil {
return err
}
rows, err := conn.Query(ctx, inspect.BLOCKING_QUERY)
defer conn.Close(context.Background())
// Ref: https://github.com/heroku/heroku-pg-extras/blob/main/commands/blocking.js#L7
rows, err := conn.Query(ctx, BlockingQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
Expand Down
15 changes: 15 additions & 0 deletions internal/inspect/blocking/blocking.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
SELECT
bl.pid AS blocked_pid,
ka.query AS blocking_statement,
age(now(), ka.query_start)::text AS blocking_duration,
kl.pid AS blocking_pid,
a.query AS blocked_statement,
age(now(), a.query_start)::text AS blocked_duration
FROM pg_catalog.pg_locks bl
JOIN pg_catalog.pg_stat_activity a
ON bl.pid = a.pid
JOIN pg_catalog.pg_locks kl
JOIN pg_catalog.pg_stat_activity ka
ON kl.pid = ka.pid
ON bl.transactionid = kl.transactionid AND bl.pid != kl.pid
WHERE NOT bl.granted
42 changes: 42 additions & 0 deletions internal/inspect/blocking/blocking_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package blocking

import (
"context"
"testing"

"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/internal/testing/pgtest"
)

var dbConfig = pgconn.Config{
Host: "127.0.0.1",
Port: 5432,
User: "admin",
Password: "password",
Database: "postgres",
}

func TestBloatCommand(t *testing.T) {
t.Run("inspects blocking", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(BlockingQuery).
Reply("SELECT 1", Result{
Blocked_pid: 1,
Blocking_statement: "select 1",
Blocking_duration: "2s",
Blocking_pid: 1,
Blocked_statement: "select 1",
Blocked_duration: "2s",
})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
// Check error
assert.NoError(t, err)
})
}
8 changes: 6 additions & 2 deletions internal/inspect/cache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,34 @@ package cache

import (
"context"
_ "embed"
"fmt"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
"github.com/supabase/cli/internal/inspect"
"github.com/supabase/cli/internal/migration/list"
"github.com/supabase/cli/internal/utils"
"github.com/supabase/cli/internal/utils/pgxv5"
)

//go:embed cache.sql
var CacheQuery string

type Result struct {
Name string
Ratio float64
}

func Run(ctx context.Context, config pgconn.Config, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
// Ref: https://github.com/heroku/heroku-pg-extras/blob/main/commands/cache_hit.js#L7
conn, err := utils.ConnectByConfig(ctx, config, options...)
if err != nil {
return err
}
defer conn.Close(context.Background())
rows, err := conn.Query(ctx, inspect.CACHE_QUERY)
rows, err := conn.Query(ctx, CacheQuery)
if err != nil {
return errors.Errorf("failed to query rows: %w", err)
}
Expand Down
9 changes: 9 additions & 0 deletions internal/inspect/cache/cache.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
SELECT
'index hit rate' AS name,
(sum(idx_blks_hit)) / nullif(sum(idx_blks_hit + idx_blks_read),0) AS ratio
FROM pg_statio_user_indexes
UNION ALL
SELECT
'table hit rate' AS name,
sum(heap_blks_hit) / nullif(sum(heap_blks_hit) + sum(heap_blks_read),0) AS ratio
FROM pg_statio_user_tables
5 changes: 2 additions & 3 deletions internal/inspect/cache/cache_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"github.com/jackc/pgconn"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/supabase/cli/internal/inspect"
"github.com/supabase/cli/internal/testing/pgtest"
)

Expand All @@ -26,7 +25,7 @@ func TestCacheCommand(t *testing.T) {
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(inspect.CACHE_QUERY).
conn.Query(CacheQuery).
Reply("SELECT 1", Result{
Name: "index hit rate",
Ratio: 0.9,
Expand All @@ -43,7 +42,7 @@ func TestCacheCommand(t *testing.T) {
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(inspect.CACHE_QUERY).
conn.Query(CacheQuery).
Reply("SELECT 1", []interface{}{})
// Run test
err := Run(context.Background(), dbConfig, fsys, conn.Intercept)
Expand Down
Loading