Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
16 changes: 13 additions & 3 deletions internal/db/diff/pgdelta.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
_ "embed"
"encoding/json"
"os"
"path/filepath"
"strings"

"github.com/go-errors/errors"
Expand Down Expand Up @@ -47,12 +48,14 @@ func isPostgresURL(ref string) bool {

// containerRef translates a host-relative catalog file path into the absolute
// path where it appears inside the edge runtime container (CWD mounted at
// /workspace). Postgres URLs and empty strings pass through unchanged.
// /workspace). Postgres URLs and empty strings pass through unchanged. Path
// separators are normalised to forward slashes so Windows paths (with `\`)
// resolve correctly inside the Linux container.
func containerRef(ref string) string {
if ref == "" || isPostgresURL(ref) {
return ref
}
return "/workspace/" + ref
return "/workspace/" + filepath.ToSlash(ref)
}

// pgDeltaFormatOptions returns the experimental.pgdelta.format_options config for
Expand Down Expand Up @@ -143,6 +146,9 @@ func DeclarativeExportPgDeltaRef(ctx context.Context, sourceRef, targetRef strin
if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaDeclarativeExportScript, binds, "error exporting declarative schema", &stdout, &stderr); err != nil {
return DeclarativeOutput{}, err
}
if stdout.Len() == 0 {
return DeclarativeOutput{}, errors.Errorf("error exporting declarative schema: edge-runtime script produced no output:\n%s", stderr.String())
}
var result DeclarativeOutput
if err := json.Unmarshal(stdout.Bytes(), &result); err != nil {
return DeclarativeOutput{}, errors.Errorf("failed to parse declarative export output: %w", err)
Expand Down Expand Up @@ -176,5 +182,9 @@ func ExportCatalogPgDelta(ctx context.Context, targetRef, role string, options .
if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportScript, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil {
return "", err
}
return strings.TrimSpace(stdout.String()), nil
snapshot := strings.TrimSpace(stdout.String())
if len(snapshot) == 0 {
return "", errors.Errorf("error exporting pg-delta catalog: edge-runtime script produced no output:\n%s", stderr.String())
}
return snapshot, nil
}
34 changes: 34 additions & 0 deletions internal/db/diff/pgdelta_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package diff

import (
"runtime"
"testing"

"github.com/stretchr/testify/assert"
)

func TestContainerRef(t *testing.T) {
t.Run("passes empty string through", func(t *testing.T) {
assert.Equal(t, "", containerRef(""))
})

t.Run("passes postgres URLs through", func(t *testing.T) {
assert.Equal(t, "postgresql://user@host:5432/db", containerRef("postgresql://user@host:5432/db"))
assert.Equal(t, "postgres://user@host:5432/db", containerRef("postgres://user@host:5432/db"))
})

t.Run("normalises Windows path separators", func(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("path separator behaviour is Windows-only")
}
// On Windows, filepath.Join produces backslashes which the Linux
// container cannot read; containerRef must convert them.
ref := `supabase\.temp\pgdelta\catalog-baseline-17.6.1.106.json`
assert.Equal(t, "/workspace/supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json", containerRef(ref))
})

t.Run("leaves unix paths untouched", func(t *testing.T) {
ref := "supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json"
assert.Equal(t, "/workspace/supabase/.temp/pgdelta/catalog-baseline-17.6.1.106.json", containerRef(ref))
})
}
7 changes: 6 additions & 1 deletion internal/db/pgcache/cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"
"time"

"github.com/go-errors/errors"
"github.com/jackc/pgconn"
"github.com/jackc/pgx/v4"
"github.com/spf13/afero"
Expand Down Expand Up @@ -255,5 +256,9 @@ func exportCatalog(ctx context.Context, targetRef string, options ...func(*pgx.C
if err := utils.RunEdgeRuntimeScript(ctx, env, pgDeltaCatalogExportTS, binds, "error exporting pg-delta catalog", &stdout, &stderr); err != nil {
return "", err
}
return strings.TrimSpace(stdout.String()), nil
snapshot := strings.TrimSpace(stdout.String())
if len(snapshot) == 0 {
return "", errors.Errorf("error exporting pg-delta catalog: edge-runtime script produced no output:\n%s", stderr.String())
}
return snapshot, nil
}
14 changes: 12 additions & 2 deletions internal/telemetry/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ const SchemaVersion = 1

const sessionRotationThreshold = 30 * time.Minute

// errMalformedState marks any read where the file existed but couldn't be
// decoded into a State — covers JSON syntax errors, unexpected types
// (e.g. session_last_active stored as a number), and field-level unmarshal
// failures from time.Time / uuid. Used to trigger fresh-state creation.
var errMalformedState = errors.New("malformed telemetry state")

type State struct {
Enabled bool `json:"enabled"`
DeviceID string `json:"device_id"`
Expand Down Expand Up @@ -48,7 +54,7 @@ func LoadState(fsys afero.Fs) (State, error) {
}
var state State
if err := json.Unmarshal(contents, &state); err != nil {
return State{}, errors.Errorf("failed to parse telemetry file: %w", err)
return State{}, errors.Errorf("%w: %v", errMalformedState, err)
}
return state, nil
}
Expand All @@ -74,7 +80,11 @@ func LoadOrCreateState(fsys afero.Fs, now time.Time) (State, bool, error) {
state.SessionLastActive = now.UTC()
return state, false, SaveState(state, fsys)
}
if !errors.Is(err, os.ErrNotExist) {
// Treat a missing file OR an unparseable file as "no existing state" and
// recreate. Identity fields (device_id, session_id) are not worth
// surfacing an error for — losing them is harmless. We only propagate
// genuine I/O errors (permissions, disk full) so the user can act.
if !errors.Is(err, os.ErrNotExist) && !errors.Is(err, errMalformedState) {
return State{}, false, err
}
state = State{
Expand Down
32 changes: 32 additions & 0 deletions internal/telemetry/state_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,38 @@ func TestLoadOrCreateState(t *testing.T) {
assert.Equal(t, now, state.SessionLastActive)
})

t.Run("recovers from corrupted state file", func(t *testing.T) {
// Each entry simulates a real-world corruption shape we've observed.
corruptions := map[string][]byte{
"empty file": {},
"truncated json": []byte(`{"enabled":tru`),
"session_last_active is a number (not a string)": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":1776770348993,"schema_version":1}`),
"session_last_active is a malformed string": []byte(`{"enabled":true,"device_id":"d","session_id":"s","session_last_active":"not-a-time","schema_version":1}`),
}
for label, contents := range corruptions {
t.Run(label, func(t *testing.T) {
t.Setenv("SUPABASE_HOME", "/tmp/supabase-home")
fsys := afero.NewMemMapFs()
path, err := telemetryPath()
require.NoError(t, err)
require.NoError(t, fsys.MkdirAll("/tmp/supabase-home", 0755))
require.NoError(t, afero.WriteFile(fsys, path, contents, 0644))

state, created, err := LoadOrCreateState(fsys, now)

require.NoError(t, err)
assert.True(t, created)
assert.True(t, state.Enabled)
assert.Equal(t, SchemaVersion, state.SchemaVersion)
assert.NoError(t, uuid.Validate(state.DeviceID))
assert.NoError(t, uuid.Validate(state.SessionID))
saved, err := LoadState(fsys)
require.NoError(t, err)
assert.Equal(t, state, saved)
})
}
})

t.Run("rotates stale session after inactivity threshold", func(t *testing.T) {
t.Setenv("SUPABASE_HOME", "/tmp/supabase-home")
fsys := afero.NewMemMapFs()
Expand Down
Loading