From 90a5feb5ea2236f6a1ea3d833b4b386e8560b563 Mon Sep 17 00:00:00 2001 From: huyplb Date: Sun, 1 Nov 2026 22:14:34 -0600 Subject: [PATCH] =?UTF-8?q?feat(utilities):=20Change=20Column=20=E2=80=94?= =?UTF-8?q?=20rename=20or=20change=20a=20type=20the=20safe=20way,=20as=20s?= =?UTF-8?q?teps=20to=20review?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Step 2 of docs/plans/2026-11-01-safe-column-change.md (board card safe-column-change): the screen over planColumnChange (#588). Utilities → Change Column, for the credential picked in the header: pick a table and column, give a new name, a new type (the schema's types are offered), or both, and a batch size. It shows what the change touches (indexes, foreign keys, views and triggers that may use it), any warning (lossy conversion, no triggers on this engine), or why the change is refused. The steps are numbered, each with its SQL, a rerun-safe badge, a note and Copy; Copy all and Open in editor take the whole script. Open in editor opens a new tab named for the change, with this connection as the only destination: the steps belong to one database, never to whatever else is checked. Nothing runs from the screen. planColumnChange reaches the browser through @foxschema/ui-shared and the shared/lib/column-change facade. utilities.columns joins the feature map. Co-Authored-By: Claude Opus 5.5 --- apps/e2e/src/generated/test-ids.ts | 59 +++ .../components/ColumnChangePanel.test.tsx | 95 +++++ .../components/ColumnChangePanel.tsx | 357 ++++++++++++++++++ .../utilities/components/UtilitiesView.tsx | 12 + .../src/frontend/shared/lib/column-change.ts | 2 + docs/FEATURE_MAP.md | 4 +- docs/feature-map.html | 6 +- docs/feature-map/data.js | 4 + docs/feature-map/template.html | 2 +- docs/testing/TEST_IDS.md | 23 +- packages/ui-shared/src/index.ts | 4 + 11 files changed, 564 insertions(+), 4 deletions(-) create mode 100644 apps/web/src/frontend/features/utilities/components/ColumnChangePanel.test.tsx create mode 100644 apps/web/src/frontend/features/utilities/components/ColumnChangePanel.tsx create mode 100644 apps/web/src/frontend/shared/lib/column-change.ts diff --git a/apps/e2e/src/generated/test-ids.ts b/apps/e2e/src/generated/test-ids.ts index f670c8a42..6519e7c76 100644 --- a/apps/e2e/src/generated/test-ids.ts +++ b/apps/e2e/src/generated/test-ids.ts @@ -2960,6 +2960,44 @@ export const TestIds = { /** button · Unlock */ cloneUnlock: 'clone-unlock', }, + ColumnChangePanel: { + /** input */ + columnChangeBatch: 'column-change-batch', + /** select · Pick a column */ + columnChangeColumn: 'column-change-column', + /** button */ + columnChangeCopyAll: 'column-change-copy-all', + /** p */ + columnChangeHint: 'column-change-hint', + /** section · What it touches */ + columnChangeImpact: 'column-change-impact', + /** p */ + columnChangeLoadError: 'column-change-load-error', + /** input */ + columnChangeNewName: 'column-change-new-name', + /** input */ + columnChangeNewType: 'column-change-new-type', + /** button · Open in editor */ + columnChangeOpenEditor: 'column-change-open-editor', + /** div · What it touches */ + columnChangePanel: 'column-change-panel', + /** input */ + columnChangePassword: 'column-change-password', + /** p */ + columnChangeRefused: 'column-change-refused', + /** article */ + columnChangeStep: (value: string | number) => `column-change-step-${value}`, + /** button */ + columnChangeStepCopy: (value: string | number) => `column-change-step-${value}-copy`, + /** section · Steps ( ) */ + columnChangeSteps: 'column-change-steps', + /** select */ + columnChangeTable: 'column-change-table', + /** button · Unlock */ + columnChangeUnlock: 'column-change-unlock', + /** ul */ + columnChangeWarnings: 'column-change-warnings', + }, DatabaseAccessModal: { /** div */ dbAccessAllForm: 'db-access-all-form', @@ -3229,6 +3267,8 @@ export const TestIds = { sqlSidebarUtilities: 'sql-sidebar-utilities', /** data · Backup & Restore */ utilitiesBackup: 'utilities-backup', + /** data · Change Column */ + utilitiesChangeColumn: 'utilities-change-column', /** data · Clone Table */ utilitiesCloneTable: 'utilities-clone-table', /** select */ @@ -4077,6 +4117,24 @@ export type TestId = | 'cloud-cred-save' | `cloud-cred-status-${string}` | 'cloud-provider-credentials' + | 'column-change-batch' + | 'column-change-column' + | 'column-change-copy-all' + | 'column-change-hint' + | 'column-change-impact' + | 'column-change-load-error' + | 'column-change-new-name' + | 'column-change-new-type' + | 'column-change-open-editor' + | 'column-change-panel' + | 'column-change-password' + | 'column-change-refused' + | `column-change-step-${string}` + | `column-change-step-${string}-copy` + | 'column-change-steps' + | 'column-change-table' + | 'column-change-unlock' + | 'column-change-warnings' | 'command-palette' | 'command-palette-btn' | 'command-palette-input' @@ -5190,6 +5248,7 @@ export type TestId = | 'user-valid-until' | 'user-warnings' | 'utilities-backup' + | 'utilities-change-column' | 'utilities-clone-table' | 'utilities-connection' | 'utilities-connection-pool' diff --git a/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.test.tsx b/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.test.tsx new file mode 100644 index 000000000..caa88d1c0 --- /dev/null +++ b/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.test.tsx @@ -0,0 +1,95 @@ +import React from 'react'; +import { fireEvent, render, screen } from '@testing-library/react'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import type { TableSchema } from '@/shared/lib/types'; + +vi.mock('@/features/compare', () => { + const state = { connections: [{ id: 'pg', name: 'Shop', dialect: 'postgres', schema: 'shop', hasPassword: true }] }; + const useSyncStore = (sel: (s: typeof state) => unknown) => sel(state); + useSyncStore.getState = () => state; + return { useSyncStore }; +}); + +import { useSqlEditorStore } from '@/features/sql-editor/state'; +import { useUiStore } from '@/platform/uiStore'; +import { ColumnChangePanel } from './ColumnChangePanel'; + +const orders: TableSchema = { + name: 'orders', + objectType: 'TABLE', + columns: [ + { name: 'id', type: 'integer', nullable: false, primaryKey: true }, + { name: 'status', type: 'varchar(20)', nullable: false, primaryKey: false }, + ], + primaryKey: { columns: ['id'] }, + indices: [{ name: 'orders_status_idx', columns: ['status'], unique: false }], + foreignKeys: [], +}; + +const pick = (testId: string, value: string) => fireEvent.change(screen.getByTestId(testId), { target: { value } }); + +describe('ColumnChangePanel', () => { + beforeEach(() => { + useSqlEditorStore.setState({ + schemaCache: { pg: { status: 'ready', tables: [orders] } }, + ensureSchema: async () => undefined, + ensureConnectionSelected: () => undefined, + } as never); + }); + + it('shows what a type change touches and its steps, and opens them in a new tab for this connection only', () => { + const setActiveView = vi.spyOn(useUiStore.getState(), 'setActiveView'); + render(); + pick('column-change-table', 'orders'); + pick('column-change-column', 'status'); + expect(screen.getByTestId('column-change-hint')).toBeTruthy(); + pick('column-change-new-type', 'varchar(40)'); + + expect(screen.getByTestId('column-change-impact').textContent).toContain('orders_status_idx'); + expect(screen.getAllByTestId(/^column-change-step-\d+$/)).toHaveLength(7); + expect(screen.getByTestId('column-change-step-1').textContent).toContain( + 'ALTER TABLE "shop"."orders" ADD COLUMN IF NOT EXISTS "status__new" varchar(40);' + ); + + const tabsBefore = useSqlEditorStore.getState().tabs.length; + fireEvent.click(screen.getByTestId('column-change-open-editor')); + const editor = useSqlEditorStore.getState(); + const tab = editor.activeTab(); + expect(editor.tabs).toHaveLength(tabsBefore + 1); + expect(tab.title).toBe('Change orders.status'); + expect(tab.sql.startsWith('-- Step 1: Add the new column status__new (varchar(40))\n')).toBe(true); + expect(tab.sql).toContain('-- Step 7: Drop the old column status__old'); + // Only this connection: the steps belong to one database. + expect(editor.activeConnectionIds()).toEqual(['pg']); + expect(setActiveView).toHaveBeenCalledWith('sqlEditor'); + }); + + it('gives a rename alone as one step', () => { + render(); + pick('column-change-table', 'orders'); + pick('column-change-column', 'status'); + pick('column-change-new-name', 'state'); + expect(screen.getAllByTestId(/^column-change-step-\d+$/)).toHaveLength(1); + expect(screen.getByTestId('column-change-step-1').textContent).toContain( + 'ALTER TABLE "shop"."orders" RENAME COLUMN "status" TO "state";' + ); + }); + + it('says why when the change is refused', () => { + render(); + pick('column-change-table', 'orders'); + pick('column-change-column', 'id'); + pick('column-change-new-type', 'bigint'); + expect(screen.getByTestId('column-change-refused').textContent).toMatch(/primary key column/); + expect(screen.queryByTestId('column-change-steps')).toBeNull(); + }); + + it('starts a new change when another column is picked', () => { + render(); + pick('column-change-table', 'orders'); + pick('column-change-column', 'status'); + pick('column-change-new-name', 'state'); + pick('column-change-column', 'id'); + expect((screen.getByTestId('column-change-new-name') as HTMLInputElement).value).toBe(''); + }); +}); diff --git a/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.tsx b/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.tsx new file mode 100644 index 000000000..fda4c88d9 --- /dev/null +++ b/apps/web/src/frontend/features/utilities/components/ColumnChangePanel.tsx @@ -0,0 +1,357 @@ +/** + * Fox Schema (foxschema) + * Copyright 2024-2026 Huy Phan + * SPDX-License-Identifier: Apache-2.0 + * + * Utilities → Change Column: rename a column or change its type the safe way. + * Shows what the change touches, then the steps as SQL to review, copy, or + * open in a new editor tab for that connection. Nothing runs from here + * (docs/plans/2026-11-01-safe-column-change.md). + */ +import React, { useCallback, useEffect, useMemo, useState } from 'react'; +import { Check, Copy, Loader2, SquareTerminal } from 'lucide-react'; +import { useSyncStore } from '@/features/compare'; +import { useSqlEditorStore } from '@/features/sql-editor/state'; +import { useUiStore } from '@/platform/uiStore'; +import { DEFAULT_COLUMN_CHANGE_BATCH, planColumnChange, type ColumnChangeStep } from '@/shared/lib/column-change'; +import { needsSessionPassword } from '@/shared/lib/provider-settings'; +import { sectionLabelCls } from '@/shared/components/surfaces'; +import type { TableSchema } from '@/shared/lib/types'; + +interface Props { + /** The credential picked in the Utilities header; none while nothing is saved. */ + lockedConnectionId?: string; +} + +const inputCls = + 'w-full rounded-lg border border-slate-700 bg-slate-950/70 px-2.5 py-1.5 text-[13px] text-slate-100 outline-none accent-focus'; + +/** The steps as one script, each under a comment naming it. */ +export function columnChangeScript(steps: ColumnChangeStep[]): string { + return steps.map((s, i) => `-- Step ${i + 1}: ${s.title}\n${s.sql}`).join('\n\n') + '\n'; +} + +const CopyButton: React.FC<{ text: string; testId: string; label: string }> = ({ text, testId, label }) => { + const [done, setDone] = useState(false); + return ( + + ); +}; + +export const ColumnChangePanel: React.FC = ({ lockedConnectionId = '' }) => { + const connections = useSyncStore((s) => s.connections); + const ensureSchema = useSqlEditorStore((s) => s.ensureSchema); + const ensureConnectionSelected = useSqlEditorStore((s) => s.ensureConnectionSelected); + const submitSessionPassword = useSqlEditorStore((s) => s.submitSessionPassword); + const sessionPasswords = useSqlEditorStore((s) => s.sessionPasswords); + const schemaCache = useSqlEditorStore((s) => s.schemaCache); + const setActiveView = useUiStore((s) => s.setActiveView); + + const [tableName, setTableName] = useState(''); + const [columnName, setColumnName] = useState(''); + const [newName, setNewName] = useState(''); + const [newType, setNewType] = useState(''); + const [batchSize, setBatchSize] = useState(DEFAULT_COLUMN_CHANGE_BATCH); + const [passwordDraft, setPasswordDraft] = useState(''); + const [loading, setLoading] = useState(false); + const [loadError, setLoadError] = useState(null); + + const conn = connections.find((c) => c.id === lockedConnectionId) ?? null; + const needsPassword = Boolean(conn && needsSessionPassword(conn, sessionPasswords[lockedConnectionId])); + const cache = lockedConnectionId ? schemaCache[lockedConnectionId] : undefined; + const all: TableSchema[] = useMemo(() => cache?.tables ?? [], [cache?.tables]); + const tables = useMemo( + () => all.filter((t) => t.objectType === 'TABLE').slice().sort((a, b) => a.name.localeCompare(b.name)), + [all] + ); + const table = tables.find((t) => t.name === tableName) ?? null; + const column = table?.columns.find((c) => c.name === columnName) ?? null; + const knownTypes = useMemo( + () => [...new Set(all.flatMap((t) => t.columns.map((c) => c.type)))].sort((a, b) => a.localeCompare(b)), + [all] + ); + + const load = useCallback(async () => { + if (!lockedConnectionId) return; + setLoading(true); + setLoadError(null); + try { + ensureConnectionSelected(lockedConnectionId); + await ensureSchema(lockedConnectionId); + const entry = useSqlEditorStore.getState().schemaCache[lockedConnectionId]; + if (entry?.status === 'error') setLoadError(entry.error || 'Could not load the schema'); + } catch (e: unknown) { + setLoadError(e instanceof Error ? e.message : String(e)); + } finally { + setLoading(false); + } + }, [lockedConnectionId, ensureConnectionSelected, ensureSchema]); + + useEffect(() => { + setTableName(''); + setColumnName(''); + if (!needsPassword) void load(); + // Reload when the credential changes, or once its password is given. + }, [lockedConnectionId, needsPassword, load]); + + // A new table or column starts a new change. + useEffect(() => { + setNewName(''); + setNewType(''); + }, [tableName, columnName]); + + const plan = useMemo(() => { + if (!conn || !table || !column || (!newName.trim() && !newType.trim())) return null; + return planColumnChange({ + dialect: conn.dialect, + schema: conn.schema?.trim() || undefined, + table, + tables: all, + column: column.name, + change: { newName: newName.trim() || undefined, newType: newType.trim() || undefined }, + batchSize, + }); + }, [conn, table, column, newName, newType, batchSize, all]); + + const script = plan?.ok ? columnChangeScript(plan.steps) : ''; + + return ( +
+ {needsPassword && ( +
+ + +
+ )} + +
+ + + + + +
+ + {loadError && ( +

+ {loadError} +

+ )} + + {!plan && column && ( +

+ Give a new name, a new type, or both. Nothing runs from here: you get the steps as SQL to review. +

+ )} + + {plan && !plan.ok && ( +

+ {plan.error} +

+ )} + + {plan?.ok && ( + <> +
+

What it touches

+
    + {plan.impact.indexes.map((i) => ( +
  • + {i.unique ? 'Unique index' : 'Index'} {i.name} ({i.columns.join(', ')}) +
  • + ))} + {plan.impact.outboundForeignKeys.map((fk) => ( +
  • + Foreign key {fk.name} → {fk.referencedTable} +
  • + ))} + {plan.impact.views.map((v) => ( +
  • + View {v} may use it +
  • + ))} + {plan.impact.triggers.map((t) => ( +
  • + Trigger {t.name} may use it +
  • + ))} + {plan.impact.indexes.length + + plan.impact.outboundForeignKeys.length + + plan.impact.views.length + + plan.impact.triggers.length === + 0 &&
  • No index, key, view or trigger uses it.
  • } +
+
+ + {plan.warnings.length > 0 && ( +
    + {plan.warnings.map((w) => ( +
  • {w}
  • + ))} +
+ )} + +
+
+

Steps ({plan.steps.length})

+ + +
+ {plan.steps.map((step, i) => ( +
+
+ + {i + 1}. {step.title} + + {step.rerunSafe && ( + safe to rerun + )} + + +
+
{step.sql}
+ {step.note &&

{step.note}

} +
+ ))} +
+ + )} + + {loading && ( +

+ Loading the schema… +

+ )} +
+ ); +}; diff --git a/apps/web/src/frontend/features/utilities/components/UtilitiesView.tsx b/apps/web/src/frontend/features/utilities/components/UtilitiesView.tsx index 69a9c3a8f..6cb2f9037 100644 --- a/apps/web/src/frontend/features/utilities/components/UtilitiesView.tsx +++ b/apps/web/src/frontend/features/utilities/components/UtilitiesView.tsx @@ -12,6 +12,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import type { Permission } from '@foxschema/shared'; import { Activity, + ArrowRightLeft, Copy, Cpu, Database, @@ -29,6 +30,7 @@ import { FileImportsPanel } from '@/features/sql-editor/ui'; import { SQL_ICON_STROKE } from '@/shared/lib/iconStyle'; import { BackupRestorePanel } from './BackupRestorePanel'; import { CloneTableModal } from './CloneTableModal'; +import { ColumnChangePanel } from './ColumnChangePanel'; import { DatabaseAccessModal } from './DatabaseAccessModal'; import { FileQueryModal } from './FileQueryModal'; import { IndexManagementModal } from './IndexManagementModal'; @@ -43,6 +45,7 @@ export type UtilityTool = | 'backup' | 'dbAccess' | 'clone' + | 'columns' | 'files' | 'pool' | 'sessions' @@ -87,6 +90,14 @@ const TOOLS: { blurb: 'Archive a huge table as name_N and recreate an empty live twin.', icon: Copy, }, + { + id: 'columns', + testId: 'utilities-change-column', + label: 'Change Column', + group: 'Maintenance', + blurb: 'Rename a column or change its type the safe way: what it touches, then the steps as SQL to review.', + icon: ArrowRightLeft, + }, { id: 'pool', testId: 'utilities-connection-pool', @@ -316,6 +327,7 @@ export const UtilitiesView: React.FC = () => { onClose={() => undefined} /> )} + {tool === 'columns' && } {insights && ( u4[utilities.backup] UTIL --> u5[utilities.dbaccess] UTIL --> u6[utilities.trace] + UTIL --> u7[utilities.columns] ACC --> a1[access.accounts] ACC --> a2[access.builder] @@ -178,6 +179,7 @@ in its folders and its e2e specs (`apps/e2e/src/tests`). |---|---|---|---|---|---|---|---| | `utilities.indexes` | Index management — fragmentation (custom probe is one read-only SELECT), batch, rebuild / reorg / optimize SQL (Db2: every index on the table), drop | Utils (rail) | `/api/schema/index-fragmentation`, `-batch` | server `access` (`index-fragmentation.service`); sql `utilities` | `utility.access`, `utility.index.drop` | — | sql 7; e2e `sql-editor-utilities`, `-utilities-dialects` | | `utilities.clone` | Clone table | Utils | (editor execute) | web `CloneTableModal` | `utility.access`, `editor.ddl` | — | (utilities e2e) | +| `utilities.columns` | Change column — rename or change a type the safe way: what it touches (indexes, foreign keys, views, triggers, lossy conversions), then numbered steps as SQL to review (add, sync trigger, batched copy, check, switch over, drop last); nothing runs | Utils | — (computed in the browser) | sql `utilities/column-change` + `providers/*/*.column-change.ts`; web `ColumnChangePanel` | `utility.access` | — | sql 56 (+ live runs on PostgreSQL, MySQL, SQL Server); web 4 | | `utilities.insights` | Server insights, table insight, query files | Utils | `/api/schema/dba-utility`, `/api/schema/table-insight` | server `access` (`dba-utilities.service`) | `utility.access` | — | (utilities e2e) | | `utilities.trace` | SQL Tracer — Live (engine sessions, ~1 s) and Fox activity (statements run from Fox on a saved connection); redacted, 15 min / 10,000 statements | Utils | `/api/sql-trace/start`, `/:id/events`, `/:id/stop` | server `sql-trace` (+ `platform/sql-activity`); sql `utilities/sql-trace` | `utility.trace` | — | server 27; web 7; e2e `utilities-sql-tracer` | | `utilities.backup` | Backup & Restore — per-engine command builders, server-side SQL backup after confirmation, recorded backups, saved defaults | Utils | `/api/backup-settings` (2) | server `backup`; sql `providers/*/*.backup.ts` | `utility.access` | `backup_settings` | server 2; e2e `backup-restore-ui`, `backup-commands-dialects` | diff --git a/docs/feature-map.html b/docs/feature-map.html index 6b0402f04..8bd70dc0a 100644 --- a/docs/feature-map.html +++ b/docs/feature-map.html @@ -241,7 +241,7 @@

Fox Feature Map

Measured on main · b12bcd5607 · 2026-10-31 · mirrors docs/FEATURE_MAP.md
    -
  • 34 features
  • +
  • 35 features
  • 158 HTTP routes
  • 18 server modules
  • 14 web features
  • @@ -781,6 +781,10 @@

    End-to-end suites

    subs: ['Clone a table definition and rows'], ui: 'Utils', api: 'through /api/sql/execute', code: 'web CloneTableModal', perms: 'utility.access, editor.ddl', tables: '—', tests: 'utilities e2e' }, + { id: 'utilities.columns', area: 'utilities', name: 'Change column', + subs: ['Rename in place', 'Type change: add, sync, batched copy, check, switch over, drop last', 'What it touches', 'Lossy conversions', 'Open in editor'], + ui: 'Utils', api: '— (computed in the browser)', code: 'sql utilities/column-change, providers/*/*.column-change · web ColumnChangePanel', perms: 'utility.access', + tables: '—', tests: 'sql 56 · web 4 · live: PostgreSQL, MySQL, SQL Server' }, { id: 'utilities.insights', area: 'utilities', name: 'Server insights', subs: ['Server insights', 'Table insight', 'Query files'], ui: 'Utils', api: '/api/schema/dba-utility, /api/schema/table-insight', diff --git a/docs/feature-map/data.js b/docs/feature-map/data.js index 2bef07833..18293cd47 100644 --- a/docs/feature-map/data.js +++ b/docs/feature-map/data.js @@ -114,6 +114,10 @@ const FEATURES = [ subs: ['Clone a table definition and rows'], ui: 'Utils', api: 'through /api/sql/execute', code: 'web CloneTableModal', perms: 'utility.access, editor.ddl', tables: '—', tests: 'utilities e2e' }, + { id: 'utilities.columns', area: 'utilities', name: 'Change column', + subs: ['Rename in place', 'Type change: add, sync, batched copy, check, switch over, drop last', 'What it touches', 'Lossy conversions', 'Open in editor'], + ui: 'Utils', api: '— (computed in the browser)', code: 'sql utilities/column-change, providers/*/*.column-change · web ColumnChangePanel', perms: 'utility.access', + tables: '—', tests: 'sql 56 · web 4 · live: PostgreSQL, MySQL, SQL Server' }, { id: 'utilities.insights', area: 'utilities', name: 'Server insights', subs: ['Server insights', 'Table insight', 'Query files'], ui: 'Utils', api: '/api/schema/dba-utility, /api/schema/table-insight', diff --git a/docs/feature-map/template.html b/docs/feature-map/template.html index 714e437ac..07772029c 100644 --- a/docs/feature-map/template.html +++ b/docs/feature-map/template.html @@ -230,7 +230,7 @@

    Fox Feature Map

    Measured on main · b12bcd5607 · 2026-10-31 · mirrors docs/FEATURE_MAP.md
      -
    • 34 features
    • +
    • 35 features
    • 158 HTTP routes
    • 18 server modules
    • 14 web features
    • diff --git a/docs/testing/TEST_IDS.md b/docs/testing/TEST_IDS.md index 1628c380d..39e0f7a4e 100644 --- a/docs/testing/TEST_IDS.md +++ b/docs/testing/TEST_IDS.md @@ -8,7 +8,7 @@ or `TestIds` for the same tree with autocomplete. `{name}` marks a part filled i time; "in FilterPicker" marks an ID a shared component draws from the prop written here. How to write a test with them: [WRITING_E2E.md](WRITING_E2E.md). -Every control has a test ID: **1015** buttons, text boxes, selects and textareas. +Every control has a test ID: **1024** buttons, text boxes, selects and textareas. ## access @@ -1578,6 +1578,26 @@ Every control has a test ID: **1015** buttons, text boxes, selects and textareas - `clone-table-name-option-{value}` · button in Autocomplete - `clone-table-name-toggle` · button in Autocomplete - `clone-unlock` · button · Unlock +- **ColumnChangePanel** · `features/utilities/components/ColumnChangePanel.tsx` + - `{testId}` · button · passed in by the parent + - `column-change-batch` · input + - `column-change-column` · select · Pick a column + - `column-change-copy-all` · button in CopyButton + - `column-change-hint` · p + - `column-change-impact` · section · What it touches + - `column-change-load-error` · p + - `column-change-new-name` · input + - `column-change-new-type` · input + - `column-change-open-editor` · button · Open in editor + - `column-change-panel` · div · What it touches + - `column-change-password` · input + - `column-change-refused` · p + - `column-change-step-{value}` · article + - `column-change-step-{value}-copy` · button in CopyButton + - `column-change-steps` · section · Steps ( ) + - `column-change-table` · select + - `column-change-unlock` · button · Unlock + - `column-change-warnings` · ul - **DatabaseAccessModal** · `features/utilities/components/DatabaseAccessModal.tsx` - `db-access-all-form` · div - `db-access-all-note` · p @@ -1714,6 +1734,7 @@ Every control has a test ID: **1015** buttons, text boxes, selects and textareas - `index-management-modal` · div - `sql-sidebar-utilities` · nav · Database utilities - `utilities-backup` · data · Backup & Restore + - `utilities-change-column` · data · Change Column - `utilities-clone-table` · data · Clone Table - `utilities-connection` · select - `utilities-connection-pool` · data · Connection Pool diff --git a/packages/ui-shared/src/index.ts b/packages/ui-shared/src/index.ts index 070b7dfd7..28e967310 100644 --- a/packages/ui-shared/src/index.ts +++ b/packages/ui-shared/src/index.ts @@ -166,6 +166,10 @@ export type { TableSizeGroup, } from '@foxschema/sql'; +// Safe column change: what a rename or type change touches, and its steps as SQL to review. +export { DEFAULT_COLUMN_CHANGE_BATCH, planColumnChange } from '@foxschema/sql'; +export type { ColumnChange, ColumnChangeImpact, ColumnChangePlan, ColumnChangeStep } from '@foxschema/sql'; + // SQL Tracer: shapes and bounds (the server redacts; the screen only shows). export { TRACE_MAX_EVENTS, TRACE_MAX_MS } from '@foxschema/sql'; export type { TraceEvent, TraceMode } from '@foxschema/sql';