Mysql Datetime data types modifiers implemented

This commit is contained in:
KarimTamani
2025-06-16 19:25:02 +01:00
parent 84dfd0e9d5
commit 3ec28c3232
13 changed files with 619 additions and 133 deletions
+2
View File
@@ -14,6 +14,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@heroui/react": "^2.7.6",
"@internationalized/date": "^3.8.2",
"@nextui-org/react": "^2.6.11",
"@powersync/drizzle-driver": "^0.4.0",
"@powersync/react": "^1.5.3",
@@ -24,6 +25,7 @@
"@uiw/react-codemirror": "^4.23.12",
"@xyflow/react": "^12.6.3",
"date-fns": "^4.1.0",
"dayjs": "^1.11.13",
"drizzle-orm": "^0.43.1",
"elkjs": "^0.10.0",
"fast-json-patch": "^3.1.1",
+18 -1
View File
@@ -70,11 +70,28 @@ export const en = {
collation : "Collation" ,
scale : "Scale" ,
max_length : "Max length" ,
integer_width : "Integer Width" ,
width : "width" ,
default_value : "Default value" ,
value : "Value" ,
length : "Length" ,
values : "Values" ,
type_enter : "Type and press enter"
type_enter : "Type and press enter" ,
precision_def : "Total digits allowed (before + after the decimal)." ,
scale_def : "Digits allowed after the decimal." ,
time_default_value : {
no_value : "No value" ,
custom : "Custom time" ,
now : "Now"
} ,
errors : {
max_length : "must be positive number, no decimals." ,
integer_default_value : "Invalid default value for Integer" ,
precision : "Precision must be positive number, no decimals." ,
scale : "Scale must be positive number, no decimals." ,
scale_max_value : "Scale must be ≤ precision."
}
},
delete: "Delete",
+29 -9
View File
@@ -1,14 +1,14 @@
export enum Modifiers {
LENGTH = "length",
UNSIGNED = "unsigned",
ZEROFILL = "zerofill",
AUTO_INCREMENT = "auto_increment",
PRECISION = "precision",
SCALE = "scale",
CHARSET = "charset",
COLLATE = "collate",
VALUES = "values" ,
LENGTH = "length",
UNSIGNED = "unsigned",
ZEROFILL = "zerofill",
AUTO_INCREMENT = "auto_increment",
PRECISION = "precision",
SCALE = "scale",
CHARSET = "charset",
COLLATE = "collate",
VALUES = "values",
}
export enum MySQLCharset {
@@ -73,4 +73,24 @@ export enum SQLiteCollation {
Binary = 'BINARY',
NoCase = 'NOCASE',
RTrim = 'RTRIM',
}
export enum DataTypes {
INTEGER = "integer",
NUMERIC = "numeric",
BOOLEAN = "boolean",
TIME = "time",
TEXT = "text",
BINARY = "binary",
JSON = "JSON",
GEOMETRY = "geometry"
}
export enum TimeDefaultValues {
NO_VALUE = "no_value",
CUSTOM = "custom" ,
NOW = "now"
}
@@ -16,7 +16,6 @@ interface RelationshipAccordionBodyProps {
const RelationshipAccordionBody: React.FC<RelationshipAccordionBodyProps> = ({ relationship }) => {
const [cardinality, setCardinality] = useState(new Set([relationship.cardinality]));
const { editRelationship, deleteRelationship } = useDatabaseOperations();
const { t } = useTranslation();
const changeCardinality = (keys: SharedSelection) => {
@@ -11,11 +11,15 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
const parser = new Parser();
const code = `
CREATE TABLE \`users\` (
username VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT "test" ,
username VARCHAR(100) NOT NULL DEFAULT "test" CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci
)
CREATE TABLE events (
id INT AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
logged_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
@@ -30,8 +34,9 @@ const SqlPreview: React.FC = ({ }) => {
useEffect(() => {
const ast = parser.astify(code, {
database: "Mysql"
})
// console.log(ast)
}) ;
console.log(ast)
}, [])
return (
@@ -0,0 +1,304 @@
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 { useTranslation } from "react-i18next";
import { ModifierValidation } from "./field-setting";
import { DataTypes, TimeDefaultValues } from "@/lib/field";
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";
interface DefaultValueType {
number?: boolean;
string?: boolean;
boolean?: boolean;
time?: boolean;
}
interface FieldDefaultValueProps {
field: FieldType;
}
const fieldDefautlValue: React.FC<FieldDefaultValueProps> = ({ field }) => {
const { editField } = useDatabaseOperations();
const { t } = useTranslation();
const [defaultValueValidation, setDefaultValueValidation] = useState<ModifierValidation>({
isValid: true,
});
const defaultValueRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const [timeSelection, setTimeSelection] = useState<string[]>(() => {
if (field.defaultValue == TimeDefaultValues.NOW)
return [TimeDefaultValues.NOW];
if (!field.defaultValue)
return [TimeDefaultValues.NO_VALUE]
if (field.type.name == "time" && field.defaultValue)
return [TimeDefaultValues.CUSTOM]
const date = new Date(field.defaultValue);
if (!isNaN(date.getTime())) {
return [TimeDefaultValues.CUSTOM];
}
return [TimeDefaultValues.NO_VALUE];
});
const [defaultDateTime, setDefaultDateTime] = useState<any>(() => {
try {
if (field.type.name == "time" && field.defaultValue) {
const [hours, minuts, seconds] = field.defaultValue.split(":");
return new Time(parseInt(hours), parseInt(minuts), parseInt(seconds))
}
if (field.defaultValue) {
const date = new Date(field.defaultValue)
return parseAbsoluteToLocal(date.toISOString());
}
return undefined;
} catch (error) {
return undefined;
}
});
const defaultValueChange = useCallback((value: any) => {
if (value && value.trim().length > 0) {
if (field.type?.type == DataTypes.INTEGER) {
const isValid: boolean = Number.isInteger(Number(value));
setDefaultValueValidation({
isValid,
errorMessage: t("db_controller.field_settings.errors.integer_default_value")
})
}
} else {
setDefaultValueValidation({
isValid: true,
errorMessage: undefined
});
}
}, [field, defaultValueRef]);
const saveDefaultValue = useCallback(() => {
if (defaultValueValidation.isValid) {
let value: string | undefined;
if (field.type.type != DataTypes.BOOLEAN)
value = defaultValueRef.current?.value;
else {
value = String(defaultValueRef.current?.checked);
}
editField({
id: field.id,
defaultValue: value ? String(value) : null,
} as FieldInsertType);
}
}, [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;
if (!selection.currentKey)
return;
if (selection.currentKey == TimeDefaultValues.NOW)
value = TimeDefaultValues.NOW;
if (selection.currentKey == TimeDefaultValues.CUSTOM) {
const currentDateTime = now(Intl.DateTimeFormat().resolvedOptions().timeZone)
const date: Date = currentDateTime?.toDate();
setDefaultDateTime(currentDateTime);
if (field.type.name == "date")
value = dayjs(date).format("YYYY-MM-DD")
else if (field.type.name == "datetime" || field.type.name == "timestamp")
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
else if (field.type.name == "time")
value = dayjs(date).format("HH:mm:ss")
}
editField(({
id: field.id,
defaultValue: value ? String(value) : null
}) as FieldInsertType)
setTimeSelection([selection?.currentKey]);
}
const saveDefaultDateTime = useCallback(() => {
let value: string | undefined;
if (field.type.name != "time") {
const date = new Date(defaultDateTime?.toDate());
if (field.type.name == "date")
value = dayjs(date).format("YYYY-MM-DD")
else if (field.type.name == "datetime" || field.type.name == "timestamp")
value = dayjs(date).format("YYYY-MM-DD HH:mm:ss")
}
else {
if (defaultDateTime) {
value = `${defaultDateTime.hour}:${defaultDateTime.minute}:${defaultDateTime.second}`
}
}
editField(({
id: field.id,
defaultValue: value ? String(value) : null
}) as FieldInsertType);
}, [field, defaultDateTime])
return (
<>
{
!defaultValueType.boolean &&
<label className="text-xs font-medium text-font/70 dark:text-font/90">
{t("db_controller.field_settings.default_value")}
</label>
}
{
(defaultValueType.number || defaultValueType.string) &&
<Input
type={defaultValueType.number ? "number" : "text"}
size="sm"
ref={defaultValueRef}
isInvalid={!defaultValueValidation.isValid}
endContent={
!defaultValueValidation.isValid && <>
<Tooltip showArrow={true} content={defaultValueValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</Tooltip>
</>
}
onValueChange={defaultValueChange}
defaultValue={field.defaultValue as string}
onBlur={saveDefaultValue}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.value")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
}
{
defaultValueType.boolean &&
<div className="flex items-center justify-between">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.default_value")}
</span>
<Checkbox
defaultSelected={field.defaultValue == "true"}
ref={defaultValueRef}
size="md"
onValueChange={saveDefaultValue}
/>
</div>
}
{
defaultValueType.time &&
<>
<Select
className="w-full"
size="sm"
variant="bordered"
aria-label="cardinality"
selectedKeys={timeSelection}
onSelectionChange={changeTimeDefaultValue}
classNames={{
trigger: "border-divider group-hover:border-primary",
}}
>
<SelectItem key={TimeDefaultValues.NO_VALUE}>{t("db_controller.field_settings.time_default_value.no_value")}</SelectItem>
<SelectItem key={TimeDefaultValues.CUSTOM}>{t("db_controller.field_settings.time_default_value.custom")}</SelectItem>
{
(field.type.name == "datetime" || field.type.name == "timestamp") ?
<SelectItem key={TimeDefaultValues.NOW}>{t("db_controller.field_settings.time_default_value.now")}</SelectItem> : null
}
</Select>
{
timeSelection.at(0) == TimeDefaultValues.CUSTOM && field.type.name != "time" &&
<>
<DatePicker
aria-label="Custom datetime"
variant="bordered"
size="sm"
showMonthAndYearPickers
radius="sm"
value={defaultDateTime}
endContent={
<Calendar className="text-icon size-4" />
}
onBlur={saveDefaultDateTime}
onChange={setDefaultDateTime}
granularity={field.type.name == "date" ? "day" : "second"}
hideTimeZone
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
}}
calendarProps={{
classNames: {
title: " text-font/90"
}
}}
/>
</>
}
{
timeSelection.at(0) == TimeDefaultValues.CUSTOM && field.type.name == "time" &&
<TimeInput aria-label="Current Time"
ref={defaultValueRef}
value={defaultDateTime}
onChange={setDefaultDateTime}
variant="bordered"
endContent={
<Clock className="text-icon size-4" />
}
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
}}
onBlur={saveDefaultDateTime}
granularity="second"
hourCycle={24}
hideTimeZone
size="sm" radius="sm" />
}
</>
}
</>
)
}
export default React.memo(fieldDefautlValue);
@@ -116,7 +116,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<KeyRound className="size-4" />
</ToggleButton>
<Popover placement="bottom" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<Popover placement="right" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
@@ -1,12 +1,14 @@
import TagInput from "@/components/tag-input/tag-input";
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/tooltip/tooltip";
import { DatabaseDialect } from "@/lib/database";
import { Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
import { DataTypes, Modifiers, MySQLCharset, MySQLCollation, PostgreSQLCharset, PostgreSQLCollation, SQLiteCharset, SQLiteCollation } from "@/lib/field";
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea } from "@heroui/react";
import { Trash2 } from "lucide-react";
import React, { Ref, useCallback, useMemo, useRef, useState } from "react";
import { Button, Checkbox, Input, Select, SelectItem, SharedSelection, Switch, Textarea, Tooltip as HeroUITooltip } from "@heroui/react";
import { CircleHelp, Trash2, TriangleAlert } from "lucide-react";
import React, { Ref, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
import FieldDefaultValue from "./field-default-value";
@@ -15,7 +17,10 @@ interface FieldSettingProps {
field: FieldType
}
export interface ModifierValidation {
isValid: boolean;
errorMessage?: string;
}
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
@@ -27,11 +32,27 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const [collation, setCollation] = useState(new Set([field.collate]));
const [note, setNote] = useState<string | undefined>(field.note as string | undefined);
const defaultNameRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const maxLengthRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const precisionRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const [maxLengthValidation, setMaxLengthValidation] = useState<ModifierValidation>({
isValid: true,
});
const [precisionValidation, setPrecisionValidation] = useState<ModifierValidation>({
isValid: true,
});
const [scaleValidation, setScaleValidation] = useState<ModifierValidation>({
isValid: true,
});
const collations = useMemo(() => {
if (!field.type)
return undefined;
@@ -59,7 +80,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const removeField = () => {
deleteField(field.id)
}
const updateFieldNote = useCallback(() => {
editField({
@@ -98,76 +119,64 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const changeCharset = useCallback((keys: SharedSelection) => {
if (keys.anchorKey != field.charset) {
editField({
id: field.id,
charset: keys.anchorKey,
} as FieldInsertType);
}
}
setCharset(keys as any);
}, [field]);
const changeCollation = useCallback((keys: SharedSelection) => {
if (keys.anchorKey != field.collate) {
editField({
id: field.id,
collate: keys.anchorKey,
} as FieldInsertType);
}
}
setCollation(keys as any);
}, [field])
const saveDefaultValue = useCallback(() => {
const value: string | undefined = defaultNameRef.current?.value;
editField({
id: field.id,
defaultValue: value ? String(value) : null,
} as FieldInsertType);
}, [defaultNameRef, field]);
const saveMaxLength = useCallback(() => {
const value: string | undefined = maxLengthRef.current?.value;
if (maxLengthValidation.isValid) {
const value: string | undefined = maxLengthRef.current?.value;
editField({
id: field.id,
maxLength: value ? value : null,
} as FieldInsertType);
}
}, [maxLengthRef, maxLengthValidation, field]);
const saveScaleAndPrecision = useCallback(() => {
const precision: number | null = Number(precisionRef.current?.value)
const scale: number | null = Number(scaleRef.current?.value);
editField({
id: field.id,
maxLength: value ? value : null,
} as FieldInsertType);
}, [maxLengthRef, field]);
const savePrecision = useCallback(() => {
const value: string | undefined = precisionRef.current?.value;
editField({
id: field.id,
precision: value ? value : null,
} as FieldInsertType);
}, [precisionRef, field]);
const saveScale = useCallback(() => {
const value: string | undefined = scaleRef.current?.value;
editField({
id: field.id,
scale: value ? value : null,
} as FieldInsertType);
}, [scaleRef, field]);
precision: precisionValidation.isValid ? (precision > 0 ? precision : null) : undefined,
scale: (scaleValidation.isValid && precision > 0) ? (scale > 0 ? scale : null) : null
} as FieldInsertType)
}, [field, scaleRef, precisionRef, scaleValidation, precisionValidation])
const updateValues = useCallback((values: string[]) => {
const jsonValues = JSON.stringify(values);
if (jsonValues != field.values)
editField({
id: field.id,
@@ -182,6 +191,72 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const maxLengthChange = useCallback((value: any) => {
if (value && value.trim().length > 0) {
const isValid = Number.isInteger(Number(value)) && value > 0;
setMaxLengthValidation({
isValid,
errorMessage: (field.type?.type == DataTypes.INTEGER ?
t("db_controller.field_settings.width")
:
t("db_controller.field_settings.max_length")) + " " + t("db_controller.field_settings.errors.max_length")
})
} else
setMaxLengthValidation({
isValid: true,
errorMessage: undefined
});
}, [maxLengthRef, field]);
const validateScaleAndPrecision = useCallback(() => {
const scale: number | null = scaleRef.current && Number(scaleRef.current?.value);
const precision: number | null = precisionRef.current && Number(precisionRef.current?.value);
if (precision) {
const isValid = Number.isInteger(precision) && precision > 0;
setPrecisionValidation({
isValid,
errorMessage: t("db_controller.field_settings.errors.precision")
});
}
else {
setPrecisionValidation({
isValid: true,
errorMessage: undefined
});
}
if (scale) {
const isPositive = Number.isInteger(scale) && scale >= 0;
let isLessThanPrecision: boolean = true;
if (precision && precision >= 0) {
isLessThanPrecision = precision >= scale;
}
const errorMessage: string | undefined = !isPositive ? t("db_controller.field_settings.errors.scale") :
((!isLessThanPrecision) ? t("db_controller.field_settings.errors.scale_max_value") : undefined)
setScaleValidation({
isValid: (isPositive && isLessThanPrecision) as boolean,
errorMessage: errorMessage
});
} else {
setScaleValidation({
isValid: true,
errorMessage: undefined
});
}
}, [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">
@@ -190,7 +265,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
<hr className="border-divider" />
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
<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} />
@@ -204,7 +279,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.AUTO_INCREMENT) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.autoIncrement")}
</span>
<Checkbox defaultSelected={field.autoIncrement as boolean} size="md" onValueChange={toggleAutoIncrement} />
@@ -213,7 +288,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.UNSIGNED) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.unsigned")}
</span>
<Checkbox defaultSelected={field.unsigned as boolean} size="md" onValueChange={toggleUnsigned} />
@@ -222,7 +297,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.ZEROFILL) &&
<div className="flex w-full justify-between">
<span className="text-xs text-icon font-medium dark:text-font/90">
<span className="text-xs text-font/70 font-medium dark:text-font/90">
{t("db_controller.field_settings.zeroFill")}
</span>
<Checkbox defaultSelected={field.zeroFill as boolean} size="md" onValueChange={toggleZeroFill} />
@@ -241,16 +316,36 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.PRECISION) &&
<div className="flex flex-col gap-2 w-full">
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.precision")}
</label>
<Tooltip>
<TooltipTrigger>
<label className="flex items-center text-font/70 justify-between text-xs font-medium dark:text-font/90">
<span>
{t("db_controller.field_settings.precision")}
</span>
<CircleHelp className="size-3.5" />
</label>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.field_settings.precision_def")}
</TooltipContent>
</Tooltip>
<Input
type="number"
size="sm"
ref={precisionRef}
defaultValue={String(field.precision)}
onBlur={savePrecision}
onBlur={saveScaleAndPrecision}
onValueChange={validateScaleAndPrecision}
isInvalid={!precisionValidation.isValid}
endContent={
!precisionValidation.isValid && <>
<HeroUITooltip showArrow={true} content={precisionValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.precision")}
@@ -265,17 +360,36 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
{
modifiers.includes(Modifiers.SCALE) &&
<div className="flex flex-col gap-2 w-full">
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.scale")}
</label>
<Input
<Tooltip>
<TooltipTrigger>
<label className="flex items-center justify-between text-xs font-medium text-icon dark:text-font/90">
<span>
{t("db_controller.field_settings.scale")}
</span>
<CircleHelp className="size-3.5" />
</label>
</TooltipTrigger>
<TooltipContent>
{t("db_controller.field_settings.scale_def")}
</TooltipContent>
</Tooltip>
<Input
type="number"
size="sm"
ref={scaleRef}
defaultValue={String(field.scale)}
onBlur={saveScale}
onBlur={saveScaleAndPrecision}
isDisabled={!precisionRef.current?.value || !precisionValidation.isValid}
onValueChange={validateScaleAndPrecision}
isInvalid={!scaleValidation.isValid}
endContent={
!scaleValidation.isValid && <>
<HeroUITooltip showArrow={true} content={scaleValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.scale")}
@@ -348,30 +462,49 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}
{
modifiers.includes(Modifiers.LENGTH) && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.max_length")}
<label className="text-xs font-medium text-font/70 dark:text-font/90">
{
field.type?.type == DataTypes.INTEGER ?
t("db_controller.field_settings.integer_width")
:
t("db_controller.field_settings.max_length")
}
</label>
<Input
type="number"
size="sm"
ref={maxLengthRef}
defaultValue={String(field.maxLength)}
onBlur={saveMaxLength}
radius="sm"
isInvalid={!maxLengthValidation.isValid}
onValueChange={maxLengthChange}
endContent={
!maxLengthValidation.isValid && <>
<HeroUITooltip showArrow={true} content={maxLengthValidation.errorMessage} radius="sm" color="danger">
<TriangleAlert className="size-4 text-danger cursor-default" />
</HeroUITooltip>
</>
}
variant="faded"
placeholder={t("db_controller.field_settings.length")}
placeholder={
field.type?.type == DataTypes.INTEGER ?
t("db_controller.field_settings.width")
:
t("db_controller.field_settings.max_length")
}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
}}
/>
</>
}
{
modifiers.includes(Modifiers.VALUES) && <>
<h3 className="font-semibold text-sm text-font/90">
{field.type.name != null && (field.type.name?.[0].toUpperCase() + field.type.name?.slice(1))} {t("db_controller.field_settings.values")}
@@ -384,26 +517,10 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
</>
}
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.default_value")}
</label>
<Input
type="text"
size="sm"
ref={defaultNameRef}
defaultValue={field.defaultValue as string}
onBlur={saveDefaultValue}
radius="sm"
variant="faded"
placeholder={t("db_controller.field_settings.value")}
className="h-8 w-full focus-visible:ring-0 shadow-none "
classNames={{
inputWrapper: "dark:bg-default border-divider group-hover:border-primary ",
}}
/>
<label className="text-xs font-medium text-icon dark:text-font/90">
<FieldDefaultValue field={field} />
<label className="text-xs font-medium text-font/70 dark:text-font/90">
{t("db_controller.field_settings.note")}
</label>
<Textarea
+6
View File
@@ -206,4 +206,10 @@ button[data-testid="remove"] svg {
display: none ;
}
[data-invalid = "true"] div[data-slot="input-wrapper"] {
border : 1px solid red !important ;
background-color: transparent ;
outline: none ;
}
+27 -8
View File
@@ -1,7 +1,7 @@
import { DatabaseDialect } from "@/lib/database";
import { Modifiers } from "@/lib/field";
import { DataTypes, Modifiers, 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";
@@ -26,8 +26,6 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
renderableTables.find((table: TableType) => table.id == id) as TableType
);
for (const table of sortedTables) {
dbAst.push(TableToAst(table, data_types));
@@ -90,7 +88,7 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
let length: number | null = null;
let scale: number | null = null;
let scale: number | string | null = null;
let character_set: any | null = null;
let collate: any | null = null;
@@ -100,8 +98,11 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
let default_val: any | null = null;
if (modifiers.includes(Modifiers.PRECISION) && field.precision)
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
length = field.precision;
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
scale = "0";
}
if (modifiers.includes(Modifiers.SCALE) && field.scale)
scale = field.scale;
@@ -153,13 +154,31 @@ export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false)
value: field.defaultValue
}
}
if (field.defaultValue == "true" || field.defaultValue == "false") {
if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW)
default_val = {
type: "default",
value: {
type: "function",
name: {
name: [
{
type: "origin",
value: "CURRENT_TIMESTAMP"
}
]
},
over: null
}
}
else if (field.defaultValue == "true" || field.defaultValue == "false") {
default_val.value.type = "bool"
default_val.value.value = Boolean(field.defaultValue);
default_val.value.value = field.defaultValue == "true" ? true : false;
}
// Check if it's a number (but not empty string or just whitespace)
if (!isNaN(Number(field.defaultValue))) {
else if (!isNaN(Number(field.defaultValue))) {
default_val.value.type = "number"
default_val.value.value = Number(field.defaultValue);
}
+11 -13
View File
@@ -8,10 +8,10 @@ export function fixCharsetPlacement(sql: string): string {
const lines = sql.split('\n');
let currentColumn = '';
const result: string[] = [];
for (const line of lines) {
const trimmed = line.trim();
// Handle table start/end and other non-column lines
if (isTableStructureLine(trimmed)) {
if (currentColumn) {
@@ -21,7 +21,7 @@ export function fixCharsetPlacement(sql: string): string {
result.push(line);
continue;
}
// Handle column definitions
if (trimmed.endsWith(',')) {
currentColumn += ' ' + trimmed.slice(0, -1);
@@ -31,12 +31,10 @@ export function fixCharsetPlacement(sql: string): string {
currentColumn += ' ' + trimmed;
}
}
// Process any remaining column
if (currentColumn) {
result.push(processColumn(currentColumn));
}
return result.join('\n');
}
@@ -44,11 +42,11 @@ export function fixCharsetPlacement(sql: string): string {
* Checks if a line is part of table structure (not a column definition)
*/
function isTableStructureLine(line: string): boolean {
return line.startsWith('CREATE TABLE') ||
line === '(' ||
line === ')' ||
line.endsWith('(') ||
line.endsWith(')');
return line.startsWith('CREATE TABLE') ||
line === '(' ||
line === ')' ||
line.endsWith('(') ||
line.endsWith(')');
}
/**
@@ -62,18 +60,18 @@ function processColumn(columnDef: string): string {
const [_, colName, dataType] = typeMatch;
const rest = columnDef.slice(typeMatch[0].length);
// Extract charset and collate
const charsetMatch = rest.match(/CHARACTER\s+SET\s+\S+/i);
const collateMatch = rest.match(/COLLATE\s+\S+/i);
// Clean the remaining attributes
const cleanRest = rest
.replace(/CHARACTER\s+SET\s+\S+/gi, '')
.replace(/COLLATE\s+\S+/gi, '')
.replace(/\s+/g, ' ')
.trim();
// Reconstruct in correct order
let reconstructed = `${colName} ${dataType}`;
if (charsetMatch) reconstructed += ` ${charsetMatch[0]}`;
+1 -2
View File
@@ -64,8 +64,7 @@ function groupBy(array: any[], key: string) {
return acc;
}, {});
}
export {
areArraysEqual,
+2 -2
View File
@@ -6,8 +6,8 @@ export default {
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
"./node_modules/@nextui-org/theme/dist/**/*.{js,ts,jsx,tsx}",
"./node_modules/@heroui/theme/dist/components/(button|code|dropdown|input|kbd|link|navbar|snippet|toggle|popover|ripple|spinner|menu|divider|form|modal|toast).js",
],
darkMode: "class",