mirror of
https://github.com/chartdb/chartdb.git
synced 2025-10-23 07:11:56 +00:00
feat(import-db): add DBML syntax to import database dialog (#768)
* feat(editor): add import DBML syntax in import database dialog * fix * fix * fix * fix * fix --------- Co-authored-by: Guy Ben-Aharon <baguy3@gmail.com>
This commit is contained in:
@@ -38,7 +38,7 @@ export interface CodeSnippetProps {
|
||||
className?: string;
|
||||
code: string;
|
||||
codeToCopy?: string;
|
||||
language?: 'sql' | 'shell';
|
||||
language?: 'sql' | 'shell' | 'dbml';
|
||||
loading?: boolean;
|
||||
autoScroll?: boolean;
|
||||
isComplete?: boolean;
|
||||
|
@@ -9,12 +9,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
base: 'vs-dark',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'comment', foreground: '6A9955' }, // Comments
|
||||
{ token: 'keyword', foreground: '569CD6' }, // Table, Ref keywords
|
||||
{ token: 'string', foreground: 'CE9178' }, // Strings
|
||||
{ token: 'annotation', foreground: '9CDCFE' }, // [annotations]
|
||||
{ token: 'delimiter', foreground: 'D4D4D4' }, // Braces {}
|
||||
{ token: 'operator', foreground: 'D4D4D4' }, // Operators
|
||||
{ token: 'datatype', foreground: '4EC9B0' }, // Data types
|
||||
{ token: 'type', foreground: '4EC9B0' }, // Data types
|
||||
{ token: 'identifier', foreground: '9CDCFE' }, // Field names
|
||||
],
|
||||
colors: {},
|
||||
});
|
||||
@@ -23,12 +25,14 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
base: 'vs',
|
||||
inherit: true,
|
||||
rules: [
|
||||
{ token: 'comment', foreground: '008000' }, // Comments
|
||||
{ token: 'keyword', foreground: '0000FF' }, // Table, Ref keywords
|
||||
{ token: 'string', foreground: 'A31515' }, // Strings
|
||||
{ token: 'annotation', foreground: '001080' }, // [annotations]
|
||||
{ token: 'delimiter', foreground: '000000' }, // Braces {}
|
||||
{ token: 'operator', foreground: '000000' }, // Operators
|
||||
{ token: 'type', foreground: '267F99' }, // Data types
|
||||
{ token: 'identifier', foreground: '001080' }, // Field names
|
||||
],
|
||||
colors: {},
|
||||
});
|
||||
@@ -37,23 +41,59 @@ export const setupDBMLLanguage = (monaco: Monaco) => {
|
||||
const datatypePattern = dataTypesNames.join('|');
|
||||
|
||||
monaco.languages.setMonarchTokensProvider('dbml', {
|
||||
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum'],
|
||||
keywords: ['Table', 'Ref', 'Indexes', 'Note', 'Enum', 'enum'],
|
||||
datatypes: dataTypesNames,
|
||||
operators: ['>', '<', '-'],
|
||||
|
||||
tokenizer: {
|
||||
root: [
|
||||
// Comments
|
||||
[/\/\/.*$/, 'comment'],
|
||||
|
||||
// Keywords - case insensitive
|
||||
[
|
||||
/\b([Tt][Aa][Bb][Ll][Ee]|[Ee][Nn][Uu][Mm]|[Rr][Ee][Ff]|[Ii][Nn][Dd][Ee][Xx][Ee][Ss]|[Nn][Oo][Tt][Ee])\b/,
|
||||
'keyword',
|
||||
],
|
||||
|
||||
// Annotations in brackets
|
||||
[/\[.*?\]/, 'annotation'],
|
||||
|
||||
// Strings
|
||||
[/'''/, 'string', '@tripleQuoteString'],
|
||||
[/".*?"/, 'string'],
|
||||
[/'.*?'/, 'string'],
|
||||
[/"([^"\\]|\\.)*$/, 'string.invalid'], // non-terminated string
|
||||
[/'([^'\\]|\\.)*$/, 'string.invalid'], // non-terminated string
|
||||
[/"/, 'string', '@string_double'],
|
||||
[/'/, 'string', '@string_single'],
|
||||
[/`.*?`/, 'string'],
|
||||
[/[{}]/, 'delimiter'],
|
||||
[/[<>]/, 'operator'],
|
||||
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'], // Added 'i' flag for case-insensitive matching
|
||||
|
||||
// Delimiters and operators
|
||||
[/[{}()]/, 'delimiter'],
|
||||
[/[<>-]/, 'operator'],
|
||||
[/:/, 'delimiter'],
|
||||
|
||||
// Data types
|
||||
[new RegExp(`\\b(${datatypePattern})\\b`, 'i'), 'type'],
|
||||
|
||||
// Numbers
|
||||
[/\d+/, 'number'],
|
||||
|
||||
// Identifiers
|
||||
[/[a-zA-Z_]\w*/, 'identifier'],
|
||||
],
|
||||
|
||||
string_double: [
|
||||
[/[^\\"]+/, 'string'],
|
||||
[/\\./, 'string.escape'],
|
||||
[/"/, 'string', '@pop'],
|
||||
],
|
||||
|
||||
string_single: [
|
||||
[/[^\\']+/, 'string'],
|
||||
[/\\./, 'string.escape'],
|
||||
[/'/, 'string', '@pop'],
|
||||
],
|
||||
|
||||
tripleQuoteString: [
|
||||
[/[^']+/, 'string'],
|
||||
[/'''/, 'string', '@pop'],
|
||||
|
@@ -42,6 +42,10 @@ import {
|
||||
type ValidationResult,
|
||||
} from '@/lib/data/sql-import/sql-validator';
|
||||
import { SQLValidationStatus } from './sql-validation-status';
|
||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||
import type { ImportMethod } from '@/lib/import-method/import-method';
|
||||
import { detectImportMethod } from '@/lib/import-method/detect-import-method';
|
||||
import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
|
||||
|
||||
const calculateContentSizeMB = (content: string): number => {
|
||||
return content.length / (1024 * 1024); // Convert to MB
|
||||
@@ -55,49 +59,6 @@ const calculateIsLargeFile = (content: string): boolean => {
|
||||
const errorScriptOutputMessage =
|
||||
'Invalid JSON. Please correct it or contact us at support@chartdb.io for help.';
|
||||
|
||||
// Helper to detect if content is likely SQL DDL or JSON
|
||||
const detectContentType = (content: string): 'query' | 'ddl' | null => {
|
||||
if (!content || content.trim().length === 0) return null;
|
||||
|
||||
// Common SQL DDL keywords
|
||||
const ddlKeywords = [
|
||||
'CREATE TABLE',
|
||||
'ALTER TABLE',
|
||||
'DROP TABLE',
|
||||
'CREATE INDEX',
|
||||
'CREATE VIEW',
|
||||
'CREATE PROCEDURE',
|
||||
'CREATE FUNCTION',
|
||||
'CREATE SCHEMA',
|
||||
'CREATE DATABASE',
|
||||
];
|
||||
|
||||
const upperContent = content.toUpperCase();
|
||||
|
||||
// Check for SQL DDL patterns
|
||||
const hasDDLKeywords = ddlKeywords.some((keyword) =>
|
||||
upperContent.includes(keyword)
|
||||
);
|
||||
if (hasDDLKeywords) return 'ddl';
|
||||
|
||||
// Check if it looks like JSON
|
||||
try {
|
||||
// Just check structure, don't need full parse for detection
|
||||
if (
|
||||
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
|
||||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
|
||||
) {
|
||||
return 'query';
|
||||
}
|
||||
} catch (error) {
|
||||
// Not valid JSON, might be partial
|
||||
console.error('Error detecting content type:', error);
|
||||
}
|
||||
|
||||
// If we can't confidently detect, return null
|
||||
return null;
|
||||
};
|
||||
|
||||
export interface ImportDatabaseProps {
|
||||
goBack?: () => void;
|
||||
onImport: () => void;
|
||||
@@ -111,8 +72,8 @@ export interface ImportDatabaseProps {
|
||||
>;
|
||||
keepDialogAfterImport?: boolean;
|
||||
title: string;
|
||||
importMethod: 'query' | 'ddl';
|
||||
setImportMethod: (method: 'query' | 'ddl') => void;
|
||||
importMethod: ImportMethod;
|
||||
setImportMethod: (method: ImportMethod) => void;
|
||||
}
|
||||
|
||||
export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
@@ -152,9 +113,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
setShowCheckJsonButton(false);
|
||||
}, [importMethod, setScriptResult]);
|
||||
|
||||
// Check if the ddl is valid
|
||||
// Check if the ddl or dbml is valid
|
||||
useEffect(() => {
|
||||
if (importMethod !== 'ddl') {
|
||||
if (importMethod === 'query') {
|
||||
setSqlValidation(null);
|
||||
setShowAutoFixButton(false);
|
||||
return;
|
||||
@@ -163,9 +124,48 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
if (!scriptResult.trim()) {
|
||||
setSqlValidation(null);
|
||||
setShowAutoFixButton(false);
|
||||
setErrorMessage('');
|
||||
return;
|
||||
}
|
||||
|
||||
if (importMethod === 'dbml') {
|
||||
// Validate DBML by parsing it
|
||||
const validateResponse = verifyDBML(scriptResult);
|
||||
if (!validateResponse.hasError) {
|
||||
setErrorMessage('');
|
||||
setSqlValidation({
|
||||
isValid: true,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
});
|
||||
} else {
|
||||
let errorMsg = 'Invalid DBML syntax';
|
||||
let line: number = 1;
|
||||
|
||||
if (validateResponse.parsedError) {
|
||||
errorMsg = validateResponse.parsedError.message;
|
||||
line = validateResponse.parsedError.line;
|
||||
}
|
||||
|
||||
setSqlValidation({
|
||||
isValid: false,
|
||||
errors: [
|
||||
{
|
||||
message: errorMsg,
|
||||
line: line,
|
||||
type: 'syntax' as const,
|
||||
},
|
||||
],
|
||||
warnings: [],
|
||||
});
|
||||
setErrorMessage(errorMsg);
|
||||
}
|
||||
|
||||
setShowAutoFixButton(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// SQL validation
|
||||
// First run our validation based on database type
|
||||
const validation = validateSQL(scriptResult, databaseType);
|
||||
setSqlValidation(validation);
|
||||
@@ -338,7 +338,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
const isLargeFile = calculateIsLargeFile(content);
|
||||
|
||||
// First, detect content type to determine if we should switch modes
|
||||
const detectedType = detectContentType(content);
|
||||
const detectedType = detectImportMethod(content);
|
||||
if (detectedType && detectedType !== importMethod) {
|
||||
// Switch to the detected mode immediately
|
||||
setImportMethod(detectedType);
|
||||
@@ -352,7 +352,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode, do NOT format as it can break the SQL
|
||||
// For DDL and DBML modes, do NOT format as it can break the syntax
|
||||
} else {
|
||||
// Content type didn't change, apply formatting based on current mode
|
||||
if (importMethod === 'query' && !isLargeFile) {
|
||||
@@ -363,7 +363,7 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
?.run();
|
||||
}, 100);
|
||||
}
|
||||
// For DDL mode or large files, do NOT format
|
||||
// For DDL and DBML modes or large files, do NOT format
|
||||
}
|
||||
});
|
||||
|
||||
@@ -410,16 +410,25 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
<div className="w-full text-center text-xs text-muted-foreground">
|
||||
{importMethod === 'query'
|
||||
? 'Smart Query Output'
|
||||
: 'SQL Script'}
|
||||
: importMethod === 'dbml'
|
||||
? 'DBML Script'
|
||||
: 'SQL Script'}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<Suspense fallback={<Spinner />}>
|
||||
<Editor
|
||||
value={scriptResult}
|
||||
onChange={debouncedHandleInputChange}
|
||||
language={importMethod === 'query' ? 'json' : 'sql'}
|
||||
language={
|
||||
importMethod === 'query'
|
||||
? 'json'
|
||||
: importMethod === 'dbml'
|
||||
? 'dbml'
|
||||
: 'sql'
|
||||
}
|
||||
loading={<Spinner />}
|
||||
onMount={handleEditorDidMount}
|
||||
beforeMount={setupDBMLLanguage}
|
||||
theme={
|
||||
effectiveTheme === 'dark'
|
||||
? 'dbml-dark'
|
||||
@@ -430,7 +439,6 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
minimap: { enabled: false },
|
||||
scrollBeyondLastLine: false,
|
||||
automaticLayout: true,
|
||||
glyphMargin: false,
|
||||
lineNumbers: 'on',
|
||||
guides: {
|
||||
indentation: false,
|
||||
@@ -455,7 +463,9 @@ export const ImportDatabase: React.FC<ImportDatabaseProps> = ({
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
{errorMessage || (importMethod === 'ddl' && sqlValidation) ? (
|
||||
{errorMessage ||
|
||||
((importMethod === 'ddl' || importMethod === 'dbml') &&
|
||||
sqlValidation) ? (
|
||||
<SQLValidationStatus
|
||||
validation={sqlValidation}
|
||||
errorMessage={errorMessage}
|
||||
|
@@ -15,9 +15,11 @@ import {
|
||||
AvatarImage,
|
||||
} from '@/components/avatar/avatar';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Code } from 'lucide-react';
|
||||
import { Code, FileCode } from 'lucide-react';
|
||||
import { SmartQueryInstructions } from './instructions/smart-query-instructions';
|
||||
import { DDLInstructions } from './instructions/ddl-instructions';
|
||||
import { DBMLInstructions } from './instructions/dbml-instructions';
|
||||
import type { ImportMethod } from '@/lib/import-method/import-method';
|
||||
|
||||
const DatabasesWithoutDDLInstructions: DatabaseType[] = [
|
||||
DatabaseType.CLICKHOUSE,
|
||||
@@ -30,8 +32,8 @@ export interface InstructionsSectionProps {
|
||||
setDatabaseEdition: React.Dispatch<
|
||||
React.SetStateAction<DatabaseEdition | undefined>
|
||||
>;
|
||||
importMethod: 'query' | 'ddl';
|
||||
setImportMethod: (method: 'query' | 'ddl') => void;
|
||||
importMethod: ImportMethod;
|
||||
setImportMethod: (method: ImportMethod) => void;
|
||||
showSSMSInfoDialog: boolean;
|
||||
setShowSSMSInfoDialog: (show: boolean) => void;
|
||||
}
|
||||
@@ -125,9 +127,9 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
|
||||
className="ml-1 flex-wrap justify-start gap-2"
|
||||
value={importMethod}
|
||||
onValueChange={(value) => {
|
||||
let selectedImportMethod: 'query' | 'ddl' = 'query';
|
||||
let selectedImportMethod: ImportMethod = 'query';
|
||||
if (value) {
|
||||
selectedImportMethod = value as 'query' | 'ddl';
|
||||
selectedImportMethod = value as ImportMethod;
|
||||
}
|
||||
|
||||
setImportMethod(selectedImportMethod);
|
||||
@@ -150,10 +152,20 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
|
||||
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
|
||||
>
|
||||
<Avatar className="size-4 rounded-none">
|
||||
<Code size={16} />
|
||||
<FileCode size={16} />
|
||||
</Avatar>
|
||||
SQL Script
|
||||
</ToggleGroupItem>
|
||||
<ToggleGroupItem
|
||||
value="dbml"
|
||||
variant="outline"
|
||||
className="h-6 gap-1 p-0 px-2 shadow-none data-[state=on]:bg-slate-200 dark:data-[state=on]:bg-slate-700"
|
||||
>
|
||||
<Avatar className="size-4 rounded-none">
|
||||
<Code size={16} />
|
||||
</Avatar>
|
||||
DBML
|
||||
</ToggleGroupItem>
|
||||
</ToggleGroup>
|
||||
</div>
|
||||
)}
|
||||
@@ -167,11 +179,16 @@ export const InstructionsSection: React.FC<InstructionsSectionProps> = ({
|
||||
showSSMSInfoDialog={showSSMSInfoDialog}
|
||||
setShowSSMSInfoDialog={setShowSSMSInfoDialog}
|
||||
/>
|
||||
) : (
|
||||
) : importMethod === 'ddl' ? (
|
||||
<DDLInstructions
|
||||
databaseType={databaseType}
|
||||
databaseEdition={databaseEdition}
|
||||
/>
|
||||
) : (
|
||||
<DBMLInstructions
|
||||
databaseType={databaseType}
|
||||
databaseEdition={databaseEdition}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -0,0 +1,47 @@
|
||||
import React from 'react';
|
||||
import type { DatabaseType } from '@/lib/domain/database-type';
|
||||
import type { DatabaseEdition } from '@/lib/domain/database-edition';
|
||||
import { CodeSnippet } from '@/components/code-snippet/code-snippet';
|
||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||
|
||||
export interface DBMLInstructionsProps {
|
||||
databaseType: DatabaseType;
|
||||
databaseEdition?: DatabaseEdition;
|
||||
}
|
||||
|
||||
export const DBMLInstructions: React.FC<DBMLInstructionsProps> = () => {
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-col gap-1 text-sm text-primary">
|
||||
<div>
|
||||
Paste your DBML (Database Markup Language) schema definition
|
||||
here →
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex h-64 flex-col gap-1 text-sm text-primary">
|
||||
<h4 className="text-xs font-medium">Example:</h4>
|
||||
<CodeSnippet
|
||||
className="h-full"
|
||||
allowCopy={false}
|
||||
editorProps={{
|
||||
beforeMount: setupDBMLLanguage,
|
||||
}}
|
||||
code={`Table users {
|
||||
id int [pk]
|
||||
username varchar
|
||||
email varchar
|
||||
}
|
||||
|
||||
Table posts {
|
||||
id int [pk]
|
||||
user_id int [ref: > users.id]
|
||||
title varchar
|
||||
content text
|
||||
}`}
|
||||
language={'dbml'}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
@@ -73,7 +73,7 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
|
||||
|
||||
{hasErrors ? (
|
||||
<div className="rounded-md border border-red-200 bg-red-50 dark:border-red-800 dark:bg-red-950">
|
||||
<ScrollArea className="h-24">
|
||||
<ScrollArea className="h-fit max-h-24">
|
||||
<div className="space-y-3 p-3 pt-2 text-red-700 dark:text-red-300">
|
||||
{validation?.errors
|
||||
.slice(0, 3)
|
||||
@@ -137,7 +137,7 @@ export const SQLValidationStatus: React.FC<SQLValidationStatusProps> = ({
|
||||
|
||||
{hasWarnings && !hasErrors ? (
|
||||
<div className="rounded-md border border-sky-200 bg-sky-50 dark:border-sky-800 dark:bg-sky-950">
|
||||
<ScrollArea className="h-24">
|
||||
<ScrollArea className="h-fit max-h-24">
|
||||
<div className="space-y-3 p-3 pt-2 text-sky-700 dark:text-sky-300">
|
||||
<div className="flex items-start gap-2">
|
||||
<AlertTriangle className="mt-0.5 size-4 shrink-0 text-sky-700 dark:text-sky-300" />
|
||||
|
@@ -22,6 +22,11 @@ import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||
import type { SelectedTable } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { filterMetadataByTables } from '@/lib/data/import-metadata/filter-metadata';
|
||||
import { MAX_TABLES_WITHOUT_SHOWING_FILTER } from '../common/select-tables/constants';
|
||||
import {
|
||||
defaultDBMLDiagramName,
|
||||
importDBMLToDiagram,
|
||||
} from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import type { ImportMethod } from '@/lib/import-method/import-method';
|
||||
|
||||
export interface CreateDiagramDialogProps extends BaseDialogProps {}
|
||||
|
||||
@@ -30,7 +35,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
}) => {
|
||||
const { diagramId } = useChartDB();
|
||||
const { t } = useTranslation();
|
||||
const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
|
||||
const [importMethod, setImportMethod] = useState<ImportMethod>('query');
|
||||
const [databaseType, setDatabaseType] = useState<DatabaseType>(
|
||||
DatabaseType.GENERIC
|
||||
);
|
||||
@@ -89,6 +94,14 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else if (importMethod === 'dbml') {
|
||||
diagram = await importDBMLToDiagram(scriptResult, {
|
||||
databaseType,
|
||||
});
|
||||
// Update the diagram name if it's the default
|
||||
if (diagram.name === defaultDBMLDiagramName) {
|
||||
diagram.name = `Diagram ${diagramNumber}`;
|
||||
}
|
||||
} else {
|
||||
let metadata: DatabaseMetadata | undefined = databaseMetadata;
|
||||
|
||||
@@ -171,7 +184,7 @@ export const CreateDiagramDialog: React.FC<CreateDiagramDialogProps> = ({
|
||||
try {
|
||||
setIsParsingMetadata(true);
|
||||
|
||||
if (importMethod === 'ddl') {
|
||||
if (importMethod === 'ddl' || importMethod === 'dbml') {
|
||||
await importNewDiagram();
|
||||
} else {
|
||||
// Parse metadata asynchronously to avoid blocking the UI
|
||||
|
@@ -15,6 +15,8 @@ import { useReactFlow } from '@xyflow/react';
|
||||
import type { BaseDialogProps } from '../common/base-dialog-props';
|
||||
import { useAlert } from '@/context/alert-context/alert-context';
|
||||
import { sqlImportToDiagram } from '@/lib/data/sql-import';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import type { ImportMethod } from '@/lib/import-method/import-method';
|
||||
|
||||
export interface ImportDatabaseDialogProps extends BaseDialogProps {
|
||||
databaseType: DatabaseType;
|
||||
@@ -24,7 +26,7 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||
dialog,
|
||||
databaseType,
|
||||
}) => {
|
||||
const [importMethod, setImportMethod] = useState<'query' | 'ddl'>('query');
|
||||
const [importMethod, setImportMethod] = useState<ImportMethod>('query');
|
||||
const { closeImportDatabaseDialog } = useDialog();
|
||||
const { showAlert } = useAlert();
|
||||
const {
|
||||
@@ -65,6 +67,10 @@ export const ImportDatabaseDialog: React.FC<ImportDatabaseDialogProps> = ({
|
||||
sourceDatabaseType: databaseType,
|
||||
targetDatabaseType: databaseType,
|
||||
});
|
||||
} else if (importMethod === 'dbml') {
|
||||
diagram = await importDBMLToDiagram(scriptResult, {
|
||||
databaseType,
|
||||
});
|
||||
} else {
|
||||
const databaseMetadata: DatabaseMetadata =
|
||||
loadDatabaseMetadata(scriptResult);
|
||||
|
@@ -23,24 +23,19 @@ import { useTranslation } from 'react-i18next';
|
||||
import { Editor } from '@/components/code-snippet/code-snippet';
|
||||
import { useTheme } from '@/hooks/use-theme';
|
||||
import { AlertCircle } from 'lucide-react';
|
||||
import {
|
||||
importDBMLToDiagram,
|
||||
sanitizeDBML,
|
||||
preprocessDBML,
|
||||
} from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
|
||||
import { useChartDB } from '@/hooks/use-chartdb';
|
||||
import { Parser } from '@dbml/core';
|
||||
import { useCanvas } from '@/hooks/use-canvas';
|
||||
import { setupDBMLLanguage } from '@/components/code-snippet/languages/dbml-language';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import { useToast } from '@/components/toast/use-toast';
|
||||
import { Spinner } from '@/components/spinner/spinner';
|
||||
import { debounce } from '@/lib/utils';
|
||||
import { parseDBMLError } from '@/lib/dbml/dbml-import/dbml-import-error';
|
||||
import {
|
||||
clearErrorHighlight,
|
||||
highlightErrorLine,
|
||||
} from '@/components/code-snippet/dbml/utils';
|
||||
import { verifyDBML } from '@/lib/dbml/dbml-import/verify-dbml';
|
||||
|
||||
export interface ImportDBMLDialogProps extends BaseDialogProps {
|
||||
withCreateEmptyDiagram?: boolean;
|
||||
@@ -93,6 +88,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
relationships,
|
||||
removeTables,
|
||||
removeRelationships,
|
||||
databaseType,
|
||||
} = useChartDB();
|
||||
const { reorderTables } = useCanvas();
|
||||
const [reorder, setReorder] = useState(false);
|
||||
@@ -126,16 +122,15 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
setErrorMessage(undefined);
|
||||
clearDecorations();
|
||||
|
||||
if (!content.trim()) return;
|
||||
if (!content.trim()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const preprocessedContent = preprocessDBML(content);
|
||||
const sanitizedContent = sanitizeDBML(preprocessedContent);
|
||||
const parser = new Parser();
|
||||
parser.parse(sanitizedContent, 'dbmlv2');
|
||||
} catch (e) {
|
||||
const parsedError = parseDBMLError(e);
|
||||
if (parsedError) {
|
||||
const validateResponse = verifyDBML(content);
|
||||
|
||||
if (validateResponse.hasError) {
|
||||
if (validateResponse.parsedError) {
|
||||
const parsedError = validateResponse.parsedError;
|
||||
setErrorMessage(
|
||||
t('import_dbml_dialog.error.description') +
|
||||
` (1 error found - in line ${parsedError.line})`
|
||||
@@ -147,9 +142,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
decorationsCollection.current,
|
||||
});
|
||||
} else {
|
||||
setErrorMessage(
|
||||
e instanceof Error ? e.message : JSON.stringify(e)
|
||||
);
|
||||
setErrorMessage(validateResponse.errorText);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -188,7 +181,9 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
if (!dbmlContent.trim() || errorMessage) return;
|
||||
|
||||
try {
|
||||
const importedDiagram = await importDBMLToDiagram(dbmlContent);
|
||||
const importedDiagram = await importDBMLToDiagram(dbmlContent, {
|
||||
databaseType,
|
||||
});
|
||||
const tableIdsToRemove = tables
|
||||
.filter((table) =>
|
||||
importedDiagram.tables?.some(
|
||||
@@ -267,6 +262,7 @@ Ref: comments.user_id > users.id // Each comment is written by one user`;
|
||||
toast,
|
||||
setReorder,
|
||||
t,
|
||||
databaseType,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
@@ -5,7 +5,7 @@ import {
|
||||
databaseTypesWithCommentSupport,
|
||||
} from '@/lib/domain/database-type';
|
||||
import type { DBTable } from '@/lib/domain/db-table';
|
||||
import type { DataType } from '../data-types/data-types';
|
||||
import { dataTypeMap, type DataType } from '../data-types/data-types';
|
||||
import { generateCacheKey, getFromCache, setInCache } from './export-sql-cache';
|
||||
import { exportMSSQL } from './export-per-type/mssql';
|
||||
import { exportPostgreSQL } from './export-per-type/postgresql';
|
||||
@@ -314,11 +314,26 @@ export const exportBaseSQL = ({
|
||||
sqlScript += `(1)`;
|
||||
}
|
||||
|
||||
// Add precision and scale for numeric types
|
||||
if (field.precision && field.scale) {
|
||||
sqlScript += `(${field.precision}, ${field.scale})`;
|
||||
} else if (field.precision) {
|
||||
sqlScript += `(${field.precision})`;
|
||||
// Add precision and scale for numeric types only
|
||||
const precisionAndScaleTypes = dataTypeMap[targetDatabaseType]
|
||||
.filter(
|
||||
(t) =>
|
||||
t.fieldAttributes?.precision && t.fieldAttributes?.scale
|
||||
)
|
||||
.map((t) => t.name);
|
||||
|
||||
const isNumericType = precisionAndScaleTypes.some(
|
||||
(t) =>
|
||||
field.type.name.toLowerCase().includes(t) ||
|
||||
typeName.toLowerCase().includes(t)
|
||||
);
|
||||
|
||||
if (isNumericType) {
|
||||
if (field.precision && field.scale) {
|
||||
sqlScript += `(${field.precision}, ${field.scale})`;
|
||||
} else if (field.precision) {
|
||||
sqlScript += `(${field.precision})`;
|
||||
}
|
||||
}
|
||||
|
||||
// Handle NOT NULL constraint
|
||||
|
@@ -1,66 +0,0 @@
|
||||
import { describe, it } from 'vitest';
|
||||
|
||||
describe('node-sql-parser - CREATE TYPE handling', () => {
|
||||
it('should show exact parser error for CREATE TYPE', async () => {
|
||||
const { Parser } = await import('node-sql-parser');
|
||||
const parser = new Parser();
|
||||
const parserOpts = {
|
||||
database: 'PostgreSQL',
|
||||
};
|
||||
|
||||
console.log('\n=== Testing CREATE TYPE statement ===');
|
||||
const createTypeSQL = `CREATE TYPE spell_element AS ENUM ('fire', 'water', 'earth', 'air');`;
|
||||
|
||||
try {
|
||||
parser.astify(createTypeSQL, parserOpts);
|
||||
console.log('CREATE TYPE parsed successfully');
|
||||
} catch (error) {
|
||||
console.log('CREATE TYPE parse error:', (error as Error).message);
|
||||
}
|
||||
|
||||
console.log('\n=== Testing CREATE EXTENSION statement ===');
|
||||
const createExtensionSQL = `CREATE EXTENSION IF NOT EXISTS "uuid-ossp";`;
|
||||
|
||||
try {
|
||||
parser.astify(createExtensionSQL, parserOpts);
|
||||
console.log('CREATE EXTENSION parsed successfully');
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'CREATE EXTENSION parse error:',
|
||||
(error as Error).message
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n=== Testing CREATE TABLE with custom type ===');
|
||||
const createTableWithTypeSQL = `CREATE TABLE wizards (
|
||||
id UUID PRIMARY KEY,
|
||||
element spell_element DEFAULT 'fire'
|
||||
);`;
|
||||
|
||||
try {
|
||||
parser.astify(createTableWithTypeSQL, parserOpts);
|
||||
console.log('CREATE TABLE with custom type parsed successfully');
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'CREATE TABLE with custom type parse error:',
|
||||
(error as Error).message
|
||||
);
|
||||
}
|
||||
|
||||
console.log('\n=== Testing CREATE TABLE with standard types only ===');
|
||||
const createTableStandardSQL = `CREATE TABLE wizards (
|
||||
id UUID PRIMARY KEY,
|
||||
element VARCHAR(20) DEFAULT 'fire'
|
||||
);`;
|
||||
|
||||
try {
|
||||
parser.astify(createTableStandardSQL, parserOpts);
|
||||
console.log('CREATE TABLE with standard types parsed successfully');
|
||||
} catch (error) {
|
||||
console.log(
|
||||
'CREATE TABLE with standard types parse error:',
|
||||
(error as Error).message
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
149
src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
Normal file
149
src/lib/dbml/dbml-import/__tests__/dbml-integration.test.ts
Normal file
@@ -0,0 +1,149 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { DatabaseType } from '@/lib/domain/database-type';
|
||||
import { importDBMLToDiagram } from '@/lib/dbml/dbml-import/dbml-import';
|
||||
|
||||
// This test verifies the DBML integration without UI components
|
||||
describe('DBML Integration Tests', () => {
|
||||
it('should handle DBML import in create diagram flow', async () => {
|
||||
const dbmlContent = `
|
||||
Table users {
|
||||
id uuid [pk, not null]
|
||||
email varchar [unique, not null]
|
||||
created_at timestamp
|
||||
}
|
||||
|
||||
Table posts {
|
||||
id uuid [pk]
|
||||
title varchar
|
||||
content text
|
||||
user_id uuid [ref: > users.id]
|
||||
created_at timestamp
|
||||
}
|
||||
|
||||
Table comments {
|
||||
id uuid [pk]
|
||||
content text
|
||||
post_id uuid [ref: > posts.id]
|
||||
user_id uuid [ref: > users.id]
|
||||
}
|
||||
|
||||
// This will be ignored
|
||||
TableGroup "Content" {
|
||||
posts
|
||||
comments
|
||||
}
|
||||
|
||||
// This will be ignored too
|
||||
Note test_note {
|
||||
'This is a test note'
|
||||
}`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlContent);
|
||||
|
||||
// Verify basic structure
|
||||
expect(diagram).toBeDefined();
|
||||
expect(diagram.tables).toHaveLength(3);
|
||||
expect(diagram.relationships).toHaveLength(3);
|
||||
|
||||
// Verify tables
|
||||
const tableNames = diagram.tables?.map((t) => t.name).sort();
|
||||
expect(tableNames).toEqual(['comments', 'posts', 'users']);
|
||||
|
||||
// Verify users table
|
||||
const usersTable = diagram.tables?.find((t) => t.name === 'users');
|
||||
expect(usersTable).toBeDefined();
|
||||
expect(usersTable?.fields).toHaveLength(3);
|
||||
|
||||
const emailField = usersTable?.fields.find((f) => f.name === 'email');
|
||||
expect(emailField?.unique).toBe(true);
|
||||
expect(emailField?.nullable).toBe(false);
|
||||
|
||||
// Verify relationships
|
||||
// There should be 3 relationships total
|
||||
expect(diagram.relationships).toHaveLength(3);
|
||||
|
||||
// Find the relationship from users to posts (DBML ref is: posts.user_id > users.id)
|
||||
// This creates a relationship FROM users TO posts (one user has many posts)
|
||||
const postsTable = diagram.tables?.find((t) => t.name === 'posts');
|
||||
const usersTableId = usersTable?.id;
|
||||
|
||||
const userPostRelation = diagram.relationships?.find(
|
||||
(r) =>
|
||||
r.sourceTableId === usersTableId &&
|
||||
r.targetTableId === postsTable?.id
|
||||
);
|
||||
|
||||
expect(userPostRelation).toBeDefined();
|
||||
expect(userPostRelation?.sourceCardinality).toBe('one');
|
||||
expect(userPostRelation?.targetCardinality).toBe('many');
|
||||
});
|
||||
|
||||
it('should handle DBML with special features', async () => {
|
||||
const dbmlContent = `
|
||||
// Enum will be converted to varchar
|
||||
Table users {
|
||||
id int [pk]
|
||||
status enum
|
||||
tags text[] // Array will be converted to text
|
||||
favorite_product_id int
|
||||
}
|
||||
|
||||
Table products [headercolor: #FF0000] {
|
||||
id int [pk]
|
||||
name varchar
|
||||
price decimal(10,2)
|
||||
}
|
||||
|
||||
Ref: products.id < users.favorite_product_id`;
|
||||
|
||||
const diagram = await importDBMLToDiagram(dbmlContent);
|
||||
|
||||
expect(diagram.tables).toHaveLength(2);
|
||||
|
||||
// Check enum conversion
|
||||
const usersTable = diagram.tables?.find((t) => t.name === 'users');
|
||||
const statusField = usersTable?.fields.find((f) => f.name === 'status');
|
||||
expect(statusField?.type.id).toBe('varchar');
|
||||
|
||||
// Check array type conversion
|
||||
const tagsField = usersTable?.fields.find((f) => f.name === 'tags');
|
||||
expect(tagsField?.type.id).toBe('text');
|
||||
|
||||
// Check that header color was removed
|
||||
const productsTable = diagram.tables?.find(
|
||||
(t) => t.name === 'products'
|
||||
);
|
||||
expect(productsTable).toBeDefined();
|
||||
expect(productsTable?.name).toBe('products');
|
||||
});
|
||||
|
||||
it('should handle empty or invalid DBML gracefully', async () => {
|
||||
// Empty DBML
|
||||
const emptyDiagram = await importDBMLToDiagram('');
|
||||
expect(emptyDiagram.tables).toHaveLength(0);
|
||||
expect(emptyDiagram.relationships).toHaveLength(0);
|
||||
|
||||
// Only comments
|
||||
const commentDiagram = await importDBMLToDiagram('// Just a comment');
|
||||
expect(commentDiagram.tables).toHaveLength(0);
|
||||
expect(commentDiagram.relationships).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should preserve diagram metadata when importing DBML', async () => {
|
||||
const dbmlContent = `Table test {
|
||||
id int [pk]
|
||||
}`;
|
||||
const diagram = await importDBMLToDiagram(dbmlContent);
|
||||
|
||||
// Default values
|
||||
expect(diagram.name).toBe('DBML Import');
|
||||
expect(diagram.databaseType).toBe(DatabaseType.GENERIC);
|
||||
|
||||
// These can be overridden by the dialog
|
||||
diagram.name = 'My Custom Diagram';
|
||||
diagram.databaseType = DatabaseType.POSTGRESQL;
|
||||
|
||||
expect(diagram.name).toBe('My Custom Diagram');
|
||||
expect(diagram.databaseType).toBe(DatabaseType.POSTGRESQL);
|
||||
});
|
||||
});
|
@@ -15,6 +15,8 @@ import {
|
||||
type DBCustomType,
|
||||
} from '@/lib/domain/db-custom-type';
|
||||
|
||||
export const defaultDBMLDiagramName = 'DBML Import';
|
||||
|
||||
// Preprocess DBML to handle unsupported features
|
||||
export const preprocessDBML = (content: string): string => {
|
||||
let processed = content;
|
||||
@@ -196,7 +198,7 @@ export const importDBMLToDiagram = async (
|
||||
if (!dbmlContent.trim()) {
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
name: defaultDBMLDiagramName,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
@@ -214,7 +216,7 @@ export const importDBMLToDiagram = async (
|
||||
if (!sanitizedContent.trim()) {
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
name: defaultDBMLDiagramName,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
@@ -229,7 +231,7 @@ export const importDBMLToDiagram = async (
|
||||
if (!parsedData.schemas || parsedData.schemas.length === 0) {
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
name: defaultDBMLDiagramName,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables: [],
|
||||
relationships: [],
|
||||
@@ -734,7 +736,7 @@ export const importDBMLToDiagram = async (
|
||||
|
||||
return {
|
||||
id: generateDiagramId(),
|
||||
name: 'DBML Import',
|
||||
name: defaultDBMLDiagramName,
|
||||
databaseType: options?.databaseType ?? DatabaseType.GENERIC,
|
||||
tables,
|
||||
relationships,
|
||||
|
52
src/lib/dbml/dbml-import/verify-dbml.ts
Normal file
52
src/lib/dbml/dbml-import/verify-dbml.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import { Parser } from '@dbml/core';
|
||||
import { preprocessDBML, sanitizeDBML } from './dbml-import';
|
||||
import type { DBMLError } from './dbml-import-error';
|
||||
import { parseDBMLError } from './dbml-import-error';
|
||||
|
||||
export const verifyDBML = (
|
||||
content: string
|
||||
):
|
||||
| {
|
||||
hasError: true;
|
||||
error: unknown;
|
||||
parsedError?: DBMLError;
|
||||
errorText: string;
|
||||
}
|
||||
| {
|
||||
hasError: false;
|
||||
} => {
|
||||
try {
|
||||
const preprocessedContent = preprocessDBML(content);
|
||||
const sanitizedContent = sanitizeDBML(preprocessedContent);
|
||||
const parser = new Parser();
|
||||
parser.parse(sanitizedContent, 'dbmlv2');
|
||||
} catch (e) {
|
||||
const parsedError = parseDBMLError(e);
|
||||
if (parsedError) {
|
||||
return {
|
||||
hasError: true,
|
||||
parsedError: parsedError,
|
||||
error: e,
|
||||
errorText: parsedError.message,
|
||||
};
|
||||
} else {
|
||||
if (e instanceof Error) {
|
||||
return {
|
||||
hasError: true,
|
||||
error: e,
|
||||
errorText: e.message,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
hasError: true,
|
||||
error: e,
|
||||
errorText: JSON.stringify(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
hasError: false,
|
||||
};
|
||||
};
|
157
src/lib/import-method/__tests__/detect-import-type.test.ts
Normal file
157
src/lib/import-method/__tests__/detect-import-type.test.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { detectImportMethod } from '../detect-import-method';
|
||||
|
||||
describe('detectImportMethod', () => {
|
||||
describe('DBML detection', () => {
|
||||
it('should detect DBML with Table definition', () => {
|
||||
const content = `Table users {
|
||||
id int [pk]
|
||||
name varchar
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with Ref definition', () => {
|
||||
const content = `Table posts {
|
||||
user_id int
|
||||
}
|
||||
|
||||
Ref: posts.user_id > users.id`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with pk attribute', () => {
|
||||
const content = `id integer [pk]`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with ref attribute', () => {
|
||||
const content = `user_id int [ref: > users.id]`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with Enum definition', () => {
|
||||
const content = `Enum status {
|
||||
active
|
||||
inactive
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with TableGroup', () => {
|
||||
const content = `TableGroup commerce {
|
||||
users
|
||||
orders
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should detect DBML with Note', () => {
|
||||
const content = `Note project_note {
|
||||
'This is a note about the project'
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should prioritize DBML over SQL when both patterns exist', () => {
|
||||
const content = `CREATE TABLE test (id int);
|
||||
Table users {
|
||||
id int [pk]
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
});
|
||||
|
||||
describe('SQL DDL detection', () => {
|
||||
it('should detect CREATE TABLE statement', () => {
|
||||
const content = `CREATE TABLE users (
|
||||
id INT PRIMARY KEY,
|
||||
name VARCHAR(255)
|
||||
);`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
|
||||
it('should detect ALTER TABLE statement', () => {
|
||||
const content = `ALTER TABLE users ADD COLUMN email VARCHAR(255);`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
|
||||
it('should detect DROP TABLE statement', () => {
|
||||
const content = `DROP TABLE IF EXISTS users;`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
|
||||
it('should detect CREATE INDEX statement', () => {
|
||||
const content = `CREATE INDEX idx_users_email ON users(email);`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
|
||||
it('should detect multiple DDL statements', () => {
|
||||
const content = `CREATE TABLE users (id INT);
|
||||
CREATE TABLE posts (id INT);
|
||||
ALTER TABLE posts ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
|
||||
it('should detect DDL case-insensitively', () => {
|
||||
const content = `create table users (id int);`;
|
||||
expect(detectImportMethod(content)).toBe('ddl');
|
||||
});
|
||||
});
|
||||
|
||||
describe('JSON detection', () => {
|
||||
it('should detect JSON object', () => {
|
||||
const content = `{
|
||||
"tables": [],
|
||||
"relationships": []
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('query');
|
||||
});
|
||||
|
||||
it('should detect JSON array', () => {
|
||||
const content = `[
|
||||
{"name": "users"},
|
||||
{"name": "posts"}
|
||||
]`;
|
||||
expect(detectImportMethod(content)).toBe('query');
|
||||
});
|
||||
|
||||
it('should detect minified JSON', () => {
|
||||
const content = `{"tables":[],"relationships":[]}`;
|
||||
expect(detectImportMethod(content)).toBe('query');
|
||||
});
|
||||
|
||||
it('should detect JSON with whitespace', () => {
|
||||
const content = ` {
|
||||
"data": true
|
||||
} `;
|
||||
expect(detectImportMethod(content)).toBe('query');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should return null for empty content', () => {
|
||||
expect(detectImportMethod('')).toBeNull();
|
||||
expect(detectImportMethod(' ')).toBeNull();
|
||||
expect(detectImportMethod('\n\n')).toBeNull();
|
||||
});
|
||||
|
||||
it('should return null for unrecognized content', () => {
|
||||
const content = `This is just some random text
|
||||
that doesn't match any pattern`;
|
||||
expect(detectImportMethod(content)).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle content with special characters', () => {
|
||||
const content = `Table users {
|
||||
name varchar // Special chars: áéíóú
|
||||
}`;
|
||||
expect(detectImportMethod(content)).toBe('dbml');
|
||||
});
|
||||
|
||||
it('should handle malformed JSON gracefully', () => {
|
||||
const content = `{ "incomplete": `;
|
||||
expect(detectImportMethod(content)).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
59
src/lib/import-method/detect-import-method.ts
Normal file
59
src/lib/import-method/detect-import-method.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import type { ImportMethod } from './import-method';
|
||||
|
||||
export const detectImportMethod = (content: string): ImportMethod | null => {
|
||||
if (!content || content.trim().length === 0) return null;
|
||||
|
||||
const upperContent = content.toUpperCase();
|
||||
|
||||
// Check for DBML patterns first (case sensitive)
|
||||
const dbmlPatterns = [
|
||||
/^Table\s+\w+\s*{/m,
|
||||
/^Ref:\s*\w+/m,
|
||||
/^Enum\s+\w+\s*{/m,
|
||||
/^TableGroup\s+/m,
|
||||
/^Note\s+\w+\s*{/m,
|
||||
/\[pk\]/,
|
||||
/\[ref:\s*[<>-]/,
|
||||
];
|
||||
|
||||
const hasDBMLPatterns = dbmlPatterns.some((pattern) =>
|
||||
pattern.test(content)
|
||||
);
|
||||
if (hasDBMLPatterns) return 'dbml';
|
||||
|
||||
// Common SQL DDL keywords
|
||||
const ddlKeywords = [
|
||||
'CREATE TABLE',
|
||||
'ALTER TABLE',
|
||||
'DROP TABLE',
|
||||
'CREATE INDEX',
|
||||
'CREATE VIEW',
|
||||
'CREATE PROCEDURE',
|
||||
'CREATE FUNCTION',
|
||||
'CREATE SCHEMA',
|
||||
'CREATE DATABASE',
|
||||
];
|
||||
|
||||
// Check for SQL DDL patterns
|
||||
const hasDDLKeywords = ddlKeywords.some((keyword) =>
|
||||
upperContent.includes(keyword)
|
||||
);
|
||||
if (hasDDLKeywords) return 'ddl';
|
||||
|
||||
// Check if it looks like JSON
|
||||
try {
|
||||
// Just check structure, don't need full parse for detection
|
||||
if (
|
||||
(content.trim().startsWith('{') && content.trim().endsWith('}')) ||
|
||||
(content.trim().startsWith('[') && content.trim().endsWith(']'))
|
||||
) {
|
||||
return 'query';
|
||||
}
|
||||
} catch (error) {
|
||||
// Not valid JSON, might be partial
|
||||
console.error('Error detecting content type:', error);
|
||||
}
|
||||
|
||||
// If we can't confidently detect, return null
|
||||
return null;
|
||||
};
|
1
src/lib/import-method/import-method.ts
Normal file
1
src/lib/import-method/import-method.ts
Normal file
@@ -0,0 +1 @@
|
||||
export type ImportMethod = 'query' | 'ddl' | 'dbml';
|
Reference in New Issue
Block a user