Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
886a008
fix: allow overriding config after load (#4336)
sweatybridge Oct 21, 2025
e5b1f6d
fix(docker): bump the docker-minor group in /pkg/config/templates wit…
dependabot[bot] Oct 22, 2025
3528950
fix(docker): bump supabase/postgres from 17.6.1.024 to 17.6.1.025 in …
dependabot[bot] Oct 22, 2025
15ecc83
fix: initialise logflare version path (#4343)
sweatybridge Oct 22, 2025
8d34a0f
fix: remove long poll router for realtime (#4344)
sweatybridge Oct 22, 2025
1240cbc
feat: command to analyze table read/write I/O patterns (#4334)
NickLittman Oct 22, 2025
9af9070
fix(docker): bump supabase/postgres from 17.6.1.025 to 17.6.1.026 in …
dependabot[bot] Oct 23, 2025
9bdb4e2
chore(deps): bump github.com/getsentry/sentry-go from 0.36.0 to 0.36.…
dependabot[bot] Oct 23, 2025
c6dc82d
fix: consistent role and search path for diff
sweatybridge Oct 23, 2025
c4e1791
chore: deprecate python migra
sweatybridge Oct 23, 2025
877d0ce
fix: deprecate python migra in db pull (#4350)
sweatybridge Oct 23, 2025
341b3c3
fix: base client should also use schema qualified reference (#4353)
sweatybridge Oct 24, 2025
580f39f
fix(docker): bump the docker-minor group across 1 directory with 2 up…
dependabot[bot] Oct 24, 2025
974db00
chore(deps): bump bin-links from 5.0.0 to 6.0.0 (#4354)
dependabot[bot] Oct 24, 2025
6d83868
fix: 2nd pass when pulling initial schema (#4357)
sweatybridge Oct 24, 2025
8bf44da
fix(docker): bump supabase/postgres from 17.6.1.026 to 17.6.1.029 in …
dependabot[bot] Oct 27, 2025
1797d16
fix(docker): bump the docker-minor group across 1 directory with 2 up…
dependabot[bot] Oct 27, 2025
b6cbd23
chore(deps): bump actions/upload-artifact from 4 to 5 (#4363)
dependabot[bot] Oct 27, 2025
e1c3bfc
chore(deps): bump actions/download-artifact from 5 to 6 (#4362)
dependabot[bot] Oct 27, 2025
64465dd
fix: validate pooler domain per profile (#4358)
sweatybridge Oct 27, 2025
a427009
chore: sync API types from infrastructure (#4345)
kiwicopple Oct 27, 2025
de23187
fix(docker): bump the docker-minor group in /pkg/config/templates wit…
dependabot[bot] Oct 28, 2025
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
4 changes: 2 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
go tool gotestsum -- -race -v -count=1 ./... \
-coverpkg="./cmd/...,./internal/...,${pkgs}" -coverprofile=coverage.out

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: code-coverage-report
path: coverage.out
Expand All @@ -39,7 +39,7 @@ jobs:
- test
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: code-coverage-report
- uses: coverallsapp/github-action@v2
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/install.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ jobs:
mv tmp.$$.json package.json
npm pack

- uses: actions/upload-artifact@v4
- uses: actions/upload-artifact@v5
with:
name: installer
path: supabase-1.28.0.tgz
Expand All @@ -41,7 +41,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: installer

Expand All @@ -57,7 +57,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: installer

Expand All @@ -73,7 +73,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: installer

Expand All @@ -96,7 +96,7 @@ jobs:
os: [ubuntu-latest, macos-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: installer

Expand All @@ -115,7 +115,7 @@ jobs:
os: [ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/download-artifact@v5
- uses: actions/download-artifact@v6
with:
name: installer

Expand Down
2 changes: 0 additions & 2 deletions cmd/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ var (
if usePgSchema {
differ = diff.DiffPgSchema
fmt.Fprintln(os.Stderr, utils.Yellow("WARNING:"), "--use-pg-schema flag is experimental and may not include all entities, such as views and grants.")
} else if !viper.GetBool("EXPERIMENTAL") {
differ = diff.DiffSchemaMigraBash
}
return diff.Run(cmd.Context(), schema, file, flags.DbConfig, differ, afero.NewOsFs())
},
Expand Down
10 changes: 10 additions & 0 deletions cmd/inspect.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/supabase/cli/internal/inspect/replication_slots"
"github.com/supabase/cli/internal/inspect/role_stats"
"github.com/supabase/cli/internal/inspect/table_stats"
"github.com/supabase/cli/internal/inspect/traffic_profile"
"github.com/supabase/cli/internal/inspect/vacuum_stats"
"github.com/supabase/cli/internal/utils/flags"
)
Expand Down Expand Up @@ -135,6 +136,14 @@ var (
},
}

inspectTrafficProfileCmd = &cobra.Command{
Use: "traffic-profile",
Short: "Show read/write activity ratio for tables based on block I/O operations",
RunE: func(cmd *cobra.Command, args []string) error {
return traffic_profile.Run(cmd.Context(), flags.DbConfig, afero.NewOsFs())
},
}

inspectCacheHitCmd = &cobra.Command{
Deprecated: `use "db-stats" instead.`,
Use: "cache-hit",
Expand Down Expand Up @@ -270,6 +279,7 @@ func init() {
inspectDBCmd.AddCommand(inspectBloatCmd)
inspectDBCmd.AddCommand(inspectVacuumStatsCmd)
inspectDBCmd.AddCommand(inspectTableStatsCmd)
inspectDBCmd.AddCommand(inspectTrafficProfileCmd)
inspectDBCmd.AddCommand(inspectRoleStatsCmd)
inspectDBCmd.AddCommand(inspectDBStatsCmd)
// DEPRECATED
Expand Down
24 changes: 24 additions & 0 deletions docs/supabase/inspect/db-traffic-profile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# db-traffic-profile

This command analyzes table I/O patterns to show read/write activity ratios based on block-level operations. It combines data from PostgreSQL's `pg_stat_user_tables` (for tuple operations) and `pg_statio_user_tables` (for block I/O) to categorize each table's workload profile.


The command classifies tables into categories:
- **Read-Heavy** - Read operations are more than 5x write operations (e.g., 1:10, 1:50)
- **Write-Heavy** - Write operations are more than 20% of read operations (e.g., 1:2, 1:4, 2:1, 10:1)
- **Balanced** - Mixed workload where writes are between 20% and 500% of reads
- **Read-Only** - Only read operations detected
- **Write-Only** - Only write operations detected

```
SCHEMA │ TABLE │ BLOCKS READ │ WRITE TUPLES │ BLOCKS WRITE │ ACTIVITY RATIO
───────┼──────────────┼─────────────┼──────────────┼──────────────┼────────────────────
public │ user_events │ 450,234 │ 9,004,680│ 23,450 │ 20:1 (Write-Heavy)
public │ users │ 89,203 │ 12,451│ 1,203 │ 7.2:1 (Read-Heavy)
public │ sessions │ 15,402 │ 14,823│ 2,341 │ ≈1:1 (Balanced)
public │ cache_data │ 123,456 │ 0│ 0 │ Read-Only
auth │ audit_logs │ 0 │ 98,234│ 12,341 │ Write-Only
```

**Note:** This command only displays tables that have had both read and write activity. Tables with no I/O operations are not shown. The classification ratio threshold (default: 5:1) determines when a table is considered "heavy" in one direction versus balanced.

2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ require (
github.com/docker/docker v28.5.1+incompatible
github.com/docker/go-connections v0.6.0
github.com/fsnotify/fsnotify v1.9.0
github.com/getsentry/sentry-go v0.36.0
github.com/getsentry/sentry-go v0.36.1
github.com/go-errors/errors v1.5.1
github.com/go-git/go-git/v5 v5.16.3
github.com/go-playground/validator/v10 v10.28.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,8 @@ github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIp
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
github.com/getkin/kin-openapi v0.131.0 h1:NO2UeHnFKRYhZ8wg6Nyh5Cq7dHk4suQQr72a4pMrDxE=
github.com/getkin/kin-openapi v0.131.0/go.mod h1:3OlG51PCYNsPByuiMB0t4fjnNlIDnaEDsjiKUV8nL58=
github.com/getsentry/sentry-go v0.36.0 h1:UkCk0zV28PiGf+2YIONSSYiYhxwlERE5Li3JPpZqEns=
github.com/getsentry/sentry-go v0.36.0/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
github.com/getsentry/sentry-go v0.36.1 h1:kMJt0WWsxWATUxkvFgVBZdIeHSk/Oiv5P0jZ9e5m/Lw=
github.com/getsentry/sentry-go v0.36.1/go.mod h1:p5Im24mJBeruET8Q4bbcMfCQ+F+Iadc4L48tB1apo2c=
github.com/ghostiam/protogetter v0.3.15 h1:1KF5sXel0HE48zh1/vn0Loiw25A9ApyseLzQuif1mLY=
github.com/ghostiam/protogetter v0.3.15/go.mod h1:WZ0nw9pfzsgxuRsPOFQomgDVSWtDLJRfQJEhsGbmQMA=
github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=
Expand Down
2 changes: 1 addition & 1 deletion internal/branches/get/get.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,7 @@ func getPoolerConfig(ctx context.Context, ref string) (api.SupavisorConfigRespon
return result, errors.Errorf("unexpected get pooler status %d: %s", resp.StatusCode(), string(resp.Body))
}
for _, config := range *resp.JSON200 {
if config.DatabaseType == api.PRIMARY {
if config.DatabaseType == api.SupavisorConfigResponseDatabaseTypePRIMARY {
return config, nil
}
}
Expand Down
19 changes: 12 additions & 7 deletions internal/db/diff/templates/migra.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { createClient } from "npm:@pgkit/client";
import { createClient, sql } from "npm:@pgkit/client";
import { Migration } from "npm:@pgkit/migra";

// Avoids error on self-signed certificate
Expand All @@ -20,7 +20,12 @@ const extensionSchemas = [
];

try {
let sql = "";
// Step down from login role to postgres
await clientHead.query(sql`set role postgres`);
// Force schema qualified references for pg_get_expr
await clientHead.query(sql`set search_path = ''`);
await clientBase.query(sql`set search_path = ''`);
let result = "";
for (const schema of includedSchemas) {
const m = await Migration.create(clientBase, clientHead, {
schema,
Expand All @@ -35,7 +40,7 @@ try {
} else {
m.add_all_changes(true);
}
sql += m.sql;
result += m.sql;
}
if (includedSchemas.length === 0) {
// Migra does not ignore custom types and triggers created by extensions, so we diff
Expand All @@ -48,7 +53,7 @@ try {
e.set_safety(false);
e.add(e.changes.schemas({ creations_only: true }));
e.add_extension_changes();
sql += e.sql;
result += e.sql;
}
// Diff user defined entities in non-managed schemas, including extensions.
const m = await Migration.create(clientBase, clientHead, {
Expand All @@ -61,7 +66,7 @@ try {
});
m.set_safety(false);
m.add_all_changes(true);
sql += m.sql;
result += m.sql;
// For managed schemas, we want to include triggers and RLS policies only.
for (const schema of managedSchemas) {
const s = await Migration.create(clientBase, clientHead, {
Expand All @@ -73,10 +78,10 @@ try {
s.add(s.changes.rlspolicies({ drops_only: true }));
s.add(s.changes.rlspolicies({ creations_only: true }));
s.add(s.changes.triggers({ creations_only: true }));
sql += s.sql;
result += s.sql;
}
}
console.log(sql);
console.log(result);
} catch (e) {
console.error(e);
} finally {
Expand Down
80 changes: 8 additions & 72 deletions internal/db/pull/pull.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ import (
"math"
"os"
"path/filepath"
"slices"
"strconv"
"strings"

Expand All @@ -17,7 +16,6 @@ import (
"github.com/spf13/afero"
"github.com/supabase/cli/internal/db/diff"
"github.com/supabase/cli/internal/db/dump"
"github.com/supabase/cli/internal/db/start"
"github.com/supabase/cli/internal/migration/list"
"github.com/supabase/cli/internal/migration/new"
"github.com/supabase/cli/internal/migration/repair"
Expand All @@ -26,10 +24,9 @@ import (
)

var (
errMissing = errors.New("No migrations found")
errInSync = errors.New("No schema changes found")
errConflict = errors.Errorf("The remote database's migration history does not match local files in %s directory.", utils.MigrationsDir)
managedSchemas = []string{"auth", "storage", "realtime"}
errMissing = errors.New("No migrations found")
errInSync = errors.New("No schema changes found")
errConflict = errors.Errorf("The remote database's migration history does not match local files in %s directory.", utils.MigrationsDir)
)

func Run(ctx context.Context, schema []string, config pgconn.Config, name string, fsys afero.Fs, options ...func(*pgx.ConnConfig)) error {
Expand Down Expand Up @@ -63,24 +60,16 @@ func run(ctx context.Context, schema []string, path string, conn *pgx.Conn, fsys
if err = dumpRemoteSchema(ctx, path, config, fsys); err != nil {
return err
}
// Pull changes in managed schemas automatically
if err = diffRemoteSchema(ctx, managedSchemas, path, config, fsys); errors.Is(err, errInSync) {
// Run a second pass to pull in changes from default privileges and managed schemas
if err = diffRemoteSchema(ctx, nil, path, config, fsys); errors.Is(err, errInSync) {
err = nil
}
return err
} else if err != nil {
return err
}
// 2. Fetch user defined schemas
if len(schema) == 0 {
var err error
if schema, err = migration.ListUserSchemas(ctx, conn); err != nil {
return err
}
schema = append(schema, managedSchemas...)
}
// 3. Fetch remote schema changes
return diffUserSchemas(ctx, schema, path, config, fsys)
// 2. Fetch remote schema changes
return diffRemoteSchema(ctx, schema, path, config, fsys)
}

func dumpRemoteSchema(ctx context.Context, path string, config pgconn.Config, fsys afero.Fs) error {
Expand All @@ -103,7 +92,7 @@ func diffRemoteSchema(ctx context.Context, schema []string, path string, config
if err != nil {
return err
}
if len(output) == 0 {
if trimmed := strings.TrimSpace(output); len(trimmed) == 0 {
return errors.New(errInSync)
}
// Append to existing migration file since we run this after dump
Expand All @@ -118,59 +107,6 @@ func diffRemoteSchema(ctx context.Context, schema []string, path string, config
return nil
}

func diffUserSchemas(ctx context.Context, schema []string, path string, config pgconn.Config, fsys afero.Fs) error {
var managed, user []string
for _, s := range schema {
if slices.Contains(managedSchemas, s) {
managed = append(managed, s)
} else {
user = append(user, s)
}
}
fmt.Fprintln(os.Stderr, "Creating shadow database...")
shadow, err := diff.CreateShadowDatabase(ctx, utils.Config.Db.ShadowPort)
if err != nil {
return err
}
defer utils.DockerRemove(shadow)
if err := start.WaitForHealthyService(ctx, start.HealthTimeout, shadow); err != nil {
return err
}
if err := diff.MigrateShadowDatabase(ctx, shadow, fsys); err != nil {
return err
}
shadowConfig := pgconn.Config{
Host: utils.Config.Hostname,
Port: utils.Config.Db.ShadowPort,
User: "postgres",
Password: utils.Config.Db.Password,
Database: "postgres",
}
// Diff managed and user defined schemas separately
var output string
if len(user) > 0 {
fmt.Fprintln(os.Stderr, "Diffing schemas:", strings.Join(user, ","))
if output, err = diff.DiffSchemaMigraBash(ctx, shadowConfig, config, user); err != nil {
return err
}
}
if len(managed) > 0 {
fmt.Fprintln(os.Stderr, "Diffing schemas:", strings.Join(managed, ","))
if result, err := diff.DiffSchemaMigra(ctx, shadowConfig, config, managed); err != nil {
return err
} else {
output += result
}
}
if len(output) == 0 {
return errors.New(errInSync)
}
if err := utils.WriteFile(path, []byte(output), fsys); err != nil {
return errors.Errorf("failed to write dump file: %w", err)
}
return nil
}

func assertRemoteInSync(ctx context.Context, conn *pgx.Conn, fsys afero.Fs) error {
remoteMigrations, err := migration.ListRemoteMigrations(ctx, conn)
if err != nil {
Expand Down
18 changes: 0 additions & 18 deletions internal/db/pull/pull_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,24 +83,6 @@ func TestPullSchema(t *testing.T) {
assert.Equal(t, []byte("test"), contents)
})

t.Run("throws error on load user schema failure", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
path := filepath.Join(utils.MigrationsDir, "0_test.sql")
require.NoError(t, afero.WriteFile(fsys, path, []byte(""), 0644))
// Setup mock postgres
conn := pgtest.NewConn()
defer conn.Close(t)
conn.Query(migration.LIST_MIGRATION_VERSION).
Reply("SELECT 1", []any{"0"}).
Query(migration.ListSchemas, migration.ManagedSchemas).
ReplyError(pgerrcode.DuplicateTable, `relation "test" already exists`)
// Run test
err := run(context.Background(), nil, "", conn.MockClient(t), fsys)
// Check error
assert.ErrorContains(t, err, `ERROR: relation "test" already exists (SQLSTATE 42P07)`)
})

t.Run("throws error on diff failure", func(t *testing.T) {
// Setup in-memory fs
fsys := afero.NewMemMapFs()
Expand Down
1 change: 1 addition & 0 deletions internal/gen/bearerjwt/bearerjwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@ func TestGenerateToken(t *testing.T) {
claims := jwt.MapClaims{}
// Setup in-memory fs
fsys := afero.NewMemMapFs()
require.NoError(t, utils.WriteFile("supabase/keys.json", []byte("[]"), fsys))
require.NoError(t, utils.WriteFile("supabase/config.toml", []byte(`
[auth]
signing_keys_path = "./keys.json"
Expand Down
Loading