-
Notifications
You must be signed in to change notification settings - Fork 515
feat(cli): upgrade pg-delta next to alpha.46 #6300
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 1 commit
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
f387bcc
feat(cli): upgrade pg-delta next to alpha.46
avallete 59285c6
fix(cli): restore image pgjwt only when it was installed
avallete d57990e
fix(cli): harden declarative shadow-prep SQL and cancel
avallete 6af0469
fix(cli): skip quoted idents when scanning CREATE EXTENSION
avallete ec9946d
refactor(cli): drop the CREATE EXTENSION lexer
avallete 1387b70
refactor(cli): drop the declarative shadow-prep checkout adapter
avallete 1ac2318
Merge branch 'develop' into feat/upgrade-pg-delta-alpha.46
avallete File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
132 changes: 132 additions & 0 deletions
132
apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-declarative-shadow-prep.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| import { Effect } from "effect"; | ||
|
|
||
| import { LegacyPgDeltaEngineError } from "./legacy-pgdelta-engine.service.ts"; | ||
|
|
||
| export type LegacyDeclarativeShadowClient = { | ||
| readonly query: (sql: string) => Promise<{ readonly rows: ReadonlyArray<unknown> }>; | ||
| }; | ||
|
|
||
| /** Image-default extensions the user may still declare; omit means keep the install. */ | ||
| const IMAGE_DEFAULT_EXTENSIONS = ["pgjwt", "pgcrypto", "uuid-ossp"] as const; | ||
|
|
||
| const IMAGE_DEFAULT_EXTENSION_SET = new Set<string>(IMAGE_DEFAULT_EXTENSIONS); | ||
|
|
||
| const DROP_IMAGE_DEFAULT_EXTENSION: Record<(typeof IMAGE_DEFAULT_EXTENSIONS)[number], string> = { | ||
| pgjwt: "DROP EXTENSION IF EXISTS pgjwt", | ||
| pgcrypto: "DROP EXTENSION IF EXISTS pgcrypto", | ||
| "uuid-ossp": 'DROP EXTENSION IF EXISTS "uuid-ossp"', | ||
| }; | ||
|
|
||
| const CREATE_EXTENSION_RE = | ||
| /\bCREATE\s+EXTENSION\s+(?:IF\s+NOT\s+EXISTS\s+)?(?:"([^"]+)"|([a-zA-Z_][\w$-]*))/gi; | ||
|
|
||
| /** Blank comments and literals so CREATE EXTENSION in those positions is ignored. */ | ||
| const maskSqlNonCode = (sql: string): string => | ||
| sql.replaceAll( | ||
| /--[^\r\n]*|\/\*[\s\S]*?\*\/|'(?:''|[^'])*'|\$(?:[a-zA-Z_][\w$]*)?\$[\s\S]*?\$(?:[a-zA-Z_][\w$]*)?\$/g, | ||
| (matched) => matched.replaceAll(/[^\r\n]/g, " "), | ||
| ); | ||
|
|
||
| export const legacyDeclaredSqlExtensions = ( | ||
| files: ReadonlyArray<{ readonly name: string; readonly sql: string }>, | ||
| ): ReadonlySet<string> => { | ||
| const declared = new Set<string>(); | ||
| for (const file of files) { | ||
| for (const match of maskSqlNonCode(file.sql).matchAll(CREATE_EXTENSION_RE)) { | ||
|
avallete marked this conversation as resolved.
Outdated
|
||
| const name = (match[1] ?? match[2] ?? "").toLowerCase(); | ||
| if (name !== "") declared.add(name); | ||
| } | ||
| } | ||
| return declared; | ||
| }; | ||
|
|
||
| const declaredImageExtensions = ( | ||
| files: ReadonlyArray<{ readonly name: string; readonly sql: string }>, | ||
| ): ReadonlySet<string> => { | ||
| const declared = new Set<string>(); | ||
| for (const name of legacyDeclaredSqlExtensions(files)) { | ||
| if (IMAGE_DEFAULT_EXTENSION_SET.has(name)) declared.add(name); | ||
| } | ||
| return declared; | ||
| }; | ||
|
|
||
| export const legacyParsePostgresMajorVersion = (serverVersion: string): number => { | ||
| const major = Number.parseInt(serverVersion, 10); | ||
| return Number.isInteger(major) ? major : 0; | ||
| }; | ||
|
|
||
| export const legacyDeclarativeBaselinePrepStatements = ( | ||
| majorVersion: number, | ||
| declared: ReadonlySet<string>, | ||
| ): ReadonlyArray<string> => { | ||
| const dropPgcrypto = declared.has("pgcrypto"); | ||
| // Image pgjwt depends on pgcrypto; drop it first so pgcrypto can drop. | ||
| const dropPgjwt = declared.has("pgjwt") || dropPgcrypto; | ||
| const dropUuidOssp = declared.has("uuid-ossp"); | ||
| const statements: string[] = []; | ||
| if (majorVersion === 14 && dropUuidOssp) { | ||
| statements.push("ALTER TABLE storage.objects ALTER COLUMN id DROP DEFAULT"); | ||
| } | ||
| if (dropPgjwt) statements.push(DROP_IMAGE_DEFAULT_EXTENSION.pgjwt); | ||
| if (dropPgcrypto) statements.push(DROP_IMAGE_DEFAULT_EXTENSION.pgcrypto); | ||
| if (dropUuidOssp) statements.push(DROP_IMAGE_DEFAULT_EXTENSION["uuid-ossp"]); | ||
| return statements; | ||
| }; | ||
|
|
||
| /** Recreate image pgjwt after a pgcrypto-only drop so omit still means keep. */ | ||
| export const legacyFilesForDeclarativeShadowLoad = ( | ||
| files: ReadonlyArray<{ readonly name: string; readonly sql: string }>, | ||
| ): ReadonlyArray<{ readonly name: string; readonly sql: string }> => { | ||
| const declared = declaredImageExtensions(files); | ||
| if (!declared.has("pgcrypto") || declared.has("pgjwt")) return files; | ||
| return [ | ||
| ...files, | ||
| { | ||
| name: "_cli/restore-pgjwt.sql", | ||
| sql: "CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA extensions;\n", | ||
| }, | ||
| ]; | ||
| }; | ||
|
|
||
| /** User cannot edit this SQL; a persistent miss is a CLI bug. */ | ||
| const DECLARATIVE_SHADOW_PREP_FAILURE_SUGGESTION = | ||
| "This statement is CLI-owned shadow prep, not a project migration or schema file. If it persists, report it with supabase issue bug."; | ||
|
|
||
| const queryError = (sql: string, cause: unknown) => | ||
| new LegacyPgDeltaEngineError({ | ||
| message: `Failed to prepare the isolated declaration shadow (${sql}): ${ | ||
| cause instanceof Error ? cause.message : String(cause) | ||
| }`, | ||
| cause, | ||
| suggestion: DECLARATIVE_SHADOW_PREP_FAILURE_SUGGESTION, | ||
| }); | ||
|
|
||
| const readServerVersion = (rows: ReadonlyArray<unknown>): string => { | ||
| const row = rows[0]; | ||
| if (row === undefined || typeof row !== "object" || row === null) return ""; | ||
| const value = Reflect.get(row, "server_version"); | ||
| return typeof value === "string" ? value : ""; | ||
| }; | ||
|
|
||
| export const legacyPrepareDeclarativeShadow = ( | ||
| client: LegacyDeclarativeShadowClient, | ||
| files: ReadonlyArray<{ readonly name: string; readonly sql: string }>, | ||
| ) => | ||
| Effect.gen(function* () { | ||
| const declared = declaredImageExtensions(files); | ||
| if (declared.size === 0) return; | ||
| const versionRows = yield* Effect.tryPromise({ | ||
| try: () => client.query("SHOW server_version"), | ||
| catch: (cause) => queryError("SHOW server_version", cause), | ||
| }); | ||
| const statements = legacyDeclarativeBaselinePrepStatements( | ||
| legacyParsePostgresMajorVersion(readServerVersion(versionRows.rows)), | ||
| declared, | ||
| ); | ||
| for (const sql of statements) { | ||
| yield* Effect.tryPromise({ | ||
| try: () => client.query(sql), | ||
| catch: (cause) => queryError(sql, cause), | ||
| }); | ||
|
avallete marked this conversation as resolved.
Outdated
|
||
| } | ||
| }); | ||
142 changes: 142 additions & 0 deletions
142
apps/cli/src/legacy/commands/db/shared/legacy-pgdelta-declarative-shadow-prep.unit.test.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,142 @@ | ||
| import { describe, expect, it } from "@effect/vitest"; | ||
| import { Cause, Effect, Exit, Option } from "effect"; | ||
|
|
||
| import { | ||
| legacyDeclarativeBaselinePrepStatements, | ||
| legacyFilesForDeclarativeShadowLoad, | ||
| legacyParsePostgresMajorVersion, | ||
| legacyPrepareDeclarativeShadow, | ||
| } from "./legacy-pgdelta-declarative-shadow-prep.ts"; | ||
| import { LegacyPgDeltaEngineError } from "./legacy-pgdelta-engine.service.ts"; | ||
|
|
||
| const allImageCreates = [ | ||
| { name: "_cluster/extensions/pgjwt.sql", sql: 'CREATE EXTENSION "pgjwt";' }, | ||
| { name: "_cluster/extensions/pgcrypto.sql", sql: 'CREATE EXTENSION "pgcrypto";' }, | ||
| { name: "_cluster/extensions/uuid-ossp.sql", sql: 'CREATE EXTENSION "uuid-ossp";' }, | ||
| ]; | ||
|
|
||
| describe("legacyDeclarativeBaselinePrepStatements", () => { | ||
| it("emits nothing when declarations do not recreate image defaults", () => { | ||
| expect(legacyDeclarativeBaselinePrepStatements(14, new Set())).toEqual([]); | ||
| expect(legacyDeclarativeBaselinePrepStatements(17, new Set())).toEqual([]); | ||
| }); | ||
|
|
||
| it("detaches PG14 storage.objects before dropping declared uuid-ossp", () => { | ||
| expect(legacyDeclarativeBaselinePrepStatements(14, new Set(["uuid-ossp"]))).toEqual([ | ||
| "ALTER TABLE storage.objects ALTER COLUMN id DROP DEFAULT", | ||
| 'DROP EXTENSION IF EXISTS "uuid-ossp"', | ||
| ]); | ||
| }); | ||
|
|
||
| it("drops image pgjwt before a declared pgcrypto recreate", () => { | ||
| expect(legacyDeclarativeBaselinePrepStatements(14, new Set(["pgcrypto"]))).toEqual([ | ||
| "DROP EXTENSION IF EXISTS pgjwt", | ||
| "DROP EXTENSION IF EXISTS pgcrypto", | ||
| ]); | ||
| expect(legacyDeclarativeBaselinePrepStatements(17, new Set(["pgcrypto"]))).toEqual([ | ||
| "DROP EXTENSION IF EXISTS pgjwt", | ||
| "DROP EXTENSION IF EXISTS pgcrypto", | ||
| ]); | ||
| }); | ||
|
|
||
| it("drops declared image defaults on PG15+, pgjwt before pgcrypto", () => { | ||
| expect( | ||
| legacyDeclarativeBaselinePrepStatements(17, new Set(["pgjwt", "pgcrypto", "uuid-ossp"])), | ||
| ).toEqual([ | ||
| "DROP EXTENSION IF EXISTS pgjwt", | ||
| "DROP EXTENSION IF EXISTS pgcrypto", | ||
| 'DROP EXTENSION IF EXISTS "uuid-ossp"', | ||
| ]); | ||
| }); | ||
| }); | ||
|
|
||
| describe("legacyFilesForDeclarativeShadowLoad", () => { | ||
| it("restores omitted pgjwt after a pgcrypto recreate", () => { | ||
| const files = [{ name: "public/01.sql", sql: "CREATE EXTENSION pgcrypto;" }]; | ||
| expect(legacyFilesForDeclarativeShadowLoad(files)).toEqual([ | ||
| ...files, | ||
| { | ||
| name: "_cli/restore-pgjwt.sql", | ||
| sql: "CREATE EXTENSION IF NOT EXISTS pgjwt WITH SCHEMA extensions;\n", | ||
| }, | ||
| ]); | ||
| }); | ||
|
|
||
| it("does not restore pgjwt when declarations recreate it", () => { | ||
| expect(legacyFilesForDeclarativeShadowLoad(allImageCreates)).toEqual(allImageCreates); | ||
| }); | ||
| }); | ||
|
|
||
| describe("legacyParsePostgresMajorVersion", () => { | ||
| it("reads the leading major from SHOW server_version", () => { | ||
| expect(legacyParsePostgresMajorVersion("17.6")).toBe(17); | ||
| expect(legacyParsePostgresMajorVersion("14.15 (Debian)")).toBe(14); | ||
| expect(legacyParsePostgresMajorVersion("")).toBe(0); | ||
| }); | ||
| }); | ||
|
|
||
| describe("legacyPrepareDeclarativeShadow", () => { | ||
| it.live("skips the shadow when declarations omit image-default extensions", () => { | ||
| const queries: string[] = []; | ||
| const client = { | ||
| query: (sql: string) => { | ||
| queries.push(sql); | ||
| return Promise.resolve({ rows: [] }); | ||
| }, | ||
| }; | ||
| return Effect.gen(function* () { | ||
| yield* legacyPrepareDeclarativeShadow(client, [ | ||
| { name: "a.sql", sql: "create table a (id int);" }, | ||
| ]); | ||
| expect(queries).toEqual([]); | ||
| }); | ||
| }); | ||
|
|
||
| it.live("names the failing prep statement", () => { | ||
| const client = { | ||
| query: (sql: string) => { | ||
| if (sql === "SHOW server_version") { | ||
| return Promise.resolve({ rows: [{ server_version: "15.8" }] }); | ||
| } | ||
| if (sql.includes("pgcrypto")) { | ||
| return Promise.reject(new Error("cannot drop extension pgcrypto (SQLSTATE 2BP01)")); | ||
| } | ||
| return Promise.resolve({ rows: [] }); | ||
| }, | ||
| }; | ||
| return Effect.gen(function* () { | ||
| const exit = yield* legacyPrepareDeclarativeShadow(client, [ | ||
| { name: "public/01.sql", sql: "CREATE EXTENSION pgcrypto;" }, | ||
| ]).pipe(Effect.exit); | ||
| expect(Exit.isFailure(exit)).toBe(true); | ||
| const error = Exit.isFailure(exit) | ||
| ? Option.getOrUndefined(Cause.findErrorOption(exit.cause)) | ||
| : undefined; | ||
| expect(error).toBeInstanceOf(LegacyPgDeltaEngineError); | ||
| expect(error instanceof LegacyPgDeltaEngineError ? error.message : "").toContain( | ||
| "DROP EXTENSION IF EXISTS pgcrypto", | ||
| ); | ||
| }); | ||
| }); | ||
|
|
||
| it.live("runs the version-selected prep statements against the shadow", () => { | ||
| const queries: string[] = []; | ||
| const client = { | ||
| query: (sql: string) => { | ||
| queries.push(sql); | ||
| return Promise.resolve({ | ||
| rows: sql === "SHOW server_version" ? [{ server_version: "17.6" }] : [], | ||
| }); | ||
| }, | ||
| }; | ||
| return Effect.gen(function* () { | ||
| yield* legacyPrepareDeclarativeShadow(client, allImageCreates); | ||
| expect(queries).toEqual([ | ||
| "SHOW server_version", | ||
| "DROP EXTENSION IF EXISTS pgjwt", | ||
| "DROP EXTENSION IF EXISTS pgcrypto", | ||
| 'DROP EXTENSION IF EXISTS "uuid-ossp"', | ||
| ]); | ||
| }); | ||
| }); | ||
| }); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.