Skip to content
Open
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
28 changes: 27 additions & 1 deletion apps/cli-e2e/src/server/pg-mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
* - Error injection: returns ErrorResponse for all queries (extended: on Execute)
* - Empty state: returns CommandComplete "SELECT 0" for extended Execute;
* returns error for simple SELECT; returns empty COPY for COPY
* - Liveness probe: a simple `SELECT 1` always succeeds (one row) regardless of
* state — the native driver uses it to force its lazy connection
* - Terminate ('X') → closes connection gracefully
*/

Expand Down Expand Up @@ -244,6 +246,23 @@ function buildStartupResponse(): Buffer {
]);
}

/**
* Connection liveness probe (`SELECT 1`). The native TS driver eagerly forces its
* lazily-connected `pg` client by running `SELECT 1` over the simple-query
* protocol (Go's pgx connects eagerly at the protocol level and issues no such
* query). A real Postgres always answers `SELECT 1` regardless of any fixtures, so
* the mock must too — otherwise the empty-state "no fixture" guard rejects the
* probe and the native command fails to connect before doing any real work.
*/
function buildProbeResponse(): Buffer {
return Buffer.concat([
buildRowDescription(["?column?"], [TEXT_OID]),
buildDataRow(["1"]),
buildCommandComplete("SELECT 1"),
buildReadyForQuery(),
]);
}

function buildSelectResponse(state: PgMockState): Buffer {
if (state.type === "error") {
const { code, message, severity = "ERROR" } = state.error;
Expand Down Expand Up @@ -372,8 +391,15 @@ function processMessages(socket: Bun.Socket<SocketData>, getState: () => PgMockS
if (msgType === 0x51) {
// 'Q' — simple query (used by pgconn.CopyTo and simple-protocol callers)
const query = payload.slice(0, payload.length - 1).toString("utf8");
const isProbe = /^\s*select\s+1\s*;?\s*$/i.test(query);
const isCopy = /^\s*COPY\s+/i.test(query);
socket.write(isCopy ? buildCopyResponse(getState()) : buildSelectResponse(getState()));
socket.write(
isProbe
? buildProbeResponse()
: isCopy
? buildCopyResponse(getState())
: buildSelectResponse(getState()),
);
} else if (msgType === 0x50) {
// 'P' Parse → extract param count from SQL, then ParseComplete
const sql = parseSqlFromPayload(payload);
Expand Down
9 changes: 9 additions & 0 deletions apps/cli-e2e/src/tests/inspect.e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,15 @@ describe("inspect:report", () => {
apiUrl: serverUrl,
accessToken: ACCESS_TOKEN,
workspaceSetup: (dir) => setupInspectWorkspace(dir, pgMockPort),
// The rules summary table is computed by csvq over the COPY CSV content,
// which the pg-mock cannot emit (it returns an empty, header-less COPY for
// every query). On those empty CSVs Go's csvq panics with an "index out of
// range" fatal error that it prints into each STATUS cell, while the native
// TS evaluator reports a clean "unknown column" — so stdout is not faithfully
// comparable here. Exit code, stderr (progress lines), request log, and the
// 14 written CSV files ARE still compared; the rules-table rendering is
// covered by the apps/cli unit + integration tests against real fixtures.
compareStdout: false,
},
["inspect", "report", "--local"],
);
Expand Down
2 changes: 1 addition & 1 deletion apps/cli/docs/go-cli-porting-status.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ These commands exist in the TS CLI today but have no direct top-level equivalent
| `db push` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. |
| `db reset` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. |
| `db start` | `missing` | `missing` | `n/a` | `n/a` | No native TS implementation yet. Wrapped in legacy shell. |
| `inspect report` | `wrapped` | `legacy/commands/inspect/report/` | `n/a` | `n/a` | Phase 0 proxy. Wrapped in legacy shell. |
| `inspect report` | `ported` | `legacy/commands/inspect/report/` | `n/a` | `n/a` | Native TS port. Runs every inspect query via server-side `COPY ... CSV`, writes 14 CSVs under `<output-dir>/<date>/`, then renders a Go-parity Glamour rules summary (bounded csvq-subset evaluator; custom `[experimental.inspect.rules]` supported). |
| `inspect db db-stats` | `ported` | `legacy/commands/inspect/db/db-stats/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. |
| `inspect db replication-slots` | `ported` | `legacy/commands/inspect/db/replication-slots/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. |
| `inspect db locks` | `ported` | `legacy/commands/inspect/db/locks/` | `n/a` | `n/a` | Native TS port. Queries Postgres directly via LegacyDbConnection; renders Go-parity Glamour tables. |
Expand Down
4 changes: 4 additions & 0 deletions apps/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@
"@supabase/stack": "workspace:*",
"@tsconfig/bun": "catalog:",
"@types/bun": "catalog:",
"@types/pg": "^8.15.0",
"@types/pg-copy-streams": "^1.2.5",
"@types/react": "^19.2.16",
"@typescript/native-preview": "catalog:",
"@vercel/detect-agent": "^1.2.3",
Expand All @@ -66,6 +68,8 @@
"oxfmt": "catalog:",
"oxlint": "catalog:",
"oxlint-tsgolint": "catalog:",
"pg": "^8.21.0",
"pg-copy-streams": "^7.0.0",
"posthog-node": "^5.36.1",
"react": "^19.2.7",
"react-devtools-core": "^7.0.1",
Expand Down
37 changes: 2 additions & 35 deletions apps/cli/src/legacy/commands/inspect/db/db.layers.ts
Original file line number Diff line number Diff line change
@@ -1,40 +1,7 @@
import { Layer } from "effect";

import { legacyCliConfigLayer } from "../../../config/legacy-cli-config.layer.ts";
import { legacyDbConfigLayer } from "../../../shared/legacy-db-config.layer.ts";
import { legacyDbConnectionLayer } from "../../../shared/legacy-db-connection.layer.ts";
import { legacyDebugLoggerLayer } from "../../../shared/legacy-debug-logger.layer.ts";
import { legacyTelemetryStateLayer } from "../../../telemetry/legacy-telemetry-state.layer.ts";
import { commandRuntimeLayer } from "../../../../shared/runtime/command-runtime.layer.ts";

/**
* `legacyCliConfigLayer` is provided to the resolver AND exposed at the top level
* because `Layer.provide` does not share to merge siblings (legacy CLAUDE.md item
* 5); the resolver requires it internally and so it is provided to `dbConfig`,
* while the merge keeps it available alongside.
*/
const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer));

const dbConfig = legacyDbConfigLayer.pipe(
Layer.provide(cliConfig),
Layer.provide(legacyDbConnectionLayer),
Layer.provide(legacyDebugLoggerLayer),
);

/**
* The services every `inspect db` subcommand shares, minus the command-runtime
* identity. Mirrors `test/test.layers.ts` minus the docker layer: the DB-config
* resolver, the Postgres connection, the CLI config, and telemetry state. The
* Management API stack is NOT merged here — it resolves an access token eagerly,
* which would break the auth-free `--local` / `--db-url` paths. The `--linked`
* path provides it lazily inside the resolver (`legacy-db-config.layer.ts`).
*/
const baseLayer = Layer.mergeAll(
dbConfig,
legacyDbConnectionLayer,
cliConfig,
legacyTelemetryStateLayer,
);
import { legacyInspectBaseLayer } from "../inspect.layers.ts";

/**
* The command-runtime path for a single `inspect db <leaf>` subcommand.
Expand All @@ -57,4 +24,4 @@ export const legacyInspectDbCommandPath = (leaf: string): ReadonlyArray<string>

/** Runtime layer for a single `supabase inspect db <leaf>` subcommand. */
export const legacyInspectDbRuntimeLayer = (leaf: string) =>
Layer.merge(baseLayer, commandRuntimeLayer(legacyInspectDbCommandPath(leaf)));
Layer.merge(legacyInspectBaseLayer, commandRuntimeLayer(legacyInspectDbCommandPath(leaf)));
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ function setup() {
Effect.succeed({
exec: () => Effect.void,
extensionExists: () => Effect.succeed(false),
copyToCsv: () => Effect.succeed(new Uint8Array()),
query: (sql: string) => {
querySql = sql;
return Effect.succeed([]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ function mockDbConnection(opts: {
return Effect.succeed({
exec: () => Effect.void,
extensionExists: () => Effect.succeed(false),
copyToCsv: () => Effect.succeed(new Uint8Array()),
query: (sql: string, params?: ReadonlyArray<unknown>) => {
querySql = sql;
queryParams = params;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ function setup(rows: ReadonlyArray<Record<string, unknown>>) {
Effect.succeed({
exec: () => Effect.void,
extensionExists: () => Effect.succeed(false),
copyToCsv: () => Effect.succeed(new Uint8Array()),
query: (sql: string, params?: ReadonlyArray<unknown>) => {
querySql = sql;
queryParams = params;
Expand Down
42 changes: 42 additions & 0 deletions apps/cli/src/legacy/commands/inspect/inspect.layers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { Layer } from "effect";

import { legacyCliConfigLayer } from "../../config/legacy-cli-config.layer.ts";
import { legacyDbConfigLayer } from "../../shared/legacy-db-config.layer.ts";
import { legacyDbConnectionLayer } from "../../shared/legacy-db-connection.layer.ts";
import { legacyDebugLoggerLayer } from "../../shared/legacy-debug-logger.layer.ts";
import { legacyTelemetryStateLayer } from "../../telemetry/legacy-telemetry-state.layer.ts";

/**
* `legacyCliConfigLayer` is provided to the resolver AND exposed at the top level
* because `Layer.provide` does not share to merge siblings (legacy CLAUDE.md item
* 5); the resolver requires it internally and so it is provided to `dbConfig`,
* while the merge keeps it available alongside.
*/
const cliConfig = legacyCliConfigLayer.pipe(Layer.provide(legacyDebugLoggerLayer));

const dbConfig = legacyDbConfigLayer.pipe(
Layer.provide(cliConfig),
Layer.provide(legacyDbConnectionLayer),
Layer.provide(legacyDebugLoggerLayer),
);

/**
* The services every `inspect` leaf shares, minus the command-runtime identity:
* the DB-config resolver, the Postgres connection, the CLI config (for the
* `--workdir` config rules `inspect report` reads), and telemetry state. Mirrors
* `test/test.layers.ts` minus the docker layer.
*
* The Management API stack is NOT merged here — it resolves an access token
* eagerly, which would break the auth-free `--local` / `--db-url` paths. The
* `--linked` path provides it lazily inside the resolver (`legacy-db-config.layer.ts`).
*
* Hoisted out of `db/db.layers.ts` so both the `inspect db <leaf>` subcommands and
* `inspect report` (a sibling of `db`, not a child) share one definition rather
* than each carrying a parallel copy.
*/
export const legacyInspectBaseLayer = Layer.mergeAll(
dbConfig,
legacyDbConnectionLayer,
cliConfig,
legacyTelemetryStateLayer,
);
Loading
Loading