From 4b2d163a1d86a22f5b263f0c054224111ef81106 Mon Sep 17 00:00:00 2001 From: Artur Zakirov Date: Fri, 2 Oct 2026 15:43:32 +0200 Subject: [PATCH] fix(orioledb): use alpha and beta conditionally based on AMI version (#51162) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Problem - Older orioledb projects show "Public Beta" instead of "Public Alpha" in UI. - List of backups in the "Scheduled backups" tab hangs. ## Solution - show in the UI "Public Alpha" for projects older than 17.11.0.001-orioledb - show in the UI "Public Beta" for new projects - enable scheduled-backup query for "Public Beta" ## Summary by CodeRabbit * **Bug Fixes** * Backup availability messages now reflect OrioleDB’s current release stage rather than always describing it as public beta. * AWS backup queries are no longer disabled for every OrioleDB project; they remain disabled during the alpha stage. * **New Features** * Added an informational notice and documentation link for scheduled backups on AWS OrioleDB projects in alpha. * Added release-stage details to the PITR availability notice. Co-authored-by: Charis <26616127+charislam@users.noreply.github.com> --- .../RestoreToNewProject.tsx | 4 +- apps/studio/data/database/backups-query.ts | 5 +- apps/studio/hooks/misc/useSelectedProject.ts | 15 ++ .../misc/useSelectedProject.utils.test.ts | 151 ++++++++++++++++++ .../hooks/misc/useSelectedProject.utils.ts | 42 +++++ .../project/[ref]/database/backups/pitr.tsx | 11 +- .../[ref]/database/backups/scheduled.tsx | 22 ++- 7 files changed, 243 insertions(+), 7 deletions(-) create mode 100644 apps/studio/hooks/misc/useSelectedProject.utils.test.ts create mode 100644 apps/studio/hooks/misc/useSelectedProject.utils.ts diff --git a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx index d551554ea05..b829edbfdf3 100644 --- a/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx +++ b/apps/studio/components/interfaces/Database/RestoreToNewProject/RestoreToNewProject.tsx @@ -30,6 +30,7 @@ import { useIsAwsK8sCloudProvider, useIsHighAvailability, useIsOrioleDb, + useOrioleDbReleaseStage, useSelectedProjectQuery, } from '@/hooks/misc/useSelectedProject' import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants' @@ -41,6 +42,7 @@ export const RestoreToNewProject = () => { const { hasAccess: hasAccessToRestoreToNewProject, isLoading: isLoadingEntitlement } = useCheckEntitlements('backup.restore_to_new_project') const isOrioleDb = useIsOrioleDb() + const orioleDbReleaseStage = useOrioleDbReleaseStage() const isAwsK8s = useIsAwsK8sCloudProvider() const isHighAvailability = useIsHighAvailability() @@ -130,7 +132,7 @@ export const RestoreToNewProject = () => { ) } diff --git a/apps/studio/data/database/backups-query.ts b/apps/studio/data/database/backups-query.ts index c1bb3e8b02b..8cdf819cd8e 100644 --- a/apps/studio/data/database/backups-query.ts +++ b/apps/studio/data/database/backups-query.ts @@ -3,7 +3,7 @@ import { useQuery } from '@tanstack/react-query' import { databaseKeys } from './keys' import type { components } from '@/data/api' import { get, handleError } from '@/data/fetchers' -import { useIsOrioleDbInAws } from '@/hooks/misc/useSelectedProject' +import { useIsOrioleDbInAws, useOrioleDbReleaseStage } from '@/hooks/misc/useSelectedProject' import { PROJECT_STATUS } from '@/lib/constants' import type { ResponseError, UseCustomQueryOptions } from '@/types' @@ -35,13 +35,14 @@ export const useBackupsQuery = ( ) => { // [Joshen] Check for specifically false to account for project not loaded yet const isOrioleDbInAws = useIsOrioleDbInAws() + const orioleDbReleaseStage = useOrioleDbReleaseStage() return useQuery({ queryKey: databaseKeys.backups(projectRef), queryFn: ({ signal }) => getBackups({ projectRef }, signal), enabled: enabled && - !isOrioleDbInAws && + !(isOrioleDbInAws && orioleDbReleaseStage === 'alpha') && typeof projectRef !== 'undefined' && projectStatus !== PROJECT_STATUS.COMING_UP && projectStatus !== PROJECT_STATUS.UNKNOWN, diff --git a/apps/studio/hooks/misc/useSelectedProject.ts b/apps/studio/hooks/misc/useSelectedProject.ts index 4fd2d5fd894..84757654077 100644 --- a/apps/studio/hooks/misc/useSelectedProject.ts +++ b/apps/studio/hooks/misc/useSelectedProject.ts @@ -1,5 +1,10 @@ import { useParams } from 'common' +import { + isOrioleDbVersionAtLeast, + ORIOLEDB_PUBLIC_BETA_VERSION, + type OrioleDbReleaseStage, +} from './useSelectedProject.utils' import { useProjectDetailQuery } from '@/data/projects/project-detail-query' import { PROJECT_STATUS, PROVIDERS } from '@/lib/constants' @@ -54,6 +59,16 @@ export const useIsOrioleDbInAws = () => { return isOrioleDbInAws } +// OrioleDB moved from Public Alpha to Public Beta at engine version 17.11.0.001. +// PITR remains unsupported for every OrioleDB version regardless of stage. +export const useOrioleDbReleaseStage = (): OrioleDbReleaseStage | undefined => { + const { data: project } = useSelectedProjectQuery() + if (!project?.dbVersion?.endsWith('orioledb')) return undefined + return isOrioleDbVersionAtLeast(project.dbVersion, ORIOLEDB_PUBLIC_BETA_VERSION) + ? 'beta' + : 'alpha' +} + // [Joshen TODO] There's a duplicate method `resolveHighAvailability` in `useHighAvailability.constants` export const useIsHighAvailability = () => { const { data: project } = useSelectedProjectQuery() diff --git a/apps/studio/hooks/misc/useSelectedProject.utils.test.ts b/apps/studio/hooks/misc/useSelectedProject.utils.test.ts new file mode 100644 index 00000000000..20d9f58cc04 --- /dev/null +++ b/apps/studio/hooks/misc/useSelectedProject.utils.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, it } from 'vitest' + +import { + appVersionAtLeast, + compareAppVersions, + isOrioleDbVersionAtLeast, + parseAppVersionKey, +} from './useSelectedProject.utils' + +describe('parseAppVersionKey', () => { + it('parses a dbVersion string into a version key', () => { + expect(parseAppVersionKey('supabase-postgres-17.9.9.999-orioledb')).toEqual([17, 9, 9, 999]) + }) + + it('parses dbVersion strings with an arch prefix', () => { + expect(parseAppVersionKey('supabase-postgres-arm64-17.9.0.019')).toEqual([17, 9, 0, 19]) + expect(parseAppVersionKey('supabase-postgres-x86_64-17.9.0.019')).toEqual([17, 9, 0, 19]) + }) + + it('returns null for non-conforming strings', () => { + expect(parseAppVersionKey('CUSTOM_AMI_SENTINEL')).toBeNull() + expect(parseAppVersionKey('not-a-version')).toBeNull() + }) + + it('returns null for null or undefined input', () => { + expect(parseAppVersionKey(null)).toBeNull() + expect(parseAppVersionKey(undefined)).toBeNull() + }) +}) + +describe('compareAppVersions', () => { + it('returns a negative number when a is older than b', () => { + expect( + compareAppVersions('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019') + ).toBeLessThan(0) + }) + + it('returns zero when versions are equal', () => { + expect(compareAppVersions('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe( + 0 + ) + }) + + it('returns a positive number when a is newer than b', () => { + expect( + compareAppVersions('supabase-postgres-17.11.0.001', 'supabase-postgres-17.9.0.019') + ).toBeGreaterThan(0) + }) + + it('compares numerically rather than by segment digit-width', () => { + // Regression case for the digit-concatenation bug in lib/helpers.ts' + // getSemanticVersion, where '15.1.1.2' (-> '15112') compares as less than + // '14.1.0.44' (-> '141044') despite being the newer version. + expect( + compareAppVersions('supabase-postgres-15.1.1.2', 'supabase-postgres-14.1.0.44') + ).toBeGreaterThan(0) + }) + + it('throws on unparseable input', () => { + expect(() => compareAppVersions('not-a-version', 'supabase-postgres-17.9.0.019')).toThrow() + expect(() => compareAppVersions('supabase-postgres-17.9.0.019', 'not-a-version')).toThrow() + }) +}) + +describe('appVersionAtLeast', () => { + it('returns true when the version is above or equal to the threshold', () => { + expect(appVersionAtLeast('supabase-postgres-17.9.9.999', 'supabase-postgres-17.9.0.019')).toBe( + true + ) + expect(appVersionAtLeast('supabase-postgres-17.9.0.019', 'supabase-postgres-17.9.0.019')).toBe( + true + ) + }) + + it('returns false when the version is below the threshold', () => { + expect(appVersionAtLeast('supabase-postgres-17.8.9.999', 'supabase-postgres-17.9.0.019')).toBe( + false + ) + }) + + it('returns false for an unparseable version instead of throwing', () => { + expect(appVersionAtLeast('not-a-version', 'supabase-postgres-17.9.0.019')).toBe(false) + }) +}) + +describe('isOrioleDbVersionAtLeast', () => { + it('returns true when the engine version is above the threshold', () => { + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.11.0.002-orioledb', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(true) + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.12.0.001-orioledb', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(true) + }) + + it('returns true when the engine version equals the threshold', () => { + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.11.0.001-orioledb', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(true) + }) + + it('returns false when the engine version is below the threshold', () => { + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.10.9.999-orioledb', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(false) + // wal-g (scheduled backup) support landed at 17.9.0.019, which is still + // below the Public Beta cutoff — these projects remain alpha. + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.9.0.019-orioledb', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(false) + }) + + it('returns false for a non-OrioleDB dbVersion', () => { + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.11.0.001', + 'supabase-postgres-17.11.0.001-orioledb' + ) + ).toBe(false) + }) + + it('returns false for undefined dbVersion', () => { + expect(isOrioleDbVersionAtLeast(undefined, 'supabase-postgres-17.11.0.001-orioledb')).toBe( + false + ) + }) + + it('returns false for a non-OrioleDB threshold', () => { + expect( + isOrioleDbVersionAtLeast( + 'supabase-postgres-17.11.0.001-orioledb', + 'supabase-postgres-17.11.0.001' + ) + ).toBe(false) + }) +}) diff --git a/apps/studio/hooks/misc/useSelectedProject.utils.ts b/apps/studio/hooks/misc/useSelectedProject.utils.ts new file mode 100644 index 00000000000..d61ad9a8a08 --- /dev/null +++ b/apps/studio/hooks/misc/useSelectedProject.utils.ts @@ -0,0 +1,42 @@ +export type OrioleDbReleaseStage = 'alpha' | 'beta' + +// OrioleDB engine versions ship as 4-part strings (e.g. `17.11.0.001`), which +// `lib/semver` doesn't parse (it's strict 3-part semver), so compare them separately here. +export const ORIOLEDB_PUBLIC_BETA_VERSION = 'supabase-postgres-17.11.0.001-orioledb' + +// Sortable [major, minor, patch, build] key parsed from a dbVersion string like +// `supabase-postgres-17.11.0.001-orioledb` or `supabase-postgres-arm64-17.11.0.001`. +const APP_VERSION_KEY_RE = /^supabase-postgres-(?:arm64-|x86_64-)?(\d+)\.(\d+)\.(\d+)\.(\d+)/ + +export type AppVersionKey = [number, number, number, number] + +// Returns null for non-conforming strings — callers should treat null as "not orderable". +export function parseAppVersionKey(version: string | null | undefined): AppVersionKey | null { + if (!version) return null + + const match = APP_VERSION_KEY_RE.exec(version) + if (!match) return null + + return [Number(match[1]), Number(match[2]), Number(match[3]), Number(match[4])] +} + +// Negative when `a` is older, zero when equal, positive when newer. +// Throws on unparseable input — null-tolerant callers parse first. +export function compareAppVersions(a: string, b: string): number { + const keyA = parseAppVersionKey(a) + const keyB = parseAppVersionKey(b) + if (!keyA) throw new Error(`unparseable app_version: ${a}`) + if (!keyB) throw new Error(`unparseable app_version: ${b}`) + + return keyA[0] - keyB[0] || keyA[1] - keyB[1] || keyA[2] - keyB[2] || keyA[3] - keyB[3] +} + +export function appVersionAtLeast(version: string, threshold: string): boolean { + if (!parseAppVersionKey(version)) return false + return compareAppVersions(version, threshold) >= 0 +} + +export const isOrioleDbVersionAtLeast = (dbVersion: string | undefined, threshold: string) => { + if (!dbVersion?.endsWith('orioledb') || !threshold.endsWith('orioledb')) return false + return appVersionAtLeast(dbVersion, threshold) +} diff --git a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx index 26bd2ac0649..220456873ad 100644 --- a/apps/studio/pages/project/[ref]/database/backups/pitr.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/pitr.tsx @@ -28,7 +28,11 @@ import { useBackupsQuery } from '@/data/database/backups-query' import { useCheckEntitlements } from '@/hooks/misc/useCheckEntitlements' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' import { useHighAvailability } from '@/hooks/misc/useHighAvailability' -import { useIsOrioleDbInAws, useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' +import { + useIsOrioleDbInAws, + useOrioleDbReleaseStage, + useSelectedProjectQuery, +} from '@/hooks/misc/useSelectedProject' import { DOCS_URL, PROJECT_STATUS } from '@/lib/constants' import type { NextPageWithLayout } from '@/types' @@ -71,6 +75,7 @@ const PITR = () => { const { hasAccess: hasAccessToPitr, isLoading: isLoadingEntitlements } = useCheckEntitlements('pitr.available_variants') const isOrioleDbInAws = useIsOrioleDbInAws() + const orioleDbReleaseStage = useOrioleDbReleaseStage() const { data: backups, error, @@ -96,8 +101,8 @@ const PITR = () => { return ( diff --git a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx index 4ac3a7a045c..3d1a47e37b1 100644 --- a/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx +++ b/apps/studio/pages/project/[ref]/database/backups/scheduled.tsx @@ -1,6 +1,7 @@ import { PermissionAction } from '@supabase/shared-types/out/constants' import { useParams } from 'common' import { DatabaseBackup, Info } from 'lucide-react' +import { Admonition } from 'ui-patterns/Admonition' import { PageContainer } from 'ui-patterns/PageContainer' import { PageHeader, @@ -17,12 +18,17 @@ import DatabaseBackupsNav from '@/components/interfaces/Database/Backups/Databas import { DatabaseLayout } from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { AlertError } from '@/components/ui/AlertError' +import { DocsButton } from '@/components/ui/DocsButton' import { HighAvailabilityDisabledEmptyState } from '@/components/ui/HighAvailability/HighAvailabilityDisabledEmptyState' import InformationBox from '@/components/ui/InformationBox' import { NoPermission } from '@/components/ui/NoPermission' import { useBackupsQuery } from '@/data/database/backups-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' -import { useIsHighAvailability } from '@/hooks/misc/useSelectedProject' +import { + useIsHighAvailability, + useIsOrioleDbInAws, + useOrioleDbReleaseStage, +} from '@/hooks/misc/useSelectedProject' import { DOCS_URL } from '@/lib/constants' import type { NextPageWithLayout } from '@/types' @@ -62,6 +68,8 @@ const ScheduledBackups = () => { } = useBackupsQuery({ projectRef }) const isHighAvailability = useIsHighAvailability() + const isOrioleDbInAws = useIsOrioleDbInAws() + const orioleDbReleaseStage = useOrioleDbReleaseStage() const isPitrEnabled = backups?.pitr_enabled const { can: canReadScheduledBackups, isSuccess: isPermissionsLoaded } = useAsyncCheckPermissions( @@ -69,6 +77,18 @@ const ScheduledBackups = () => { 'back_ups' ) + if (isOrioleDbInAws && orioleDbReleaseStage === 'alpha') { + return ( + + + + ) + } + if (isHighAvailability) { return (