Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
49 commits
Select commit Hold shift + click to select a range
47050a6
feat(cli): port supabase test db and test new
Coly010 Jun 9, 2026
d583aa0
fix(cli): align test db connection resolver with Go CLI parity
Coly010 Jun 9, 2026
e3a27ca
fix(cli): close test db Go-parity gaps from Codex review
Coly010 Jun 9, 2026
ae9f810
fix(cli): match Go pgconn for test db remote connections
Coly010 Jun 10, 2026
659eee2
fix(cli): return read errors for unreadable test db config.toml
Coly010 Jun 10, 2026
480959e
fix(cli): keep test db secrets out of docker argv and match Go pooler…
Coly010 Jun 10, 2026
91b2806
fix(cli): default omitted URL user to OS account in test db connectio…
Coly010 Jun 10, 2026
13445e2
fix(cli): preserve SNI for all TLS modes when DoH-resolving test db host
Coly010 Jun 10, 2026
da2320b
fix(cli): expand env(VAR) in test db config.toml port/password fields
Coly010 Jun 10, 2026
0da7957
fix(cli): reject invalid configured test db ports instead of defaulting
Coly010 Jun 10, 2026
e9a8316
fix(cli): honor PGUSER when defaulting test db connection user
Coly010 Jun 10, 2026
db43094
fix(cli): load project .env files before expanding env() in test db c…
Coly010 Jun 10, 2026
fa91dcf
fix(cli): apply libpq env defaults to db URLs and strip dotenv inline…
Coly010 Jun 10, 2026
cee0ab7
fix(cli): unbracket IPv6 db URL hosts and connect plaintext for sslmo…
Coly010 Jun 10, 2026
049b96a
fix(cli): reject non-Postgres db URL schemes and add sslmode=allow TL…
Coly010 Jun 10, 2026
b59290a
fix(cli): fill local db-url password from config when omitted
Coly010 Jun 10, 2026
7808132
fix(cli): retry all DoH-resolved database addresses
Coly010 Jun 10, 2026
75bf377
fix(cli): bracket IPv6 hosts when building option connection URLs
Coly010 Jun 10, 2026
24c154d
fix(cli): fail on unreadable project .env files in test db config
Coly010 Jun 10, 2026
2ffabf4
fix(cli): honor SUPABASE_DB_* env overrides for test db config
Coly010 Jun 10, 2026
67339d0
fix(cli): honor libpq query params in db URLs for test db
Coly010 Jun 10, 2026
a2443c7
fix(cli): read linked DB password from project .env files
Coly010 Jun 10, 2026
083895b
fix(cli): accept godotenv colon assignments and reject non-numeric qu…
Coly010 Jun 10, 2026
ace702f
fix(cli): honor PGSSLMODE when db-url omits sslmode
Coly010 Jun 10, 2026
60806bc
fix(cli): reject invalid PGPORT instead of defaulting to 5432
Coly010 Jun 10, 2026
f62af7b
fix(cli): reject empty/explicit invalid db-url ?port= overrides
Coly010 Jun 10, 2026
a9f3324
fix(cli): resolve test db pg_prove image through the configured registry
Coly010 Jun 10, 2026
4fe6df5
fix(cli): prefer plaintext fallback and verify-ca hostname skip for t…
Coly010 Jun 10, 2026
2bad585
fix(cli): reject invalid sslmode and honor sslrootcert for test db TLS
Coly010 Jun 10, 2026
5738da6
fix(cli): fall back to .pgpass for omitted test db password
Coly010 Jun 10, 2026
71b1b67
fix(cli): match pgconn TLS/password/multi-host parity for test db con…
Coly010 Jun 11, 2026
6b9024e
fix(cli): strip IPv6 brackets from DOCKER_HOST to match Go net.SplitH…
Coly010 Jun 11, 2026
8515b8d
fix(cli): match pgconn unix-socket plaintext and auth-failure fallbac…
Coly010 Jun 11, 2026
6770756
fix(cli): honor DSN passfile and project dotenv PG* defaults for test…
Coly010 Jun 11, 2026
34ca900
fix(cli): expand $VAR references in project .env files to match godot…
Coly010 Jun 11, 2026
582c1f4
fix(cli): omit docker --security-opt in Bitbucket Pipelines to match …
Coly010 Jun 11, 2026
02313c2
fix(cli): percent-encode unix-socket host in connection URL so socket…
Coly010 Jun 11, 2026
85dc98c
fix(cli): resolve libpq service DSNs for test db connections to match…
Coly010 Jun 11, 2026
2ebf98d
fix(cli): match Go connect semantics for test db (eager probe, local …
Coly010 Jun 11, 2026
b780c37
fix(cli): match pgconn DSN parsing for test db (backslashes, connect_…
Coly010 Jun 11, 2026
fc60062
fix(cli): tighten libpq DSN parsing parity for test db (ports, servic…
Coly010 Jun 11, 2026
070c6ac
fix(cli): pgconn parity for database= alias, empty service values, an…
Coly010 Jun 11, 2026
f280cd3
fix(cli): more pgconn DSN parse parity for test db (empty host/servic…
Coly010 Jun 11, 2026
319307b
fix(cli): more Go parity for test db (CA per fallback, DSN alias last…
Coly010 Jun 11, 2026
09f4498
fix(cli): scan multiline quoted dotenv values to match godotenv (revi…
Coly010 Jun 11, 2026
dbd1f31
fix(cli): keep test db TAP stdout clean on machine-format run failure…
Coly010 Jun 11, 2026
461bba3
fix(cli): validate DoH-resolved values are IPs to prevent credential …
Coly010 Jun 11, 2026
b33b755
fix(cli): recognize Windows absolute socket paths to match pgconn isA…
Coly010 Jun 11, 2026
bf4c538
fix(cli): default user from OS account and honor empty passfile overr…
Coly010 Jun 11, 2026
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: 15 additions & 1 deletion apps/cli-e2e/src/tests/database-core.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,5 +365,19 @@ describe("test db", () => {
expect(result.stderr).toContain("connect");
});

testParity(["test", "db", "--local"]);
// No testParity for `test db --local`: with no local Postgres listening in the
// harness, the only reachable path is the connection-failure path, and its
// stderr diverges by driver in ways that aren't cosmetic and can't be
// normalized away. Both now emit Go's leading diagnostic to stderr:
// Connecting to local database...
// but the connect-error body and trailing hint still differ by driver. Go (pgx):
// failed to connect to postgres: failed to connect to `host=… user=… database=…`: dial error (dial tcp …: connect: connection refused)
// Make sure your local IP is allowed in Network Restrictions and Network Bans.
// http://…/project/_/database/settings
// The TS port (@effect/sql-pg) prints the effect SqlError and the --debug hint:
// failed to connect to postgres: effect/sql/SqlError: PgClient: Failed to connect
// Try rerunning the command with --debug to troubleshoot the error.
// The meaningful contract (non-zero exit + a connect error on stderr) is
// covered by the behaviour test above. A real connect-path parity test would
// need a live local database in the harness.
});
116 changes: 58 additions & 58 deletions apps/cli/docs/go-cli-porting-status.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@clack/prompts": "^1.5.1",
"@effect/atom-react": "catalog:",
"@effect/platform-bun": "catalog:",
"@effect/sql-pg": "catalog:",
"@effect/vitest": "catalog:",
"@modelcontextprotocol/sdk": "^1.29.0",
"@napi-rs/keyring": "^1.3.0",
Expand Down Expand Up @@ -70,6 +71,7 @@
"react-devtools-core": "^7.0.1",
"semantic-release": "^25.0.3",
"smol-toml": "^1.6.1",
"tldts": "catalog:",
"vitest": "catalog:",
"yaml": "^2.9.0"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ function mockCliConfig(opts: {
profile: opts.profile ?? "supabase",
apiUrl: opts.apiUrl ?? "https://api.supabase.com",
projectHost: opts.projectHost ?? "supabase.co",
poolerHost: "supabase.com",
accessToken:
opts.accessToken === undefined ? Option.none() : Option.some(Redacted.make(opts.accessToken)),
projectId: Option.none(),
Expand Down
47 changes: 0 additions & 47 deletions apps/cli/src/legacy/commands/bootstrap/bootstrap.dotenv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,50 +125,3 @@ export function marshalDotEnv(env: Readonly<Record<string, string>>): string {
lines.sort();
return lines.join("\n");
}

// godotenv.Parse-compatible enough for `.env.example`: `KEY=VALUE` / `KEY="VALUE"`
// lines, `#` comments, blank lines. A line with an empty / invalid variable name
// throws (Go's `godotenv.Parse` surfaces `unexpected character ... in variable name`).
const EXPORT_PREFIX = /^\s*export\s+/;

/**
* Minimal godotenv parser for `.env.example`. Returns the parsed key/value map.
* Throws an `Error` whose message mirrors Go's parser for a malformed variable
* name so the caller can surface the same failure (`"!="` → unexpected character).
*/
export function parseDotEnv(contents: string): Record<string, string> {
const result: Record<string, string> = {};
for (const rawLine of contents.split("\n")) {
const line = rawLine.replace(EXPORT_PREFIX, "").trim();
if (line.length === 0 || line.startsWith("#")) {
continue;
}
const eq = line.indexOf("=");
if (eq <= 0) {
const offending = line.slice(0, eq < 0 ? line.length : eq + 1);
throw new Error(
`unexpected character "${line[0] ?? ""}" in variable name near "${offending}"`,
);
}
const key = line.slice(0, eq).trim();
if (!/^[A-Za-z_][A-Za-z0-9_.]*$/.test(key)) {
throw new Error(`unexpected character "${key[0] ?? ""}" in variable name near "${line}"`);
}
let value = line.slice(eq + 1).trim();
if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
// godotenv expands escapes inside double-quoted values: `\n` / `\r` become
// real newlines, and a backslash before any other char (except `$`) is
// dropped (`\"` -> `"`, `\\` -> `\`).
value = value
.slice(1, -1)
.replaceAll("\\n", "\n")
.replaceAll("\\r", "\r")
.replace(/\\([^$])/g, "$1");
} else if (value.startsWith("'") && value.endsWith("'") && value.length >= 2) {
// Single-quoted values are taken literally (no escape expansion).
value = value.slice(1, -1);
}
result[key] = value;
}
return result;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { ApiKeyResponse } from "@supabase/api/effect";
import { describe, expect, it } from "vitest";

import { buildDotEnv, marshalDotEnv, parseDotEnv } from "./bootstrap.dotenv.ts";
import { buildDotEnv, marshalDotEnv } from "./bootstrap.dotenv.ts";
import type { LegacyDbConfig } from "./bootstrap.pgconfig.ts";

type ApiKey = typeof ApiKeyResponse.Type;
Expand Down Expand Up @@ -94,28 +94,3 @@ describe("marshalDotEnv", () => {
);
});
});

describe("parseDotEnv", () => {
it("parses KEY=VALUE lines, skipping comments and blanks, and strips quotes", () => {
expect(parseDotEnv('# comment\nFOO=bar\n\nBAZ="quoted"\nexport QUX=1')).toEqual({
FOO: "bar",
BAZ: "quoted",
QUX: "1",
});
});

it("expands escape sequences in double-quoted values (godotenv parity)", () => {
expect(parseDotEnv('A="line1\\nline2"\nB="a\\"b\\\\c"')).toEqual({
A: "line1\nline2",
B: 'a"b\\c',
});
});

it("takes single-quoted values literally (no escape expansion)", () => {
expect(parseDotEnv("A='line1\\nline2'")).toEqual({ A: "line1\\nline2" });
});

it("throws Go's 'unexpected character' error on a malformed variable name", () => {
expect(() => parseDotEnv("!=")).toThrow(/unexpected character "!" in variable name/);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,9 @@ import { legacyLinkServicesCore } from "../../shared/legacy-link-services-core.t
import { legacyProjectCreateCore } from "../../shared/legacy-project-create-core.ts";
import { legacyTempPaths } from "../../shared/legacy-temp-paths.ts";
import { legacyExtractServiceKeys } from "../../shared/legacy-tenant-keys.ts";
import { parseDotEnv } from "../../shared/legacy-dotenv.ts";
import { initProject } from "../../../shared/init/project-init.ts";
import { buildDotEnv, marshalDotEnv, parseDotEnv } from "./bootstrap.dotenv.ts";
import { buildDotEnv, marshalDotEnv } from "./bootstrap.dotenv.ts";
import {
LegacyBootstrapHealthError,
LegacyBootstrapInvalidTemplateError,
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/src/legacy/commands/gen/types/types.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,10 @@ import { LegacyLinkedProjectCache } from "../../../telemetry/legacy-linked-proje
import { LegacyTelemetryState } from "../../../telemetry/legacy-telemetry-state.service.ts";
import type { LegacyGenTypesFlags } from "./types.command.ts";
import { LegacyGenTypesNetworkError, LegacyGenTypesUnexpectedStatusError } from "./types.errors.ts";
import { legacyGetHostname } from "../../../shared/legacy-hostname.ts";
import {
buildPostgresUrl,
defaultSchemas,
getServicesHostname,
localDbContainerId,
localDbPassword,
localNetworkId,
Expand Down Expand Up @@ -400,7 +400,7 @@ export const legacyGenTypes = Effect.fn("legacy.gen.types")(function* (flags: Le
}),
host: "db",
port: 5432,
probeHost: getServicesHostname(),
probeHost: legacyGetHostname(),
probePort: loaded.config.db.port,
networkMode: localNetworkId(projectId),
includedSchemas,
Expand Down
4 changes: 0 additions & 4 deletions apps/cli/src/legacy/commands/gen/types/types.shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,10 +117,6 @@ export function parseQueryTimeoutSeconds(
});
}

export function getServicesHostname() {
return process.env["SUPABASE_SERVICES_HOSTNAME"] || "127.0.0.1";
}

/**
* The default generated docker network name for a local project (Go's `utils.NetId`
* fallback, `GetId("network")`). The `--network-id` override is applied at the docker
Expand Down
12 changes: 7 additions & 5 deletions apps/cli/src/legacy/commands/gen/types/types.unit.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createServer, type Server, type Socket } from "node:net";
import { describe, expect, it } from "@effect/vitest";
import { Effect, Exit } from "effect";
import { legacyGetHostname } from "../../../shared/legacy-hostname.ts";
import {
buildPostgresUrl,
defaultSchemas,
getServicesHostname,
legacyRootCaBundle,
localDbContainerId,
localDbPassword,
Expand Down Expand Up @@ -209,10 +209,12 @@ describe("schema and id helpers", () => {
});

it("reads the services hostname and db password from the environment", () => {
expect(withEnv("SUPABASE_SERVICES_HOSTNAME", undefined, () => getServicesHostname())).toBe(
"127.0.0.1",
);
expect(withEnv("SUPABASE_SERVICES_HOSTNAME", "db.internal", () => getServicesHostname())).toBe(
expect(
withEnv("DOCKER_HOST", undefined, () =>
withEnv("SUPABASE_SERVICES_HOSTNAME", undefined, () => legacyGetHostname()),
),
).toBe("127.0.0.1");
expect(withEnv("SUPABASE_SERVICES_HOSTNAME", "db.internal", () => legacyGetHostname())).toBe(
"db.internal",
);
expect(withEnv("SUPABASE_DB_PASSWORD", undefined, () => localDbPassword())).toBe("postgres");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function setup(
profile: "supabase",
apiUrl: "https://api.supabase.com",
projectHost: "supabase.co",
poolerHost: "supabase.com",
accessToken: Option.none(),
projectId: Option.none(),
workdir: process.cwd(),
Expand Down
Loading
Loading