fix charset and collation order whene rendering a text column declaration sql

This commit is contained in:
KarimTamani
2025-06-14 23:03:27 +01:00
parent 12d2ac693e
commit 84dfd0e9d5
11 changed files with 268 additions and 72 deletions
+1 -2
View File
@@ -19,8 +19,7 @@ const RenameDatabase: React.FC<RenameDatabaseProps> = ({ database}) => {
const { t } = useTranslation();
const [isLoading, setIsLoading] = useState<boolean>(false);
useEffect(() => {
console.log (database.name) ;
useEffect(() => {
setDbName(database.name);
}, [database.name])
+1 -1
View File
@@ -49,7 +49,7 @@ const TagInput: React.FC<TagInputProps> = ({ defaultItems = [], onItemsChange })
tags={tags}
classNames={{
tag: "font-normal inline-block m-0.5 border-1 border-default rounded-full px-2 py-1 flex-row ",
remove: "bg-green-500 bg-default-900 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
remove: " bg-default-900 rounded-full text-xs text-center ml-1 min-w-[15px] max-w-[15px] min-h-[15px] max-h-[15px] transition-colors duration-300 hover:bg-black",
tagInputField: "relative w-full inline-flex flex-row items-center bg-default-100 border-1 border-divider hover:border-primary focus-within:border-default-400 h-8 min-h-8 px-2 rounded-small transition-background !duration-150 transition-colors outline-none dark:bg-default placeholder:text-foreground-500 mt-2" ,
tags : "max-h-[256px] overflow-auto "
}}
+15 -10
View File
@@ -7,7 +7,8 @@ import { useEffect, useState } from "react";
import { Parser } from "node-sql-parser";
import { DatabaseType } from "@/lib/schemas/database-schema";
import { format } from 'sql-formatter';
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
import { getDatabaseByDialect } from "@/lib/database";
import { fixCharsetPlacement } from "@/utils/render/parsers/render-uttils";
const parser = new Parser();
@@ -17,18 +18,22 @@ export const useRenderSql = (database: DatabaseType) => {
const { data_types } = useDatabaseOperations();
useEffect(() => {
const dbAst: any = DatabaseToAst(database, data_types);
const formattedSqlCode = format(parser.sqlify(dbAst, {
database: getDatabaseByDialect(database.dialect ).name
}), { language: 'sql' });
setSql(
formattedSqlCode
);
try {
const dbAst: any = DatabaseToAst(database, data_types);
const sql = fixCharsetPlacement(format(parser.sqlify(dbAst, {
database: getDatabaseByDialect(database.dialect).name
}), { language: "sql" } ));
const formattedSqlCode = format(sql, { language: 'sql' });
setSql(
(formattedSqlCode)
);
} catch (error) {
console.log(error)
}
}, [database]);
return sql;
}
@@ -6,33 +6,16 @@ import CodeMirror, { EditorView } from '@uiw/react-codemirror';
import { sql } from '@codemirror/lang-sql';
import { oneDark } from '@codemirror/theme-one-dark';
import { useTheme } from "next-themes";
import { Parser } from "node-sql-parser";
import { Parser } from "node-sql-parser";
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
import { DatabaseType } from "@/lib/schemas/database-schema";
const parser = new Parser();
const code = `
CREATE TABLE \`users\` (
id BIGINT NOT NULL PRIMARY KEY UNIQUE,
name VARCHAR(255) NOT NULL,
lastname VARCHAR(255) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
profile_url VARCHAR(255),
age SMALLINT,
birthday DATE,
created_at TIMESTAMP NOT NULL
);
CREATE TABLE \`addresses\` (
id BIGINT NOT NULL PRIMARY KEY UNIQUE,
street_line VARCHAR(255) NOT NULL,
latitude FLOAT,
longitude FLOAT, -- fixed spelling
state_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
FOREIGN KEY (\`user_id\`) REFERENCES \`users\` (\`id\`)
);
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
)
`
@@ -41,14 +24,14 @@ CREATE TABLE \`addresses\` (
const SqlPreview: React.FC = ({ }) => {
const { database } = useDatabase();
const sqlCode = useRenderSql(database);
const sqlCode = useRenderSql(database as DatabaseType);
const { resolvedTheme } = useTheme();
useEffect(() => {
const ast = parser.astify(code, {
database: "Mysql"
})
console.log(ast)
// console.log(ast)
}, [])
return (
@@ -59,7 +42,7 @@ const SqlPreview: React.FC = ({ }) => {
value={sqlCode}
className="flex flex-1 w-full"
extensions={[sql()]}
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark , overrideDarkTheme] }
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
/>
}
</div>
@@ -85,7 +85,6 @@ const FieldItem: React.FC<Props> = ({ field }) => {
placeholder={t("db_controller.name")}
value={fieldName}
onValueChange={setFieldName}
onBlur={saveFieldName}
classNames={{
inputWrapper: "border-divider group-hover:border-primary",
@@ -109,8 +108,6 @@ const FieldItem: React.FC<Props> = ({ field }) => {
>
N
</ToggleButton>
<ToggleButton
onToggle={togglePrimaryKey}
active={field.isPrimary as boolean}
@@ -119,7 +116,7 @@ const FieldItem: React.FC<Props> = ({ field }) => {
<KeyRound className="size-4" />
</ToggleButton>
<Popover placement="right-start" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<Popover placement="bottom" radius="sm" shadow="sm" isOpen={popOverOpen} onOpenChange={setPopOverOpen} >
<PopoverTrigger>
<Button
size="sm"
@@ -1,10 +1,11 @@
import TagInput from "@/components/tag-input/tag-input";
import { Modifiers, PostgreSQLCharset, PostgreSQLCollation } from "@/lib/field";
import { DatabaseDialect } from "@/lib/database";
import { 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, useRef, useState } from "react";
import React, { Ref, useCallback, useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next";
@@ -18,8 +19,7 @@ interface FieldSettingProps {
const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
console.log(field.type)
const modifiers: string[] = field.type.modifiers ? JSON.parse(field.type.modifiers) : [];
const modifiers: string[] = field.type?.modifiers ? JSON.parse(field.type.modifiers) : [];
const { deleteField, editField } = useDatabaseOperations();
const { t } = useTranslation();
@@ -32,9 +32,34 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const scaleRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const precisionRef: Ref<HTMLInputElement> = useRef<HTMLInputElement>(null);
const collations = useMemo(() => {
if (!field.type)
return undefined;
if (field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB)
return MySQLCollation;
if (field.type.dialect == DatabaseDialect.POSTGRES)
return PostgreSQLCollation;
if (field.type.dialect == DatabaseDialect.SQLITE)
return SQLiteCollation;
}, [field]);
const charsets = useMemo(() => {
if (!field.type)
return undefined;
if (field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB)
return MySQLCharset;
if (field.type.dialect == DatabaseDialect.POSTGRES)
return PostgreSQLCharset;
if (field.type.dialect == DatabaseDialect.SQLITE)
return SQLiteCharset;
}, [field])
const removeField = () => {
deleteField(field.id)
}
const updateFieldNote = useCallback(() => {
editField({
@@ -73,29 +98,29 @@ 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.charset) {
if (keys.anchorKey != field.collate) {
editField({
id: field.id,
collate: keys.anchorKey,
} as FieldInsertType);
}
}
setCollation(keys as any);
}, [field])
@@ -142,7 +167,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const updateValues = useCallback((values: string[]) => {
const jsonValues = JSON.stringify(values);
console.log(jsonValues)
if (jsonValues != field.values)
editField({
id: field.id,
@@ -155,7 +180,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
const showDecimalModifiers: boolean = modifiers.includes(Modifiers.PRECISION) || modifiers.includes(Modifiers.SCALE);
const showTextModifiers: boolean = modifiers.includes(Modifiers.COLLATE) || modifiers.includes(Modifiers.CHARSET);
return (
<div className="w-full flex flex-col gap-2 p-2 min-w-[260px] max-w-[260px]">
@@ -273,7 +298,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
</h3>
<hr className="border-divider" />
{
modifiers.includes(Modifiers.CHARSET) && <>
modifiers.includes(Modifiers.CHARSET) && charsets && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.charset")}
</label>
@@ -290,13 +315,13 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}}
>
{
Object.values(PostgreSQLCharset).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
Object.values(charsets).map((charset: string) => (<SelectItem key={charset}>{charset}</SelectItem>))
}
</Select>
</>
}
{
modifiers.includes(Modifiers.COLLATE) && <>
modifiers.includes(Modifiers.COLLATE) && collations && <>
<label className="text-xs font-medium text-icon dark:text-font/90">
{t("db_controller.field_settings.collation")}
</label>
@@ -313,7 +338,7 @@ const FieldSetting: React.FC<FieldSettingProps> = ({ field }) => {
}}
>
{
Object.values(PostgreSQLCollation).map((collation: string) => (<SelectItem key={collation}>{collation}</SelectItem>))
Object.values(collations).map((collation: string) => (<SelectItem key={collation}>{collation}</SelectItem>))
}
</Select>
</>
@@ -63,8 +63,7 @@ const DatabaseHistoryProvider: React.FC<Props> = ({ children }) => {
const operations: DBDiffOperation[] = mapDiffToDBDiffOperation(differences);
(async () => {
try {
console.log(operations)
try {
await executeDbDiffOps(operations)
setIsProcessing(false);
}
@@ -16,6 +16,7 @@ import { IndexInsertType, indices } from "@/lib/schemas/index-schema";
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";
@@ -81,12 +82,23 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
else
database = undefined as any;
// Fetch all data types
// Fetch all data types
let { data: data_types, isLoading: loadingDataTypes } = useQuery(toCompilableQuery(
db.query.data_types.findMany({
where: (data_types, { eq }) => eq(data_types.dialect, (database as any)?.dialect)
})
));
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()) ;
const grouped_data_types: any = useMemo(() => {
return groupBy(data_types, "type");
@@ -98,7 +110,6 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
if (databases.length > 0 && !currentDatabaseId) {
switchDatabase(databases[0].id);
}
}, [currentDatabaseId, databases]);
const isLoading: boolean = loadingDataTypes || loadingDatabases || loadingCurrentDatabase;
+100 -6
View File
@@ -1,6 +1,7 @@
import { DatabaseDialect } from "@/lib/database";
import { Modifiers } 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";
@@ -41,7 +42,7 @@ export const DatabaseToAst = (database: DatabaseType, data_types: DataType[]) =>
};
} catch (error) {
console.log(error);
}
}
return dbAst;
}
@@ -77,26 +78,119 @@ export const TableToAst = (table: TableType, data_types: DataType[]) => {
}
}
export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false) => {
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
let suffix: string[] = [];
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
suffix.push(Modifiers.ZEROFILL.toUpperCase());
}
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
suffix.push(Modifiers.UNSIGNED.toUpperCase());
}
let length: number | null = null;
let scale: number | null = null;
let character_set: any | null = null;
let collate: any | null = null;
let values: any[] = [];
let valuesExpr: any | null;
let default_val: any | null = null;
if (modifiers.includes(Modifiers.PRECISION) && field.precision)
length = field.precision;
if (modifiers.includes(Modifiers.SCALE) && field.scale)
scale = field.scale;
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
length = field.maxLength;
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
character_set = {
type: "CHARACTER SET",
value: {
type: "default",
value: field.charset
}
}
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
collate = {
keyword: "collate",
type: "collate",
collate: {
name: field.collate,
}
}
if (modifiers.includes(Modifiers.VALUES) && field.values) {
const jsonValues = JSON.parse(field.values);
if (jsonValues.length > 0) {
values = jsonValues.map((value: string) => ({
type: "single_quote_string",
value
}));
valuesExpr = {
parentheses: true,
type: "expr_list",
value: values
}
}
}
if (field.defaultValue && field.defaultValue.trim().length > 0) {
default_val = {
type: "default",
value: {
type: "single_quote_string",
value: field.defaultValue
}
}
if (field.defaultValue == "true" || field.defaultValue == "false") {
default_val.value.type = "bool"
default_val.value.value = Boolean(field.defaultValue);
}
// Check if it's a number (but not empty string or just whitespace)
if (!isNaN(Number(field.defaultValue))) {
default_val.value.type = "number"
default_val.value.value = Number(field.defaultValue);
}
}
// field.type?.name == "varchar" && (field.type?.dialect == DatabaseDialect.MYSQL || field.type?.dialect == DatabaseDialect.MARIADB) ? 255 : null
return {
column: {
type: "column_ref",
column: {
expr: {
type: "default", value: field.name
type: "default", value: field.name,
}
},
},
default_val: null,
collate,
character_set,
default_val,
unique: field.unique ? "unique" : null,
auto_increment: (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement) ? "auto_increment" : null,
nullable: {
type: field.nullable ? "null" : "not null",
value: field.nullable ? "null" : "not null",
},
definition: {
dataType: field.type?.name?.toLocaleUpperCase(),
length: field.type?.name == "varchar" && (field.type?.dialect == DatabaseDialect.MYSQL || field.type?.dialect == DatabaseDialect.MARIADB) ? 255 : null,
length,
scale,
suffix,
expr: valuesExpr
},
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
resource: "column"
+84
View File
@@ -0,0 +1,84 @@
/**
* Fixes charset/collation placement in SQL CREATE TABLE statements
* @param sql The SQL string to process
* @returns SQL with properly placed charset/collation clauses
*/
export function fixCharsetPlacement(sql: string): string {
// Split into lines to handle multi-line cases better
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) {
result.push(processColumn(currentColumn));
currentColumn = '';
}
result.push(line);
continue;
}
// Handle column definitions
if (trimmed.endsWith(',')) {
currentColumn += ' ' + trimmed.slice(0, -1);
result.push(processColumn(currentColumn) + ',');
currentColumn = '';
} else {
currentColumn += ' ' + trimmed;
}
}
// Process any remaining column
if (currentColumn) {
result.push(processColumn(currentColumn));
}
return result.join('\n');
}
/**
* 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(')');
}
/**
* Processes individual column definitions to fix charset/collation placement
*/
function processColumn(columnDef: string): string {
// Extract column name and data type
const typeMatch = columnDef.match(/^\s*(\w+)\s+(\w+(?:\([^)]*\))?)/i);
if (!typeMatch) return columnDef;
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]}`;
if (collateMatch) reconstructed += ` ${collateMatch[0]}`;
if (cleanRest) reconstructed += ` ${cleanRest}`;
return reconstructed;
}
+1 -2
View File
@@ -40,8 +40,7 @@ const adjustTablesPositions = async (
sources: [rel.sourceTableId],
targets: [rel.targetTableId],
})),
};
console.log(graph.children);
};
// Run ELK layout (async)
const layoutedGraph = await elk.layout(graph);