Column Modifier for Postgres Implemented

This commit is contained in:
KarimTamani
2025-06-17 18:47:03 +01:00
parent 3ec28c3232
commit 93cf912243
9 changed files with 143 additions and 53 deletions
+2 -1
View File
@@ -91,7 +91,8 @@ export const en = {
scale : "Scale must be positive number, no decimals." ,
scale_max_value : "Scale must be ≤ precision."
}
} ,
pick_value : "Pick value"
},
delete: "Delete",
+8 -2
View File
@@ -9,6 +9,8 @@ export enum Modifiers {
CHARSET = "charset",
COLLATE = "collate",
VALUES = "values",
NO_DEFAULT = "no_default" ,
NO_UNIQUE = "no_unique"
}
export enum MySQLCharset {
@@ -85,7 +87,8 @@ export enum DataTypes {
TEXT = "text",
BINARY = "binary",
JSON = "JSON",
GEOMETRY = "geometry"
GEOMETRY = "geometry",
ENUM = "enum"
}
@@ -93,4 +96,7 @@ export enum TimeDefaultValues {
NO_VALUE = "no_value",
CUSTOM = "custom" ,
NOW = "now"
}
}
export const MYSQL_MAX_VAR_LENGTH = 255 ;
@@ -11,15 +11,7 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
const parser = new Parser();
const code = `
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
logged_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
@@ -33,7 +25,7 @@ const SqlPreview: React.FC = ({ }) => {
useEffect(() => {
const ast = parser.astify(code, {
database: "Mysql"
database: "Postgresql"
}) ;
console.log(ast)
@@ -1,6 +1,6 @@
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema"
import { Checkbox, DatePicker, Input, Select, SelectItem, Switch, TimeInput, Tooltip } from "@heroui/react";
import React, { Ref, useCallback, useMemo, useRef, useState } from "react";
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import { ModifierValidation } from "./field-setting";
import { DataTypes, TimeDefaultValues } from "@/lib/field";
@@ -8,12 +8,15 @@ import { Calendar, Clock, TriangleAlert } from "lucide-react";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import dayjs from 'dayjs';
import { now, parseAbsoluteToLocal, Time } from "@internationalized/date";
import { DatabaseDialect } from "@/lib/database";
interface DefaultValueType {
number?: boolean;
string?: boolean;
boolean?: boolean;
time?: boolean;
select?: boolean;
multiSelect?: boolean
}
interface FieldDefaultValueProps {
@@ -31,6 +34,8 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
});
const defaultValueRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const [values, setValues] = useState<string[]>([]);
const [timeSelection, setTimeSelection] = useState<string[]>(() => {
@@ -68,6 +73,41 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
}
});
const defaultValueType: DefaultValueType = useMemo(() => {
return {
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
string: field.type?.type == DataTypes.TEXT || field.type?.name == "year" ,
boolean: field.type?.type == DataTypes.BOOLEAN,
time: field.type?.type == DataTypes.TIME && field.type?.name != "year",
select: field.type?.type == DataTypes.ENUM && field.type?.name != "set",
multiSelect: field.type?.type == DataTypes.ENUM && field.type?.name == "set"
}
}, [field]);
const [selectedValues, setSelectedValues] = useState<string[]>(() => {
if (!field.defaultValue)
return []
if (defaultValueType.multiSelect)
return field.defaultValue.split(",")
return [field.defaultValue as string];
});
useEffect(() => {
if (field.values && field.values.length > 0) {
try {
const jsonValues: string[] = JSON.parse(field.values);
setValues(jsonValues);
} catch (error) {
setValues([]);
}
}
}, [field.values]);
const defaultValueChange = useCallback((value: any) => {
if (value && value.trim().length > 0) {
if (field.type?.type == DataTypes.INTEGER) {
@@ -105,14 +145,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
}, [defaultValueRef, field, defaultValueValidation]);
const defaultValueType: DefaultValueType = useMemo(() => {
return {
number: field.type?.type == DataTypes.INTEGER || field.type?.type == DataTypes.NUMERIC,
string: field.type.type == DataTypes.TEXT || field.type.name == "year",
boolean: field.type.type == DataTypes.BOOLEAN,
time: field.type.type == DataTypes.TIME && field.type.name != "year"
}
}, [field])
const changeTimeDefaultValue = (selection: any) => {
let value: string | undefined;
@@ -165,8 +198,26 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
id: field.id,
defaultValue: value ? String(value) : null
}) as FieldInsertType);
}, [field, defaultDateTime])
}, [field, defaultDateTime]);
const enumValueChange = useCallback((selection: any) => {
let values: string | null = null;
const arraySelection = Array.from(selection);
if (arraySelection.length > 0) {
values = arraySelection.join(',');
} else {
values = null;
}
editField({
id: field.id,
defaultValue: values
} as FieldInsertType);
setSelectedValues(selection);
}, [field])
return (
@@ -226,7 +277,7 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
className="w-full"
size="sm"
variant="bordered"
aria-label="cardinality"
aria-label="Time"
selectedKeys={timeSelection}
onSelectionChange={changeTimeDefaultValue}
classNames={{
@@ -295,6 +346,31 @@ const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
</>
}
{
(defaultValueType.select || defaultValueType.multiSelect) &&
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="Enum Values"
placeholder={t('db_controller.field_settings.pick_value')}
selectedKeys={selectedValues}
onSelectionChange={enumValueChange}
selectionMode={!defaultValueType.multiSelect ? 'single' : "multiple"}
classNames={{
trigger: "border-divider group-hover:border-primary",
}}
>
{
values.map((value: string) => (
<SelectItem key={value}>{value}</SelectItem>
))
}
</Select>
}
</>
)
@@ -175,6 +175,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const updateValues = useCallback((values: string[]) => {
const jsonValues = JSON.stringify(values);
if (jsonValues != field.values)
@@ -183,6 +184,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
values: jsonValues,
} as FieldInsertType);
}, [field])
const showNumericModifiers: boolean = modifiers.includes(Modifiers.AUTO_INCREMENT) || modifiers.includes(Modifiers.UNSIGNED) || modifiers.includes(Modifiers.ZEROFILL)
@@ -257,19 +259,22 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}, [precisionRef, scaleRef])
return (
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
<h3 className="font-semibold text-sm text-font/90">
{t("db_controller.field_settings.title")}
</h3>
<hr className="border-divider" />
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.unique")}
</span>
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
</div>
{
!modifiers.includes(Modifiers.NO_UNIQUE) &&
<div className="flex w-full justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.unique")}
</span>
<Checkbox defaultSelected={field.unique as boolean} size="md" onValueChange={toggleUnqiue} />
</div>
}
{
showNumericModifiers && <>
<h3 className="font-semibold text-sm text-font/90">
@@ -517,9 +522,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
</>
}
{
!modifiers.includes(Modifiers.NO_DEFAULT) &&
<FieldDefaultValue field={field} />
}
<label className="text-xs font-medium text-font/70 dark:text-font/90">
{t("db_controller.field_settings.note")}
</label>
@@ -30,7 +30,6 @@ const TablesController: React.FC<Props> = ({ }) => {
const { focusedTableId } = useDiagram();
const nameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
useEffect(() => setTables(allTables), [allTables]);
const addNewTable = useCallback(async () => {
@@ -52,13 +51,13 @@ const TablesController: React.FC<Props> = ({ }) => {
name: "id",
isPrimary: true,
unique: true,
typeId: getDefaultPrimaryKeyType()?.id
typeId: getDefaultPrimaryKeyType(database?.dialect)?.id
}]
} as TableInsertType);
setSelectedTable(new Set([newTableId]) as any);
}, [tables, getViewport, getDefaultPrimaryKeyType]);
}, [database , tables, getViewport, getDefaultPrimaryKeyType]);
useEffect(() => {
@@ -1,4 +1,5 @@
import { DatabaseDialect } from "@/lib/database";
import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseInsertType, DatabaseType } from "@/lib/schemas/database-schema";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
@@ -20,7 +21,7 @@ interface DatabaseDataContextType {
isLoading: boolean,
isSwitchingDatabase: boolean,
getField: (tableId: string, id: string) => FieldType | undefined,
getDefaultPrimaryKeyType : () => DataType | undefined
getDefaultPrimaryKeyType : (dialect? : DatabaseDialect) => DataType | undefined
}
@@ -17,6 +17,7 @@ import { field_indices } from "@/lib/schemas/field_index-schema";
import { v4 } from "uuid";
import { DataType } from "@/lib/schemas/data-type-schema";
import { Modifiers } from "@/lib/field";
import { DatabaseDialect } from "@/lib/database";
@@ -90,16 +91,16 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
));
const charsetOrCollationDataTypes = data_types.filter((dataType : DataType) => {
const modifiers : string[] | undefined = dataType.modifiers ? JSON.parse(dataType.modifiers) : undefined ;
if ( modifiers) {
return modifiers.includes(Modifiers.COLLATE || Modifiers.CHARSET)
const charsetOrCollationDataTypes = data_types.filter((dataType: DataType) => {
const modifiers: string[] | undefined = dataType.modifiers ? JSON.parse(dataType.modifiers) : undefined;
if (modifiers) {
return modifiers.includes(Modifiers.COLLATE || Modifiers.CHARSET)
}
}).map((dataType : DataType) => dataType.name?.toUpperCase()) ;
}).map((dataType: DataType) => dataType.name?.toUpperCase());
const grouped_data_types: any = useMemo(() => {
return groupBy(data_types, "type");
}, [data_types]);
@@ -371,8 +372,11 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}, [db, currentDatabaseId]);
const getDefaultPrimaryKeyType = useCallback(() => {
return data_types.find((dataType: DataType) => dataType.name == "bigint")
const getDefaultPrimaryKeyType = useCallback((dialect?: DatabaseDialect) => {
if (!dialect || dialect != DatabaseDialect.POSTGRES)
return data_types.find((dataType: DataType) => dataType.name == "bigint");
else if (dialect == DatabaseDialect.POSTGRES)
return data_types.find((dataType: DataType) => dataType.name == "bigserial");
}, [data_types]);
const databaseOpsValue = useMemo(() => ({
@@ -441,7 +445,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
}}>
<DatabaseOperationsContext.Provider value={databaseOpsValue}>
{
!database && !isLoading &&
!database && !isLoading &&
<>
{children}
</>
+11 -6
View File
@@ -1,7 +1,7 @@
import { DatabaseDialect } from "@/lib/database";
import { DataTypes, Modifiers, TimeDefaultValues } from "@/lib/field";
import { DataTypes, Modifiers, MYSQL_MAX_VAR_LENGTH, TimeDefaultValues } from "@/lib/field";
import { DataType } from "@/lib/schemas/data-type-schema";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { FieldType } from "@/lib/schemas/field-schema";
@@ -97,6 +97,8 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
let valuesExpr: any | null;
let default_val: any | null = null;
let dataType : string | undefined = field.type?.name?.toLocaleUpperCase() ;
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
length = field.precision;
@@ -109,7 +111,8 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
length = field.maxLength;
else if (modifiers.includes(Modifiers.LENGTH) && field.type.dialect == DatabaseDialect.MYSQL && field.type.name?.startsWith('var') && !field.maxLength)
length = MYSQL_MAX_VAR_LENGTH ;
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
character_set = {
@@ -172,7 +175,6 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
}
else if (field.defaultValue == "true" || field.defaultValue == "false") {
default_val.value.type = "bool"
default_val.value.value = field.defaultValue == "true" ? true : false;
}
@@ -184,6 +186,9 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
}
}
// field.type?.name == "varchar" && (field.type?.dialect == DatabaseDialect.MYSQL || field.type?.dialect == DatabaseDialect.MARIADB) ? 255 : null
return {
column: {
@@ -198,14 +203,14 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
character_set,
default_val,
unique: field.unique ? "unique" : null,
auto_increment: (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement) ? "auto_increment" : null,
auto_increment: (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) ? "auto_increment" : null,
nullable: {
type: field.nullable ? "null" : "not null",
type: field.nullable ? "null" : "not null",
value: field.nullable ? "null" : "not null",
},
definition: {
dataType: field.type?.name?.toLocaleUpperCase(),
dataType ,
length,
scale,
suffix,