import { createMemo, createSignal, Index, Match, Show, Switch } from "solid-js"; import type { Accessor } from "solid-js"; import { createForm } from "@tanstack/solid-form"; import { useQueryClient } from "@tanstack/solid-query"; import { showToast } from "@/components/ui/toast"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from "@/components/ui/dialog"; import { SheetHeader, SheetTitle, SheetFooter } from "@/components/ui/sheet"; import { createTable, alterTable } from "@/lib/table"; import { randomName } from "@/lib/name"; import { buildBoolFormField, buildTextFormField, } from "@/components/FormFields"; import { SheetContainer } from "@/components/SafeSheet"; import { PrimaryKeyColumnSubForm, ColumnSubForm, primaryKeyPresets, } from "@/components/tables/CreateAlterColumnForm"; import { invalidateConfig } from "@/lib/config"; import type { Column } from "@bindings/Column"; import type { Table } from "@bindings/Table"; import type { AlterTableOperation } from "@bindings/AlterTableOperation"; import type { QualifiedName } from "@bindings/QualifiedName"; function newDefaultColumn(index: number, existingNames?: string[]): Column { let name = `new_${index}`; if (existingNames !== undefined) { for (let i = 0; i < 1000; ++i) { if (existingNames.find((n) => n === name) === undefined) { break; } name = `new_${index + i}`; } } return { name, data_type: "Text", options: [{ Default: "''" }], }; } function columnsEqual(a: Column, b: Column): boolean { return ( a.name === b.name && a.data_type === b.data_type && JSON.stringify(a.options) === JSON.stringify(b.options) ); } export function CreateAlterTableForm(props: { close: () => void; markDirty: () => void; schemaRefetch: () => Promise; allTables: Table[]; setSelected: (tableName: QualifiedName) => void; schema?: Table; }) { const queryClient = useQueryClient(); const [sql, setSql] = createSignal(); const copyOriginal = (): Table | undefined => props.schema ? JSON.parse(JSON.stringify(props.schema)) : undefined; const original = createMemo(() => copyOriginal()); const isCreateTable = () => original() === undefined; // Columns are treated as append only. Instead of removing it and inducing animation junk and other stuff when // shifting offset, we simply don't render columns that were marked as deleted. const [deletedColumns, setDeletedColumn] = createSignal([]); const isDeleted = (i: number): boolean => deletedColumns().find((idx) => idx === i) !== undefined; const onSubmit = async (value: Table, dryRun: boolean) => { /* eslint-disable solid/reactivity */ console.debug("Table schema:", value); try { const o = original(); if (o !== undefined) { // Alter table // Build operations. Remember columns are append-only. const operations: AlterTableOperation[] = []; value.columns.forEach((column, i) => { if (i < o.columns.length) { // Pre-existing column. const originalName = o.columns[i].name; if (isDeleted(i)) { operations.push({ DropColumn: { name: originalName } }); return; } if (!columnsEqual(o.columns[i], column)) { operations.push({ AlterColumn: { name: originalName, column, }, }); return; } } else { // Newly added columns. if (!isDeleted(i)) { operations.push({ AddColumn: { column } }); } } }); const response = await alterTable({ source_schema: o, operations, dry_run: dryRun, }); console.debug(`AlterTableResponse [dry: ${dryRun}]:`, response); if (dryRun) { setSql(response.sql); } } else { // Create table value.columns = value.columns.filter((_, i) => !isDeleted(i)); const response = await createTable({ schema: value, dry_run: dryRun }); console.debug(`CreateTableResponse [dry: ${dryRun}]:`, response); if (dryRun) { setSql(response.sql); } } if (!dryRun) { // Trigger config reload invalidateConfig(queryClient); // Reload schemas. props.schemaRefetch().then(() => { props.setSelected(value.name); }); // Close dialog/sheet. props.close(); } } catch (err) { showToast({ title: "Uncaught Error", description: `${err}`, variant: "error", }); } }; const form = createForm(() => ({ onSubmit: async ({ value }) => await onSubmit(value, /*dryRun=*/ false), defaultValues: copyOriginal() ?? ({ name: { name: randomName(), database_schema: null, }, strict: true, indexes: [], columns: [ { name: "id", ...primaryKeyPresets[0][1]("id"), }, newDefaultColumn(1), ] satisfies Column[], // Table constraints: https://www.sqlite.org/syntax/table-constraint.html unique: [], foreign_keys: [], checks: [], virtual_table: false, temporary: false, } as Table), })); form.useStore((state) => { if (state.isDirty && !state.isSubmitted) { props.markDirty(); } }); return ( {isCreateTable() ? "Add New Table" : "Alter Table"}
{ e.preventDefault(); form.handleSubmit(); }} >
{ return value ? undefined : "Table name missing"; }, }} > {buildTextFormField({ label: () => , })} "STRICT (type-safe)", })} /> {/* columns */}

Columns

{(field) => (
{(c: Accessor, i: number) => ( setDeletedColumn([i, ...deletedColumns()]) } /> )}
)}
({ canSubmit: state.canSubmit, isSubmitting: state.isSubmitting, })} > {(state) => { return (
{ if (!open) { setSql(undefined); } }} > SQL
{sql() === "" ? "" : sql()}
); }}
); } function TextLabel(props: { text: string }) { return
{props.text}
; }