diff --git a/src/app/storybook/column-type/page.tsx b/src/app/storybook/column-type/page.tsx
new file mode 100644
index 00000000..595660b0
--- /dev/null
+++ b/src/app/storybook/column-type/page.tsx
@@ -0,0 +1,18 @@
+"use client";
+import ColumnTypeSelector from "@/components/gui/schema-editor/column-type-selector";
+import { MYSQL_DATA_TYPE_SUGGESTION } from "@/drivers/mysql/mysql-data-type";
+import { useState } from "react";
+
+export default function ColumnTypeStorybook() {
+ const [value, setValue] = useState("");
+
+ return (
+
| null
@@ -90,6 +90,46 @@ function changeColumnOnIndex(
});
}
+function ColumnItemType({
+ value,
+ onChange,
+ disabled,
+}: {
+ value: string;
+ onChange: (type: string) => void;
+ disabled?: boolean;
+}) {
+ const { databaseDriver } = useDatabaseDriver();
+
+ if (
+ databaseDriver.columnTypeSelector.type === "dropdown" &&
+ databaseDriver.columnTypeSelector.dropdownOptions
+ ) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
function ColumnItem({
value,
idx,
@@ -128,13 +168,6 @@ function ColumnItem({
const column = value.new || value.old;
if (!column) return null;
- const normalizeType = convertSqliteType(column.type);
- let type = "TEXT";
-
- if (normalizeType === TableColumnDataType.INTEGER) type = "INTEGER";
- if (normalizeType === TableColumnDataType.REAL) type = "REAL";
- if (normalizeType === TableColumnDataType.BLOB) type = "BLOB";
-
let highlightClassName = "";
if (value.new === null) {
highlightClassName = "bg-red-400 dark:bg-red-800";
@@ -173,21 +206,11 @@ function ColumnItem({
/>
-
+ />
|
;
}
+export interface ColumnTypeSuggestionGroup {
+ name: string;
+ suggestions: ColumnTypeSuggestion[];
+}
+export interface ColumnTypeSuggestion {
+ name: string;
+ parameters?: {
+ name: string;
+ description?: string;
+ default: string;
+ }[];
+ description?: string | ((type: string, parameters?: string[]) => string);
+}
+export interface ColumnTypeSelector {
+ type: "dropdown" | "text";
+ dropdownOptions?: { value: string; text: string }[];
+ typeSuggestions?: ColumnTypeSuggestionGroup[];
+}
+
export interface DriverFlags {
defaultSchema: string;
optionalSchema: boolean;
@@ -246,6 +265,7 @@ export abstract class BaseDriver {
// Flags
abstract getFlags(): DriverFlags;
abstract getCurrentSchema(): Promise;
+ abstract columnTypeSelector: ColumnTypeSelector;
// Helper class
abstract escapeId(id: string): string;
diff --git a/src/drivers/mysql/generate-schema.ts b/src/drivers/mysql/generate-schema.ts
new file mode 100644
index 00000000..60c8cc9e
--- /dev/null
+++ b/src/drivers/mysql/generate-schema.ts
@@ -0,0 +1,187 @@
+import {
+ BaseDriver,
+ DatabaseTableColumn,
+ DatabaseTableColumnConstraint,
+ DatabaseTableSchemaChange,
+} from "../base-driver";
+
+import { omit, isEqual } from "lodash";
+
+function wrapParen(str: string) {
+ if (str.length >= 2 && str.startsWith("(") && str.endsWith(")")) return str;
+ return "(" + str + ")";
+}
+
+function generateCreateColumn(
+ driver: BaseDriver,
+ col: DatabaseTableColumn
+): string {
+ const tokens: string[] = [driver.escapeId(col.name), col.type];
+
+ if (col.constraint?.primaryKey) {
+ tokens.push(
+ [
+ "PRIMARY KEY",
+ col.constraint.primaryKeyOrder,
+ col.constraint.primaryKeyConflict
+ ? `ON CONFLICT ${col.constraint.primaryKeyConflict}`
+ : undefined,
+ col.constraint.autoIncrement ? "AUTO_INCREMENT" : undefined,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ );
+ }
+
+ if (col.constraint?.unique) {
+ tokens.push(
+ [
+ "UNIQUE",
+ col.constraint.uniqueConflict
+ ? `ON CONFLICT ${col.constraint.uniqueConflict}`
+ : undefined,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ );
+ }
+
+ if (col.constraint?.notNull) {
+ tokens.push(
+ [
+ "NOT NULL",
+ col.constraint.notNullConflict
+ ? `ON CONFLICT ${col.constraint.notNullConflict}`
+ : undefined,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ );
+ }
+
+ if (col.constraint?.defaultValue) {
+ tokens.push(
+ ["DEFAULT", driver.escapeValue(col.constraint.defaultValue)].join(" ")
+ );
+ }
+
+ if (col.constraint?.defaultExpression) {
+ tokens.push(
+ ["DEFAULT", wrapParen(col.constraint.defaultExpression)].join(" ")
+ );
+ }
+
+ if (col.constraint?.generatedExpression) {
+ tokens.push(
+ [
+ "GENERATED ALWAYS AS",
+ wrapParen(col.constraint.generatedExpression),
+ col.constraint.generatedType,
+ ].join(" ")
+ );
+ }
+
+ if (col.constraint?.checkExpression) {
+ tokens.push("CHECK " + wrapParen(col.constraint.checkExpression));
+ }
+
+ const foreignTableName = col.constraint?.foreignKey?.foreignTableName;
+ const foreignColumnName = (col.constraint?.foreignKey?.foreignColumns ?? [
+ undefined,
+ ])[0];
+
+ if (foreignTableName && foreignColumnName) {
+ tokens.push(
+ [
+ "REFERENCES",
+ driver.escapeId(foreignTableName) +
+ `(${driver.escapeId(foreignColumnName)})`,
+ ].join(" ")
+ );
+ }
+
+ return tokens.join(" ");
+}
+
+function generateConstraintScript(
+ driver: BaseDriver,
+ con: DatabaseTableColumnConstraint
+) {
+ if (con.primaryKey) {
+ return `PRIMARY KEY (${con.primaryColumns?.map(driver.escapeId).join(", ")})`;
+ } else if (con.unique) {
+ return `UNIQUE (${con.uniqueColumns?.map(driver.escapeId).join(", ")})`;
+ } else if (con.checkExpression !== undefined) {
+ return `CHECK (${con.checkExpression})`;
+ } else if (con.foreignKey) {
+ return (
+ `FOREIGN KEY (${con.foreignKey.columns?.map(driver.escapeId).join(", ")}) ` +
+ `REFERENCES ${driver.escapeId(con.foreignKey.foreignTableName ?? "")} ` +
+ `(${con.foreignKey.foreignColumns?.map(driver.escapeId).join(", ")})`
+ );
+ }
+}
+
+// https://dev.mysql.com/doc/refman/8.4/en/create-table.html
+export function generateMySqlSchemaChange(
+ driver: BaseDriver,
+ change: DatabaseTableSchemaChange
+): string[] {
+ const isCreateScript = !change.name.old;
+
+ const lines = [];
+
+ for (const col of change.columns) {
+ if (col.new === null) lines.push(`DROP COLUMN ${col.old?.name}`);
+ else if (col.old === null) {
+ if (isCreateScript) {
+ lines.push(generateCreateColumn(driver, col.new));
+ } else {
+ lines.push("ADD " + generateCreateColumn(driver, col.new));
+ }
+ } else {
+ // check if there is rename
+ if (col.new.name !== col.old.name) {
+ lines.push(
+ `RENAME COLUMN ${driver.escapeId(col.old.name)} TO ${driver.escapeId(
+ col.new.name
+ )}`
+ );
+ }
+
+ console.log(col.old, col.new);
+
+ // check if there is any changed except name
+ if (!isEqual(omit(col.old, ["name"]), omit(col.new, ["name"]))) {
+ lines.push(`MODIFY COLUMN ${generateCreateColumn(driver, col.new)}`);
+ }
+ }
+ }
+
+ for (const con of change.constraints) {
+ if (con.new) {
+ if (isCreateScript) {
+ lines.push(generateConstraintScript(driver, con.new));
+ }
+ }
+ }
+
+ if (!isCreateScript) {
+ if (change.name.new !== change.name.old) {
+ lines.push(
+ `RENAME TO ${driver.escapeId(change.schemaName ?? "main")}.${driver.escapeId(change.name.new ?? "")}`
+ );
+ }
+ }
+
+ if (isCreateScript) {
+ return [
+ `CREATE TABLE ${driver.escapeId(change.schemaName ?? "main")}.${driver.escapeId(
+ change.name.new || "no_table_name"
+ )}(\n${lines.map((line) => " " + line).join(",\n")}\n)`,
+ ];
+ } else {
+ const alter = `ALTER TABLE ${driver.escapeId(change.schemaName ?? "main")}.${driver.escapeId(change.name.old ?? "")} `;
+ return lines.map((line) => alter + line);
+ }
+}
diff --git a/src/drivers/mysql/mysql-data-type.ts b/src/drivers/mysql/mysql-data-type.ts
new file mode 100644
index 00000000..8a14b3fa
--- /dev/null
+++ b/src/drivers/mysql/mysql-data-type.ts
@@ -0,0 +1,195 @@
+import { ColumnTypeSelector } from "../base-driver";
+
+export const MYSQL_DATA_TYPE_SUGGESTION: ColumnTypeSelector = {
+ type: "text",
+ typeSuggestions: [
+ {
+ name: "Integer",
+ suggestions: [
+ {
+ name: "tinyint",
+ description:
+ "A very small integer. The signed range is -128 to 127. The unsigned range is 0 to 255",
+ },
+ {
+ name: "smallint",
+ description:
+ "A small integer. The signed range is -32,768 to 32,767. The unsigned range is 0 to 65,535",
+ },
+ {
+ name: "mediumint",
+ description:
+ "A medium-sized integer. The signed range is -8,388,608 to 8,388,607. The unsigned range is 0 to 16,777,215",
+ },
+ {
+ name: "int",
+ description:
+ "A normal-size integer. The signed range is -2,147,483,648 to 2,147,483,647. The unsigned range is 0 to 4,294,967,295",
+ },
+ {
+ name: "bigint",
+ description:
+ "A large integer. The signed range is -9,223,372,036,854,775,808 to 9,223,372,036,854,775,807. The unsigned range is 0 to 18,446,744,073,709,551,615",
+ },
+ ],
+ },
+ {
+ name: "Real",
+ suggestions: [
+ { name: "float", description: "4 byte float" },
+ { name: "double", description: "8 byte float" },
+ {
+ name: "decimal",
+ parameters: [
+ {
+ name: "precision",
+ description: "Total number of digits",
+ default: "10",
+ },
+ {
+ name: "scale",
+ description: "Number of digits after the decimal point",
+ default: "2",
+ },
+ ],
+ description: decimalDescription,
+ },
+ ],
+ },
+ {
+ name: "Text",
+ suggestions: [
+ {
+ name: "char",
+ parameters: [{ name: "length", default: "255" }],
+ description: "Fixed-length string",
+ },
+ {
+ name: "varchar",
+ parameters: [{ name: "length", default: "255" }],
+ description: "Variable-length string",
+ },
+ {
+ name: "tinytext",
+ description: "A text column with a maximum length of 255 characters.",
+ },
+ {
+ name: "text",
+ description: "A text column with a maximum length of 65,535.",
+ },
+ {
+ name: "mediumtext",
+ description: "A text column with a maximum length of 16,777,215",
+ },
+ {
+ name: "longtext",
+ description: "A text column with a maximum length of 4,294,960,295",
+ },
+ {
+ name: "json",
+ description:
+ "Document stored in JSON column are converted to an internal format that permits quick read access to document elements",
+ },
+ {
+ name: "uuid",
+ description:
+ "The UUID data type is intended for the storage of 128.bit UUID.",
+ },
+ ],
+ },
+ {
+ name: "Binary",
+ suggestions: [
+ {
+ name: "binary",
+ parameters: [{ name: "length", default: "255" }],
+ description: "Fixed-length binary",
+ },
+ {
+ name: "varbinary",
+ parameters: [{ name: "length", default: "255" }],
+ description: "Variable-length binary",
+ },
+ {
+ name: "tinyblob",
+ description: "A blob column with a maximum length of 255 bytes",
+ },
+ {
+ name: "blob",
+ description: "A blob column with a maximum length of 65,535 bytes",
+ },
+ {
+ name: "mediumblob",
+ description:
+ "A blob column with a maximum length of 16,777,215 bytes",
+ },
+ {
+ name: "longblob",
+ description:
+ "A blob column with a maximum length of 4,294,967,295 bytes",
+ },
+ ],
+ },
+ {
+ name: "Date",
+ suggestions: [
+ { name: "date", description: "Date" },
+ { name: "time", description: "Time" },
+ { name: "datetime", description: "Date and time" },
+ { name: "timestamp", description: "Date and time" },
+ { name: "year", description: "Year" },
+ ],
+ },
+ {
+ name: "Geometry",
+ suggestions: [
+ { name: "geometry", description: "Geometry" },
+ { name: "point", description: "Point" },
+ { name: "linestring", description: "Line string" },
+ { name: "polygon", description: "Polygon" },
+ { name: "multipoint", description: "Multi point" },
+ { name: "multilinestring", description: "Multi line string" },
+ { name: "multipolygon", description: "Multi polygon" },
+ { name: "geometrycollection", description: "Geometry collection" },
+ ],
+ },
+ {
+ name: "Miscellaneous",
+ suggestions: [
+ { name: "enum", description: "Enumerated string" },
+ { name: "set", description: "" },
+ ],
+ },
+ ],
+};
+
+function decimalDescription(_: string, parameters?: string[]) {
+ const precision = Number(parameters?.[0]);
+ const scale = Number(parameters?.[1] ?? 0);
+
+ if (
+ Number.isFinite(precision) &&
+ Number.isFinite(scale) &&
+ Number.isInteger(precision) &&
+ Number.isInteger(scale) &&
+ precision > 0 &&
+ precision < 20
+ ) {
+ const exampleNumber = "12345678901234567890".substring(0, precision);
+ const exampleBeforeDot = exampleNumber.substring(0, precision - scale);
+ const exampleAfterDot = exampleNumber.substring(precision - scale);
+
+ return `
+ Fixed-point number
+
+ Precision
+
+ ${exampleBeforeDot}.${exampleAfterDot}
+
+ Scale
+
+ `;
+ }
+
+ return `Fixed-point number `;
+}
diff --git a/src/drivers/mysql/mysql-driver.ts b/src/drivers/mysql/mysql-driver.ts
index 1afe3d13..363cb413 100644
--- a/src/drivers/mysql/mysql-driver.ts
+++ b/src/drivers/mysql/mysql-driver.ts
@@ -6,9 +6,13 @@ import {
DatabaseSchemaItem,
DatabaseTableColumn,
TableColumnDataType,
+ DatabaseTableSchemaChange,
+ ColumnTypeSelector,
} from "../base-driver";
import CommonSQLImplement from "../common-sql-imp";
import { escapeSqlValue } from "../sqlite/sql-helper";
+import { generateMySqlSchemaChange } from "./generate-schema";
+import { MYSQL_DATA_TYPE_SUGGESTION } from "./mysql-data-type";
interface MySqlDatabase {
SCHEMA_NAME: string;
@@ -37,6 +41,8 @@ interface MySqlTable {
}
export default abstract class MySQLLikeDriver extends CommonSQLImplement {
+ columnTypeSelector: ColumnTypeSelector = MYSQL_DATA_TYPE_SUGGESTION;
+
escapeId(id: string) {
return `\`${id.replace(/`/g, "``")}\``;
}
@@ -50,9 +56,9 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
defaultSchema: "",
optionalSchema: false,
supportBigInt: false,
- supportModifyColumn: false,
+ supportModifyColumn: true,
mismatchDetection: false,
- supportCreateUpdateTable: false,
+ supportCreateUpdateTable: true,
dialect: "mysql",
supportUseStatement: true,
@@ -82,7 +88,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
.rows as unknown as MySqlTable[];
const columnSql =
- "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')";
+ "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, EXTRA FROM information_schema.columns WHERE TABLE_SCHEMA NOT IN ('mysql', 'information_schema', 'performance_schema', 'sys')";
const columnResult = (await this.query(columnSql))
.rows as unknown as MySqlColumn[];
@@ -120,6 +126,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
const column: DatabaseTableColumn = {
name: c.COLUMN_NAME,
type: c.COLUMN_TYPE,
+ constraint: undefined,
};
const tableKey = c.TABLE_SCHEMA + "." + c.TABLE_NAME;
@@ -136,7 +143,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
schemaName: string,
tableName: string
): Promise {
- const columnSql = `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, DATA_TYPE, EXTRA, COLUMN_KEY FROM information_schema.columns WHERE TABLE_NAME=${escapeSqlValue(tableName)} AND TABLE_SCHEMA=${escapeSqlValue(schemaName)} ORDER BY ORDINAL_POSITION`;
+ const columnSql = `SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLUMN_TYPE, DATA_TYPE, EXTRA, COLUMN_KEY FROM information_schema.columns WHERE TABLE_NAME=${escapeSqlValue(tableName)} AND TABLE_SCHEMA=${escapeSqlValue(schemaName)} ORDER BY ORDINAL_POSITION`;
const columnResult = (await this.query(columnSql))
.rows as unknown as MySqlColumn[];
@@ -156,6 +163,7 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
columns: columnResult.map((c) => ({
name: c.COLUMN_NAME,
type: c.COLUMN_TYPE,
+ constraint: undefined,
})),
};
}
@@ -164,8 +172,8 @@ export default abstract class MySQLLikeDriver extends CommonSQLImplement {
throw new Error("Not implemented");
}
- createUpdateTableSchema(): string[] {
- throw new Error("Not implemented");
+ createUpdateTableSchema(change: DatabaseTableSchemaChange): string[] {
+ return generateMySqlSchemaChange(this, change);
}
inferTypeFromHeader(): TableColumnDataType | undefined {
diff --git a/src/drivers/postgres/postgres-driver.ts b/src/drivers/postgres/postgres-driver.ts
index 0114efb0..cf9e3bdd 100644
--- a/src/drivers/postgres/postgres-driver.ts
+++ b/src/drivers/postgres/postgres-driver.ts
@@ -1,4 +1,5 @@
import {
+ ColumnTypeSelector,
DatabaseSchemaItem,
DatabaseSchemas,
DatabaseTableColumn,
@@ -54,6 +55,10 @@ interface PostgresConstraintRow {
}
export default abstract class PostgresLikeDriver extends CommonSQLImplement {
+ columnTypeSelector: ColumnTypeSelector = {
+ type: "text",
+ };
+
escapeId(id: string) {
return `"${id.replace(/"/g, '""')}"`;
}
diff --git a/src/drivers/sqlite-base-driver.ts b/src/drivers/sqlite-base-driver.ts
index 4ce5d765..e6be8ad3 100644
--- a/src/drivers/sqlite-base-driver.ts
+++ b/src/drivers/sqlite-base-driver.ts
@@ -1,4 +1,5 @@
import type {
+ ColumnTypeSelector,
DatabaseResultSet,
DatabaseSchemaItem,
DatabaseSchemas,
@@ -21,6 +22,16 @@ import generateSqlSchemaChange from "./sqlite/sqlite-generate-schema";
export abstract class SqliteLikeBaseDriver extends CommonSQLImplement {
supportPragmaList = true;
+ columnTypeSelector: ColumnTypeSelector = {
+ type: "dropdown",
+ dropdownOptions: [
+ { text: "Integer", value: "INTEGER" },
+ { text: "Real", value: "REAL" },
+ { text: "Text", value: "TEXT" },
+ { text: "Blob", value: "BLOB" },
+ ],
+ };
+
escapeId(id: string) {
return `"${id.replace(/"/g, '""')}"`;
}
|