From d8d312fee90ce503cf5573fc80f2ec0626903b71 Mon Sep 17 00:00:00 2001 From: Danny White <3104761+dnywh@users.noreply.github.com> Date: Thu, 9 Jul 2026 15:18:59 -0400 Subject: [PATCH] cleanup replication and basic warehouse addition --- .../interfaces/ConnectSheet/Connect.types.ts | 2 +- .../ConnectSheet/ConnectConfigSection.tsx | 3 +- .../interfaces/ConnectSheet/ConnectSheet.tsx | 7 +- .../ConnectSheet/WarehouseConnectPanel.tsx | 59 ++++++ .../interfaces/ConnectSheet/connect.schema.ts | 6 + .../ConnectSheet/useAvailableConnectModes.ts | 16 +- .../Database/Replication/Destinations.tsx | 39 ++-- .../interfaces/Database/Tables/TableList.tsx | 28 ++- .../Warehouse/WAREHOUSE_API_CONTRACT.md | 79 ++++++++ .../Database/Warehouse/WAREHOUSE_NAMING.md | 75 +++++++ .../Database/Warehouse/WAREHOUSE_WHOLESALE.md | 49 +++++ .../Warehouse/WarehouseDestinationRow.tsx | 64 ++++++ .../Warehouse/WarehouseDisableModal.tsx | 62 ++++++ .../Warehouse/WarehouseEnableModal.tsx | 103 ++++++++++ .../Warehouse/WarehouseObservabilityPanel.tsx | 50 +++++ .../Warehouse/WarehouseProjectCard.tsx | 84 ++++++++ .../Warehouse/WarehouseSchemaScope.tsx | 85 ++++++++ .../Database/Warehouse/WarehouseSyncChip.tsx | 23 +++ .../Warehouse/managedWarehouse.resources.ts | 8 + .../Database/Warehouse/warehouseDemoStore.ts | 185 ++++++++++++++++++ .../Warehouse/warehouseReplication.utils.ts | 62 ++++++ .../interfaces/SQLEditor/SQLEditor.tsx | 55 +++--- .../SQLEditor/SqlWarehouseTargetToggle.tsx | 70 +++++++ .../SQLEditor/UtilityPanel/UtilityActions.tsx | 2 + .../hooks/misc/useIsWarehouseEnabled.ts | 22 +++ .../[ref]/database/replication/index.tsx | 6 + packages/common/constants/local-storage.ts | 1 + packages/common/telemetry-constants.ts | 2 +- 28 files changed, 1199 insertions(+), 48 deletions(-) create mode 100644 apps/studio/components/interfaces/ConnectSheet/WarehouseConnectPanel.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_API_CONTRACT.md create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_NAMING.md create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_WHOLESALE.md create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseDestinationRow.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseDisableModal.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseEnableModal.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseObservabilityPanel.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseProjectCard.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseSchemaScope.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/WarehouseSyncChip.tsx create mode 100644 apps/studio/components/interfaces/Database/Warehouse/managedWarehouse.resources.ts create mode 100644 apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts create mode 100644 apps/studio/components/interfaces/Database/Warehouse/warehouseReplication.utils.ts create mode 100644 apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx create mode 100644 apps/studio/hooks/misc/useIsWarehouseEnabled.ts diff --git a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts index 675d6d7c473..6102997de24 100644 --- a/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts +++ b/apps/studio/components/interfaces/ConnectSheet/Connect.types.ts @@ -57,7 +57,7 @@ export type ConditionalValue = // Schema Types - Modes // ============================================================================ -export const CONNECT_MODES = ['framework', 'direct', 'orm', 'mcp', 'server'] as const +export const CONNECT_MODES = ['framework', 'direct', 'orm', 'mcp', 'server', 'warehouse'] as const export type ConnectMode = (typeof CONNECT_MODES)[number] export interface ModeDefinition { diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx index 566bc55d720..320e3cb34d1 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectConfigSection.tsx @@ -1,4 +1,4 @@ -import { Box, Cable, Database, Server, Sparkles } from 'lucide-react' +import { Box, Cable, Database, Server, Sparkles, Warehouse } from 'lucide-react' import { cn, RadioGroupStacked, @@ -28,6 +28,7 @@ const MODE_ICONS: Record = { orm: , mcp: , server: , + warehouse: , } interface ConnectConfigSectionProps { diff --git a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx index fc3fb8158db..9fc11041dfd 100644 --- a/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx +++ b/apps/studio/components/interfaces/ConnectSheet/ConnectSheet.tsx @@ -12,6 +12,7 @@ import { useAvailableConnectModes } from './useAvailableConnectModes' import { useConnectSheetParams } from './useConnectSheetParams' import { useConnectSheetShortcut } from './useConnectSheetShortcut' import { useConnectState } from './useConnectState' +import { WarehouseConnectPanel } from './WarehouseConnectPanel' import { useAPIKeys } from '@/data/api-keys/api-keys-query' import { useProjectApiUrl } from '@/data/config/project-endpoint-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' @@ -233,7 +234,11 @@ export const ConnectSheet = () => { )} - + {state.mode === 'warehouse' ? ( + + ) : ( + + )} diff --git a/apps/studio/components/interfaces/ConnectSheet/WarehouseConnectPanel.tsx b/apps/studio/components/interfaces/ConnectSheet/WarehouseConnectPanel.tsx new file mode 100644 index 00000000000..7f9c35d3e60 --- /dev/null +++ b/apps/studio/components/interfaces/ConnectSheet/WarehouseConnectPanel.tsx @@ -0,0 +1,59 @@ +import { useParams } from 'common' +import { useMemo } from 'react' +import { CodeBlock } from 'ui-patterns/CodeBlock' + +import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore' +import { buildWarehouseConnectionString } from '@/components/interfaces/Database/Warehouse/warehouseReplication.utils' + +export function WarehouseConnectPanel() { + const { ref: projectRef } = useParams() + const warehouseState = useWarehouseProjectState(projectRef) + + const connectionString = useMemo( + () => + buildWarehouseConnectionString({ + projectRef: projectRef ?? '', + databaseName: 'postgres', + }), + [projectRef] + ) + + if (!warehouseState.enabled) { + return ( +
+

+ Enable Warehouse from Database → Replication to view connection credentials. +

+
+ ) + } + + return ( +
+
+
+

Warehouse endpoint

+

+ Connect directly to your Warehouse replica. Schema and table names match Postgres (for + example, public.events on both endpoints). +

+
+
+

Host

+ + {warehouseState.warehouseHost} + +
+
+

Connection string

+ + {connectionString} + +
+

+ Replicated schemas: {warehouseState.includedSchemas.join(', ') || 'none'} +

+
+
+ ) +} diff --git a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts index 15204084a0f..b0763807924 100644 --- a/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts +++ b/apps/studio/components/interfaces/ConnectSheet/connect.schema.ts @@ -269,6 +269,12 @@ export const connectSchema: ConnectSchema = { description: 'Connect your agent', fields: ['mcpClient', 'mcpReadonly', 'mcpFeatures'], }, + { + id: 'warehouse', + label: 'Warehouse', + description: 'Warehouse endpoint', + fields: [], + }, ], // ------------------------------------------------------------------------- diff --git a/apps/studio/components/interfaces/ConnectSheet/useAvailableConnectModes.ts b/apps/studio/components/interfaces/ConnectSheet/useAvailableConnectModes.ts index 0fff82dfa37..0685615147f 100644 --- a/apps/studio/components/interfaces/ConnectSheet/useAvailableConnectModes.ts +++ b/apps/studio/components/interfaces/ConnectSheet/useAvailableConnectModes.ts @@ -1,9 +1,16 @@ +import { useParams } from 'common' import { useMemo } from 'react' import type { ConnectMode } from './Connect.types' +import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled' export function useAvailableConnectModes(): ConnectMode[] { + const { ref: projectRef } = useParams() + const isWarehouseFeatureEnabled = useIsWarehouseEnabled() + const warehouseState = useWarehouseProjectState(projectRef) + const warehouseProjectEnabled = warehouseState.enabled const { projectConnectionShowAppFrameworks: showAppFrameworks, projectConnectionShowMobileFrameworks: showMobileFrameworks, @@ -21,7 +28,14 @@ export function useAvailableConnectModes(): ConnectMode[] { { id: 'direct', enabled: true }, { id: 'orm', enabled: showOrms }, { id: 'mcp', enabled: true }, + { id: 'warehouse', enabled: isWarehouseFeatureEnabled && warehouseProjectEnabled }, ] return allModes.filter((m) => m.enabled).map((m) => m.id) - }, [showAppFrameworks, showMobileFrameworks, showOrms]) + }, [ + showAppFrameworks, + showMobileFrameworks, + showOrms, + isWarehouseFeatureEnabled, + warehouseProjectEnabled, + ]) } diff --git a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx index b137bb77234..81c51548ba4 100644 --- a/apps/studio/components/interfaces/Database/Replication/Destinations.tsx +++ b/apps/studio/components/interfaces/Database/Replication/Destinations.tsx @@ -1,5 +1,6 @@ import { useQueryClient } from '@tanstack/react-query' import { useParams } from 'common' +import { Database } from 'icons' import { MoreVertical, Plus, Search, X } from 'lucide-react' import { parseAsStringEnum, useQueryState } from 'nuqs' import { useEffect, useMemo, useRef, useState } from 'react' @@ -7,7 +8,6 @@ import { Button, Card, CardContent, - cn, DropdownMenu, DropdownMenuContent, DropdownMenuItem, @@ -21,6 +21,7 @@ import { } from 'ui' import { Admonition } from 'ui-patterns/admonition' import { Input } from 'ui-patterns/DataInputs/Input' +import { EmptyStatePresentational } from 'ui-patterns/EmptyStatePresentational' import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { REPLICA_STATUS } from '../../Settings/Infrastructure/InfrastructureConfiguration/InstanceConfiguration.constants' @@ -35,6 +36,8 @@ import { useIsETLIcebergPrivateAlpha, useIsETLSnowflakePrivateAlpha, } from './useIsETLPrivateAlpha' +import { useWarehouseProjectState } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore' +import { WarehouseDestinationRow } from '@/components/interfaces/Database/Warehouse/WarehouseDestinationRow' import { AlertError } from '@/components/ui/AlertError' import { DocsButton } from '@/components/ui/DocsButton' import { Shortcut } from '@/components/ui/Shortcut' @@ -46,6 +49,7 @@ import { useReplicationPipelinesQuery } from '@/data/replication/pipelines-query import { useReplicationSourcesQuery } from '@/data/replication/sources-query' import { checkLocalETLNotSetUp } from '@/data/replication/utils' import { useIsFeatureEnabled } from '@/hooks/misc/useIsFeatureEnabled' +import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled' import { DOCS_URL } from '@/lib/constants' import { SHORTCUT_IDS } from '@/state/shortcuts/registry' import { useShortcut } from '@/state/shortcuts/useShortcut' @@ -58,6 +62,9 @@ export const Destinations = () => { const etlEnableIceberg = useIsETLIcebergPrivateAlpha() const etlEnableDucklake = useIsETLDucklakePrivateAlpha() const etlEnableSnowflake = useIsETLSnowflakePrivateAlpha() + const isWarehouseFeatureEnabled = useIsWarehouseEnabled() + const warehouseState = useWarehouseProjectState(projectRef) + const hasManagedWarehouse = isWarehouseFeatureEnabled && warehouseState.enabled const { infrastructureReadReplicas } = useIsFeatureEnabled(['infrastructure:read_replicas']) const newDestinationDefaultType = infrastructureReadReplicas @@ -285,14 +292,15 @@ export const Destinations = () => { {isLocalETLNotSetUp && ( )} {isLoading ? ( - ) : hasReplicas || hasDestinations ? ( + ) : hasReplicas || hasDestinations || hasManagedWarehouse ? ( @@ -313,6 +321,8 @@ export const Destinations = () => { + {hasManagedWarehouse && } + {filteredReplicas.map((replica) => { return ( { {!isLoading && filteredDestinations.length === 0 && filteredReplicas.length === 0 && + !hasManagedWarehouse && (hasReplicas || hasDestinations) && ( @@ -347,27 +358,21 @@ export const Destinations = () => { ) : ( !isLoading && !hasErrorsFetchingData && ( -
-

Replication keeps your data in sync across systems

-

- Deploy Read Replicas for lower latency and workload isolation, or add a Pipelines - destination for analytics workloads. -

-
+ ) )} diff --git a/apps/studio/components/interfaces/Database/Tables/TableList.tsx b/apps/studio/components/interfaces/Database/Tables/TableList.tsx index 55f6cc664d0..d0f017a3d8d 100644 --- a/apps/studio/components/interfaces/Database/Tables/TableList.tsx +++ b/apps/studio/components/interfaces/Database/Tables/TableList.tsx @@ -37,6 +37,10 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { ProtectedSchemaWarning } from '../ProtectedSchemaWarning' import { formatAllEntities } from './Tables.utils' import { buildTableEditorUrl } from '@/components/grid/SupabaseGrid.utils' +import { + isTableReplicated, + useWarehouseProjectState, +} from '@/components/interfaces/Database/Warehouse/warehouseDemoStore' import { AlertError } from '@/components/ui/AlertError' import { ButtonTooltip } from '@/components/ui/ButtonTooltip' import { DropdownMenuItemTooltip } from '@/components/ui/DropdownMenuItemTooltip' @@ -51,6 +55,7 @@ import { usePrefetchEditorTablePage } from '@/data/prefetchers/project.$ref.edit import { useInfiniteTablesQuery } from '@/data/tables/tables-query' import { useViewsQuery } from '@/data/views/views-query' import { useAsyncCheckPermissions } from '@/hooks/misc/useCheckPermissions' +import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled' import { useQuerySchemaState } from '@/hooks/misc/useSchemaQueryState' import { useSelectedProjectQuery } from '@/hooks/misc/useSelectedProject' import { useIsProtectedSchema } from '@/hooks/useProtectedSchemas' @@ -75,6 +80,9 @@ export const TableList = ({ const router = useRouter() const { ref } = useParams() const { data: project } = useSelectedProjectQuery() + const isWarehouseFeatureEnabled = useIsWarehouseEnabled() + const warehouseState = useWarehouseProjectState(ref) + const showReplicatedColumn = isWarehouseFeatureEnabled && warehouseState.enabled const prefetchEditorTablePage = usePrefetchEditorTablePage() @@ -377,6 +385,7 @@ export const TableList = ({ Columns Rows (Estimated) Size (Estimated) + {showReplicatedColumn && Replicated} Realtime
@@ -385,7 +394,7 @@ export const TableList = ({ <> {entities.length === 0 && filterString.length === 0 && ( - + {visibleTypes.length === 0 ? ( <>

@@ -420,7 +429,7 @@ export const TableList = ({ )} {entities.length === 0 && filterString.length > 0 && ( - +

No results found

Your search for "{filterString}" did not return any results @@ -489,6 +498,21 @@ export const TableList = ({

–

)}
+ {showReplicatedColumn && ( + + {isTableReplicated(ref, x.schema, x.name) ? ( +
+ +

Yes

+
+ ) : ( +
+ +

No

+
+ )} +
+ )} {(realtimePublication?.tables ?? []).find( (table) => table.id === x.id diff --git a/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_API_CONTRACT.md b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_API_CONTRACT.md new file mode 100644 index 00000000000..211b3411408 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_API_CONTRACT.md @@ -0,0 +1,79 @@ +# Warehouse API contract (planned) + +> **Prototype note:** Studio wholesale branch uses `warehouseDemoStore` only. Endpoints below are the target platform contract; not implemented in this branch. + +## Model + +- **Project-wide** enable/disable (not per-table hero API) +- Separate **`warehouse.*` host** with **1:1 schema.table names** +- Managed publication: `supabase_warehouse` +- Disable **deletes** all Warehouse replicas; re-enable triggers full backfill + +## Endpoints (planned) + +### `POST /platform/projects/{ref}/warehouse/enable` + +Enable Warehouse for the project. + +**Request body:** + +```json +{ + "included_schemas": ["public", "storage"] +} +``` + +**Response:** + +```json +{ + "enabled": true, + "replication_phase": "provisioning", + "pipeline_id": "7022", + "warehouse_host": "warehouse..supabase.co", + "publication_name": "supabase_warehouse", + "included_schemas": ["public", "storage"] +} +``` + +### `PATCH /platform/projects/{ref}/warehouse/schemas` + +Update which schemas are replicated. + +**Request body:** + +```json +{ + "included_schemas": ["public"] +} +``` + +Adding schemas triggers backfill for new tables. Removing schemas drops Warehouse replicas for those schemas. + +### `DELETE /platform/projects/{ref}/warehouse` + +Disable Warehouse and delete all replicas. + +**Response:** `204 No Content` + +### `GET /platform/projects/{ref}/warehouse` + +Project Warehouse status (phase, lag, included schemas, pipeline metadata). + +## Table replicated check (Studio) + +A table is **replicated** when: + +1. Project Warehouse is enabled +2. Table's schema is in `included_schemas` +3. Table is not on the system denylist (e.g. `supabase_migrations`) + +Studio table list **Replicated** column is read-only metadata derived from this. + +## SQL query target (Studio) + +Studio SQL Editor target toggle (`postgres` | `warehouse`) is a **query routing hint** for the execution layer. Wholesale prototype does not route queries; it shows `Served by: Warehouse` in the results footer when Warehouse is selected. + +## Config (out of Studio scope) + +Notion simplification referenced `[warehouse] enabled = true` in `config.toml`. Studio wholesale MVP does not edit config; enable/disable is project-scoped in the Replication UI. diff --git a/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_NAMING.md b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_NAMING.md new file mode 100644 index 00000000000..d17acfe693d --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_NAMING.md @@ -0,0 +1,75 @@ +# Warehouse product naming + +Studio copy for Warehouse uses **replicate** as the shared verb and **qualified nouns** for the destination. The goal is one replication mental model across destinations (Warehouse, read replicas, external pipelines) without teaching another product concept. + +## Principles + +1. **Verb stays lean** — `replicate`, `replicating`, `stop replicating` +2. **Noun carries the destination** — always qualify: **Warehouse replica**, **read replica** +3. **Never bare "replica"** in user-facing UI without a destination prefix +4. **Avoid loaded terms** — do not use `link`, `linked`, `copy`, or `mirror` in product copy (platform API types may still use them) + +## Endpoint model (wholesale MVP) + +Warehouse uses a **separate host** with **identical schema and table names**: + +| Postgres (transactional) | Warehouse (analytical) | +| ---------------------------- | ----------------------------------- | +| `public.events` | `public.events` | +| Host: `db..supabase.co` | Host: `warehouse..supabase.co` | + +Do **not** use `public_warehouse.events` or other renamed schemas in user-facing copy. That was a pre-pivot exploration. + +## User-facing vocabulary + +| Concept | Use | Avoid | +| -------------------- | --------------------------------------------------------- | ------------------------------------- | +| Project action | **Enable Warehouse** | Turn on Warehouse, Link project | +| In progress | **Replicating** / **Backfilling** | Syncing, Linking | +| Steady state | **Streaming** / **Active** | Replica live (unless emphasis needed) | +| The analytical table | **Warehouse replica** (`public.events` on Warehouse host) | Linked table, Warehouse copy | +| Ongoing process | **Warehouse replication** | Warehouse link, sync | +| Remove | **Disable Warehouse** | Unlink, Detach, Delete copy | +| Table list column | **Replicated** (generic; not Warehouse-branded) | Warehouse column, Linked | +| Postgres product | **Read replica** | Replica (alone) | + +## Status labels + +| Internal phase | UI label (Destinations) | UI label (card chip) | +| -------------- | ----------------------- | -------------------- | +| `provisioning` | Starting | Starting | +| `backfilling` | Backfilling | Backfilling | +| `streaming` | Active | Active | +| `failed` | Failed | Failed | + +## Example copy + +| Surface | Copy | +| ---------------------- | --------------------------------------------------- | +| Enable CTA | Enable Warehouse | +| Enable dialog title | Enable Warehouse | +| Disable confirm title | Disable Warehouse? | +| Disable confirm body | This deletes all Warehouse replicas | +| Replication card (off) | Analytical replica on a separate Warehouse endpoint | +| SQL Editor toggle | Postgres \| Warehouse | +| SQL Editor footer | Served by: Warehouse | +| Connect tab | Warehouse | + +## Internal code (not user-facing) + +| Internal | Meaning | +| -------------------- | ------------------------------- | +| `includedSchemas` | Schemas replicated to Warehouse | +| `replicationPhase` | Project pipeline phase | +| `publicationName` | `supabase_warehouse` (managed) | +| `warehouseDemoStore` | Prototype mock state | + +See `WAREHOUSE_API_CONTRACT.md` for planned platform endpoints. + +## Terms we rejected + +| Term | Why | +| --------------------------- | --------------------------------------------------- | +| **Link / linked** | Reads like foreign keys or ETL pipeline config | +| **Copy** | Implies a one-off snapshot, not ongoing replication | +| **public_warehouse schema** | Pre-pivot; replaced by 1:1 endpoint mapping | diff --git a/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_WHOLESALE.md b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_WHOLESALE.md new file mode 100644 index 00000000000..a0e53cdef52 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WAREHOUSE_WHOLESALE.md @@ -0,0 +1,49 @@ +# Warehouse Wholesale MVP + +Branch: `dnywh/prototype/warehouse-wholesale` + +## Intent + +Project-wide Warehouse prototype for Studio. Postgres remains the transactional database; Warehouse is an analytical replica on a **separate `warehouse.*` host** with **1:1 schema and table names** (`public.events` on both). + +All platform behavior is mocked via `warehouseDemoStore` (localStorage). No real API endpoints. + +## In scope + +- **Replication page** — `WarehouseProjectCard` (enable/disable, schema scope, observability) +- **Destinations table** — read-only managed row (`DuckLake` / `supabase_warehouse`) +- **Table list** — read-only **Replicated** column +- **SQL Editor** — Postgres / Warehouse target toggle + mock "Served by: Warehouse" footer +- **Connect sheet** — Warehouse tab with connection credentials when enabled +- **Feature flag** — `useIsWarehouseEnabled` (ConfigCat `warehouse` org slug allowlist) + +## Out of scope + +- Per-table enablement hero UX +- `?view=warehouse`, table-detail storage tab +- `public_warehouse.*` FDW naming (pre-pivot model) +- Catalog integration install gate +- Real query routing to Warehouse +- config.toml editing in Studio + +## Demo store + +State key: `supabase-warehouse-wholesale-demo` in localStorage, keyed by project ref. + +```ts +// apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts +enableWarehouseProject(projectRef, includedSchemas) +disableWarehouseProject(projectRef) +updateWarehouseIncludedSchemas(projectRef, schemas) +isTableReplicated(projectRef, schema, table) +``` + +Enable simulates `provisioning` → `backfilling` → `streaming` phase progression. + +## QA checklist + +1. Flag off → no Warehouse UI +2. Flag on, disabled → Replication card with Enable CTA only +3. Enable → observability metrics, schema checkboxes, Destinations row, Replicated column, SQL toggle, Connect Warehouse tab +4. Uncheck schema → tables in that schema show Replicated = No +5. Disable → confirm destructive modal, all Warehouse UI hidden diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseDestinationRow.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseDestinationRow.tsx new file mode 100644 index 00000000000..7f17401f9dd --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseDestinationRow.tsx @@ -0,0 +1,64 @@ +import { useParams } from 'common' +import { Database, Minus } from 'lucide-react' +import { TableCell, TableRow, Tooltip, TooltipContent, TooltipTrigger } from 'ui' + +import { MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP } from './managedWarehouse.resources' +import { useWarehouseProjectState } from './warehouseDemoStore' +import { + formatReplicationLagSeconds, + getWarehouseDestinationStatusLabel, +} from './warehouseReplication.utils' +import { WarehouseSyncChip } from './WarehouseSyncChip' + +export function WarehouseDestinationRow() { + const { ref: projectRef } = useParams() + const state = useWarehouseProjectState(projectRef) + + if (!state.enabled || !state.pipelineId) return null + + const statusLabel = getWarehouseDestinationStatusLabel(state.replicationPhase) + const lag = formatReplicationLagSeconds(state.lagSeconds) + const isCaughtUp = state.lagSeconds === 0 || state.lagSeconds === 2 + + return ( + + + + + +
+

DuckLake (Pipeline ID: {state.pipelineId})

+

{state.destinationName}

+
+
+ + + {statusLabel === 'Failed' && ( +

Managed Warehouse pipeline

+ )} +
+ + {state.replicationPhase === 'streaming' || state.replicationPhase === 'backfilling' ? ( + isCaughtUp ? ( +

Caught up

+ ) : ( +

{lag}

+ ) + ) : ( + + )} +
+ + + + {state.publicationName} + + + {MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP} + + + + +
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseDisableModal.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseDisableModal.tsx new file mode 100644 index 00000000000..73624e32cd4 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseDisableModal.tsx @@ -0,0 +1,62 @@ +import { useParams } from 'common' +import { useState } from 'react' +import { + Button, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, +} from 'ui' +import { Admonition } from 'ui-patterns/admonition' + +import { disableWarehouseProject } from './warehouseDemoStore' + +interface WarehouseDisableModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function WarehouseDisableModal({ open, onOpenChange }: WarehouseDisableModalProps) { + const { ref: projectRef } = useParams() + const [isDisabling, setIsDisabling] = useState(false) + + const onDisable = async () => { + if (!projectRef) return + setIsDisabling(true) + disableWarehouseProject(projectRef) + setIsDisabling(false) + onOpenChange(false) + } + + return ( + + + + Disable Warehouse? + + +

+ This deletes all Warehouse replicas for this project. +

+ +
+ + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseEnableModal.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseEnableModal.tsx new file mode 100644 index 00000000000..a76f2c22c59 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseEnableModal.tsx @@ -0,0 +1,103 @@ +import { useParams } from 'common' +import { useEffect, useState } from 'react' +import { + Button, + Checkbox, + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogSection, + DialogSectionSeparator, + DialogTitle, + Label, +} from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { enableWarehouseProject } from './warehouseDemoStore' +import { useDefaultWarehouseSchemas } from './WarehouseSchemaScope' + +interface WarehouseEnableModalProps { + open: boolean + onOpenChange: (open: boolean) => void +} + +export function WarehouseEnableModal({ open, onOpenChange }: WarehouseEnableModalProps) { + const { ref: projectRef } = useParams() + const defaultSchemas = useDefaultWarehouseSchemas() + const [selectedSchemas, setSelectedSchemas] = useState(defaultSchemas) + const [isEnabling, setIsEnabling] = useState(false) + + useEffect(() => { + if (open) setSelectedSchemas(defaultSchemas) + }, [open, defaultSchemas]) + + const selectedSet = new Set(selectedSchemas) + + const onToggle = (schema: string, checked: boolean) => { + setSelectedSchemas((prev) => (checked ? [...prev, schema] : prev.filter((s) => s !== schema))) + } + + const onEnable = async () => { + if (!projectRef || selectedSchemas.length === 0) return + setIsEnabling(true) + enableWarehouseProject(projectRef, selectedSchemas) + setIsEnabling(false) + onOpenChange(false) + } + + return ( + + + + Enable Warehouse + + +

+ Create an analytical replica of your Postgres data on a separate Warehouse endpoint. + Tables use the same schema and table names (for example, public.events). +

+

+ Select which schemas to replicate. You can change this later from the Warehouse card. +

+
+ {defaultSchemas.map((schema) => ( + + onToggle(schema, checked === true)} + /> + + + ))} +
+ +
+ + + + + +
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseObservabilityPanel.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseObservabilityPanel.tsx new file mode 100644 index 00000000000..33a20e68cb6 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseObservabilityPanel.tsx @@ -0,0 +1,50 @@ +import { useParams } from 'common' +import { Card, CardContent } from 'ui' +import { ChartMetric } from 'ui-patterns/Chart' + +import { useWarehouseProjectState } from './warehouseDemoStore' +import { formatReplicationLagSeconds, formatReplicationPhase } from './warehouseReplication.utils' +import { WarehouseSyncChip } from './WarehouseSyncChip' + +export function WarehouseObservabilityPanel() { + const { ref: projectRef } = useParams() + const state = useWarehouseProjectState(projectRef) + + if (!state.enabled) return null + + return ( + + +
+
+ +
+ +
+
+
+ +
+
+ +
+
+
+
+ ) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseProjectCard.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseProjectCard.tsx new file mode 100644 index 00000000000..1a6f865810f --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseProjectCard.tsx @@ -0,0 +1,84 @@ +import { useParams } from 'common' +import { parseAsBoolean, useQueryState } from 'nuqs' +import { useState } from 'react' +import { Button, Card, CardContent } from 'ui' + +import { useWarehouseProjectState } from './warehouseDemoStore' +import { WarehouseDisableModal } from './WarehouseDisableModal' +import { WarehouseEnableModal } from './WarehouseEnableModal' +import { WarehouseObservabilityPanel } from './WarehouseObservabilityPanel' +import { WarehouseSchemaScope } from './WarehouseSchemaScope' +import { WarehouseSyncChip } from './WarehouseSyncChip' +import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled' + +export function WarehouseProjectCard() { + const { ref: projectRef } = useParams() + const isWarehouseFeatureEnabled = useIsWarehouseEnabled() + const warehouseState = useWarehouseProjectState(projectRef) + + const [showEnableModal, setShowEnableModal] = useState(false) + const [showDisableModal, setShowDisableModal] = useState(false) + const [, setShowConnect] = useQueryState('showConnect', parseAsBoolean.withDefault(false)) + const [, setConnectTab] = useQueryState('connectTab') + + if (!isWarehouseFeatureEnabled) return null + + return ( + <> + + +
+
+
+

Warehouse

+ {warehouseState.enabled && ( + + )} +
+

+ {warehouseState.enabled + ? 'Analytical replica of your Postgres data on a separate Warehouse endpoint. Query the same schema and table names against the Warehouse host.' + : 'Enable an analytical replica of your Postgres data on a separate Warehouse endpoint for analytics workloads.'} +

+
+
+ {warehouseState.enabled ? ( + <> + + + + ) : ( + + )} +
+
+ + {warehouseState.enabled && ( + <> + + + + )} +
+
+ + + + + ) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseSchemaScope.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseSchemaScope.tsx new file mode 100644 index 00000000000..6ab1c96294f --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseSchemaScope.tsx @@ -0,0 +1,85 @@ +import { useParams } from 'common' +import { useMemo } from 'react' +import { Checkbox, Label } from 'ui' +import { FormItemLayout } from 'ui-patterns/form/FormItemLayout/FormItemLayout' + +import { + getDefaultIncludedSchemas, + isReplicableSchema, + updateWarehouseIncludedSchemas, + useWarehouseProjectState, +} from './warehouseDemoStore' +import { useSchemasQuery } from '@/data/database/schemas-query' + +interface WarehouseSchemaScopeProps { + disabled?: boolean +} + +export function WarehouseSchemaScope({ disabled = false }: WarehouseSchemaScopeProps) { + const { ref: projectRef } = useParams() + const { data: schemas = [] } = useSchemasQuery({ projectRef }) + const warehouseState = useWarehouseProjectState(projectRef) + + const replicableSchemas = useMemo( + () => + schemas + .map((s) => s.name) + .filter(isReplicableSchema) + .sort(), + [schemas] + ) + + const includedSet = new Set(warehouseState.includedSchemas) + + const onToggle = (schema: string, checked: boolean) => { + if (!projectRef) return + const next = checked + ? [...warehouseState.includedSchemas, schema] + : warehouseState.includedSchemas.filter((s) => s !== schema) + updateWarehouseIncludedSchemas(projectRef, next) + } + + if (!warehouseState.enabled) return null + + return ( +
+
+

Replicated schemas

+

+ Tables in selected schemas are replicated to Warehouse with the same schema and table + names. +

+
+
+ {replicableSchemas.map((schema) => ( + + onToggle(schema, checked === true)} + /> + + + ))} + {replicableSchemas.length === 0 && ( +

No replicable schemas found.

+ )} +
+
+ ) +} + +export function useDefaultWarehouseSchemas(): string[] { + const { ref: projectRef } = useParams() + const { data: schemas = [] } = useSchemasQuery({ projectRef }) + return useMemo(() => getDefaultIncludedSchemas(schemas.map((s) => s.name)), [schemas]) +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/WarehouseSyncChip.tsx b/apps/studio/components/interfaces/Database/Warehouse/WarehouseSyncChip.tsx new file mode 100644 index 00000000000..6df6d8c4389 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/WarehouseSyncChip.tsx @@ -0,0 +1,23 @@ +import { Badge } from 'ui' + +import type { ReplicationPhase } from './warehouseDemoStore' +import { getWarehouseDestinationStatusLabel } from './warehouseReplication.utils' + +interface WarehouseSyncChipProps { + phase: ReplicationPhase +} + +export function WarehouseSyncChip({ phase }: WarehouseSyncChipProps) { + const label = getWarehouseDestinationStatusLabel(phase) + + const variant = + phase === 'failed' + ? 'destructive' + : phase === 'backfilling' || phase === 'provisioning' + ? 'warning' + : phase === 'streaming' + ? 'success' + : 'default' + + return {label} +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/managedWarehouse.resources.ts b/apps/studio/components/interfaces/Database/Warehouse/managedWarehouse.resources.ts new file mode 100644 index 00000000000..1cb1fd73615 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/managedWarehouse.resources.ts @@ -0,0 +1,8 @@ +export const SUPABASE_MANAGED_WAREHOUSE_RESOURCE_NAME = 'supabase_warehouse' + +export function isSupabaseManagedWarehousePublicationName(name: string): boolean { + return name === SUPABASE_MANAGED_WAREHOUSE_RESOURCE_NAME +} + +export const MANAGED_WAREHOUSE_PUBLICATION_TOOLTIP = + 'Managed by Supabase for Warehouse replication. Configure schemas on the Warehouse card above.' diff --git a/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts b/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts new file mode 100644 index 00000000000..37ccce76595 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/warehouseDemoStore.ts @@ -0,0 +1,185 @@ +import { proxy, subscribe, useSnapshot } from 'valtio' + +import { SUPABASE_MANAGED_WAREHOUSE_RESOURCE_NAME } from './managedWarehouse.resources' +import { getMockWarehouseHost } from './warehouseReplication.utils' +import { INTERNAL_SCHEMAS } from '@/hooks/useProtectedSchemas' + +export type ReplicationPhase = 'idle' | 'provisioning' | 'backfilling' | 'streaming' | 'failed' + +export interface WarehouseProjectState { + enabled: boolean + replicationPhase: ReplicationPhase + lagSeconds: number | null + includedSchemas: string[] + pipelineId: string | null + destinationName: string + publicationName: string + warehouseHost: string + enabledAt: string | null +} + +const DEFAULT_STATE: WarehouseProjectState = { + enabled: false, + replicationPhase: 'idle', + lagSeconds: null, + includedSchemas: [], + pipelineId: null, + destinationName: 'DuckLake', + publicationName: SUPABASE_MANAGED_WAREHOUSE_RESOURCE_NAME, + warehouseHost: '', + enabledAt: null, +} + +const STORAGE_KEY = 'supabase-warehouse-wholesale-demo' + +export const warehouseDemoStore = proxy<{ + projects: Record +}>({ + projects: {}, +}) + +function loadFromStorage(): void { + if (typeof window === 'undefined') return + try { + const raw = window.localStorage.getItem(STORAGE_KEY) + if (!raw) return + const parsed = JSON.parse(raw) as Record + warehouseDemoStore.projects = parsed + } catch { + // ignore corrupt storage + } +} + +function saveToStorage(): void { + if (typeof window === 'undefined') return + window.localStorage.setItem(STORAGE_KEY, JSON.stringify(warehouseDemoStore.projects)) +} + +loadFromStorage() +subscribe(warehouseDemoStore, saveToStorage) + +const SYSTEM_TABLE_DENYLIST = new Set(['supabase_migrations', 'schema_migrations']) + +const NON_REPLICABLE_SCHEMAS = new Set(INTERNAL_SCHEMAS) + +export function isReplicableSchema(schema: string): boolean { + return !NON_REPLICABLE_SCHEMAS.has(schema) +} + +export function getDefaultIncludedSchemas(availableSchemas: string[]): string[] { + return availableSchemas.filter(isReplicableSchema) +} + +function getProjectState(projectRef: string): WarehouseProjectState { + return warehouseDemoStore.projects[projectRef] ?? { ...DEFAULT_STATE } +} + +function setProjectState(projectRef: string, next: WarehouseProjectState): void { + warehouseDemoStore.projects[projectRef] = next +} + +export function useWarehouseProjectState(projectRef: string | undefined) { + const snap = useSnapshot(warehouseDemoStore) + if (!projectRef) return { ...DEFAULT_STATE } + return snap.projects[projectRef] ?? { ...DEFAULT_STATE } +} + +export function isWarehouseProjectEnabled(projectRef: string | undefined): boolean { + if (!projectRef) return false + return getProjectState(projectRef).enabled +} + +export function isTableReplicated( + projectRef: string | undefined, + schema: string, + table: string +): boolean { + if (!projectRef) return false + const state = getProjectState(projectRef) + if (!state.enabled) return false + if (!state.includedSchemas.includes(schema)) return false + if (SYSTEM_TABLE_DENYLIST.has(table)) return false + return isReplicableSchema(schema) +} + +let phaseTimer: ReturnType | null = null + +function clearPhaseTimer(): void { + if (phaseTimer) { + clearTimeout(phaseTimer) + phaseTimer = null + } +} + +function simulatePhaseProgression(projectRef: string): void { + clearPhaseTimer() + + phaseTimer = setTimeout(() => { + const current = getProjectState(projectRef) + if (!current.enabled || current.replicationPhase !== 'provisioning') return + setProjectState(projectRef, { + ...current, + replicationPhase: 'backfilling', + lagSeconds: 45, + }) + + phaseTimer = setTimeout(() => { + const backfilling = getProjectState(projectRef) + if (!backfilling.enabled || backfilling.replicationPhase !== 'backfilling') return + setProjectState(projectRef, { + ...backfilling, + replicationPhase: 'streaming', + lagSeconds: 2, + }) + }, 6000) + }, 3000) +} + +export function enableWarehouseProject(projectRef: string, includedSchemas: string[]): void { + const pipelineId = String(7000 + Math.floor(Math.random() * 100)) + setProjectState(projectRef, { + enabled: true, + replicationPhase: 'provisioning', + lagSeconds: null, + includedSchemas, + pipelineId, + destinationName: 'DuckLake', + publicationName: SUPABASE_MANAGED_WAREHOUSE_RESOURCE_NAME, + warehouseHost: getMockWarehouseHost(projectRef), + enabledAt: new Date().toISOString(), + }) + simulatePhaseProgression(projectRef) +} + +export function disableWarehouseProject(projectRef: string): void { + clearPhaseTimer() + setProjectState(projectRef, { ...DEFAULT_STATE }) +} + +export function updateWarehouseIncludedSchemas( + projectRef: string, + includedSchemas: string[] +): void { + const current = getProjectState(projectRef) + if (!current.enabled) return + + const hadNewSchemas = includedSchemas.some((s) => !current.includedSchemas.includes(s)) + setProjectState(projectRef, { + ...current, + includedSchemas, + replicationPhase: hadNewSchemas ? 'backfilling' : current.replicationPhase, + lagSeconds: hadNewSchemas ? 30 : current.lagSeconds, + }) + + if (hadNewSchemas) { + phaseTimer = setTimeout(() => { + const state = getProjectState(projectRef) + if (!state.enabled) return + setProjectState(projectRef, { + ...state, + replicationPhase: 'streaming', + lagSeconds: 2, + }) + }, 4000) + } +} diff --git a/apps/studio/components/interfaces/Database/Warehouse/warehouseReplication.utils.ts b/apps/studio/components/interfaces/Database/Warehouse/warehouseReplication.utils.ts new file mode 100644 index 00000000000..d661110d1c7 --- /dev/null +++ b/apps/studio/components/interfaces/Database/Warehouse/warehouseReplication.utils.ts @@ -0,0 +1,62 @@ +import type { ReplicationPhase, WarehouseProjectState } from './warehouseDemoStore' + +export function formatReplicationPhase(phase: ReplicationPhase): string { + switch (phase) { + case 'idle': + return 'Not enabled' + case 'provisioning': + return 'Provisioning' + case 'backfilling': + return 'Backfilling' + case 'streaming': + return 'Streaming' + case 'failed': + return 'Error' + default: + return phase + } +} + +export function formatReplicationLagSeconds(lagSeconds: number | null): string { + if (lagSeconds === null) return '—' + if (lagSeconds === 0) return 'Caught up' + if (lagSeconds < 60) return `${lagSeconds}s` + if (lagSeconds < 3600) return `${Math.round(lagSeconds / 60)}m` + return `${(lagSeconds / 3600).toFixed(1)}h` +} + +export function getWarehouseDestinationStatusLabel(phase: ReplicationPhase): string { + switch (phase) { + case 'provisioning': + return 'Starting' + case 'backfilling': + return 'Backfilling' + case 'streaming': + return 'Active' + case 'failed': + return 'Failed' + default: + return 'Unknown' + } +} + +export function getMockWarehouseHost(projectRef: string): string { + return `warehouse.${projectRef}.supabase.co` +} + +export function buildWarehouseConnectionString({ + projectRef, + databaseName = 'postgres', + port = 5432, +}: { + projectRef: string + databaseName?: string + port?: number +}): string { + const host = getMockWarehouseHost(projectRef) + return `postgresql://postgres.[YOUR-PASSWORD]@${host}:${port}/${databaseName}` +} + +export function isWarehouseReplicationHealthy(state: WarehouseProjectState): boolean { + return state.enabled && state.replicationPhase === 'streaming' && (state.lagSeconds ?? 0) < 120 +} diff --git a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx index 085cfdf1714..23dc1fa3575 100644 --- a/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx +++ b/apps/studio/components/interfaces/SQLEditor/SQLEditor.tsx @@ -56,6 +56,7 @@ import { isUpdateWithoutWhere, suffixWithLimit, } from './SQLEditor.utils' +import { useSqlQueryTarget } from './SqlWarehouseTargetToggle' import { useAddDefinitions } from './useAddDefinitions' import { UtilityPanel } from './UtilityPanel/UtilityPanel' import { @@ -129,6 +130,7 @@ export const SQLEditor = () => { const getImpersonatedRoleState = useGetImpersonatedRoleState() const databaseSelectorState = useDatabaseSelectorStateSnapshot() const { aiOptInLevel } = useOrgAiOptInLevel() + const sqlQueryTarget = useSqlQueryTarget() // [Ali] Kill switch to hide the SQL Editor Explain tab and its entry points const disablePrettyExplain = useFlag('DisablePrettyExplainOnSqlEditor') @@ -1063,31 +1065,36 @@ export const SQLEditor = () => {
{results?.rows !== undefined && !isExecuting && ( - - -

- - {results.rows.length} row{results.rows.length > 1 ? 's' : ''} - - - {results.autoLimit !== undefined && - ` (Limited to only ${results.autoLimit} rows)`} - -

-
- -

- - Results are automatically limited to preserve browser performance, in - particular if your query returns an exceptionally large number of rows. - +

+ + +

+ + {results.rows.length} row{results.rows.length > 1 ? 's' : ''} + + + {results.autoLimit !== undefined && + ` (Limited to only ${results.autoLimit} rows)`} + +

+
+ +

+ + Results are automatically limited to preserve browser performance, in + particular if your query returns an exceptionally large number of rows. + - - You may change or remove this limit from the dropdown on the right - -

-
-
+ + You may change or remove this limit from the dropdown on the right + +

+ + + {sqlQueryTarget === 'warehouse' && ( +

Served by: Warehouse

+ )} +
{results.autoLimit !== undefined && ( diff --git a/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx b/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx new file mode 100644 index 00000000000..673d5ee6df5 --- /dev/null +++ b/apps/studio/components/interfaces/SQLEditor/SqlWarehouseTargetToggle.tsx @@ -0,0 +1,70 @@ +import { LOCAL_STORAGE_KEYS, useParams } from 'common' +import { cn } from 'ui' + +import { isWarehouseProjectEnabled } from '@/components/interfaces/Database/Warehouse/warehouseDemoStore' +import { useIsWarehouseEnabled } from '@/hooks/misc/useIsWarehouseEnabled' +import { useLocalStorageQuery } from '@/hooks/misc/useLocalStorage' + +export type SqlQueryTarget = 'postgres' | 'warehouse' + +interface SqlWarehouseTargetToggleProps { + className?: string +} + +export function SqlWarehouseTargetToggle({ className }: SqlWarehouseTargetToggleProps) { + const { ref: projectRef } = useParams() + const isWarehouseFeatureEnabled = useIsWarehouseEnabled() + const warehouseEnabled = isWarehouseProjectEnabled(projectRef) + + const [queryTarget, setQueryTarget] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string), + 'postgres' + ) + + if (!isWarehouseFeatureEnabled || !warehouseEnabled) return null + + return ( +
+ + +
+ ) +} + +export function useSqlQueryTarget(): SqlQueryTarget { + const { ref: projectRef } = useParams() + const [queryTarget] = useLocalStorageQuery( + LOCAL_STORAGE_KEYS.SQL_EDITOR_QUERY_TARGET(projectRef as string), + 'postgres' + ) + return queryTarget +} diff --git a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx index ed3809fbc56..8b2a507b953 100644 --- a/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx +++ b/apps/studio/components/interfaces/SQLEditor/UtilityPanel/UtilityActions.tsx @@ -16,6 +16,7 @@ import { TooltipTrigger, } from 'ui' +import { SqlWarehouseTargetToggle } from '../SqlWarehouseTargetToggle' import { AutosaveStatus } from './AutosaveStatus' import { SqlRunButton } from './RunButton' import { SqlSaveButton } from './SaveButton' @@ -212,6 +213,7 @@ export const UtilityActions = ({
+
{IS_PLATFORM && ( ('warehouse') + const { data: organization } = useSelectedOrganizationQuery() + + const allowedOrgSlugs = + typeof flagValue === 'string' ? flagValue.split(',').map((slug) => slug.trim()) : [] + + if (allowedOrgSlugs.includes('all')) return true + + return allowedOrgSlugs.includes(organization?.slug ?? '') +} diff --git a/apps/studio/pages/project/[ref]/database/replication/index.tsx b/apps/studio/pages/project/[ref]/database/replication/index.tsx index 2d48e5b2721..6a8dd329afa 100644 --- a/apps/studio/pages/project/[ref]/database/replication/index.tsx +++ b/apps/studio/pages/project/[ref]/database/replication/index.tsx @@ -2,6 +2,7 @@ import { GenericSkeletonLoader } from 'ui-patterns/ShimmeringLoader' import { Destinations } from '@/components/interfaces/Database/Replication/Destinations' import { ReplicationDiagram } from '@/components/interfaces/Database/Replication/ReplicationDiagram' +import { WarehouseProjectCard } from '@/components/interfaces/Database/Warehouse/WarehouseProjectCard' import DatabaseLayout from '@/components/layouts/DatabaseLayout/DatabaseLayout' import { DefaultLayout } from '@/components/layouts/DefaultLayout' import { ScaffoldContainer, ScaffoldSection } from '@/components/layouts/Scaffold' @@ -55,6 +56,11 @@ const DatabaseReplicationPage: NextPageWithLayout = () => { ) : ( <> + + + + + diff --git a/packages/common/constants/local-storage.ts b/packages/common/constants/local-storage.ts index 0968d6892df..833846a82ba 100644 --- a/packages/common/constants/local-storage.ts +++ b/packages/common/constants/local-storage.ts @@ -47,6 +47,7 @@ export const LOCAL_STORAGE_KEYS = { SQL_EDITOR_AI_SCHEMA: (ref: string) => `supabase_sql-editor-ai-schema-enabled-${ref}`, SQL_EDITOR_AI_OPEN: 'supabase_sql-editor-ai-open', SQL_EDITOR_LAST_SELECTED_DB: (ref: string) => `sql-editor-last-selected-db-${ref}`, + SQL_EDITOR_QUERY_TARGET: (ref: string) => `sql-editor-query-target-${ref}`, SQL_EDITOR_SQL_BLOCK_ACKNOWLEDGED: (ref: string) => `sql-editor-sql-block-acknowledged-${ref}`, SQL_EDITOR_SECTION_STATE: (ref: string) => `sql-editor-section-state-${ref}`, SQL_EDITOR_SORT: (ref: string) => `sql-editor-sort-${ref}`, diff --git a/packages/common/telemetry-constants.ts b/packages/common/telemetry-constants.ts index 659541bcc9e..c72415e19ac 100644 --- a/packages/common/telemetry-constants.ts +++ b/packages/common/telemetry-constants.ts @@ -2099,7 +2099,7 @@ export interface HomeConnectActionClickedEvent { /** * The connect action/tile that was clicked */ - mode: 'framework' | 'direct' | 'orm' | 'mcp' | 'server' | 'api_keys' + mode: 'framework' | 'direct' | 'orm' | 'mcp' | 'server' | 'warehouse' | 'api_keys' } groups: TelemetryGroups }