mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 17:35:10 +03:00
<!-- ccr-slack-attribution --> _Requested by **Pam Chia** · [Slack thread](https://supabase.slack.com/archives/C076KTY11DF/p1789979663384919?thread_ts=1789953229.116889&cid=C076KTY11DF)_ ## I have read the [CONTRIBUTING.md](https://github.com/supabase/supabase/blob/master/CONTRIBUTING.md) file. YES ## What kind of change does this PR introduce? Bug fix (telemetry). ## What is the current behavior? **Before:** Disabling Warehouse fires `warehouse_disabled` with no properties at all, while enabling it fires `warehouse_enabled` with `schemaTargetCount` and `tableTargetCount`. Disables can be counted, but nothing says how much was being replicated when the user turned it off, so churn cannot be segmented by the size or shape of the setup being torn down. ## What is the new behavior? **After:** `warehouse_disabled` carries `schemaTargetCount` and `tableTargetCount` with exactly the same meaning they have on `warehouse_enabled`: schemas replicated in full, and tables replicated individually on top of those. A disable of a project replicating one whole schema plus two loose tables now reports one schema target and two table targets, so enable and disable volume line up on the same two properties. ## Additional context **How:** The counts are read once, when the user confirms the dialog, and held in a ref until the mutation succeeds. The setup mutation's own `onSuccess` invalidates the setup-status and replication-sources queries and awaits those refetches before the caller's callback runs, so anything read inside `onSuccess` already reflects the post-disable state and would report nothing replicated. The event is tracked from that hook-level `onSuccess` rather than a `mutateAsync` callback: the status refetch swaps the Disable card out of the panel, and mutate-level callbacks are skipped once the component has unmounted. The shape is reproduced from the `supabase_warehouse` publication through the same helpers the table picker uses — the publication's tables become a selection, and that selection is mapped back to targets against the project's selectable schemas. Counting distinct schemas and tables off the replicated-table list instead would put a different meaning behind the same property names: a fully covered schema would be counted as its individual tables rather than as one schema target, and the two events would no longer be comparable. Both properties are optional. The replicated-table list is assembled from four queries, and when they have not resolved the properties are omitted rather than sent as `0`, so "unknown" is never recorded as "nothing was replicated". Tests: unit tests for the extracted `buildSchemasWithTables` helper, and a component test that drives the disable dialog against a publication covering one schema in full plus one table from another. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_0197pGnhiAkhiiYiRxY3qVFY --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Pamela Chia <pamelachiamayyee@gmail.com>
303 lines
11 KiB
TypeScript
303 lines
11 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
|
|
import {
|
|
buildSchemasWithTables,
|
|
buildSelectionFromPublicationTables,
|
|
buildWarehouseSetupTargets,
|
|
getSchemaCheckedState,
|
|
getSchemaTableKey,
|
|
getSelectedTableCount,
|
|
hasSelectionChanged,
|
|
isSelectableWarehouseSchema,
|
|
type SchemaTableSelection,
|
|
type SchemaWithTables,
|
|
} from './Warehouse.utils'
|
|
import { WAREHOUSE_METADATA_SCHEMA } from '@/lib/warehouse'
|
|
|
|
describe('WarehouseModePanel.utils:isSelectableWarehouseSchema', () => {
|
|
test('excludes information_schema', () => {
|
|
expect(isSelectableWarehouseSchema('information_schema')).toBe(false)
|
|
})
|
|
|
|
test('excludes any schema starting with pg_', () => {
|
|
expect(isSelectableWarehouseSchema('pg_catalog')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('pg_toast')).toBe(false)
|
|
})
|
|
|
|
test('excludes Supabase infrastructure schemas', () => {
|
|
// Replicating secrets or internal bookkeeping into a warehouse is never intended
|
|
expect(isSelectableWarehouseSchema('vault')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('pgsodium')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('realtime')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('_realtime')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('cron')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('supabase_migrations')).toBe(false)
|
|
expect(isSelectableWarehouseSchema('extensions')).toBe(false)
|
|
})
|
|
|
|
test('includes public and other user schemas', () => {
|
|
expect(isSelectableWarehouseSchema('public')).toBe(true)
|
|
expect(isSelectableWarehouseSchema('analytics')).toBe(true)
|
|
})
|
|
|
|
test('includes auth and storage, which hold product data users replicate', () => {
|
|
expect(isSelectableWarehouseSchema('auth')).toBe(true)
|
|
expect(isSelectableWarehouseSchema('storage')).toBe(true)
|
|
})
|
|
|
|
test("excludes the Warehouse's own DuckLake catalog schema", () => {
|
|
// Replicating the catalog that describes the Warehouse would feed it its own metadata
|
|
expect(isSelectableWarehouseSchema(WAREHOUSE_METADATA_SCHEMA)).toBe(false)
|
|
expect(isSelectableWarehouseSchema('ducklake')).toBe(false)
|
|
})
|
|
|
|
test('includes schemas whose names merely resemble the catalog schema', () => {
|
|
expect(isSelectableWarehouseSchema('ducklake_staging')).toBe(true)
|
|
expect(isSelectableWarehouseSchema('my_ducklake')).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:buildSelectionFromPublicationTables', () => {
|
|
test('returns an empty selection when the publication has no tables', () => {
|
|
expect(buildSelectionFromPublicationTables([])).toEqual({})
|
|
})
|
|
|
|
test('marks every publication table as selected', () => {
|
|
expect(
|
|
buildSelectionFromPublicationTables([
|
|
{ schema: 'public', name: 'orders' },
|
|
{ schema: 'auth', name: 'users' },
|
|
])
|
|
).toEqual({ 'public.orders': true, 'auth.users': true })
|
|
})
|
|
|
|
test('produces a fully checked schema when every table of that schema is published', () => {
|
|
const schemaTables = ['orders', 'customers']
|
|
const selection = buildSelectionFromPublicationTables(
|
|
schemaTables.map((name) => ({ schema: 'public', name }))
|
|
)
|
|
const selectedCount = schemaTables.filter(
|
|
(name) => selection[getSchemaTableKey('public', name)]
|
|
).length
|
|
|
|
expect(getSchemaCheckedState({ selectedCount, totalCount: schemaTables.length })).toBe(true)
|
|
})
|
|
|
|
test('produces an indeterminate schema when only some of its tables are published', () => {
|
|
const selection = buildSelectionFromPublicationTables([{ schema: 'public', name: 'orders' }])
|
|
const selectedCount = ['orders', 'customers'].filter(
|
|
(name) => selection[getSchemaTableKey('public', name)]
|
|
).length
|
|
|
|
expect(getSchemaCheckedState({ selectedCount, totalCount: 2 })).toBe('indeterminate')
|
|
})
|
|
|
|
test('round-trips through buildWarehouseSetupTargets as a schema target when fully published', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders', 'customers'] }]
|
|
const selection = buildSelectionFromPublicationTables([
|
|
{ schema: 'public', name: 'orders' },
|
|
{ schema: 'public', name: 'customers' },
|
|
])
|
|
|
|
expect(buildWarehouseSetupTargets(selection, schemas)).toEqual([
|
|
{ type: 'schema', schema: 'public' },
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:getSchemaCheckedState', () => {
|
|
test('is unchecked when nothing is selected', () => {
|
|
expect(getSchemaCheckedState({ selectedCount: 0, totalCount: 3 })).toBe(false)
|
|
})
|
|
|
|
test('is indeterminate when only some tables are selected', () => {
|
|
expect(getSchemaCheckedState({ selectedCount: 1, totalCount: 3 })).toBe('indeterminate')
|
|
expect(getSchemaCheckedState({ selectedCount: 2, totalCount: 3 })).toBe('indeterminate')
|
|
})
|
|
|
|
test('is checked when every table is selected', () => {
|
|
expect(getSchemaCheckedState({ selectedCount: 3, totalCount: 3 })).toBe(true)
|
|
})
|
|
|
|
test('is unchecked for an empty schema rather than checked', () => {
|
|
expect(getSchemaCheckedState({ selectedCount: 0, totalCount: 0 })).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:getSchemaTableKey', () => {
|
|
test('joins schema and table with a dot', () => {
|
|
expect(getSchemaTableKey('public', 'orders')).toBe('public.orders')
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:getSelectedTableCount', () => {
|
|
test('returns 0 for an empty selection', () => {
|
|
expect(getSelectedTableCount({})).toBe(0)
|
|
})
|
|
|
|
test('counts only truthy entries', () => {
|
|
const selection: SchemaTableSelection = {
|
|
'public.orders': true,
|
|
'public.customers': false,
|
|
'public.events': true,
|
|
}
|
|
expect(getSelectedTableCount(selection)).toBe(2)
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:hasSelectionChanged', () => {
|
|
test('is false for two empty selections', () => {
|
|
expect(hasSelectionChanged({}, {})).toBe(false)
|
|
})
|
|
|
|
test('is false when the same tables are selected', () => {
|
|
const initial: SchemaTableSelection = { 'public.orders': true, 'public.customers': false }
|
|
const selection: SchemaTableSelection = { 'public.orders': true }
|
|
expect(hasSelectionChanged(selection, initial)).toBe(false)
|
|
})
|
|
|
|
test('is true when a table is added on top of the initial selection', () => {
|
|
const initial: SchemaTableSelection = { 'public.orders': true }
|
|
const selection: SchemaTableSelection = { 'public.orders': true, 'public.customers': true }
|
|
expect(hasSelectionChanged(selection, initial)).toBe(true)
|
|
})
|
|
|
|
test('is true when a table is removed from the initial selection', () => {
|
|
const initial: SchemaTableSelection = { 'public.orders': true, 'public.customers': true }
|
|
const selection: SchemaTableSelection = { 'public.orders': true, 'public.customers': false }
|
|
expect(hasSelectionChanged(selection, initial)).toBe(true)
|
|
})
|
|
|
|
test('ignores explicit false entries that mirror an absent key', () => {
|
|
const initial: SchemaTableSelection = {}
|
|
const selection: SchemaTableSelection = { 'public.orders': false }
|
|
expect(hasSelectionChanged(selection, initial)).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:buildWarehouseSetupTargets', () => {
|
|
test('returns an empty array for an empty selection', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders', 'customers'] }]
|
|
expect(buildWarehouseSetupTargets({}, schemas)).toEqual([])
|
|
})
|
|
|
|
test('returns an empty array when there are no schemas', () => {
|
|
expect(buildWarehouseSetupTargets({ 'public.orders': true }, [])).toEqual([])
|
|
})
|
|
|
|
test('emits a schema target when every table in that schema is selected', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders', 'customers'] }]
|
|
const selection: SchemaTableSelection = {
|
|
'public.orders': true,
|
|
'public.customers': true,
|
|
}
|
|
expect(buildWarehouseSetupTargets(selection, schemas)).toEqual([
|
|
{ type: 'schema', schema: 'public' },
|
|
])
|
|
})
|
|
|
|
test('emits per-table targets when only some tables in a schema are selected', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders', 'customers'] }]
|
|
const selection: SchemaTableSelection = {
|
|
'public.orders': true,
|
|
'public.customers': false,
|
|
}
|
|
expect(buildWarehouseSetupTargets(selection, schemas)).toEqual([
|
|
{ type: 'table', schema: 'public', name: 'orders' },
|
|
])
|
|
})
|
|
|
|
test('skips schemas with no tables', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'empty_schema', tables: [] }]
|
|
expect(buildWarehouseSetupTargets({ 'empty_schema.foo': true }, schemas)).toEqual([])
|
|
})
|
|
|
|
test('skips schemas with no selected tables', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders'] }]
|
|
expect(buildWarehouseSetupTargets({ 'public.orders': false }, schemas)).toEqual([])
|
|
})
|
|
|
|
test('ignores selection keys that reference tables outside the given schemas', () => {
|
|
const schemas: SchemaWithTables[] = [{ schema: 'public', tables: ['orders'] }]
|
|
const selection: SchemaTableSelection = {
|
|
'public.orders': true,
|
|
'other.table': true,
|
|
}
|
|
expect(buildWarehouseSetupTargets(selection, schemas)).toEqual([
|
|
{ type: 'schema', schema: 'public' },
|
|
])
|
|
})
|
|
|
|
test('handles multiple schemas with a mix of full, partial, and no selection', () => {
|
|
const schemas: SchemaWithTables[] = [
|
|
{ schema: 'public', tables: ['orders', 'customers'] },
|
|
{ schema: 'auth', tables: ['users', 'sessions'] },
|
|
{ schema: 'storage', tables: ['objects'] },
|
|
]
|
|
const selection: SchemaTableSelection = {
|
|
'public.orders': true,
|
|
'public.customers': true,
|
|
'auth.users': true,
|
|
'auth.sessions': false,
|
|
'storage.objects': false,
|
|
}
|
|
expect(buildWarehouseSetupTargets(selection, schemas)).toEqual([
|
|
{ type: 'schema', schema: 'public' },
|
|
{ type: 'table', schema: 'auth', name: 'users' },
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('WarehouseModePanel.utils:buildSchemasWithTables', () => {
|
|
test('returns an empty array when there are no schemas', () => {
|
|
expect(buildSchemasWithTables([], [{ schema: 'public', name: 'orders' }])).toEqual([])
|
|
})
|
|
|
|
test('groups each schema with only its own tables', () => {
|
|
const schemas = [{ name: 'public' }, { name: 'analytics' }]
|
|
const tables = [
|
|
{ schema: 'public', name: 'orders' },
|
|
{ schema: 'analytics', name: 'events' },
|
|
{ schema: 'public', name: 'customers' },
|
|
]
|
|
expect(buildSchemasWithTables(schemas, tables)).toEqual([
|
|
{ schema: 'analytics', tables: ['events'] },
|
|
{ schema: 'public', tables: ['orders', 'customers'] },
|
|
])
|
|
})
|
|
|
|
test('keeps schemas that have no tables', () => {
|
|
expect(buildSchemasWithTables([{ name: 'public' }], [])).toEqual([
|
|
{ schema: 'public', tables: [] },
|
|
])
|
|
})
|
|
|
|
test('excludes schemas that cannot be replicated', () => {
|
|
const schemas = [
|
|
{ name: 'public' },
|
|
{ name: 'vault' },
|
|
{ name: 'pg_catalog' },
|
|
{ name: WAREHOUSE_METADATA_SCHEMA },
|
|
{ name: 'auth' },
|
|
]
|
|
const tables = [
|
|
{ schema: 'public', name: 'orders' },
|
|
{ schema: 'vault', name: 'secrets' },
|
|
{ schema: 'auth', name: 'users' },
|
|
]
|
|
expect(buildSchemasWithTables(schemas, tables)).toEqual([
|
|
{ schema: 'auth', tables: ['users'] },
|
|
{ schema: 'public', tables: ['orders'] },
|
|
])
|
|
})
|
|
|
|
test('sorts schemas by name', () => {
|
|
const schemas = [{ name: 'zebra' }, { name: 'public' }, { name: 'analytics' }]
|
|
expect(buildSchemasWithTables(schemas, []).map(({ schema }) => schema)).toEqual([
|
|
'analytics',
|
|
'public',
|
|
'zebra',
|
|
])
|
|
})
|
|
})
|