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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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 DownExpand 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 numberDiff line numberDiff 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 numberDiff line numberDiff 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