mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-10 19:25:44 +00:00
Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| eb6c80417c | |||
| 5cce8cc03d | |||
| caa277577a | |||
| 6c3b431ee2 | |||
| 3985e53886 | |||
| ef27393d9a | |||
| 9feb0397bc |
Generated
+2512
-2199
File diff suppressed because it is too large
Load Diff
+7
-4
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "StackRender",
|
||||
"private": true,
|
||||
"version": "1.0.2",
|
||||
"version": "1.4.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
@@ -13,6 +13,7 @@
|
||||
"@codemirror/lang-sql": "^6.9.0",
|
||||
"@dnd-kit/core": "^6.3.1",
|
||||
"@dnd-kit/sortable": "^10.0.0",
|
||||
"@guanmingchiu/sqlparser-ts": "^0.61.1",
|
||||
"@internationalized/date": "^3.8.2",
|
||||
"@nextui-org/react": "^2.6.11",
|
||||
"@powersync/drizzle-driver": "^0.4.0",
|
||||
@@ -53,7 +54,7 @@
|
||||
"fast-json-patch": "^3.1.1",
|
||||
"framer-motion": "11.15.0",
|
||||
"i18next-browser-languagedetector": "^8.0.5",
|
||||
"lodash": "^4.17.21",
|
||||
"lodash": "^4.18.1",
|
||||
"lucide-react": "^0.501.0",
|
||||
"node-sql-parser": "^5.3.9",
|
||||
"object-hash": "^3.0.0",
|
||||
@@ -64,7 +65,7 @@
|
||||
"react-dom": "18.3.1",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-resizable-panels": "^2.1.9",
|
||||
"react-router-dom": "6.23.0",
|
||||
"react-router-dom": "6.30.3",
|
||||
"react-tag-input": "^6.10.6",
|
||||
"sonner": "^2.0.7",
|
||||
"sql-formatter": "^15.6.3",
|
||||
@@ -78,7 +79,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/better-sqlite3": "^7.6.13",
|
||||
"@types/node": "20.5.7",
|
||||
"@types/node": "20.19.0",
|
||||
"@types/object-hash": "^3.0.6",
|
||||
"@types/pluralize": "^0.0.33",
|
||||
"@types/react": "18.3.3",
|
||||
@@ -87,6 +88,7 @@
|
||||
"@typescript-eslint/parser": "8.11.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"autoprefixer": "10.4.19",
|
||||
"esbuild": "^0.28.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-prettier": "9.1.0",
|
||||
"eslint-plugin-import": "^2.26.0",
|
||||
@@ -97,6 +99,7 @@
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unused-imports": "4.1.4",
|
||||
"prettier": "3.3.3",
|
||||
"rollup": "^4.61.1",
|
||||
"tw-animate-css": "^1.3.6",
|
||||
"typescript": "5.6.3",
|
||||
"vite": "^5.2.0",
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -1,7 +1,6 @@
|
||||
|
||||
|
||||
import "@/styles/globals.css"
|
||||
|
||||
import { ReactFlowProvider } from "@xyflow/react";
|
||||
import useAppRoutes from "./routes/app-route";
|
||||
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
||||
@@ -10,8 +9,6 @@ import DiagramProvider from "./providers/diagram-provider/diagram-provider";
|
||||
import { ModalProvider } from "./providers/modal-provider/modal-provider";
|
||||
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
||||
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
const appRoutes = useAppRoutes();
|
||||
@@ -21,7 +18,6 @@ function App() {
|
||||
<ReactFlowProvider>
|
||||
<DatabaseProvider>
|
||||
<DiagramProvider>
|
||||
|
||||
<ModalProvider>
|
||||
<DatabaseHotkeysProvider>
|
||||
{appRoutes}
|
||||
|
||||
@@ -41,14 +41,13 @@ const Clipboard: React.FC<ClipboardProps> = ({ text }) => {
|
||||
}, [text])
|
||||
|
||||
return (
|
||||
<Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<Button
|
||||
size="icon"
|
||||
|
||||
variant="outline"
|
||||
className="size-8"
|
||||
className="w-7 h-7 text-muted-foreground bg-card/50 backdrop-blur-xs dark:backdrop-blur-md shadow-lg"
|
||||
onClick={copyToClipboard}
|
||||
>
|
||||
{
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Spinner } from "./ui/shadcn-io/spinner";
|
||||
|
||||
interface CodeEditorProps {
|
||||
defaultValue?: string;
|
||||
value?: string;
|
||||
className?: string | undefined,
|
||||
readOnly?: boolean,
|
||||
onChange?: (sql: string) => void
|
||||
}
|
||||
|
||||
const CodeEditor: React.FC<CodeEditorProps> = ({
|
||||
defaultValue,
|
||||
value,
|
||||
className,
|
||||
readOnly = false,
|
||||
onChange
|
||||
}) => {
|
||||
|
||||
|
||||
const [editor, setEditor] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
let mounted = true;
|
||||
|
||||
Promise.all([
|
||||
import("@uiw/react-codemirror"),
|
||||
import("@codemirror/lang-sql"),
|
||||
import("@codemirror/view"),
|
||||
]).then(([cm, sqlLang, view]) => {
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
const overrideDarkTheme = view.EditorView.theme({
|
||||
|
||||
'.cm-content': {
|
||||
backgroundColor: "#1c2025",
|
||||
},
|
||||
".cm-gutter": {
|
||||
backgroundColor: "#1c2025",
|
||||
},
|
||||
|
||||
".cm-gutterElement": {
|
||||
color: "#4b515a"
|
||||
},
|
||||
|
||||
".ͼp": {
|
||||
color: "#A994FF"
|
||||
},
|
||||
|
||||
".cm-line .ͼq": {
|
||||
color: "#ff6363"
|
||||
},
|
||||
".ͼu": {
|
||||
color: "#B6E672"
|
||||
},
|
||||
".ͼv": {
|
||||
color: "#6cdcc4"
|
||||
}
|
||||
}, { dark: true });
|
||||
|
||||
const overrideLightTheme = view.EditorView.theme({
|
||||
|
||||
".ͼb": {
|
||||
color: "#2A1D66"
|
||||
},
|
||||
".cm-gutterElement": {
|
||||
color: "#62748e"
|
||||
},
|
||||
".cm-gutter": {
|
||||
backgroundColor: "white",
|
||||
|
||||
},
|
||||
".cm-gutters": {
|
||||
borderColor: "#f2f4f6"
|
||||
},
|
||||
".cm-line": {
|
||||
color: "#0f172b"
|
||||
},
|
||||
|
||||
})
|
||||
setEditor({
|
||||
CodeMirror: cm.default,
|
||||
oneDark: cm.oneDark,
|
||||
sql: sqlLang.sql,
|
||||
overrideDarkTheme,
|
||||
overrideLightTheme,
|
||||
});
|
||||
});
|
||||
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
if (!editor) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex flex-1 w-full min-h-9 rounded-sm border border-border bg-card items-center justify-center",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<Spinner className="text-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { CodeMirror, oneDark, sql, overrideDarkTheme, overrideLightTheme } = editor;
|
||||
return (
|
||||
<CodeMirror
|
||||
defaultValue={defaultValue}
|
||||
value={value}
|
||||
className={cn("flex flex-1 w-full min-h-9 rounded-sm h-full bg-card border-1 border-border !min-w-0 overflow-hidden", className)}
|
||||
extensions={[sql()]}
|
||||
readOnly={readOnly}
|
||||
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
onChange={onChange}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
export default React.memo(CodeEditor);
|
||||
@@ -16,11 +16,11 @@ export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
||||
const sidebarData = useSidebarData();
|
||||
const { openController } = useDiagramOps();
|
||||
return (
|
||||
<Sidebar collapsible='icon' variant='floating' {...props} className='bg-card border-r' >
|
||||
<Sidebar collapsible='icon' {...props} >
|
||||
<SidebarHeader>
|
||||
<div className='flex items-center'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<img
|
||||
className='w-8 h-8 p-[6px] rounded-md '
|
||||
className='w-8 p-[4px] rounded-md '
|
||||
src='/stackrender.png'
|
||||
/>
|
||||
<h3 className='data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground truncate font-semibold text-sm '>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
|
||||
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
|
||||
import { AppSidebar } from "@/components/layout/app-sidebar/app-sidebar";
|
||||
import { Header } from "@/components/header";
|
||||
|
||||
@@ -14,24 +13,20 @@ interface Props {
|
||||
const Dashboard: React.FC<Props> = ({ children }) => {
|
||||
return (
|
||||
<SidebarProvider defaultOpen={false}>
|
||||
<AppSidebar />
|
||||
<div
|
||||
id='content'
|
||||
className={cn(
|
||||
'ml-auto w-full max-w-full',
|
||||
'peer-data-[state=collapsed]:w-[calc(100%-var(--sidebar-width-icon)-1rem)]',
|
||||
'peer-data-[state=expanded]:w-[calc(100%-var(--sidebar-width))]',
|
||||
'sm:transition-[width] sm:duration-200 sm:ease-linear',
|
||||
'flex h-svh flex-col',
|
||||
'group-data-[scroll-locked=1]/body:h-full',
|
||||
'has-[main.fixed-main]:group-data-[scroll-locked=1]/body:h-svh'
|
||||
)}
|
||||
>
|
||||
<Header className="bg-card border-b pl-3"/>
|
||||
{
|
||||
children ? children : <Outlet />
|
||||
}
|
||||
</div>
|
||||
<AppSidebar variant="inset" />
|
||||
{/* dark:p-0 dark:border-l-1 */}
|
||||
<SidebarInset className=" overflow-hidden max-h-screen flex flex-col flex-1 !m-0 !rounded-none bg-sidebar p-1 ">
|
||||
{/*dark:border-none dark:rounded-none */}
|
||||
<div className="border-4 border-sidebar-border ring-1 ring-border/30 dark:!border-background dark:ring-sidebar-border flex flex-col min-h-0 !overflow-hidden rounded-2xl h-screen">
|
||||
<div className="overflow-hidden h-full flex flex-col flex-1 bg-white dark:bg-background dark:!border-1 dark:border-sidebar-border dark:rounded-[14px] !overflow-hidden" >
|
||||
<Header className="bg-card border-b pl-3" />
|
||||
{
|
||||
children ? children : <Outlet />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</SidebarInset>
|
||||
|
||||
</SidebarProvider>
|
||||
)
|
||||
|
||||
@@ -48,8 +48,8 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
>
|
||||
<svg
|
||||
fill="transparent"
|
||||
className={selected ? "stroke-ring dark:!stroke-primary-foreground" : "stroke-ring/60 dark:!stroke-muted-foreground/60"}
|
||||
strokeWidth="4"
|
||||
className={selected ? ' !stroke-[oklch(0.60_0.028_263.3984)] dark:!stroke-primary-foreground' : 'stroke-[oklch(0.72_0.022_263.3984)] dark:!stroke-muted-foreground'}
|
||||
strokeWidth="6"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 100 100">
|
||||
@@ -74,24 +74,24 @@ const CardinalityMarker: React.FC<CardinalityMarkerProps> = ({ selected = false,
|
||||
<circle
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="8"
|
||||
r="6"
|
||||
strokeWidth="1"
|
||||
className={
|
||||
cn(" fill-background",
|
||||
selected ? " stroke-ring fill-background dark:!stroke-primary-foreground" :
|
||||
" stroke-ring/60 dark:!stroke-muted-foreground "
|
||||
cn(" fill-background ",
|
||||
selected ? " stroke-[oklch(0.60_0.028_263.3984)] fill-background dark:!stroke-primary-foreground" :
|
||||
" stroke-[oklch(0.72_0.022_263.3984)] dark:!stroke-muted-foreground "
|
||||
)
|
||||
}
|
||||
/>
|
||||
<text
|
||||
x="12"
|
||||
y="13"
|
||||
y="12.5"
|
||||
textAnchor="middle"
|
||||
dominantBaseline="middle"
|
||||
fontSize="8"
|
||||
fontSize="6"
|
||||
className={
|
||||
cn("fill-ring/60 font-semibold dark:!fill-muted-foreground" ,
|
||||
selected ? "fill-ring dark:!fill-primary-foreground" : "" ,
|
||||
cn("fill-[oklch(0.72_0.022_263.3984)] font-semibold dark:!fill-muted-foreground" ,
|
||||
selected ? "fill-[oklch(0.60_0.028_263.3984)] dark:!fill-primary-foreground" : "" ,
|
||||
)
|
||||
}
|
||||
>
|
||||
|
||||
@@ -9,7 +9,7 @@ import { useDatabaseHistory } from "@/providers/database-history/database-histor
|
||||
import { useOnViewportChange, useReactFlow } from "@xyflow/react";
|
||||
|
||||
import { LayoutGrid, Redo, Scan, Undo, ZoomIn, ZoomOut } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
interface DbControlButtons {
|
||||
@@ -58,8 +58,8 @@ const DatabaseControlButtons: React.FC<DbControlButtons> = ({ adjustPositions })
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex !rounded-md p-2 border-1 !overflow-hidden !bg-background/20 dark:!bg-card/20 text-muted-foreground shadow-md backdrop-blur-sm"
|
||||
>
|
||||
<div className="flex !rounded-md p-2 border-1 !overflow-hidden !bg-input/50 dark:!bg-card/50 text-muted-foreground shadow-md backdrop-blur-xs dark:backdrop-blur-md">
|
||||
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
|
||||
@@ -52,6 +52,7 @@ import { Modals } from "@/providers/modal-provider/modal-contxet";
|
||||
import { useModal } from "@/providers/modal-provider/modal-provider";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { cn } from "@/lib/utils";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
|
||||
|
||||
|
||||
@@ -72,7 +73,7 @@ const DatabaseDiagram: React.FC = () => {
|
||||
|
||||
// Diagram-related state (e.g. connection in progress)
|
||||
const { setIsConnectionInProgress, cardinalityStyle, showController, openController } = useDiagramOps();
|
||||
|
||||
const raise = useToast() ;
|
||||
// Destructure tables and relationships from database
|
||||
const { tables, relationships } = database || { tables: [], relationships: [] };
|
||||
|
||||
@@ -145,13 +146,11 @@ const DatabaseDiagram: React.FC = () => {
|
||||
} else {
|
||||
|
||||
|
||||
toast(t("db_controller.invalid_relationship.title"), {
|
||||
description: t("db_controller.invalid_relationship.description"),
|
||||
classNames: {
|
||||
description: "!text-destructive",
|
||||
title: "!text-destructive"
|
||||
},
|
||||
})
|
||||
raise(
|
||||
t("db_controller.invalid_relationship.title"),
|
||||
t("db_controller.invalid_relationship.description"),
|
||||
"ERROR"
|
||||
)
|
||||
}
|
||||
|
||||
setIsConnectionInProgress(false);
|
||||
@@ -266,17 +265,18 @@ const DatabaseDiagram: React.FC = () => {
|
||||
</Controls >
|
||||
<MiniMap
|
||||
nodeStrokeWidth={4}
|
||||
className="!bg-background border-1 rounded-lg overflow-hidden "
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#e2e8f0"}
|
||||
maskColor={resolvedTheme == "dark" ? "#21262d77" : "#62748e05"}
|
||||
className="!bg-input/50 ring-1 ring-border shadow-sm rounded-md overflow-hidden shadow-md backdrop-blur-xs dark:!bg-background/50 "
|
||||
maskStrokeColor={resolvedTheme == "dark" ? "#FFFFFF1A" : "#cad5e2"}
|
||||
maskColor={resolvedTheme == "dark" ? "#21262d88" : "#f1f5f9cc"}
|
||||
maskStrokeWidth={1}
|
||||
nodeClassName={"!fill-muted-foreground/20 "}
|
||||
|
||||
nodeClassName={"!fill-foreground/20 "}
|
||||
style={{
|
||||
width: 164,
|
||||
width: 148,
|
||||
height: 128
|
||||
}}
|
||||
/>
|
||||
<Background color="#62748e" className="dark:!bg-background " />
|
||||
<Background color="#62748e" className="!bg-background dark:!bg-background" />
|
||||
|
||||
</ReactFlow>
|
||||
<div
|
||||
|
||||
@@ -52,7 +52,7 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
||||
deleteRelationship(id)
|
||||
}, [])
|
||||
return (
|
||||
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
|
||||
<div className="flex flex-col gap-2 w-full h-full items-center pt-12 ">
|
||||
<AlertTriangle
|
||||
className="size-12 text-destructive"
|
||||
/>
|
||||
@@ -63,14 +63,14 @@ const CircularDependencyAlert: React.FC<CircularDependencyAlertProps> = ({ error
|
||||
{t("db_controller.circular_dependency.description")} , <span className="font-medium text-foreground"> {t("db_controller.circular_dependency.suggestion")} </span>
|
||||
</p>
|
||||
|
||||
<ul aria-label="Relationships" className="w-[80%] max-w-[360px]"
|
||||
<ul aria-label="Relationships" className="w-[80%] max-w-[360px] mt-4"
|
||||
>
|
||||
{
|
||||
circularRelationships.map((relationship: RelationshipType) => (
|
||||
<li
|
||||
onClick={() => focus(relationship.id)}
|
||||
key={relationship.id}>
|
||||
<div className="hover:bg-secondary flex items-center justify-between h-10 mb-2 px-3 rounded-md cursor-pointer">
|
||||
<div className="hover:bg-secondary flex items-center justify-between h-10 mb-2 px-3 rounded-md cursor-pointer bg-card border-1">
|
||||
<span className="text-sm">
|
||||
{relationship.sourceTable.name} -> {relationship.targetTable.name}
|
||||
</span>
|
||||
|
||||
@@ -1,33 +1,35 @@
|
||||
import { useRenderSql } from "@/hooks/user-render-sql";
|
||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
||||
import React, { useEffect, useMemo } from "react";
|
||||
|
||||
import CodeMirror from '@uiw/react-codemirror';
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { oneDark } from '@codemirror/theme-one-dark';
|
||||
|
||||
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import React, { useEffect, useMemo, useState } from "react";
|
||||
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import CircularDependencyAlert from "./circular-dependecy-alert";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Clipboard from "@/components/clipboard";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
|
||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import BaseDatabaseRenderer from "@/utils/render/database/base-database-renderer";
|
||||
import { CircularDependencyError, getRenderer } from "@/utils/render/render-uttils";
|
||||
import { areArraysEqual } from "@/utils/utils";
|
||||
import CodeEditor from "@/components/code-editor";
|
||||
import useToast from "@/hooks/use-toast";
|
||||
|
||||
|
||||
interface SqlPreviewProps {
|
||||
tableFilterIds?: string[]
|
||||
tableFilterIds?: string[] ,
|
||||
className? : string ;
|
||||
}
|
||||
|
||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds , className}) => {
|
||||
|
||||
|
||||
|
||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
|
||||
const { database: currentDatabase } = useDatabase();
|
||||
const { data_types } = useDatabaseOperations();
|
||||
const [sqlCode, setSqlCode] = useState<string>("");
|
||||
const raise = useToast();
|
||||
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined);
|
||||
|
||||
const database = useMemo(() => {
|
||||
if (!tableFilterIds)
|
||||
return currentDatabase;
|
||||
@@ -39,44 +41,68 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||
),
|
||||
} as DatabaseType;
|
||||
}, [currentDatabase, tableFilterIds])
|
||||
|
||||
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
|
||||
const { theme } = useTheme();
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
|
||||
if (database?.dialect && data_types.length > 0) {
|
||||
|
||||
try {
|
||||
const renderer: BaseDatabaseRenderer = getRenderer(database.dialect, data_types);
|
||||
const sql: string = await renderer.renderDDL(database)
|
||||
|
||||
setSqlCode(sql);
|
||||
setCircularDependency(undefined);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
if ((error as CircularDependencyError)?.cycle)
|
||||
setCircularDependency((previousError) => {
|
||||
if (!previousError)
|
||||
return error as CircularDependencyError;
|
||||
else if (Array.isArray(previousError.cycle) && Array.isArray((error as CircularDependencyError).cycle) && !(areArraysEqual(previousError.cycle, (error as CircularDependencyError).cycle)))
|
||||
|
||||
return error as CircularDependencyError;
|
||||
return previousError;
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
})()
|
||||
}, [database, data_types]);
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
if (circularDependency)
|
||||
toast(t("db_controller.circular_dependency.title"), {
|
||||
description: t("db_controller.circular_dependency.description"),
|
||||
classNames: {
|
||||
description: "!text-destructive",
|
||||
title: "!text-destructive"
|
||||
},
|
||||
});
|
||||
raise(
|
||||
t("db_controller.circular_dependency.title"),
|
||||
t("db_controller.circular_dependency.description"),
|
||||
"ERROR"
|
||||
);
|
||||
|
||||
}, [circularDependency]);
|
||||
|
||||
|
||||
if (circularDependency)
|
||||
return <CircularDependencyAlert error={circularDependency} />
|
||||
|
||||
else
|
||||
return (
|
||||
<div className="flex w-full h-full relative ">
|
||||
<div className="absolute right-[12px] top-[4px] z-[1] ">
|
||||
<div className="flex w-full h-full relative min-w-0 !min-h-0 ">
|
||||
<div className="absolute right-2 top-2 z-1">
|
||||
<Clipboard
|
||||
text={sqlCode}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CodeMirror
|
||||
<CodeEditor
|
||||
defaultValue={sqlCode}
|
||||
value={sqlCode}
|
||||
className="flex flex-1 w-full "
|
||||
extensions={[sql()]}
|
||||
readOnly
|
||||
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
|
||||
className={ className}
|
||||
/>
|
||||
|
||||
</div>
|
||||
)
|
||||
|
||||
|
||||
@@ -206,9 +206,10 @@ const TablesController: React.FC = ({ }) => {
|
||||
}
|
||||
{
|
||||
allTables.length > 0 && showSqlPreview &&
|
||||
<div className=" flex-1 overflow-auto -ml-3">
|
||||
<div className=" flex-1 overflow-auto ">
|
||||
<SqlPreview
|
||||
tableFilterIds={tableFilterIds}
|
||||
className="rounded-none border-0 border-t-1"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -17,7 +17,8 @@ interface Props {
|
||||
field: FieldType,
|
||||
showHandles?: boolean,
|
||||
highlight?: boolean,
|
||||
color?: string
|
||||
color?: string,
|
||||
className?: string
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +29,7 @@ export const TARGET_PREFIX = "target_";
|
||||
|
||||
const Field: React.FC<Props> = (props) => {
|
||||
|
||||
const { field, showHandles, highlight, color } = props;
|
||||
const { field, showHandles, highlight, color, className } = props;
|
||||
const [editMode, setEditMode] = useState<boolean>(false);
|
||||
const { deleteField, editField } = useDatabaseOperations();
|
||||
const [fieldName, setFieldName] = useState<string>(field.name);
|
||||
@@ -39,14 +40,17 @@ const Field: React.FC<Props> = (props) => {
|
||||
}, [field.name]);
|
||||
|
||||
const removeField = useCallback(() => {
|
||||
|
||||
deleteField(field.id)
|
||||
}, [])
|
||||
}, [field.id])
|
||||
|
||||
const saveFieldName = useCallback(() => {
|
||||
|
||||
editField({
|
||||
id: field.id,
|
||||
name: fieldName
|
||||
} as FieldType);
|
||||
|
||||
setEditMode(false);
|
||||
}, [fieldName]);
|
||||
|
||||
@@ -54,16 +58,17 @@ const Field: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className={cn(
|
||||
"group relative flex h-8 items-center justify-between gap-1 px-1.5 text-sm hover:bg-secondary transition-all duration-200 ease-in-out ",
|
||||
highlight ? "bg-secondary" : ""
|
||||
highlight ? "bg-secondary" : "",
|
||||
className
|
||||
)}>
|
||||
<div className="text-muted-foreground flex items-center truncate gap-1.5">
|
||||
<div className="text-muted-foreground flex items-center truncate w-full gap-1.5 min-w-0 ">
|
||||
{
|
||||
field.isPrimary &&
|
||||
<IconKey className="size-3" />
|
||||
}
|
||||
{
|
||||
field.nullable && !field.isPrimary &&
|
||||
<IconKeyframe className="size-3" />
|
||||
<IconKeyframe className="size-3 stroke-3" />
|
||||
}
|
||||
{
|
||||
!field.nullable && !field.isPrimary &&
|
||||
@@ -72,7 +77,7 @@ const Field: React.FC<Props> = (props) => {
|
||||
{
|
||||
!editMode ?
|
||||
<label
|
||||
className={"truncate flex gap-1 text-xs text-foreground "}
|
||||
className={"truncate flex gap-1 text-xs text-foreground font-medium "}
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
>
|
||||
{fieldName}
|
||||
@@ -98,7 +103,7 @@ const Field: React.FC<Props> = (props) => {
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
onChange={(e) => setFieldName(e.target.value)}
|
||||
className="h-6 font-bold pb-1.5 rounded-sm px-1"
|
||||
onKeyDown={(e: any) => {
|
||||
onKeyDown={(e: any) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
saveFieldName();
|
||||
@@ -106,15 +111,15 @@ const Field: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
}
|
||||
</div>
|
||||
{
|
||||
!editMode ?
|
||||
<div className="text-xs text-muted-foreground flex shrink-0 ">
|
||||
<div className="group-hover:opacity-0">
|
||||
<div className={cn(" !text-muted-foreground font-medium group-hover:opacity-0")}>
|
||||
{field.type?.name?.split(' ')[0]}
|
||||
</div>
|
||||
|
||||
<div className="flex gap-1 opacity-0 shrink-0 flex-row group-hover:opacity-100 transition-opacity duration-200 absolute right-1 top-1 ">
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
|
||||
<IconPencil className="size-3 text-muted-foreground " />
|
||||
@@ -123,6 +128,7 @@ const Field: React.FC<Props> = (props) => {
|
||||
<IconTrash className="size-3 text-destructive " />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
:
|
||||
<div className="text-xs text-muted-foreground flex ml-1">
|
||||
|
||||
@@ -80,7 +80,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
|
||||
sourceY,
|
||||
targetX: targetSide === 'left' ? targetLeftX : targetRightX,
|
||||
targetY,
|
||||
borderRadius: 6,
|
||||
borderRadius: 8,
|
||||
sourcePosition:
|
||||
sourceSide === 'left' ? Position.Left : Position.Right,
|
||||
targetPosition:
|
||||
@@ -125,7 +125,7 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
|
||||
markerEnd={`url(#${endMarker})`}
|
||||
fill="none"
|
||||
className={cn([
|
||||
`!stroke-1 ${selected ? '!stroke-ring dark:!stroke-primary-foreground' : 'stroke-ring/60 dark:!stroke-muted-foreground'}`,
|
||||
`!stroke-[1.5px] ${selected ? ' !stroke-[oklch(0.60_0.028_263.3984)] dark:!stroke-primary-foreground' : 'stroke-[oklch(0.72_0.022_263.3984)] dark:!stroke-muted-foreground'}`,
|
||||
])}
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
@@ -143,7 +143,6 @@ const Relationship: React.FC<EdgeProps<RelationshipProps>> = (props) => {
|
||||
fill="none"
|
||||
strokeOpacity={0}
|
||||
strokeWidth={16}
|
||||
|
||||
className="react-flow__edge-interaction"
|
||||
onClick={(e) => {
|
||||
if (e.detail === 2) {
|
||||
|
||||
@@ -56,7 +56,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
|
||||
|
||||
const fields: React.ReactNode[] = useMemo(() => {
|
||||
return table.fields.map((field: FieldType) => {
|
||||
return table.fields.map((field: FieldType , index : number) => {
|
||||
|
||||
const highlight: boolean = highlightedEdges.find((edge: any) =>
|
||||
(edge.data?.relationship as RelationshipType).sourceFieldId == field.id ||
|
||||
@@ -68,6 +68,7 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
showHandles={selected}
|
||||
highlight={highlight}
|
||||
color={table.color as string}
|
||||
className={ index == table.fields.length - 1 ? "!rounded-b-md" : undefined }
|
||||
/>)
|
||||
})
|
||||
}, [table.fields, selected, highlightedEdges]);
|
||||
@@ -78,11 +79,11 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Card
|
||||
<Card
|
||||
className={cn(
|
||||
"rounded-[12px] p-0.5 gap-0 transition-all duration-200",
|
||||
"rounded-lg p-0.5 gap-0 transition-all duration-200 border-none ring-1 ring-slate-300 shadow-xs dark:ring-border ",
|
||||
overlapping
|
||||
? 'ring-1 ring-destructive scale-105 shadow-danger '
|
||||
? 'ring-1 !ring-destructive scale-105 shadow-danger '
|
||||
: '',
|
||||
!pulsing && overlapping
|
||||
? 'scale-105'
|
||||
@@ -91,23 +92,23 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
? 'scale-110'
|
||||
: '',
|
||||
|
||||
selected && !overlapping ? "ring-1 ring-primary" : ""
|
||||
selected && !overlapping ? "ring-[1.5px] !ring-primary" : ""
|
||||
)}
|
||||
style={
|
||||
(selected && table.color && !overlapping) ?
|
||||
{
|
||||
boxShadow: "0 0 0 1px " + table.color,
|
||||
boxShadow: "0 0 0 1.5px " + table.color,
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
onDoubleClick={focus}
|
||||
|
||||
|
||||
>
|
||||
<CardHeader className="group rounded-t rounded-t-md p-1.5 flex items-center mb-0 bg-primary/10 "
|
||||
<CardHeader className="group rounded-t rounded-t-md p-1.5 border-1 border-primary/20 flex items-center mb-0 bg-primary/10 "
|
||||
style={
|
||||
table.color ? {
|
||||
backgroundColor: table.color + "20" as string
|
||||
backgroundColor: table.color + "20" as string,
|
||||
border: " 1px solid " + table.color + "60"
|
||||
} : undefined
|
||||
}
|
||||
>
|
||||
@@ -115,15 +116,17 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
{!editMode ? <>
|
||||
<label
|
||||
className=" w-full text-editable truncate py-0.5 text-sm font-bold text-primary"
|
||||
onDoubleClick={() => setEditMode(true)}
|
||||
onDoubleClick={ () => setEditMode(true) }
|
||||
style={{ color: table.color as string }}
|
||||
>
|
||||
{tableName}
|
||||
</label>
|
||||
<div className="flex gap-1 hidden shrink-0 flex-row group-hover:flex ">
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
|
||||
<IconPencil className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={() => setEditMode(true)}>
|
||||
<IconPencil className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
|
||||
<Button variant="outline" size="icon" className="size-6 shrink-0 shadow-sm rounded-sm" onClick={focus}>
|
||||
<IconFocus2 className="size-3 text-muted-foreground " />
|
||||
</Button>
|
||||
@@ -159,14 +162,14 @@ const Table: React.FC<NodeProps<TableProps>> = (props) => {
|
||||
|
||||
}
|
||||
</CardHeader>
|
||||
<CardContent className="p-0 mt-0">
|
||||
<CardContent className="p-0 mt-0 rounded-b-md ">
|
||||
{
|
||||
!showMore ? fields.slice(0, MAX_FIELDS) : fields
|
||||
}
|
||||
|
||||
{fields.length > MAX_FIELDS && (
|
||||
<div
|
||||
className="flex h-8 cursor-pointer items-center gap-1 justify-center text-xs transition-colors duration-200 "
|
||||
className="flex h-8 cursor-pointer items-center gap-1 justify-center text-xs transition-colors duration-200 "
|
||||
onClick={toggleShowMore}
|
||||
>
|
||||
{showMore ? (
|
||||
|
||||
@@ -5,25 +5,28 @@ import Modal, { ModalProps } from "@/components/modal";
|
||||
import React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import SqlPreview from "../components/db-controller/sql-preview";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
|
||||
const ExportSqlModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.export_sql")}
|
||||
className="w-full lg:min-w-[960px]"
|
||||
className="w-full lg:min-w-[960px] "
|
||||
description={t("modals.export_sql_header")}
|
||||
>
|
||||
<div className="h-full max-h-[72vh] min-w-0">
|
||||
<Separator />
|
||||
<div className="h-full min-h-[65vh] max-h-[65vh] min-w-0 !bg-background p-1 border-1 rounded-md shadow-xs" >
|
||||
<SqlPreview />
|
||||
</div>
|
||||
<Separator />
|
||||
|
||||
</Modal>
|
||||
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
@@ -1,18 +1,13 @@
|
||||
import Modal, { ModalProps } from "@/components/modal"
|
||||
import ReactCodeMirror, { oneDark } from "@uiw/react-codemirror";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react"
|
||||
import { useTranslation } from "react-i18next"
|
||||
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||
import { sql } from '@codemirror/lang-sql';
|
||||
import { DatabaseDialect, ImportDatabaseMethod, ImportDatabaseOption, ImportMethodType, MARIADB_DUMP_EXAMPLE, MARIADB_DUMP_INSTRUCTIONS, MYSQL_DUMP_EXAMPLE, MYSQL_DUMP_INSTRUCTIONS, PG_DUMP_EXAMPLE, PG_DUMP_INSTRUCTIONS, SQLITE_DUMP_EXAMPLE, SQLITE_DUMP_INSRUCTION } from "@/lib/database";
|
||||
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
|
||||
import { AlertCircleIcon, Code, HelpCircle } from "lucide-react";
|
||||
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
|
||||
import Clipboard from "@/components/clipboard";
|
||||
import { Trans } from 'react-i18next';
|
||||
import { Node, useReactFlow } from "@xyflow/react";
|
||||
import { adjustTablesPositions } from "@/utils/tables";
|
||||
import { adjustTablesPositions, getTableNextSequence } from "@/utils/tables";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||
import { Label } from "@/components/ui/label";
|
||||
@@ -20,6 +15,9 @@ import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { buttonVariants } from "@/components/ui/button";
|
||||
import { BaseSqlImporter } from "@/utils/import/base-sql-importer";
|
||||
import { getImporter } from "@/utils/import/import-utils";
|
||||
import CodeEditor from "@/components/code-editor";
|
||||
|
||||
|
||||
const options: ImportDatabaseOption[] = [{
|
||||
@@ -107,6 +105,27 @@ const options: ImportDatabaseOption[] = [{
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}
|
||||
, {
|
||||
dialect: DatabaseDialect.MSSQL,
|
||||
docUrl: "https://stackrender.io/docs/import/mssql",
|
||||
methods: [{
|
||||
id: "ssms",
|
||||
name: "SSMS",
|
||||
logo: "/ssms.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 5,
|
||||
}]
|
||||
}, {
|
||||
dialect: DatabaseDialect.ORACLE,
|
||||
docUrl: "https://stackrender.io/docs/import/oracle",
|
||||
methods: [{
|
||||
id: "sqldeveloper",
|
||||
name: "SQL Developer",
|
||||
logo: "/sqldeveloper.png",
|
||||
type: ImportMethodType.DB_CLIENT,
|
||||
numberOfInstructions: 6,
|
||||
}]
|
||||
}
|
||||
];
|
||||
|
||||
@@ -114,7 +133,6 @@ const options: ImportDatabaseOption[] = [{
|
||||
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
||||
const { theme: resolvedTheme } = useTheme();
|
||||
const { database, isLoading, isSwitchingDatabase } = useDatabase();
|
||||
const { data_types, importDatabase } = useDatabaseOperations();
|
||||
@@ -123,6 +141,17 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
const [error, setError] = useState<boolean>(false);
|
||||
const { fitView } = useReactFlow();
|
||||
|
||||
const [importer, setImporter] = useState<BaseSqlImporter | undefined>(undefined);
|
||||
|
||||
useEffect(() => {
|
||||
if (isLoading || data_types.length == 0 || !database?.dialect)
|
||||
return;
|
||||
|
||||
else if (data_types.length > 0) {
|
||||
setImporter(getImporter(database.dialect, data_types))
|
||||
}
|
||||
}, [isLoading, data_types]);
|
||||
|
||||
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
|
||||
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
|
||||
}, [database]);
|
||||
@@ -141,59 +170,69 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId) as ImportDatabaseMethod;
|
||||
}, [selectedMethodId, currentOption])
|
||||
|
||||
|
||||
const validateSql = useCallback((code: string) => {
|
||||
try {
|
||||
const parsedDatabase = SqlToDatabase(code, data_types, database?.dialect as DatabaseDialect);
|
||||
if (importer)
|
||||
try {
|
||||
const parsedDatabase = importer.parseSql(code);
|
||||
setParsedDatabase(parsedDatabase);
|
||||
setError(false);
|
||||
|
||||
setParsedDatabase(parsedDatabase);
|
||||
setError(false);
|
||||
|
||||
} catch (error) {
|
||||
|
||||
setError(true);
|
||||
setParsedDatabase(undefined)
|
||||
} catch (error) {
|
||||
setError(true);
|
||||
setParsedDatabase(undefined)
|
||||
}
|
||||
else {
|
||||
console.error("Importer not loaded");
|
||||
}
|
||||
setSqlCode(code)
|
||||
|
||||
}, [database?.dialect, data_types])
|
||||
|
||||
|
||||
}, [database?.dialect, data_types, importer])
|
||||
|
||||
const onImport = useCallback(async () => {
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
const nodes: Node[] = parsedDatabase.tables.map((table: TableInsertType) => ({
|
||||
id: table.id,
|
||||
data: {
|
||||
table
|
||||
|
||||
let nextTableSequence: number = getTableNextSequence(database ? database.tables : []);
|
||||
if (parsedDatabase)
|
||||
return new Promise(async (res, rej) => {
|
||||
try {
|
||||
const nodes: Node[] = parsedDatabase.tables.map((table: TableInsertType) => ({
|
||||
id: table.id,
|
||||
data: {
|
||||
table
|
||||
}
|
||||
}))
|
||||
|
||||
const adjustedTables = await adjustTablesPositions(nodes, parsedDatabase.relationships);
|
||||
|
||||
for (let index = 0; index < adjustedTables.length; index++) {
|
||||
adjustedTables[index].sequence = nextTableSequence;
|
||||
nextTableSequence += 1;
|
||||
}
|
||||
}))
|
||||
|
||||
const adjustedTables = await adjustTablesPositions(nodes, parsedDatabase.relationships);
|
||||
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indices);
|
||||
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indexes);
|
||||
|
||||
onOpenChange && onOpenChange(false);
|
||||
onOpenChange && onOpenChange(false);
|
||||
|
||||
setTimeout(() => {
|
||||
fitView({
|
||||
duration: 500
|
||||
});
|
||||
}, 300);
|
||||
setTimeout(() => {
|
||||
fitView({
|
||||
duration: 500
|
||||
});
|
||||
}, 300);
|
||||
|
||||
} catch (error) {
|
||||
} catch (error) {
|
||||
|
||||
setError(true);
|
||||
rej()
|
||||
}
|
||||
})
|
||||
}, [parsedDatabase]);
|
||||
setError(true);
|
||||
rej()
|
||||
}
|
||||
})
|
||||
}, [parsedDatabase, database]);
|
||||
|
||||
|
||||
if (isLoading || isSwitchingDatabase)
|
||||
return;
|
||||
|
||||
return (
|
||||
<Modal
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
title={t("modals.import_database.title")}
|
||||
@@ -203,7 +242,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
isDisabled={!parsedDatabase}
|
||||
>
|
||||
<div className="flex flex-col gap-4 !min-w-0 ">
|
||||
<div className="w-full space-y-2 !min-w-0 ">
|
||||
<div className="w-full space-y-2 min-w-0 ">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{t("modals.import_database.import_options")}
|
||||
</p>
|
||||
@@ -256,6 +295,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
</RadioGroup>
|
||||
|
||||
}
|
||||
|
||||
</div>
|
||||
{
|
||||
selectedImportMethod?.type == ImportMethodType.DUMP &&
|
||||
@@ -274,7 +314,7 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
<div className=" font-normal w-full">
|
||||
<span className="border-1 border-border p-1 px-2 rounded-md w-full flex items-center justify-between">
|
||||
{selectedImportMethod.instruction}
|
||||
<Clipboard text={selectedImportMethod.example} />
|
||||
<Clipboard text={selectedImportMethod.instruction} />
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -320,18 +360,17 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
}
|
||||
|
||||
</div>
|
||||
<div className="flex flex-col ">
|
||||
<ReactCodeMirror
|
||||
<div className="flex flex-1 flex-col ">
|
||||
<CodeEditor
|
||||
className={
|
||||
cn("flex w-full border-1 rounded-md border-divider overflow-hidden ",
|
||||
cn("flex w-full border-1 rounded-md border-divider overflow-hidden ",
|
||||
(error || parsedDatabase?.errors?.length > 0) ? "min-h-[360px] max-h-[360px]" : "min-h-[424px] max-h-[424px]"
|
||||
)
|
||||
}
|
||||
extensions={[sql()]}
|
||||
value={sqlCode}
|
||||
onChange={validateSql}
|
||||
theme={resolvedTheme == "light" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||
/>
|
||||
|
||||
<div className="mt-4">
|
||||
{
|
||||
error &&
|
||||
@@ -352,13 +391,13 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
||||
(parsedDatabase?.errors && parsedDatabase?.errors?.length > 0) &&
|
||||
<Alert
|
||||
variant={"default"}
|
||||
className="text-chart-5 dark:text-chart-3"
|
||||
className="text-chart-4"
|
||||
>
|
||||
|
||||
<AlertTitle>
|
||||
{t("modals.import_database.import_warning")}
|
||||
</AlertTitle>
|
||||
<AlertDescription className="text-chart-5 dark:text-chart-3">
|
||||
<AlertDescription className="text-chart-4 ">
|
||||
{t("modals.import_database.import_warning_description")}
|
||||
</AlertDescription>
|
||||
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
|
||||
import { IconCircleCheck } from "@tabler/icons-react";
|
||||
import { CircleAlert, OctagonX } from "lucide-react";
|
||||
import { useCallback } from "react";
|
||||
import { toast } from "sonner";
|
||||
export type ToastVariant = "INFO" | "ERROR" | "SUCCESS";
|
||||
|
||||
export interface ToastAction {
|
||||
title: string,
|
||||
actionHandler: () => void;
|
||||
}
|
||||
|
||||
const useToast = () => {
|
||||
|
||||
const raise = useCallback((title: string, description: React.ReactNode, variant: ToastVariant = "INFO", action?: ToastAction) => {
|
||||
|
||||
toast(title, {
|
||||
description,
|
||||
...toastStyles[variant],
|
||||
action: action ? {
|
||||
label: action.title,
|
||||
onClick: action.actionHandler,
|
||||
} : undefined,
|
||||
|
||||
});
|
||||
}, []);
|
||||
|
||||
return raise;
|
||||
}
|
||||
|
||||
|
||||
|
||||
const toastStyles = {
|
||||
SUCCESS: {
|
||||
classNames: {
|
||||
description: "!text-chart-2"
|
||||
},
|
||||
|
||||
style: {
|
||||
'--normal-bg':
|
||||
'color-mix(in oklab, light-dark(var(--color-chart-2), var(--color-chart-2)) 10%, var(--background))',
|
||||
'--normal-text': 'light-dark(var(--color-chart-2), var(--color-chart-2))',
|
||||
'--normal-border': 'light-dark(var(--color-chart-2), var(--color-chart-2))'
|
||||
} as React.CSSProperties,
|
||||
icon: <IconCircleCheck className="size-5" />
|
||||
},
|
||||
INFO: {
|
||||
classNames: {
|
||||
description: "!text-primary"
|
||||
},
|
||||
|
||||
style: {
|
||||
'--normal-bg':
|
||||
'color-mix(in oklab, light-dark(var(--color-primary), var(--primary)) 10%, var(--background))',
|
||||
'--normal-text': 'light-dark(var(--color-primary), var(--primary))',
|
||||
'--normal-border': 'light-dark(var(--color-primary), var(--primary))'
|
||||
} as React.CSSProperties,
|
||||
icon: <CircleAlert className="size-4" />
|
||||
|
||||
},
|
||||
ERROR: {
|
||||
classNames: {
|
||||
description: "!text-destructive"
|
||||
},
|
||||
style: {
|
||||
'--normal-bg': 'color-mix(in oklab, var(--destructive) 10%, var(--background))',
|
||||
'--normal-text': 'var(--destructive)',
|
||||
'--normal-border': 'var(--destructive)'
|
||||
} as React.CSSProperties ,
|
||||
icon : <OctagonX className="size-4" />
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export default useToast;
|
||||
@@ -1,62 +0,0 @@
|
||||
|
||||
|
||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||
import { DatabaseToAst } from "@/utils/render/parsers/database_to_ast";
|
||||
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 { CircularDependencyError, fixCharsetPlacement, fixSQLiteColumnOrder } from "@/utils/render/render-uttils";
|
||||
import { areArraysEqual } from "@/utils/utils";
|
||||
import { decomposeManyToMany } from "@/utils/relationship";
|
||||
|
||||
const parser = new Parser();
|
||||
|
||||
|
||||
export const useRenderSql = (database: DatabaseType) => {
|
||||
const [sql, setSql] = useState<string>("");
|
||||
const { data_types } = useDatabaseOperations();
|
||||
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined) ;
|
||||
|
||||
useEffect(() => {
|
||||
try {
|
||||
|
||||
const decomposedDatabase = decomposeManyToMany(database) ;
|
||||
|
||||
const dbAst: any = DatabaseToAst(decomposedDatabase, data_types);
|
||||
|
||||
let sql: string = parser.sqlify(dbAst, {
|
||||
database: getDatabaseByDialect(database.dialect).name
|
||||
});
|
||||
|
||||
if (database.dialect != DatabaseDialect.SQLITE)
|
||||
sql = fixCharsetPlacement(format(sql, { language: "sql" }));
|
||||
else
|
||||
sql = fixSQLiteColumnOrder(format(sql, { language: "sql" }));
|
||||
|
||||
const formattedSqlCode = format(sql, { language: 'sql' });
|
||||
|
||||
setSql(
|
||||
(formattedSqlCode)
|
||||
);
|
||||
setCircularDependency(undefined);
|
||||
} catch (error) {
|
||||
|
||||
if ((error as CircularDependencyError)?.cycle)
|
||||
setCircularDependency((previousError) => {
|
||||
if (!previousError)
|
||||
return error as CircularDependencyError;
|
||||
else if (Array.isArray(previousError.cycle) && Array.isArray((error as CircularDependencyError).cycle) && !(areArraysEqual(previousError.cycle, (error as CircularDependencyError).cycle)))
|
||||
|
||||
return error as CircularDependencyError;
|
||||
return previousError;
|
||||
})
|
||||
|
||||
}
|
||||
}, [database , data_types]);
|
||||
|
||||
return { sql, circularDependency };
|
||||
|
||||
}
|
||||
+17
-2
@@ -6,7 +6,7 @@ export const ar = {
|
||||
tables: "الجداول",
|
||||
relationships: "العلاقات",
|
||||
database: "قاعدة البيانات",
|
||||
documentation : "الوثائق"
|
||||
documentation: "الوثائق"
|
||||
},
|
||||
color_picker: {
|
||||
default_color: "اللون الافتراضي"
|
||||
@@ -196,7 +196,7 @@ export const ar = {
|
||||
import_database: {
|
||||
title: "استيراد قاعدة البيانات",
|
||||
import: "استيراد",
|
||||
view_docs : "عرض الوثائق" ,
|
||||
view_docs: "عرض الوثائق",
|
||||
import_options: "هل ترغب في الاستيراد باستخدام:",
|
||||
import_error: "خطأ في تحليل SQL",
|
||||
import_error_description: "تعذر استيراد SQL الخاص بك بسبب وجود صيغة غير صحيحة.",
|
||||
@@ -259,6 +259,21 @@ export const ar = {
|
||||
step3: "من القائمة، اختر <bold>File > Export > Database to SQL file</bold>.",
|
||||
step4: "في النافذة، اختر <bold>Export schema only</bold> ثم انقر <bold>Save</bold>.",
|
||||
step5: "أخيرًا، انسخ محتوى ملف <code>.sql</code> إلى محرر الكود أدناه."
|
||||
},
|
||||
ssms: {
|
||||
"step1": "افتح SQL Server Management Studio (SSMS).",
|
||||
"step2": "انقر بزر الفأرة الأيمن على قاعدة البيانات، ثم اختر Tasks → Generate Scripts من القائمة.",
|
||||
"step3": "في خطوة Choose Objects، اختر Choose specific database objects ثم قم بتحديد جميع الجداول.",
|
||||
"step4": "في خطوة Set Scripting Options، اختر Save to file وحدد مكان حفظ ملف .sql.",
|
||||
"step5": "أكمل المعالج، ثم افتح ملف .sql الذي تم إنشاؤه وانسخ محتواه إلى محرر الكود أدناه."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "افتح Oracle SQL Developer واتصل بقاعدة البيانات الخاصة بك.",
|
||||
"step2": "من القائمة العلوية، اذهب إلى Tools → Database Export.",
|
||||
"step3": "في نافذة التصدير، اختر الاتصال الخاص بك وتأكد من تفعيل Pretty Print و Terminator فقط، وإلغاء تحديد Export Data لضمان تصدير المخطط فقط.",
|
||||
"step4": "اختر مكان حفظ ملف التصدير ثم تابع إلى الخطوة التالية.",
|
||||
"step5": "تحت Standard Object Types، اختر فقط Tables و Indexes و Constraints و Referential Constraints، واترك باقي الخيارات بدون تحديد.",
|
||||
"step6": "أكمل المعالج، ثم افتح ملف .sql الناتج وانسخ محتواه إلى محرر الكود أدناه."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+17
-2
@@ -6,7 +6,7 @@ export const de = {
|
||||
tables: "Tabellen",
|
||||
relationships: "Beziehungen",
|
||||
database: "Datenbank",
|
||||
documentation : "Dokumentation"
|
||||
documentation: "Dokumentation"
|
||||
},
|
||||
color_picker: {
|
||||
default_color: "Standardfarbe"
|
||||
@@ -196,7 +196,7 @@ export const de = {
|
||||
import_database: {
|
||||
title: "Datenbank importieren",
|
||||
import: "Importieren",
|
||||
view_docs : "Dokumentation anzeigen" ,
|
||||
view_docs: "Dokumentation anzeigen",
|
||||
import_options: "Möchtest du importieren mit:",
|
||||
import_error: "SQL-Parsing-Fehler",
|
||||
import_error_description: "Wir konnten dein SQL nicht importieren, da es ungültige Syntax enthält.",
|
||||
@@ -259,6 +259,21 @@ export const de = {
|
||||
step3: "Gehe zu <bold>File > Export > Database to SQL file</bold>.",
|
||||
step4: "Wähle im Dialog <bold>Export schema only</bold> und klicke auf <bold>Save</bold>.",
|
||||
step5: "Kopiere abschließend den Inhalt der <code>.sql</code>-Datei in den Editor."
|
||||
},
|
||||
ssms: {
|
||||
"step1": "Öffnen Sie SQL Server Management Studio (SSMS).",
|
||||
"step2": "Klicken Sie mit der rechten Maustaste auf Ihre Datenbank und wählen Sie im Kontextmenü Tasks → Skripte generieren.",
|
||||
"step3": "Im Schritt Objekte auswählen wählen Sie Bestimmte Datenbankobjekte auswählen und markieren Sie alle Tabellen.",
|
||||
"step4": "Im Schritt Skripting-Optionen festlegen wählen Sie In Datei speichern und wählen Sie den Speicherort für die .sql-Datei.",
|
||||
"step5": "Schließen Sie den Assistenten ab, öffnen Sie anschließend die generierte .sql-Datei und kopieren Sie deren Inhalt in den Code-Editor unten."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Öffnen Sie Oracle SQL Developer und verbinden Sie sich mit Ihrer Datenbank.",
|
||||
"step2": "Gehen Sie im oberen Menü zu Tools → Datenbank-Export.",
|
||||
"step3": "Im Exportdialog wählen Sie Ihre Verbindung und stellen Sie sicher, dass nur Pretty Print und Terminator aktiviert sind und Export Data deaktiviert ist, um nur das Schema zu exportieren.",
|
||||
"step4": "Wählen Sie den Speicherort der Exportdatei und fahren Sie mit dem nächsten Schritt fort.",
|
||||
"step5": "Unter Standard-Objekttypen wählen Sie nur Tabellen, Indizes, Einschränkungen und referenzielle Einschränkungen aus. Lassen Sie alle anderen Optionen deaktiviert.",
|
||||
"step6": "Schließen Sie den Assistenten ab, öffnen Sie anschließend die generierte .sql-Datei und kopieren Sie deren Inhalt in den Code-Editor unten."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -277,6 +277,23 @@ export const en = {
|
||||
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
||||
|
||||
},
|
||||
|
||||
ssms: {
|
||||
"step1": "Open <bold>SQL Server Management Studio (SSMS)</bold>.",
|
||||
"step2": "Right-click your database, then select <bold>Tasks → Generate Scripts</bold> from the context menu.",
|
||||
"step3": "In the <bold>Choose Objects</bold> step, select <bold>Choose specific database objects</bold>, then check all tables.",
|
||||
"step4": "In the <bold>Set Scripting Options</bold> step, select <bold>Save to file</bold> and choose where to save the <code>.sql</code> file.",
|
||||
"step5": "Complete the wizard, then open the generated <code>.sql</code> file and copy its contents into the <bold>code editor</bold> below."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Open <bold>Oracle SQL Developer</bold> and connect to your database.",
|
||||
"step2": "From the top menu, go to <bold>Tools → Database Export</bold>.",
|
||||
"step3": "In the export dialog, select your connection, then make sure only <bold>Pretty Print</bold> and <bold>Terminator</bold> are checked, and <bold>Export Data</bold> is unchecked to ensure schema-only export.",
|
||||
"step4": "Choose where to save the export file, then continue to the next step.",
|
||||
"step5": "Under <bold>Standard Object Types</bold>, check only <bold>Tables</bold>, <bold>Indexes</bold>, <bold>Constraints</bold>, and <bold>Referential Constraints</bold>. Leave all other options unchecked.",
|
||||
"step6": "Complete the wizard, then open the generated <code>.sql</code> file and copy its contents into the <bold>code editor</bold> below."
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -273,6 +273,22 @@ export const es = {
|
||||
step5: "Finalmente, copia el contenido del archivo <code>.sql</code> en el <bold>editor de código</bold> a continuación."
|
||||
|
||||
},
|
||||
|
||||
ssms: {
|
||||
"step1": "Abra SQL Server Management Studio (SSMS).",
|
||||
"step2": "Haga clic derecho en su base de datos y luego seleccione Tareas → Generar scripts en el menú contextual.",
|
||||
"step3": "En el paso Elegir objetos, seleccione Elegir objetos específicos de la base de datos y marque todas las tablas.",
|
||||
"step4": "En el paso Establecer opciones de script, seleccione Guardar en archivo y elija dónde guardar el archivo .sql.",
|
||||
"step5": "Complete el asistente, luego abra el archivo .sql generado y copie su contenido en el editor de código a continuación."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Abra Oracle SQL Developer y conéctese a su base de datos.",
|
||||
"step2": "En el menú superior, vaya a Herramientas → Exportación de base de datos.",
|
||||
"step3": "En el diálogo de exportación, seleccione su conexión y asegúrese de que solo estén marcadas Pretty Print y Terminator, y que Export Data esté desmarcado para asegurar exportación solo del esquema.",
|
||||
"step4": "Elija dónde guardar el archivo de exportación y continúe al siguiente paso.",
|
||||
"step5": "En Tipos de objetos estándar, marque solo Tablas, Índices, Restricciones y Restricciones referenciales. Deje todo lo demás desmarcado.",
|
||||
"step6": "Complete el asistente, luego abra el archivo .sql generado y copie su contenido en el editor de código a continuación."
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -262,6 +262,21 @@ export const fr = {
|
||||
step3: "Allez à <bold>Fichier > Exporter > Base de données vers fichier SQL</bold>.",
|
||||
step4: "Dans la fenêtre, sélectionnez <bold>Exporter uniquement le schéma</bold> et cliquez sur <bold>Enregistrer</bold>.",
|
||||
step5: "Enfin, copiez le contenu du fichier <code>.sql</code> dans l’éditeur de code ci-dessous."
|
||||
},
|
||||
ssms: {
|
||||
"step1": "Ouvrez SQL Server Management Studio (SSMS).",
|
||||
"step2": "Faites un clic droit sur votre base de données, puis sélectionnez Tâches → Générer des scripts dans le menu contextuel.",
|
||||
"step3": "Dans l’étape Choisir les objets, sélectionnez Choisir des objets spécifiques de la base de données, puis cochez toutes les tables.",
|
||||
"step4": "Dans l’étape Définir les options de script, sélectionnez Enregistrer dans un fichier et choisissez l’emplacement où enregistrer le fichier .sql.",
|
||||
"step5": "Terminez l’assistant, puis ouvrez le fichier .sql généré et copiez son contenu dans l’éditeur de code ci-dessous."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Ouvrez Oracle SQL Developer et connectez-vous à votre base de données.",
|
||||
"step2": "Dans le menu supérieur, allez dans Outils → Exportation de base de données.",
|
||||
"step3": "Dans la boîte de dialogue d’exportation, sélectionnez votre connexion, puis assurez-vous que seules les options Pretty Print et Terminator sont cochées, et que Export Data est décoché pour garantir une exportation du schéma uniquement.",
|
||||
"step4": "Choisissez l’emplacement du fichier d’exportation, puis passez à l’étape suivante.",
|
||||
"step5": "Sous Types d’objets standards, cochez uniquement Tables, Index, Contraintes et Contraintes référentielles. Laissez toutes les autres options décochées.",
|
||||
"step6": "Terminez l’assistant, puis ouvrez le fichier .sql généré et copiez son contenu dans l’éditeur de code ci-dessous."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -268,6 +268,21 @@ export const hi = {
|
||||
step3: "शीर्ष मेनू से <bold>File > Export > Database to SQL file</bold> पर जाएं।",
|
||||
step4: "डायलॉग में <bold>Export schema only</bold> चुनें और <bold>Save</bold> पर क्लिक करें।",
|
||||
step5: "अंत में, <code>.sql</code> फ़ाइल की सामग्री को नीचे दिए गए <bold>code editor</bold> में कॉपी करें।"
|
||||
} ,
|
||||
ssms: {
|
||||
"step1": "SQL Server Management Studio (SSMS) खोलें।",
|
||||
"step2": "अपना डेटाबेस पर राइट-क्लिक करें, फिर संदर्भ मेनू से Tasks → Generate Scripts चुनें।",
|
||||
"step3": "Choose Objects चरण में, Choose specific database objects चुनें, फिर सभी टेबल्स को चेक करें।",
|
||||
"step4": "Set Scripting Options चरण में, Save to file चुनें और .sql फ़ाइल को सेव करने का स्थान चुनें।",
|
||||
"step5": "विज़ार्ड पूरा करें, फिर जनरेट की गई .sql फ़ाइल खोलें और उसकी सामग्री को नीचे दिए गए कोड एडिटर में कॉपी करें।"
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Oracle SQL Developer खोलें और अपने डेटाबेस से कनेक्ट करें।",
|
||||
"step2": "ऊपरी मेनू से Tools → Database Export पर जाएँ।",
|
||||
"step3": "Export डायलॉग में अपना कनेक्शन चुनें, फिर सुनिश्चित करें कि केवल Pretty Print और Terminator चुने गए हैं, और Export Data अनचेक है ताकि केवल schema export हो।",
|
||||
"step4": "एक्सपोर्ट फ़ाइल को सेव करने का स्थान चुनें, फिर अगले चरण पर जाएँ।",
|
||||
"step5": "Standard Object Types के अंतर्गत केवल Tables, Indexes, Constraints, और Referential Constraints चुनें। बाकी सभी विकल्प अनचेक रखें।",
|
||||
"step6": "विज़ार्ड पूरा करें, फिर जनरेट की गई .sql फ़ाइल खोलें और उसकी सामग्री को नीचे दिए गए कोड एडिटर में कॉपी करें।"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -269,6 +269,21 @@ export const pt = {
|
||||
step3: "Vá para <bold>Arquivo > Exportar > Banco de Dados para Arquivo SQL</bold>.",
|
||||
step4: "Na janela de diálogo, escolha <bold>Exportar apenas o esquema</bold> e clique em <bold>Salvar</bold>.",
|
||||
step5: "Finalmente, copie o conteúdo do arquivo <code>.sql</code> para o <bold>editor de código</bold> abaixo."
|
||||
} ,
|
||||
ssms: {
|
||||
"step1": "Abra o SQL Server Management Studio (SSMS).",
|
||||
"step2": "Clique com o botão direito no seu banco de dados e selecione Tarefas → Gerar Scripts no menu de contexto.",
|
||||
"step3": "Na etapa Escolher Objetos, selecione Escolher objetos específicos do banco de dados e marque todas as tabelas.",
|
||||
"step4": "Na etapa Definir opções de script, selecione Salvar em arquivo e escolha onde salvar o arquivo .sql.",
|
||||
"step5": "Conclua o assistente e depois abra o arquivo .sql gerado e copie seu conteúdo para o editor de código abaixo."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Abra o Oracle SQL Developer e conecte-se ao seu banco de dados.",
|
||||
"step2": "No menu superior, vá em Ferramentas → Exportação de Banco de Dados.",
|
||||
"step3": "Na janela de exportação, selecione sua conexão e certifique-se de que apenas Pretty Print e Terminator estejam marcados, e que Export Data esteja desmarcado para garantir exportação apenas do schema.",
|
||||
"step4": "Escolha onde salvar o arquivo de exportação e continue para a próxima etapa.",
|
||||
"step5": "Em Tipos de Objetos Padrão, marque apenas Tabelas, Índices, Restrições e Restrições referenciais. Deixe todas as outras opções desmarcadas.",
|
||||
"step6": "Conclua o assistente e depois abra o arquivo .sql gerado e copie seu conteúdo para o editor de código abaixo."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -257,6 +257,21 @@ export const ru = {
|
||||
step3: "Выберите <bold>Файл > Экспорт > База данных в SQL файл</bold>.",
|
||||
step4: "Выберите <bold>Экспортировать только схему</bold> и нажмите <bold>Сохранить</bold>.",
|
||||
step5: "Скопируйте содержимое <code>.sql</code> файла в редактор ниже."
|
||||
},
|
||||
ssms: {
|
||||
"step1": "Откройте SQL Server Management Studio (SSMS).",
|
||||
"step2": "Щёлкните правой кнопкой мыши по вашей базе данных, затем выберите Tasks → Generate Scripts в контекстном меню.",
|
||||
"step3": "В шаге Choose Objects выберите Select specific database objects и отметьте все таблицы.",
|
||||
"step4": "В шаге Set Scripting Options выберите Save to file и укажите, куда сохранить файл .sql.",
|
||||
"step5": "Завершите мастер, затем откройте созданный файл .sql и скопируйте его содержимое в редактор кода ниже."
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "Откройте Oracle SQL Developer и подключитесь к базе данных.",
|
||||
"step2": "В верхнем меню перейдите в Tools → Database Export.",
|
||||
"step3": "В окне экспорта выберите соединение и убедитесь, что отмечены только Pretty Print и Terminator, а Export Data отключён, чтобы экспортировать только схему.",
|
||||
"step4": "Выберите место сохранения файла экспорта и перейдите к следующему шагу.",
|
||||
"step5": "В разделе Standard Object Types отметьте только Tables, Indexes, Constraints и Referential Constraints. Остальные параметры оставьте отключёнными.",
|
||||
"step6": "Завершите мастер, затем откройте созданный файл .sql и скопируйте его содержимое в редактор кода ниже."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+16
-1
@@ -210,7 +210,7 @@ export const zh = {
|
||||
import_database: {
|
||||
title: "导入数据库",
|
||||
import: "导入",
|
||||
view_docs : "查看文档" ,
|
||||
view_docs: "查看文档",
|
||||
import_options: "请选择导入方式:",
|
||||
import_error: "SQL 解析错误",
|
||||
import_error_description: "我们无法导入您的 SQL,因为它包含无效语法。",
|
||||
@@ -276,6 +276,21 @@ export const zh = {
|
||||
step3: "顶部菜单选择 <bold>文件 > 导出 > 导出为 SQL 文件</bold>。",
|
||||
step4: "选择 <bold>仅导出结构</bold> 并点击 <bold>保存</bold>。",
|
||||
step5: "将 .sql 文件内容复制到下方 <bold>代码编辑器</bold> 中。"
|
||||
},
|
||||
ssms: {
|
||||
"step1": "打开 SQL Server Management Studio (SSMS)。",
|
||||
"step2": "右键单击你的数据库,然后在上下文菜单中选择 任务 → 生成脚本。",
|
||||
"step3": "在“选择对象”步骤中,选择“选择特定数据库对象”,然后勾选所有表。",
|
||||
"step4": "在“设置脚本选项”步骤中,选择“保存到文件”,并选择保存 .sql 文件的位置。",
|
||||
"step5": "完成向导后,打开生成的 .sql 文件,并将其内容复制到下面的代码编辑器中。"
|
||||
},
|
||||
sqldeveloper: {
|
||||
"step1": "打开 Oracle SQL Developer 并连接到你的数据库。",
|
||||
"step2": "从顶部菜单进入 工具 → 数据库导出。",
|
||||
"step3": "在导出对话框中,选择你的连接,并确保只勾选 Pretty Print 和 Terminator,同时取消勾选 Export Data,以确保仅导出结构。",
|
||||
"step4": "选择导出文件的保存位置,然后进入下一步。",
|
||||
"step5": "在标准对象类型下,仅勾选 表、索引、约束 和 参照约束。取消所有其他选项。",
|
||||
"step6": "完成向导后,打开生成的 .sql 文件,并将其内容复制到下面的代码编辑器中。"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+118
-165
@@ -1,72 +1,87 @@
|
||||
@import 'tailwindcss';
|
||||
@import "tailwindcss";
|
||||
@import 'tw-animate-css';
|
||||
|
||||
@custom-variant dark (&:is(.dark *));
|
||||
|
||||
:root {
|
||||
--radius: 0.625rem;
|
||||
--background: oklch(1 0 0);
|
||||
--foreground: oklch(0.129 0.042 264.695);
|
||||
--card: oklch(1 0 0);
|
||||
--card-foreground: oklch(0.129 0.042 264.695);
|
||||
--popover: oklch(1 0 0);
|
||||
--popover-foreground: oklch(0.129 0.042 264.695);
|
||||
--primary: oklch(0.606 0.25 292.717);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.968 0.007 247.896);
|
||||
--secondary-foreground: oklch(0.208 0.042 265.755);
|
||||
--muted: oklch(0.968 0.007 247.896);
|
||||
--muted-foreground: oklch(0.554 0.046 257.417);
|
||||
--accent: oklch(0.968 0.007 247.896);
|
||||
--accent-foreground: oklch(0.208 0.042 265.755);
|
||||
--destructive: oklch(0.577 0.245 27.325);
|
||||
--border: oklch(0.929 0.013 255.508);
|
||||
--input: oklch(0.929 0.013 255.508);
|
||||
--ring: oklch(0.704 0.04 256.788);
|
||||
--chart-1: oklch(0.646 0.222 41.116);
|
||||
--chart-2: oklch(0.6 0.118 184.704);
|
||||
--chart-3: oklch(0.398 0.07 227.392);
|
||||
--chart-4: oklch(0.828 0.189 84.429);
|
||||
--chart-5: oklch(0.769 0.188 70.08);
|
||||
--background: oklch(0.9848 0 0);
|
||||
--foreground: oklch(0.2496 0.0417 263.3984);
|
||||
--card: oklch(1.0000 0 0);
|
||||
--card-foreground: oklch(0.2496 0.0417 263.3984);
|
||||
--popover: oklch(1.0000 0 0);
|
||||
--popover-foreground: oklch(0.2496 0.0417 263.3984);
|
||||
--primary: oklch(0.5424 0.2454 293.0160);
|
||||
--primary-foreground: oklch(0.9843 0.0017 247.8393);
|
||||
--secondary: oklch(0.9684 0.0068 247.8951);
|
||||
--secondary-foreground: oklch(0.2079 0.0399 265.7275);
|
||||
--muted: oklch(0.9684 0.0068 247.8951);
|
||||
--muted-foreground: oklch(0.5547 0.0407 257.4404);
|
||||
--accent: oklch(0.9544 0.0226 302.5680);
|
||||
--accent-foreground: oklch(0.5424 0.2454 293.0160);
|
||||
--destructive: oklch(0.6368 0.2078 25.3259);
|
||||
--destructive-foreground: oklch(0.9843 0.0017 247.8393);
|
||||
--border: oklch(0.9290 0.0126 255.5317);
|
||||
--input: oklch(0.9290 0.0126 255.5317);
|
||||
--ring: oklch(0.5424 0.2454 293.0160);
|
||||
--chart-1: oklch(0.5424 0.2454 293.0160);
|
||||
--chart-2: oklch(0.6309 0.1013 183.4907);
|
||||
--chart-3: oklch(0.3787 0.0440 225.5393);
|
||||
--chart-4: oklch(0.8336 0.1186 88.1463);
|
||||
--chart-5: oklch(0.7834 0.1261 58.7491);
|
||||
--sidebar: oklch(0.2064 0.0388 265.5472);
|
||||
--sidebar-foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--sidebar-primary: oklch(0.5424 0.2454 293.0160);
|
||||
--sidebar-primary-foreground: oklch(1.0000 0 0);
|
||||
--sidebar-accent: oklch(0.2709 0.0592 265.1803);
|
||||
--sidebar-accent-foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--sidebar-border: oklch(0.2887 0.0646 265.1126);
|
||||
--sidebar-ring: oklch(0.5424 0.2454 293.0160);
|
||||
--font-sans: Inter, -apple-system, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.75rem;
|
||||
--shadow-x: 0px;
|
||||
--shadow-y: 4px;
|
||||
--shadow-blur: 15px;
|
||||
--shadow-spread: 0px;
|
||||
--shadow-opacity: 0.05;
|
||||
--shadow-color: hsl(220, 40%, 15%);
|
||||
--shadow-2xs: 0px 4px 15px 0px hsl(220 40% 15% / 0.03);
|
||||
--shadow-xs: 0px 4px 15px 0px hsl(220 40% 15% / 0.03);
|
||||
--shadow-sm: 0px 4px 15px 0px hsl(220 40% 15% / 0.05), 0px 1px 2px -1px hsl(220 40% 15% / 0.05);
|
||||
--shadow: 0px 4px 15px 0px hsl(220 40% 15% / 0.05), 0px 1px 2px -1px hsl(220 40% 15% / 0.05);
|
||||
--shadow-md: 0px 4px 15px 0px hsl(220 40% 15% / 0.05), 0px 2px 4px -1px hsl(220 40% 15% / 0.05);
|
||||
--shadow-lg: 0px 4px 15px 0px hsl(220 40% 15% / 0.05), 0px 4px 6px -1px hsl(220 40% 15% / 0.05);
|
||||
--shadow-xl: 0px 4px 15px 0px hsl(220 40% 15% / 0.05), 0px 8px 10px -1px hsl(220 40% 15% / 0.05);
|
||||
--shadow-2xl: 0px 4px 15px 0px hsl(220 40% 15% / 0.13);
|
||||
--tracking-normal: -0.015em;
|
||||
--spacing: 0.25rem;
|
||||
|
||||
--sidebar: var(--background);
|
||||
--sidebar-foreground: var(--foreground);
|
||||
--sidebar-primary: var(--primary);
|
||||
--sidebar-primary-foreground: var(--primary-foreground);
|
||||
--sidebar-accent: var(--accent);
|
||||
--sidebar-accent-foreground: var(--accent-foreground);
|
||||
--sidebar-border: var(--border);
|
||||
--sidebar-ring: var(--ring);
|
||||
|
||||
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: oklch(0.240 0.011 256.8);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
|
||||
|
||||
--background: oklch(0.240 0.011 256.8);
|
||||
--foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--card: oklch(0.266 0.015 257.5);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--card-foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--popover: oklch(0.266 0.015 257.5);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--popover-foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--primary: oklch(0.4865 0.2423 291.8661);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.295 0.015 257.7);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--secondary-foreground: oklch(0.9838 0.0035 247.8583);
|
||||
--muted: oklch(0.295 0.015 257.7);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.295 0.015 257.7);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--accent-foreground: oklch(0.5424 0.2454 293.0160);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 10%);
|
||||
--ring: oklch(0.541 0.281 293.009);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
--ring: oklch(0.4865 0.2423 291.8661);
|
||||
|
||||
--sidebar: oklch(0.266 0.015 257.5);
|
||||
--sidebar: oklch(0.2639 0.0161 264.25);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
@@ -74,59 +89,21 @@
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
|
||||
--chart-1: oklch(0.5424 0.2454 293.0160);
|
||||
--chart-2: oklch(0.6309 0.1013 183.4907);
|
||||
--chart-3: oklch(0.3787 0.0440 225.5393);
|
||||
--chart-4: oklch(0.8336 0.1186 88.1463);
|
||||
--chart-5: oklch(0.7834 0.1261 58.7491);
|
||||
|
||||
|
||||
--font-sans: Inter, -apple-system, sans-serif;
|
||||
--font-serif: Georgia, serif;
|
||||
--font-mono: JetBrains Mono, monospace;
|
||||
--radius: 0.75rem;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
.dark {
|
||||
--background: oklch(0.240 0.011 256.8);
|
||||
--foreground: oklch(0.985 0 0);
|
||||
--card: oklch(0.266 0.015 257.5);
|
||||
--card-foreground: oklch(0.985 0 0);
|
||||
--popover: oklch(0.266 0.015 257.5);
|
||||
--popover-foreground: oklch(0.985 0 0);
|
||||
--primary: oklch(0.541 0.281 293.009);
|
||||
--primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--secondary: oklch(0.274 0.006 286.033);
|
||||
--secondary-foreground: oklch(0.985 0 0);
|
||||
--muted: oklch(0.274 0.006 286.033);
|
||||
--muted-foreground: oklch(0.705 0.015 286.067);
|
||||
--accent: oklch(0.274 0.006 286.033);
|
||||
--accent-foreground: oklch(0.985 0 0);
|
||||
--destructive: oklch(0.704 0.191 22.216);
|
||||
--border: oklch(1 0 0 / 10%);
|
||||
--input: oklch(1 0 0 / 15%);
|
||||
--ring: oklch(0.541 0.281 293.009);
|
||||
--chart-1: oklch(0.488 0.243 264.376);
|
||||
--chart-2: oklch(0.696 0.17 162.48);
|
||||
--chart-3: oklch(0.769 0.188 70.08);
|
||||
--chart-4: oklch(0.627 0.265 303.9);
|
||||
--chart-5: oklch(0.645 0.246 16.439);
|
||||
|
||||
--sidebar: oklch(0.295 0.015 256.8);
|
||||
--sidebar-foreground: oklch(0.985 0 0);
|
||||
--sidebar-primary: oklch(0.541 0.281 293.009);
|
||||
--sidebar-primary-foreground: oklch(0.969 0.016 293.756);
|
||||
--sidebar-accent: oklch(0.274 0.006 286.033);
|
||||
--sidebar-accent-foreground: oklch(0.985 0 0);
|
||||
--sidebar-border: oklch(1 0 0 / 10%);
|
||||
--sidebar-ring: oklch(0.541 0.281 293.009);
|
||||
}
|
||||
|
||||
|
||||
|
||||
*/
|
||||
|
||||
@theme inline {
|
||||
--font-inter: 'Inter', 'sans-serif';
|
||||
--font-manrope: 'Manrope', 'sans-serif';
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
--color-background: var(--background);
|
||||
--color-foreground: var(--foreground);
|
||||
--color-card: var(--card);
|
||||
@@ -142,6 +119,7 @@
|
||||
--color-accent: var(--accent);
|
||||
--color-accent-foreground: var(--accent-foreground);
|
||||
--color-destructive: var(--destructive);
|
||||
--color-destructive-foreground: var(--destructive-foreground);
|
||||
--color-border: var(--border);
|
||||
--color-input: var(--input);
|
||||
--color-ring: var(--ring);
|
||||
@@ -158,19 +136,41 @@
|
||||
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
|
||||
--color-sidebar-border: var(--sidebar-border);
|
||||
--color-sidebar-ring: var(--sidebar-ring);
|
||||
|
||||
--font-sans: var(--font-sans);
|
||||
--font-mono: var(--font-mono);
|
||||
--font-serif: var(--font-serif);
|
||||
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-lg: var(--radius);
|
||||
--radius-xl: calc(var(--radius) + 4px);
|
||||
|
||||
--shadow-2xs: var(--shadow-2xs);
|
||||
--shadow-xs: var(--shadow-xs);
|
||||
--shadow-sm: var(--shadow-sm);
|
||||
--shadow: var(--shadow);
|
||||
--shadow-md: var(--shadow-md);
|
||||
--shadow-lg: var(--shadow-lg);
|
||||
--shadow-xl: var(--shadow-xl);
|
||||
--shadow-2xl: var(--shadow-2xl);
|
||||
|
||||
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
|
||||
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
|
||||
--tracking-normal: var(--tracking-normal);
|
||||
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
|
||||
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
|
||||
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
|
||||
}
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50;
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--border) transparent;
|
||||
}
|
||||
html {
|
||||
@apply overflow-x-hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
@apply bg-background text-foreground min-h-svh w-full;
|
||||
@apply bg-background text-foreground;
|
||||
letter-spacing: var(--tracking-normal);
|
||||
}
|
||||
|
||||
button:not(:disabled),
|
||||
@@ -180,74 +180,27 @@
|
||||
|
||||
/* Prevent focus zoom on mobile devices */
|
||||
@media screen and (max-width: 767px) {
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-size: 16px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@utility container {
|
||||
margin-inline: auto;
|
||||
padding-inline: 2rem;
|
||||
}
|
||||
|
||||
@utility no-scrollbar {
|
||||
/* Hide scrollbar for Chrome, Safari and Opera */
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
/* Hide scrollbar for IE, Edge and Firefox */
|
||||
-ms-overflow-style: none; /* IE and Edge */
|
||||
scrollbar-width: none; /* Firefox */
|
||||
}
|
||||
|
||||
@utility faded-bottom {
|
||||
@apply after:pointer-events-none after:absolute after:bottom-0 after:left-0 after:hidden after:h-32 after:w-full after:bg-[linear-gradient(180deg,_transparent_10%,_var(--background)_70%)] md:after:block;
|
||||
}
|
||||
|
||||
/* styles.css */
|
||||
.CollapsibleContent {
|
||||
overflow: hidden;
|
||||
}
|
||||
.CollapsibleContent[data-state='open'] {
|
||||
animation: slideDown 300ms ease-out;
|
||||
}
|
||||
.CollapsibleContent[data-state='closed'] {
|
||||
animation: slideUp 300ms ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
height: 0;
|
||||
}
|
||||
to {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes slideUp {
|
||||
from {
|
||||
height: var(--radix-collapsible-content-height);
|
||||
}
|
||||
to {
|
||||
height: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@layer components {
|
||||
.prose :where(h1, h2, h3, h4, h5, h6) {
|
||||
@apply font-bold text-foreground my-4;
|
||||
}
|
||||
|
||||
.prose :where(p) {
|
||||
@apply text-muted-foreground block mb-2;
|
||||
/* Target the scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
/* Width of the scrollbar */
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
position: absolute !important;
|
||||
|
||||
}
|
||||
.prose :where(strong) {
|
||||
@apply text-foreground font-semibold mb-2;
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border);
|
||||
border-radius: 46px !important;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+2
-53
@@ -1,6 +1,5 @@
|
||||
|
||||
import { EditorView } from '@uiw/react-codemirror';
|
||||
|
||||
|
||||
|
||||
export const colorOptions = [
|
||||
|
||||
@@ -25,54 +24,4 @@ export const randomColor = () => {
|
||||
|
||||
|
||||
|
||||
export const overrideDarkTheme = EditorView.theme({
|
||||
|
||||
'.cm-content': {
|
||||
backgroundColor: "#20252c" ,
|
||||
},
|
||||
".cm-gutter": {
|
||||
backgroundColor: "#20252c",
|
||||
},
|
||||
|
||||
".cm-gutterElement": {
|
||||
color: "#4b515a"
|
||||
},
|
||||
|
||||
".ͼp": {
|
||||
color: "#A994FF"
|
||||
},
|
||||
|
||||
".cm-line .ͼq": {
|
||||
color: "#ff6363"
|
||||
},
|
||||
".ͼu": {
|
||||
color: "#B6E672"
|
||||
},
|
||||
".ͼv": {
|
||||
color: "#6cdcc4"
|
||||
}
|
||||
|
||||
}, { dark: true });
|
||||
|
||||
|
||||
|
||||
export const overrideLightTheme = EditorView.theme({
|
||||
|
||||
".ͼb": {
|
||||
color: "#2A1D66"
|
||||
},
|
||||
".cm-gutterElement": {
|
||||
color: "#a2a4a8"
|
||||
},
|
||||
".cm-gutter": {
|
||||
backgroundColor: "white",
|
||||
|
||||
},
|
||||
".cm-gutters": {
|
||||
borderColor: "#f2f4f6"
|
||||
},
|
||||
".cm-line": {
|
||||
color: "#333639"
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
import { DataInsertType } from "../schemas/data-type-schema";
|
||||
|
||||
export const MSSQLDataType: Partial<DataInsertType>[] = [
|
||||
{
|
||||
id: 'c7a6fd94-ecec-4b79-9ea6-8ca25bae8d9c',
|
||||
name: 'tinyint',
|
||||
type: 'integer',
|
||||
modifiers: ['auto_increment']
|
||||
},
|
||||
{
|
||||
id: '48bdd059-9b52-4f93-b5c1-42d9dc2b7d13',
|
||||
name: 'smallint',
|
||||
type: 'integer',
|
||||
|
||||
modifiers: ['auto_increment']
|
||||
},
|
||||
{
|
||||
id: 'ae5e05da-21e0-4746-b972-6069ab628dc2',
|
||||
name: 'int',
|
||||
type: 'integer',
|
||||
|
||||
modifiers: ['auto_increment']
|
||||
},
|
||||
{
|
||||
id: '8a719f0c-0b0f-4be2-b660-61502575a5bd',
|
||||
name: 'bigint',
|
||||
type: 'integer',
|
||||
|
||||
modifiers: ['auto_increment']
|
||||
},
|
||||
{
|
||||
id: '442c88ae-6d53-41d5-bdaf-cb95192ab2e7',
|
||||
name: 'decimal',
|
||||
type: 'numeric',
|
||||
modifiers: ['precision', 'scale'],
|
||||
synonyms: ['numeric']
|
||||
},
|
||||
{
|
||||
id: 'c3102982-a9ee-48c9-b050-bc3e8318efda',
|
||||
name: 'float',
|
||||
type: 'numeric',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '1858ca0e-ba61-45aa-9236-8028ab7d7570',
|
||||
name: 'real',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
id: '3842960d-f0a9-411b-99d2-be3cfaf03fe7',
|
||||
name: 'money',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
id: '664aacc0-d646-4052-a616-d0b3d5d546df',
|
||||
name: 'smallmoney',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
id: 'fbdfd7d5-72d1-4c0b-9cfb-ca843ac444a4',
|
||||
name: 'char',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: '1a6f261b-1abf-4f13-b803-a9533198f4c9',
|
||||
name: 'varchar',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: '0a27e11f-1af7-4a62-babd-cfc1ecdd5f1f',
|
||||
name: 'text',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: 'c8cd26da-a09e-4a18-8b2f-11a2e4fe049c',
|
||||
name: 'nchar',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: '53f552a2-b782-4084-a236-6928651b4f43',
|
||||
name: 'nvarchar',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: 'e0071583-b97a-4f67-80b1-6ff25b8b8c37',
|
||||
name: 'ntext',
|
||||
type: 'text'
|
||||
},
|
||||
{
|
||||
id: '9df5d149-c9f1-4d26-a3ae-832accc08831',
|
||||
name: 'binary',
|
||||
type: 'binary',
|
||||
modifiers: ['length', 'no_default']
|
||||
},
|
||||
{
|
||||
id: '3c351e8c-92ba-40aa-843c-df2d8409e95a',
|
||||
name: 'varbinary',
|
||||
type: 'binary',
|
||||
modifiers: ['length', 'no_default']
|
||||
},
|
||||
{
|
||||
id: '731051f0-050f-4eb2-a327-5e10dd6bf6ca',
|
||||
name: 'image',
|
||||
type: 'binary',
|
||||
modifiers: ['no_unique', 'no_default']
|
||||
},
|
||||
{
|
||||
id: 'b412d41c-255b-413d-b866-4b652f291606',
|
||||
name: 'date',
|
||||
type: 'time'
|
||||
},
|
||||
{
|
||||
id: 'cc0f4f94-772c-4f4e-b1d3-f2fe0a18ee49',
|
||||
name: 'time',
|
||||
type: 'time',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '83a37c4f-9501-485e-bc10-802673ca0594',
|
||||
name: 'datetime',
|
||||
type: 'time'
|
||||
},
|
||||
{
|
||||
id: '85a466ba-b97a-4c47-a204-1a8f0e16d275',
|
||||
name: 'smalldatetime',
|
||||
type: 'time'
|
||||
},
|
||||
{
|
||||
id: '562887bf-8d60-4e9e-b038-e760a6da05a2',
|
||||
name: 'datetime2',
|
||||
type: 'time',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '1165eda1-6b45-4c45-82de-ddc90bb87f86',
|
||||
name: 'datetimeoffset',
|
||||
type: 'time',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '00ea0771-47e0-4067-ba74-a153421c2d69',
|
||||
name: 'uniqueidentifier',
|
||||
type: 'uuid',
|
||||
|
||||
},
|
||||
{
|
||||
id: '03db80b0-d2c0-47bc-879b-8d0c17a36a9e',
|
||||
name: 'bit',
|
||||
type: 'boolean'
|
||||
},
|
||||
{
|
||||
id: '060d32ca-4e8a-496f-a29e-36d1947d0469',
|
||||
name: 'json',
|
||||
type: 'json'
|
||||
},
|
||||
{
|
||||
id: '090ad5cd-43b6-4443-93ce-61a6f12c6f4e',
|
||||
name: 'xml',
|
||||
type: 'xml'
|
||||
},
|
||||
{
|
||||
id: '9e9ddfc5-176b-4574-a097-3a1cbf927d9c',
|
||||
name: 'geometry',
|
||||
type: 'geometric'
|
||||
},
|
||||
{
|
||||
id: 'c6cdd823-5b8e-45ed-a489-3b68ce10b698',
|
||||
name: 'geography',
|
||||
type: 'geometric'
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,151 @@
|
||||
import { DataInsertType } from "../schemas/data-type-schema";
|
||||
|
||||
export const OracleDataType: Partial<DataInsertType>[] = [
|
||||
{
|
||||
id: '7b79fc89-38ae-4cf7-bdb8-1a270c2c4f0b',
|
||||
name: 'number',
|
||||
type: 'numeric',
|
||||
modifiers: ['precision', 'scale', "auto_increment"],
|
||||
synonyms: ['decimal', 'dec', 'numeric']
|
||||
},
|
||||
{
|
||||
id: 'fc4ba819-cbf8-4550-9e78-6f7a05883f55',
|
||||
name: 'integer',
|
||||
type: 'integer',
|
||||
modifiers: ["auto_increment"],
|
||||
synonyms: ['int', 'smallint']
|
||||
},
|
||||
{
|
||||
id: 'd4a2a12b-1c72-443a-9c9b-b30a0a4e64d3',
|
||||
name: 'float',
|
||||
type: 'numeric',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: 'd2b819bd-028a-4790-8e32-a1e06c6cb82c',
|
||||
name: 'binary_float',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
id: '6a04aad8-b97e-4a57-ae1a-b270f3885821',
|
||||
name: 'binary_double',
|
||||
type: 'numeric'
|
||||
},
|
||||
{
|
||||
id: 'a15c03d8-e8f1-4285-8148-134311b403e4',
|
||||
name: 'varchar2',
|
||||
type: 'text',
|
||||
modifiers: ['length'],
|
||||
synonyms: ['varchar', 'character varying']
|
||||
},
|
||||
{
|
||||
id: 'f5932edf-41bc-4d52-ad1d-4501acbe77f1',
|
||||
name: 'char',
|
||||
type: 'text',
|
||||
modifiers: ['length'],
|
||||
synonyms: ['character']
|
||||
},
|
||||
{
|
||||
id: 'ac67bfe8-6c83-4729-b9b2-ac6fbca5699f',
|
||||
name: 'nchar',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: 'bd515b9f-1355-436a-9ff6-f007d61361f2',
|
||||
name: 'nvarchar2',
|
||||
type: 'text',
|
||||
modifiers: ['length']
|
||||
},
|
||||
{
|
||||
id: '16115bbb-a06e-4663-96c5-6169398aba60',
|
||||
name: 'clob',
|
||||
type: 'lob',
|
||||
modifiers: ['no_default' , 'no_unique']
|
||||
},
|
||||
{
|
||||
id: 'a0e84fd7-dae8-4f1c-b989-acae67eedbf5',
|
||||
name: 'nclob',
|
||||
type: 'lob',
|
||||
modifiers: ['no_default' , 'no_unique']
|
||||
},
|
||||
{
|
||||
id: '18762e8d-8fa7-43cf-9e0a-4205f2b2f3b1',
|
||||
name: 'blob',
|
||||
type: 'lob',
|
||||
modifiers: ['no_default' , 'no_unique']
|
||||
},
|
||||
{
|
||||
id: 'c19f392c-6edc-439c-a65b-a52f1d616051',
|
||||
name: 'bfile',
|
||||
type: 'lob',
|
||||
modifiers: ['no_default' , 'no_unique']
|
||||
},
|
||||
{
|
||||
id: '699d85c7-080e-4ec9-ae3c-2dafdaf4cd27',
|
||||
name: 'raw',
|
||||
type: 'binary',
|
||||
modifiers: ['length' , 'no_default' ]
|
||||
},
|
||||
{
|
||||
id: '8521fef1-0105-49ca-a927-cde3cf4224d3',
|
||||
name: 'date',
|
||||
type: 'time',
|
||||
},
|
||||
{
|
||||
id: '5306e486-a2db-4095-9c4d-bc216e2b3b57',
|
||||
name: 'timestamp',
|
||||
type: 'time',
|
||||
modifiers: ['precision'],
|
||||
synonyms: ['timestamp without time zone']
|
||||
},
|
||||
{
|
||||
id: '8604a573-7472-4603-952e-316fe1ef3407',
|
||||
name: 'timestamp with time zone',
|
||||
type: 'time',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '6ec0d6d2-dc41-49b1-9e15-2ded2d75b5ef',
|
||||
name: 'timestamp with local time zone',
|
||||
type: 'time',
|
||||
modifiers: ['precision']
|
||||
},
|
||||
{
|
||||
id: '7076c729-ff35-4d5e-8f21-8c02bfc90d5c',
|
||||
name: 'interval year to month',
|
||||
type: 'time' ,
|
||||
modifiers: ['precision' , "no_default"]
|
||||
},
|
||||
{
|
||||
id: '6e2ca449-5062-4feb-a5ce-19fcc751ff2b',
|
||||
name: 'interval day to second',
|
||||
type: 'time',
|
||||
modifiers: ['precision' , "no_default"]
|
||||
},
|
||||
{
|
||||
id: '3c3cd09e-a970-4c48-9f6e-6636757e3dd7',
|
||||
name: 'rowid',
|
||||
type: 'rowid',
|
||||
modifiers: ['no_default' , 'no_unique']
|
||||
},
|
||||
{
|
||||
id: 'f8ca2603-b196-4044-baec-bdc02803f3b8',
|
||||
name: 'urowid',
|
||||
type: 'rowid',
|
||||
modifiers: ['no_default', 'no_unique']
|
||||
},
|
||||
{
|
||||
id: 'c9abbfda-e2b3-4746-a536-36f34204b383',
|
||||
name: 'xmltype',
|
||||
type: 'xml',
|
||||
modifiers: ['no_default']
|
||||
},
|
||||
{
|
||||
id: '92fb0e59-b573-43a6-a7d3-f357867a3b00',
|
||||
name: 'json',
|
||||
type: 'json',
|
||||
modifiers: ['no_default']
|
||||
},
|
||||
|
||||
]
|
||||
@@ -5,6 +5,8 @@ import { PostgresDataType } from "./postgres_data_types";
|
||||
import { SqliteDataTypes } from "./sqlite_data_types";
|
||||
import { MariaDbDataType } from "./mariadb_data_types";
|
||||
import { DatabaseDialect } from "../database";
|
||||
import { OracleDataType } from "./oracle_data_types";
|
||||
import { MSSQLDataType } from "./mssql_data_types";
|
||||
|
||||
|
||||
|
||||
@@ -18,7 +20,9 @@ export const seedDataTypes = async (db: any) => {
|
||||
...mapToDataType(MysqlDataType , DatabaseDialect.MYSQL) ,
|
||||
...mapToDataType(PostgresDataType , DatabaseDialect.POSTGRES) ,
|
||||
...mapToDataType(SqliteDataTypes , DatabaseDialect.SQLITE) ,
|
||||
...mapToDataType(MariaDbDataType , DatabaseDialect.MARIADB)
|
||||
...mapToDataType(MariaDbDataType , DatabaseDialect.MARIADB) ,
|
||||
...mapToDataType(OracleDataType , DatabaseDialect.ORACLE) ,
|
||||
...mapToDataType(MSSQLDataType , DatabaseDialect.MSSQL) ,
|
||||
] as DataInsertType[])
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -1,3 +1,5 @@
|
||||
import _ from "lodash";
|
||||
import { DatabaseType as DatabaseSchemaType } from "./schemas/database-schema";
|
||||
|
||||
|
||||
export interface DatabaseType {
|
||||
@@ -13,7 +15,7 @@ export enum DatabaseDialect {
|
||||
POSTGRES = "postgres",
|
||||
SQLITE = "sqlite",
|
||||
MARIADB = "mariadb",
|
||||
SQL_SERVER = "sql_server",
|
||||
MSSQL = "mssql",
|
||||
ORACLE = "oracle"
|
||||
};
|
||||
|
||||
@@ -45,10 +47,10 @@ export const DBTypes: DatabaseType[] = [
|
||||
|
||||
{
|
||||
name: "SQL Server",
|
||||
dialect: DatabaseDialect.SQL_SERVER,
|
||||
dialect: DatabaseDialect.MSSQL,
|
||||
logo: "/sql_server_logo_small.png",
|
||||
small_logo: "/sql_server_logo_small.png" ,
|
||||
comming : true
|
||||
|
||||
},
|
||||
|
||||
{
|
||||
@@ -56,7 +58,7 @@ export const DBTypes: DatabaseType[] = [
|
||||
dialect: DatabaseDialect.ORACLE,
|
||||
logo: "/oracle_logo_small.png",
|
||||
small_logo: "/oracle_logo_small.png" ,
|
||||
comming : true
|
||||
|
||||
},
|
||||
]
|
||||
|
||||
@@ -115,3 +117,14 @@ export enum CardinalityStyle {
|
||||
SYMBOLIC = "SYMBOLIC"
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const emptyDb = (database: DatabaseSchemaType) : DatabaseSchemaType => {
|
||||
|
||||
let cloneDb : any = _.cloneDeep(database);
|
||||
cloneDb.tables = [];
|
||||
cloneDb.relationships = [];
|
||||
cloneDb.numOfTables = 0;
|
||||
|
||||
return cloneDb as DatabaseSchemaType;
|
||||
}
|
||||
+2
-1
@@ -106,4 +106,5 @@ export enum ForeignKeyActions {
|
||||
|
||||
}
|
||||
|
||||
export const MYSQL_MAX_VAR_LENGTH = 255;
|
||||
export const MYSQL_MAX_VAR_LENGTH = 255;
|
||||
export const DEFAULT_LENGTH_PARAM = 100;
|
||||
@@ -9,7 +9,7 @@ export const data_types = sqliteTable('data_types', {
|
||||
name: text('name'),
|
||||
|
||||
dialect: text("dialect", {
|
||||
enum: ["postgres", "mysql", "sqlite", "mariadb"],
|
||||
enum: ["postgres", "mysql", "sqlite", "mariadb" , "mssql" , "oracle"],
|
||||
}).notNull().default("postgres"),
|
||||
|
||||
type: text('type').notNull(),
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
|
||||
import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
import App from "./App.tsx";
|
||||
import { UIProviders } from "./provider.tsx";
|
||||
import "./styles/globals.css";
|
||||
import "./index.css";
|
||||
|
||||
import "./i18/index.ts";
|
||||
|
||||
ReactDOM.createRoot(document.getElementById("root")!).render(
|
||||
|
||||
+1
-1
@@ -12,7 +12,7 @@ export function UIProviders({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
|
||||
<FontProvider>
|
||||
<ThemeProvider defaultTheme ={"dark"}>
|
||||
<ThemeProvider defaultTheme ={"light"}>
|
||||
<TooltipProvider delayDuration={0}>
|
||||
<Toaster />
|
||||
<SearchProvider>
|
||||
|
||||
@@ -17,6 +17,7 @@ import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-s
|
||||
import { v4 } from "uuid";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { deleteFieldsWithCascade, deleteIndicesWithCascade, deleteTablesWithCascade } from "@/utils/cascade";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
const TYPE_ORDER = ["integer", "text", "boolean", "numeric", "time", "enum"] as const;
|
||||
|
||||
@@ -87,10 +88,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
database = database[0] as any;
|
||||
else
|
||||
database = undefined as any;
|
||||
|
||||
useEffect(() => {
|
||||
console.log(database);
|
||||
}, [database])
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -431,7 +429,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
||||
}, [db, currentDatabaseId]);
|
||||
|
||||
const getInteger = useCallback(() => {
|
||||
return data_types.find((dataType: DataType) => dataType.name == "integer");
|
||||
return data_types.find((dataType: DataType) => dataType.name == "integer" || (dataType.name == "int" && dataType.dialect == DatabaseDialect.MSSQL));
|
||||
}, [data_types]);
|
||||
|
||||
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
|
||||
|
||||
@@ -0,0 +1,778 @@
|
||||
|
||||
import { randomColor } from '@/lib/colors';
|
||||
import { DatabaseDialect } from '@/lib/database';
|
||||
import { DataTypes, ForeignKeyActions, Modifiers, TimeDefaultValues } from '@/lib/field';
|
||||
import { DataType } from '@/lib/schemas/data-type-schema';
|
||||
import { FieldInsertType } from '@/lib/schemas/field-schema';
|
||||
import { IndexInsertType } from '@/lib/schemas/index-schema';
|
||||
import { Cardinality, RelationshipInsertType } from '@/lib/schemas/relationship-schema';
|
||||
import { TableInsertType } from '@/lib/schemas/table-schema';
|
||||
import { init, parse, ReferentialAction } from '@guanmingchiu/sqlparser-ts';
|
||||
import { v4 } from 'uuid';
|
||||
|
||||
|
||||
export class BaseSqlImporter {
|
||||
|
||||
|
||||
protected data_types: DataType[] = [];
|
||||
protected dialect: DatabaseDialect | undefined;
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
this.data_types = data_types;
|
||||
init();
|
||||
}
|
||||
|
||||
public parseSql(sql: string) {
|
||||
|
||||
let errors: Error[] = [];
|
||||
|
||||
const tables: TableInsertType[] = [];
|
||||
const relationships: RelationshipInsertType[] = [];
|
||||
const indexes: IndexInsertType[] = [];
|
||||
const fk_constraints: any[] = [];
|
||||
|
||||
const cleaned = this.cleanSql(sql);
|
||||
|
||||
const { createIndexStatements, alterTableStatements, createTableStatements } = this.extractStatments(cleaned);
|
||||
|
||||
try {
|
||||
for (const statment of createTableStatements) {
|
||||
|
||||
try {
|
||||
const astStatment: any = parse(statment, this.dialect as any)?.pop();
|
||||
|
||||
if (astStatment?.CreateTable) {
|
||||
const { table, errors: tableErrors, fk_constraints: tableFkConstraints } = this.astToTable(astStatment);
|
||||
if (table)
|
||||
tables.push(table);
|
||||
errors.push(...tableErrors);
|
||||
|
||||
if (tableFkConstraints)
|
||||
fk_constraints.push(...tableFkConstraints);
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
|
||||
}
|
||||
for (const statment of alterTableStatements) {
|
||||
try {
|
||||
const astStatment: any = parse(statment, this.dialect as any)?.pop();
|
||||
|
||||
if (astStatment?.AlterTable) {
|
||||
|
||||
for (const operation of astStatment.AlterTable.operations) {
|
||||
|
||||
if (operation.AddConstraint?.constraint?.ForeignKey) {
|
||||
|
||||
const targetTableName: string | undefined = astStatment.AlterTable.name?.at(-1)?.Identifier?.value;
|
||||
if (!targetTableName)
|
||||
continue;
|
||||
|
||||
fk_constraints.push({
|
||||
...operation.AddConstraint.constraint.ForeignKey,
|
||||
target_table: targetTableName
|
||||
})
|
||||
|
||||
}
|
||||
else if (operation.AddConstraint?.constraint?.Unique) {
|
||||
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == astStatment.AlterTable.name?.at(-1)?.Identifier?.value);
|
||||
|
||||
const columns: any[] | undefined = operation.AddConstraint?.constraint?.Unique?.columns;
|
||||
|
||||
if (!table || !columns) {
|
||||
continue;
|
||||
}
|
||||
if (columns && columns.length > 0 && table.fields) {
|
||||
|
||||
for (const { column } of columns) {
|
||||
const index = table.fields.findIndex((field: FieldInsertType) => field.name == column?.expr?.Identifier?.value);
|
||||
if (index >= 0) {
|
||||
table.fields[index].unique = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
} else if (operation.AddConstraint?.constraint.PrimaryKey) {
|
||||
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == astStatment.AlterTable.name?.at(-1)?.Identifier?.value);
|
||||
|
||||
const columns: any[] | undefined = operation.AddConstraint?.constraint?.PrimaryKey?.columns;
|
||||
|
||||
if (!table || !columns) {
|
||||
continue;
|
||||
}
|
||||
if (columns && columns.length > 0 && table.fields) {
|
||||
for (const { column } of columns) {
|
||||
const index = table.fields.findIndex((field: FieldInsertType) => field.name == column?.expr?.Identifier?.value);
|
||||
if (index >= 0) {
|
||||
table.fields[index].isPrimary = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error)
|
||||
continue;
|
||||
}
|
||||
|
||||
}
|
||||
for (const statment of createIndexStatements) {
|
||||
try {
|
||||
const astStatment: any = parse(statment, this.dialect as any)?.pop();
|
||||
|
||||
if (astStatment.CreateIndex) {
|
||||
const { index, error: indexErrors } = this.astToIndex(astStatment, tables);
|
||||
|
||||
|
||||
if (indexErrors)
|
||||
errors.push(indexErrors);
|
||||
|
||||
if (index)
|
||||
indexes.push(index)
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
for (const constraint of fk_constraints) {
|
||||
try {
|
||||
relationships.push(this.astToRelationship(constraint, tables))
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
if (tables.length == 0)
|
||||
throw Error("Can't parse sql")
|
||||
return {
|
||||
tables,
|
||||
relationships,
|
||||
indexes,
|
||||
errors
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected astToTable(ast: any): ParsedTable {
|
||||
const errors: Error[] = []
|
||||
const tableName: string | undefined = ast.CreateTable.name?.at(-1)?.Identifier?.value;
|
||||
const columns: any[] = ast.CreateTable.columns;
|
||||
const fk_constraints: any[] = [];
|
||||
|
||||
|
||||
if (!tableName || !columns)
|
||||
throw Error("Invalid table defintiion");
|
||||
|
||||
const fields: FieldInsertType[] = []
|
||||
|
||||
for (let index = 0; index < columns.length; index++) {
|
||||
try {
|
||||
|
||||
const { field, fk_constraint } = this.astToField(columns[index], index);
|
||||
fields.push(field);
|
||||
|
||||
if (fk_constraint) {
|
||||
fk_constraints.push({
|
||||
...fk_constraint,
|
||||
columns: [{
|
||||
value: field.name
|
||||
}],
|
||||
target_table: tableName
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if (ast.CreateTable.constraints && ast.CreateTable.constraints.length > 0) {
|
||||
for (const constraint of ast.CreateTable.constraints) {
|
||||
if (constraint.ForeignKey) {
|
||||
fk_constraints.push({
|
||||
...constraint.ForeignKey,
|
||||
target_table: tableName
|
||||
});
|
||||
}
|
||||
else if (constraint.PrimaryKey) {
|
||||
for (const { column } of constraint.PrimaryKey.columns) {
|
||||
|
||||
const columnName: string | undefined = column.expr?.Identifier?.value;
|
||||
if (!columnName)
|
||||
continue;
|
||||
const index = fields.findIndex((field: FieldInsertType) => field.name === columnName);
|
||||
if (index >= 0) {
|
||||
fields[index].isPrimary = true;
|
||||
}
|
||||
}
|
||||
} else if (constraint.Unique) {
|
||||
const columns: any[] | undefined = constraint.Unique?.columns;
|
||||
if (columns && columns.length > 0) {
|
||||
|
||||
for (const { column } of columns) {
|
||||
const index = fields.findIndex((field: FieldInsertType) => field.name == column?.expr?.Identifier?.value);
|
||||
if (index >= 0) {
|
||||
fields[index].unique = true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return {
|
||||
table: {
|
||||
id: v4(),
|
||||
name: tableName,
|
||||
fields: fields,
|
||||
color: randomColor()
|
||||
} as TableInsertType,
|
||||
errors,
|
||||
fk_constraints
|
||||
}
|
||||
}
|
||||
|
||||
protected astToField(ast: any, sequence: number): ParsedField {
|
||||
|
||||
let isPrimary: boolean = false;
|
||||
let nullable: boolean = true;
|
||||
let unique: boolean = false;
|
||||
let autoIncrement: boolean = false;
|
||||
let maxLength: number | undefined;
|
||||
let precision: number | undefined;
|
||||
let scale: number | undefined;
|
||||
let values: any[] | string | undefined = undefined;
|
||||
let defaultValue: string | undefined;
|
||||
let typeName: string | undefined;
|
||||
let extraParam: number | undefined;
|
||||
let fk_constraint: any | undefined;
|
||||
let params: any[] | undefined;
|
||||
|
||||
|
||||
// if it's a basic type like (texts , booleans .. ect) we get the name directly
|
||||
// if it's a type that take params , the data type become an object
|
||||
if (typeof ast.data_type === "string") {
|
||||
typeName = ast.data_type.toLowerCase();
|
||||
|
||||
|
||||
} else if (typeof ast.data_type === "object" && !ast.data_type.Custom) {
|
||||
const attributeName = Object.keys(ast.data_type)[0];
|
||||
if (ast.data_type[attributeName]?.IntegerLength) {
|
||||
|
||||
maxLength = ast.data_type[attributeName]?.IntegerLength?.length;
|
||||
}
|
||||
if (ast.data_type[attributeName]?.Precision) {
|
||||
precision = ast.data_type[attributeName]?.Precision;
|
||||
}
|
||||
|
||||
if (ast.data_type[attributeName]?.PrecisionAndScale && ast.data_type[attributeName]?.PrecisionAndScale?.length == 2) {
|
||||
precision = ast.data_type[attributeName]?.PrecisionAndScale[0];
|
||||
scale = ast.data_type[attributeName]?.PrecisionAndScale[1];
|
||||
}
|
||||
|
||||
if (typeof ast.data_type[attributeName] == "number") {
|
||||
extraParam = ast.data_type[attributeName];
|
||||
}
|
||||
|
||||
if (Array.isArray(ast.data_type[attributeName])) {
|
||||
params = ast.data_type[attributeName];
|
||||
}
|
||||
typeName = attributeName.toLowerCase();
|
||||
|
||||
|
||||
if (typeName == "timestamp" && this.dialect == DatabaseDialect.ORACLE && ast.data_type[attributeName].length >= 2 && ast.data_type[attributeName][1] == "WithTimeZone") {
|
||||
typeName += "withtimezone" ;
|
||||
}
|
||||
}
|
||||
|
||||
else if (ast.data_type.Custom && Array.isArray(ast.data_type.Custom)) {
|
||||
if (ast.data_type.Custom.length > 1 && ast.data_type.Custom[1]?.[0] && !isNaN(Number(ast.data_type.Custom[1]?.[0])))
|
||||
extraParam = Number(ast.data_type.Custom[1]?.[0]);
|
||||
|
||||
typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
|
||||
|
||||
}
|
||||
|
||||
const dataType: DataType | undefined = this.processDataType(typeName as string);
|
||||
|
||||
if (!dataType) {
|
||||
throw Error("Data type not found");
|
||||
}
|
||||
// get data type modifiers
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
// the parser pass an extra param in case if the type is cutsom , that extra param can be either a length or precision
|
||||
// to know which one we have to test what kind of modifier the data type support
|
||||
if (extraParam && modifiers.includes(Modifiers.LENGTH))
|
||||
maxLength = extraParam;
|
||||
|
||||
if (extraParam && modifiers.includes(Modifiers.PRECISION))
|
||||
precision = extraParam;
|
||||
|
||||
if (params && params.length > 0 && Array.isArray(params[0]) && modifiers.includes(Modifiers.VALUES)) {
|
||||
|
||||
const paramValues = params[0].filter((value: any) => value.Name !== undefined).map((value: any) => value.Name);
|
||||
if (paramValues.every(v => typeof v === "string")) {
|
||||
values = JSON.stringify(paramValues);
|
||||
}
|
||||
}
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && params && params.length > 0 && !isNaN(params[0]) && !precision) {
|
||||
precision = params[0];
|
||||
}
|
||||
|
||||
// get field defintion options
|
||||
const options: any | undefined = [] = ast.options;
|
||||
|
||||
try {
|
||||
for (const option of options) {
|
||||
if (typeof option.option == "string") {
|
||||
|
||||
if (option.option == "Null")
|
||||
nullable = true;
|
||||
|
||||
if (option.option == "NotNull")
|
||||
nullable = false;
|
||||
|
||||
continue;
|
||||
}
|
||||
else if (typeof option.option === "object") {
|
||||
|
||||
if (option.option.PrimaryKey) {
|
||||
isPrimary = true;
|
||||
continue;
|
||||
}
|
||||
if (option.option.Unique) {
|
||||
unique = true;
|
||||
continue;
|
||||
}
|
||||
if (option.option.Default) {
|
||||
|
||||
defaultValue = this.processDefaultValue(option.option.Default, dataType);
|
||||
continue;
|
||||
}
|
||||
if (option.option.DialectSpecific && option.option.DialectSpecific.length > 0) {
|
||||
for (const { Word } of option.option.DialectSpecific) {
|
||||
if (Word?.keyword == "AUTO_INCREMENT") {
|
||||
autoIncrement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (option.option.ForeignKey) {
|
||||
fk_constraint = option.option.ForeignKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
throw Error("Failed to extract column options")
|
||||
}
|
||||
|
||||
if (isPrimary) {
|
||||
nullable = false;
|
||||
}
|
||||
|
||||
return {
|
||||
field: {
|
||||
id: v4(),
|
||||
name: ast.name.value,
|
||||
typeId: dataType?.id,
|
||||
nullable,
|
||||
autoIncrement,
|
||||
unique,
|
||||
isPrimary,
|
||||
maxLength,
|
||||
precision,
|
||||
scale,
|
||||
defaultValue,
|
||||
sequence,
|
||||
values
|
||||
} as FieldInsertType,
|
||||
fk_constraint
|
||||
};
|
||||
|
||||
}
|
||||
protected astToRelationship(ast: any, tables: TableInsertType[]): RelationshipInsertType {
|
||||
|
||||
|
||||
let targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.target_table);
|
||||
let sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.foreign_table?.at(-1)?.Identifier?.value);
|
||||
let sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == ast.referred_columns?.[0]?.value);
|
||||
let targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == ast.columns?.[0]?.value);
|
||||
|
||||
if (!sourceTable || !sourceField || !targetField || !targetTable) {
|
||||
throw Error("Inavlid FK Constraint")
|
||||
|
||||
}
|
||||
|
||||
const onDelete: ForeignKeyActions | undefined = ast.on_delete ? this.astToForiegnKeyAction(ast.on_delete) : undefined;
|
||||
const onUpdate: ForeignKeyActions | undefined = ast.on_update ? this.astToForiegnKeyAction(ast.on_update) : undefined;
|
||||
const name: string | undefined = ast.name?.value;
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name,
|
||||
sourceFieldId: sourceField.id,
|
||||
targetFieldId: targetField.id,
|
||||
sourceTableId: sourceTable.id,
|
||||
targetTableId: targetTable.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete,
|
||||
onUpdate
|
||||
} as RelationshipInsertType
|
||||
}
|
||||
protected astToIndex(ast: any, tables: TableInsertType[]): ParsedIndex {
|
||||
|
||||
const tableName: string = ast.CreateIndex.table_name?.at(-1)?.Identifier?.value;
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||
|
||||
if (!table)
|
||||
throw Error("index table not found");
|
||||
|
||||
const columnNames: string[] = ast.CreateIndex.columns.map(({ column }: any) => column.expr?.Identifier?.value)
|
||||
.filter((column: string | undefined) => column != undefined);
|
||||
|
||||
const fieldIds: string[] = (table.fields?.filter((field: FieldInsertType) => columnNames.includes(field.name)) as FieldInsertType[])
|
||||
.map((field: FieldInsertType) => field.id);
|
||||
|
||||
let indexName: string = ast.CreateIndex.name?.[0]?.Identifier?.value;
|
||||
if (!indexName) {
|
||||
indexName = `${tableName}_${columnNames.join("_")}_index`;
|
||||
}
|
||||
|
||||
return {
|
||||
index: {
|
||||
id: v4(),
|
||||
name: indexName,
|
||||
tableId: table.id,
|
||||
unique: ast.CreateIndex.unique,
|
||||
fieldIndices: fieldIds?.map((id: string) => ({
|
||||
id: v4(),
|
||||
fieldId: id
|
||||
}))
|
||||
} as IndexInsertType,
|
||||
error: undefined
|
||||
}
|
||||
}
|
||||
|
||||
protected astToForiegnKeyAction(ast: ReferentialAction): ForeignKeyActions {
|
||||
switch (ast) {
|
||||
case "Cascade":
|
||||
return ForeignKeyActions.CASCADE;
|
||||
case "SetNull":
|
||||
return ForeignKeyActions.SET_NULL;
|
||||
case 'Restrict':
|
||||
return ForeignKeyActions.RESTRICT;
|
||||
case "SetDefault":
|
||||
return ForeignKeyActions.SET_DEFAULT;
|
||||
}
|
||||
return ForeignKeyActions.NO_ACTION;
|
||||
}
|
||||
|
||||
protected processDataType(typeName: string): DataType | undefined {
|
||||
|
||||
|
||||
// get data type from the supported ones
|
||||
return this.data_types.find((dataType: DataType) => {
|
||||
let synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
synonyms = synonyms.map((synonym: string) => synonym.replace(/ /g, ''));
|
||||
return (dataType.name as string).replace(/ /g, '') == typeName || synonyms.includes(typeName as string)
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
|
||||
if (ast.Value && ast.Value.value) {
|
||||
|
||||
const value: any = ast.Value.value;
|
||||
if (value.Number && Array.isArray(value.Number) && value.Number.length > 0 && !isNaN(Number(value.Number[0]))) {
|
||||
return value.Number[0];
|
||||
}
|
||||
|
||||
else if (value.SingleQuotedString || value.DoubleQuotedString) {
|
||||
const valuesArray = Object.values(value);
|
||||
if (valuesArray.length > 0)
|
||||
return valuesArray[0] as string;
|
||||
}
|
||||
|
||||
else if (value.Boolean) {
|
||||
return "true";
|
||||
}
|
||||
|
||||
else if (value.NationalStringLiteral) {
|
||||
return value.NationalStringLiteral
|
||||
}
|
||||
|
||||
} else if (ast.Function) {
|
||||
if (dataType.type == DataTypes.TIME) {
|
||||
if (ast.Function.name?.[0]?.Identifier?.value && this.isCurrentTimesTampFunction(ast.Function.name?.[0]?.Identifier?.value))
|
||||
return TimeDefaultValues.NOW;
|
||||
|
||||
}
|
||||
} else if (ast.TypedString) {
|
||||
|
||||
const value = ast.TypedString.value?.value;
|
||||
|
||||
if (value.SingleQuotedString || value.DoubleQuotedString) {
|
||||
const valuesArray = Object.values(value);
|
||||
if (valuesArray.length > 0)
|
||||
return valuesArray[0] as string;
|
||||
}
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||
const upper: string = value.toUpperCase();
|
||||
|
||||
return upper == "CURRENT_TIMESTAMP" || upper == "NOW";
|
||||
}
|
||||
|
||||
|
||||
protected cleanSql(sql: string): string {
|
||||
// Clean up SQL: remove comments and normalize
|
||||
const cleanedSql = sql
|
||||
.replace(/--.*$/gm, '') // remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
||||
.replace(/\s+/g, ' ') // normalize whitespace
|
||||
.replace(/;\s*/g, ';\n'); // separate statements
|
||||
return cleanedSql;
|
||||
}
|
||||
|
||||
protected extractStatments(sql: string): ExtractedStatments {
|
||||
|
||||
const createTableStatements: string[] = [];
|
||||
const alterTableStatements: string[] = [];
|
||||
const createIndexStatements: string[] = [];
|
||||
|
||||
// Split into individual statements
|
||||
const statements = sql
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
|
||||
for (const stmt of statements) {
|
||||
const upper = stmt.toUpperCase();
|
||||
|
||||
if (upper.startsWith('CREATE TABLE')) {
|
||||
createTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('ALTER TABLE')) {
|
||||
alterTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) {
|
||||
createIndexStatements.push(stmt);
|
||||
}
|
||||
}
|
||||
return {
|
||||
createTableStatements,
|
||||
alterTableStatements,
|
||||
createIndexStatements
|
||||
} as ExtractedStatments;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export type ParsedDatabaase = {
|
||||
tables: TableInsertType[],
|
||||
relationships: RelationshipInsertType[],
|
||||
indexes: IndexInsertType[],
|
||||
errors: Error[]
|
||||
}
|
||||
|
||||
export type ParsedTable = {
|
||||
table: TableInsertType;
|
||||
errors: Error[];
|
||||
fk_constraints: any[] | undefined,
|
||||
}
|
||||
|
||||
export type ParsedIndex = {
|
||||
index: IndexInsertType;
|
||||
error?: Error;
|
||||
|
||||
}
|
||||
|
||||
export type ParsedField = {
|
||||
field: FieldInsertType,
|
||||
fk_constraint: any | undefined
|
||||
}
|
||||
|
||||
export type ExtractedStatments = {
|
||||
createTableStatements: string[],
|
||||
alterTableStatements: string[],
|
||||
createIndexStatements: string[],
|
||||
createPostgresTypesStatements?: string[],
|
||||
alterSequenceStatments?: string[]
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
START TRANSACTION;
|
||||
|
||||
CREATE TABLE `users` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX `users_usernae_index` ON `users` (`username`);
|
||||
|
||||
COMMIT;
|
||||
|
||||
|
||||
CREATE TABLE `users` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
username VARCHAR(255) NOT NULL UNIQUE,
|
||||
email VARCHAR(255) NOT NULL UNIQUE,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
last_login_at DATETIME NULL,
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
updated_at TEXT NULL DEFAULT 'Hello world' UNIQUE
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_users_created_at_index` ON `users` (`created_at`);
|
||||
|
||||
CREATE INDEX `idx_users_last_login_at` ON `users` (`last_login_at`);
|
||||
|
||||
CREATE TABLE `categories` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `tags` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL UNIQUE
|
||||
);
|
||||
|
||||
CREATE TABLE `projects` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
category_id INTEGER NULL,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NULL,
|
||||
due_date DATETIME NULL,
|
||||
status ENUM (
|
||||
'planning',
|
||||
'in_progress',
|
||||
'completed',
|
||||
'on_hold',
|
||||
'draft'
|
||||
) NOT NULL DEFAULT 'draft',
|
||||
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`),
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE TABLE `tasks` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description TEXT NULL,
|
||||
status ENUM ('todo', 'in_progress', 'done') NOT NULL DEFAULT 'todo',
|
||||
priority ENUM ('low', 'medium', 'high') NULL DEFAULT 'medium',
|
||||
due_date DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
project_id INTEGER NULL,
|
||||
assigned_to_user_id INTEGER NULL,
|
||||
completed_at DATETIME NULL,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`),
|
||||
FOREIGN KEY (`assigned_to_user_id`) REFERENCES `users` (`id`)
|
||||
);
|
||||
|
||||
CREATE TABLE `comments` (
|
||||
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||
task_id INTEGER NOT NULL,
|
||||
comment TEXT NOT NULL,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL,
|
||||
updated_at DATETIME NULL,
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||
FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`)
|
||||
);
|
||||
|
||||
CREATE INDEX `idx_comments_created_at` ON `comments` (`created_at`);
|
||||
|
||||
CREATE INDEX `idx_comments_user_id` ON `comments` (`user_id`);
|
||||
|
||||
CREATE INDEX `idx_comments_task_id` ON `comments` (`task_id`);
|
||||
|
||||
CREATE TABLE `task_tags` (
|
||||
task_id INTEGER NOT NULL,
|
||||
tag_id INTEGER NOT NULL,
|
||||
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
|
||||
FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`)
|
||||
)
|
||||
|
||||
CREATE TABLE all_mysql_types (
|
||||
-- Numeric types
|
||||
col_tinyint TINYINT(3),
|
||||
col_smallint SMALLINT(5),
|
||||
col_mediumint MEDIUMINT(7),
|
||||
col_int INT(11),
|
||||
col_bigint BIGINT(20),
|
||||
|
||||
col_decimal DECIMAL(10, 2),
|
||||
col_numeric NUMERIC(8, 3),
|
||||
|
||||
col_float FLOAT(7, 4),
|
||||
col_double DOUBLE(15, 8),
|
||||
col_real REAL,
|
||||
|
||||
col_boolean BOOLEAN,
|
||||
|
||||
-- Date & time types
|
||||
col_date DATE,
|
||||
col_datetime DATETIME(6),
|
||||
col_timestamp TIMESTAMP(6),
|
||||
col_time TIME(6),
|
||||
col_year YEAR,
|
||||
|
||||
-- String types
|
||||
col_char CHAR(10),
|
||||
col_varchar VARCHAR(255),
|
||||
|
||||
col_binary BINARY(16),
|
||||
col_varbinary VARBINARY(255),
|
||||
|
||||
col_tinytext TINYTEXT,
|
||||
col_text TEXT,
|
||||
col_mediumtext MEDIUMTEXT,
|
||||
col_longtext LONGTEXT,
|
||||
|
||||
col_tinyblob TINYBLOB,
|
||||
col_blob BLOB,
|
||||
col_mediumblob MEDIUMBLOB,
|
||||
col_longblob LONGBLOB,
|
||||
|
||||
col_enum ENUM('A', 'B', 'C'),
|
||||
col_set SET('X', 'Y', 'Z'),
|
||||
|
||||
-- JSON
|
||||
col_json JSON,
|
||||
|
||||
-- Spatial types
|
||||
col_geometry GEOMETRY,
|
||||
col_point POINT,
|
||||
col_linestring LINESTRING,
|
||||
col_polygon POLYGON
|
||||
);
|
||||
|
||||
|
||||
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,40 @@
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { BaseSqlImporter } from "./base-sql-importer";
|
||||
import { MysqlImporter } from "./mysql-importer";
|
||||
import { MariaDbImporter } from "./mariadb-importer";
|
||||
import { SqliteImporter } from "./sqlite-importer";
|
||||
import { PostgreSqlImporter } from "./postgresql-importer";
|
||||
import { OracleImporter } from "./oracle-importer";
|
||||
import { MSSQLImporter } from "./mssql-importer";
|
||||
|
||||
export const getImporter = (dialect: DatabaseDialect, data_types: DataType[]) => {
|
||||
let importer: BaseSqlImporter | undefined = undefined;
|
||||
|
||||
|
||||
switch (dialect) {
|
||||
case DatabaseDialect.MYSQL:
|
||||
importer = (new MysqlImporter(data_types));
|
||||
break;
|
||||
case DatabaseDialect.MARIADB:
|
||||
importer = (new MariaDbImporter(data_types));
|
||||
break;
|
||||
case DatabaseDialect.SQLITE:
|
||||
importer = (new SqliteImporter(data_types));
|
||||
break;
|
||||
case DatabaseDialect.POSTGRES:
|
||||
importer = (new PostgreSqlImporter(data_types));
|
||||
break;
|
||||
case DatabaseDialect.ORACLE:
|
||||
importer = (new OracleImporter(data_types));
|
||||
break;
|
||||
case DatabaseDialect.MSSQL:
|
||||
importer = (new MSSQLImporter(data_types));
|
||||
break;
|
||||
default:
|
||||
importer = new BaseSqlImporter(data_types);
|
||||
break;
|
||||
}
|
||||
|
||||
return importer;
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { MysqlImporter } from "./mysql-importer";
|
||||
|
||||
export class MariaDbImporter extends MysqlImporter {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.MYSQL;
|
||||
}
|
||||
|
||||
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
|
||||
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||
|
||||
if (!defaultValue) {
|
||||
if (ast.Nested?.Function) {
|
||||
if ( ast.Nested.Function.name?.[0]?.Identifier?.value?.toUpperCase() == "UUID" ) {
|
||||
|
||||
return "random" ;
|
||||
}
|
||||
}
|
||||
if (ast.Function) {
|
||||
if ( ast.Function.name?.[0]?.Identifier?.value.toUpperCase() == "UUID" ) {
|
||||
|
||||
return "random" ;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,702 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||
import { TimeDefaultValues } from "@/lib/field";
|
||||
|
||||
export class MSSQLImporter extends BaseSqlImporter {
|
||||
|
||||
|
||||
private dfConstraints: string[] = [];
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.MSSQL;
|
||||
}
|
||||
|
||||
public parseSql(sql: string) {
|
||||
try {
|
||||
this.dfConstraints = [];
|
||||
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||
for (const constraint of this.dfConstraints) {
|
||||
try {
|
||||
this.processDefaultValueConstraint(constraint, parsedDatabaase.tables);
|
||||
} catch (error) {
|
||||
parsedDatabaase.errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
return parsedDatabaase;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected astToField(ast: any, sequence: number): ParsedField {
|
||||
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||
// get field defintion options
|
||||
const options: any | undefined = [] = ast.options;
|
||||
for (const option of options) {
|
||||
if (option.option?.Identity) {
|
||||
field.autoIncrement = true;
|
||||
}
|
||||
}
|
||||
|
||||
return { field, fk_constraint };
|
||||
}
|
||||
private processDefaultValueConstraint(constraint: string, tables: TableInsertType[]) {
|
||||
const regex =
|
||||
/ALTER\s+TABLE\s+(?:(?:\[(?<schema1>[^\]]+)\]|(?<schema2>\w+))\.)?(?:\[(?<table1>[^\]]+)\]|(?<table2>\w+))\s+ADD\s+CONSTRAINT\s+(?:\[(?<constraint1>[^\]]+)\]|(?<constraint2>\w+))\s+DEFAULT\s*\((?<defaultValue>.*?)\)\s+FOR\s+(?:\[(?<column1>[^\]]+)\]|(?<column2>\w+))/is;
|
||||
|
||||
const match = constraint.match(regex);
|
||||
|
||||
if (match?.groups) {
|
||||
const schema =
|
||||
match.groups.schema1 || match.groups.schema2 || null;
|
||||
|
||||
const tableName =
|
||||
match.groups.table1 || match.groups.table2;
|
||||
|
||||
const constraint =
|
||||
match.groups.constraint1 || match.groups.constraint2;
|
||||
|
||||
const column =
|
||||
match.groups.column1 || match.groups.column2;
|
||||
|
||||
const defaultValue =
|
||||
match.groups.defaultValue?.trim()
|
||||
// remove only wrapping parentheses around the whole value
|
||||
.replace(/^\((.*)\)$/s, "$1")
|
||||
|
||||
// repeat once more for cases like ((1))
|
||||
.replace(/^\((.*)\)$/s, "$1")
|
||||
|
||||
// remove surrounding quotes
|
||||
.replace(/^['"](.*)['"]$/s, "$1");
|
||||
|
||||
|
||||
if (tableName && column && defaultValue) {
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||
if (!table)
|
||||
throw Error("Table in DF constraint not found : " + constraint);
|
||||
|
||||
const field: FieldInsertType | undefined = table.fields?.find((field: FieldInsertType) => field.name == column);
|
||||
if (!field) {
|
||||
throw Error("field not found in DF constraint : " + constraint);
|
||||
}
|
||||
|
||||
const isFunctionRegex =
|
||||
/^[a-zA-Z_][a-zA-Z0-9_]*\s*\([^()]*\)$/;
|
||||
|
||||
if (isFunctionRegex.test(defaultValue)) {
|
||||
// in case the default value is a function sysdatetimeoffset() , CONVERT([date],getdate()) ... ect
|
||||
// if it's a function of time current timestamp.
|
||||
const upperDefaultValue: string = defaultValue.toUpperCase();
|
||||
if (upperDefaultValue.includes("GETDATE") || upperDefaultValue.includes("SYSDATETIME") || upperDefaultValue.includes("SYSDATETIMEOFFSET")) {
|
||||
field.defaultValue = TimeDefaultValues.NOW;
|
||||
} else if (upperDefaultValue.includes("NEWID")) {
|
||||
|
||||
field.defaultValue = "random";
|
||||
}
|
||||
|
||||
} else {
|
||||
field.defaultValue = defaultValue;
|
||||
}
|
||||
|
||||
} else
|
||||
throw Error("Failed to process a MSSQL DF constraint : " + constraint);
|
||||
}
|
||||
|
||||
}
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||
if (dataType.name == "bit" && defaultValue) {
|
||||
return defaultValue == "1" ? "true" : "false";
|
||||
}
|
||||
else if (!defaultValue && dataType.name == "uniqueidentifier") {
|
||||
if (ast.Function) {
|
||||
if (ast.Function.name?.[0]?.Identifier?.value && (ast.Function.name?.[0]?.Identifier?.value == "NEWSEQUENTIALID" || ast.Function.name?.[0]?.Identifier?.value == "NEWID")) {
|
||||
return "random";
|
||||
}
|
||||
}
|
||||
return defaultValue
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||
return value == "GETDATE" || value == "SYSDATETIME" || value == "SYSDATETIMEOFFSET";
|
||||
}
|
||||
|
||||
protected cleanSql(sql: string): string {
|
||||
const cleanedSql = sql // remove GO batches
|
||||
.replace(/([^;\s])\s*\r?\nGO\b/gm, '$1;')
|
||||
.replace(/^\s*GO\s*$/gm, '')
|
||||
|
||||
|
||||
// remove WITH CHECK
|
||||
.replace(/\bWITH\s+CHECK\b\s*/gi, '')
|
||||
|
||||
// remove CHECK CONSTRAINT enable statements (FK validation step)
|
||||
.replace(
|
||||
/ALTER TABLE\s+\[?.*?\]?\.?\[?.*?\]?\s+CHECK\s+CONSTRAINT\s+\[?.*?\]?;\s*/gi,
|
||||
''
|
||||
)
|
||||
// remove computed columns
|
||||
.replace(
|
||||
/^\s*(?:\[[^\]]+\]|\w+)\s+AS\s+.*?(?:PERSISTED\s*)?,?\s*$/gim,
|
||||
''
|
||||
)
|
||||
.replace(
|
||||
/(\[[^\]]+\]|\w+)\s+([^,]*?)\s+FOREIGN\s+KEY\s+REFERENCES\s+([^\s,()]+)\s*\(\s*([^\s,()]+)\s*\)/gi,
|
||||
(_match, col, beforeRefs, table, refCol) => {
|
||||
return `${col} ${beforeRefs.trim()} REFERENCES ${table}(${refCol})`;
|
||||
}
|
||||
)
|
||||
// remove CLUSTERED / NONCLUSTERED
|
||||
.replace(/\bCLUSTERED\b/g, '')
|
||||
.replace(/\bNONCLUSTERED\b/g, '')
|
||||
|
||||
// remove ASC / DESC inside constraints
|
||||
.replace(/\s+\bASC\b/g, '')
|
||||
.replace(/\s+\bDESC\b/g, '')
|
||||
|
||||
// remove WITH (...) ON [PRIMARY]
|
||||
.replace(/\)\s*WITH\s*\([^)]+\)\s*ON\s*\[[^\]]+\]/g, ')')
|
||||
|
||||
// remove trailing table storage clauses
|
||||
.replace(/\)\s*ON\s*\[[^\]]+\](\s*TEXTIMAGE_ON\s*\[[^\]]+\])?/g, ')')
|
||||
|
||||
// cleanup extra spaces
|
||||
.replace(/[ \t]+/g, ' ')
|
||||
.replace(/\n\s+\n/g, '\n\n');
|
||||
|
||||
|
||||
return super.cleanSql(cleanedSql);
|
||||
}
|
||||
|
||||
protected extractStatments(sql: string): ExtractedStatments {
|
||||
|
||||
const statments: ExtractedStatments = super.extractStatments(sql);
|
||||
|
||||
statments.alterTableStatements = statments.alterTableStatements.filter((statment: string) => {
|
||||
if (/ALTER\s+TABLE\s+\[[^\]]+\]\.\[[^\]]+\]\s+ADD\s+CONSTRAINT\s+\[[^\]]+\]\s+DEFAULT\s*\(.*?\)\s+FOR\s+\[[^\]]+\]/i.test(statment)) {
|
||||
|
||||
this.dfConstraints.push(statment);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
return statment;
|
||||
}
|
||||
})
|
||||
return statments
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
-- =============================================
|
||||
-- Enterprise E-Commerce Platform Database
|
||||
-- Version: 1.0 (No CHECK Constraints)
|
||||
-- =============================================
|
||||
|
||||
-- Drop database if exists (for clean creation)
|
||||
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'ECommerceDB')
|
||||
BEGIN
|
||||
ALTER DATABASE ECommerceDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||
DROP DATABASE ECommerceDB;
|
||||
END
|
||||
GO
|
||||
|
||||
CREATE DATABASE ECommerceDB;
|
||||
GO
|
||||
|
||||
USE ECommerceDB;
|
||||
GO
|
||||
|
||||
-- =============================================
|
||||
-- 1. Customer Management Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE Customers (
|
||||
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerNumber AS 'CUST-' + RIGHT('000000' + CAST(CustomerID AS VARCHAR(6)), 6) PERSISTED,
|
||||
Email VARCHAR(255) NOT NULL UNIQUE,
|
||||
Phone VARCHAR(20) NOT NULL,
|
||||
FirstName NVARCHAR(50) NOT NULL,
|
||||
LastName NVARCHAR(50) NOT NULL,
|
||||
DateOfBirth DATE NULL,
|
||||
TaxID VARCHAR(50) NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
UpdatedAt DATETIME2 NOT NULL DEFAULT GETDATE()
|
||||
);
|
||||
|
||||
CREATE TABLE CustomerAddresses (
|
||||
AddressID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerID INT NOT NULL,
|
||||
AddressType VARCHAR(20) NOT NULL, -- Billing, Shipping, Both
|
||||
AddressLine1 NVARCHAR(200) NOT NULL,
|
||||
AddressLine2 NVARCHAR(200) NULL,
|
||||
City NVARCHAR(100) NOT NULL,
|
||||
StateProvince NVARCHAR(100) NOT NULL,
|
||||
PostalCode VARCHAR(20) NOT NULL,
|
||||
CountryCode CHAR(2) NOT NULL,
|
||||
IsDefault BIT NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE CustomerLoyalty (
|
||||
LoyaltyID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerID INT NOT NULL UNIQUE,
|
||||
PointsBalance INT NOT NULL DEFAULT 0,
|
||||
TierLevel VARCHAR(20) NOT NULL DEFAULT 'Bronze',
|
||||
TotalSpent DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
JoinDate DATE NOT NULL DEFAULT GETDATE(),
|
||||
PointsExpiryDate DATE NULL,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE CustomerPreferences (
|
||||
PreferenceID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerID INT NOT NULL,
|
||||
PreferenceKey VARCHAR(50) NOT NULL,
|
||||
PreferenceValue NVARCHAR(500) NULL,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE,
|
||||
CONSTRAINT UQ_Customer_Preference UNIQUE (CustomerID, PreferenceKey)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 2. Product Catalog Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE Categories (
|
||||
CategoryID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ParentCategoryID INT NULL,
|
||||
CategoryName NVARCHAR(100) NOT NULL,
|
||||
CategoryCode VARCHAR(50) NOT NULL UNIQUE,
|
||||
Description NVARCHAR(500) NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
DisplayOrder INT NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (ParentCategoryID) REFERENCES Categories(CategoryID)
|
||||
);
|
||||
|
||||
CREATE TABLE Brands (
|
||||
BrandID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
BrandName NVARCHAR(100) NOT NULL UNIQUE,
|
||||
BrandCode VARCHAR(50) NOT NULL UNIQUE,
|
||||
Website VARCHAR(255) NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE Products (
|
||||
ProductID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
SKU VARCHAR(50) NOT NULL UNIQUE,
|
||||
ProductName NVARCHAR(200) NOT NULL,
|
||||
Description NVARCHAR(MAX) NULL,
|
||||
CategoryID INT NOT NULL,
|
||||
BrandID INT NULL,
|
||||
UnitPrice DECIMAL(18,2) NOT NULL,
|
||||
Weight DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
IsTaxable BIT NOT NULL DEFAULT 1,
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
ModifiedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (CategoryID) REFERENCES Categories(CategoryID),
|
||||
FOREIGN KEY (BrandID) REFERENCES Brands(BrandID)
|
||||
);
|
||||
|
||||
CREATE TABLE ProductAttributes (
|
||||
AttributeID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
AttributeName NVARCHAR(100) NOT NULL UNIQUE,
|
||||
DataType VARCHAR(20) NOT NULL -- String, Integer, Decimal, Boolean, Date
|
||||
);
|
||||
|
||||
CREATE TABLE ProductAttributeValues (
|
||||
ProductID INT NOT NULL,
|
||||
AttributeID INT NOT NULL,
|
||||
AttributeValue NVARCHAR(500) NOT NULL,
|
||||
PRIMARY KEY (ProductID, AttributeID),
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (AttributeID) REFERENCES ProductAttributes(AttributeID)
|
||||
);
|
||||
|
||||
CREATE TABLE Inventory (
|
||||
InventoryID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ProductID INT NOT NULL,
|
||||
WarehouseCode VARCHAR(20) NOT NULL,
|
||||
QuantityOnHand INT NOT NULL DEFAULT 0,
|
||||
QuantityReserved INT NOT NULL DEFAULT 0,
|
||||
ReorderLevel INT NOT NULL DEFAULT 10,
|
||||
ReorderQuantity INT NOT NULL DEFAULT 50,
|
||||
LastRestockedDate DATETIME2 NULL,
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
|
||||
CONSTRAINT UQ_Product_Warehouse UNIQUE (ProductID, WarehouseCode)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 3. Pricing & Promotions Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE PriceTiers (
|
||||
TierID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
TierName VARCHAR(50) NOT NULL UNIQUE,
|
||||
MinQuantity INT NOT NULL,
|
||||
DiscountPercentage DECIMAL(5,2) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE ProductPricing (
|
||||
PricingID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ProductID INT NOT NULL,
|
||||
TierID INT NOT NULL,
|
||||
Price DECIMAL(18,2) NOT NULL,
|
||||
EffectiveFrom DATE NOT NULL,
|
||||
EffectiveTo DATE NULL,
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
|
||||
FOREIGN KEY (TierID) REFERENCES PriceTiers(TierID)
|
||||
);
|
||||
|
||||
CREATE TABLE Promotions (
|
||||
PromotionID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
PromotionCode VARCHAR(50) NOT NULL UNIQUE,
|
||||
PromotionName NVARCHAR(200) NOT NULL,
|
||||
DiscountType VARCHAR(20) NOT NULL, -- Percentage, FixedAmount, BuyOneGetOne
|
||||
DiscountValue DECIMAL(18,2) NOT NULL,
|
||||
StartDate DATETIME2 NOT NULL,
|
||||
EndDate DATETIME2 NOT NULL,
|
||||
MinOrderAmount DECIMAL(18,2) NULL,
|
||||
MaxDiscountAmount DECIMAL(18,2) NULL,
|
||||
UsageLimit INT NULL,
|
||||
UsedCount INT NOT NULL DEFAULT 0,
|
||||
IsActive BIT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE PromotionApplicability (
|
||||
ApplicabilityID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
PromotionID INT NOT NULL,
|
||||
ApplicableToType VARCHAR(20) NOT NULL, -- AllProducts, SpecificProduct, SpecificCategory, SpecificBrand
|
||||
ReferenceID INT NULL,
|
||||
FOREIGN KEY (PromotionID) REFERENCES Promotions(PromotionID)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 4. Order Management Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE OrderStatus (
|
||||
StatusID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
StatusName VARCHAR(50) NOT NULL UNIQUE,
|
||||
StatusDescription NVARCHAR(200) NULL,
|
||||
IsFinalState BIT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE Orders (
|
||||
OrderID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderNumber AS 'ORD-' + FORMAT(OrderID, '000000') PERSISTED,
|
||||
CustomerID INT NOT NULL,
|
||||
OrderDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
StatusID INT NOT NULL,
|
||||
PaymentStatus VARCHAR(30) NOT NULL DEFAULT 'Pending', -- Pending, Paid, Failed, Refunded
|
||||
ShippingAddressID INT NOT NULL,
|
||||
BillingAddressID INT NOT NULL,
|
||||
Subtotal DECIMAL(18,2) NOT NULL,
|
||||
TaxAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
ShippingAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
DiscountAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
TotalAmount DECIMAL(18,2) NOT NULL,
|
||||
PromotionID INT NULL,
|
||||
Notes NVARCHAR(MAX) NULL,
|
||||
OrderWeight DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||
CompletedDate DATETIME2 NULL,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID),
|
||||
FOREIGN KEY (StatusID) REFERENCES OrderStatus(StatusID),
|
||||
FOREIGN KEY (ShippingAddressID) REFERENCES CustomerAddresses(AddressID),
|
||||
FOREIGN KEY (BillingAddressID) REFERENCES CustomerAddresses(AddressID),
|
||||
FOREIGN KEY (PromotionID) REFERENCES Promotions(PromotionID)
|
||||
);
|
||||
|
||||
CREATE TABLE OrderItems (
|
||||
OrderItemID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderID INT NOT NULL,
|
||||
ProductID INT NOT NULL,
|
||||
Quantity INT NOT NULL,
|
||||
UnitPrice DECIMAL(18,2) NOT NULL,
|
||||
DiscountApplied DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
TotalPrice DECIMAL(18,2) NOT NULL,
|
||||
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID)
|
||||
);
|
||||
|
||||
CREATE TABLE OrderTracking (
|
||||
TrackingID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderID INT NOT NULL,
|
||||
StatusID INT NOT NULL,
|
||||
StatusDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
Notes NVARCHAR(500) NULL,
|
||||
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (StatusID) REFERENCES OrderStatus(StatusID)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 5. Payment & Transaction Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE PaymentMethods (
|
||||
PaymentMethodID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
MethodName VARCHAR(50) NOT NULL UNIQUE, -- CreditCard, PayPal, BankTransfer, Cash
|
||||
IsActive BIT NOT NULL DEFAULT 1,
|
||||
ProcessingFee DECIMAL(5,2) NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE Currencies (
|
||||
CurrencyID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CurrencyCode CHAR(3) NOT NULL UNIQUE, -- USD, EUR, GBP, JPY
|
||||
CurrencyName NVARCHAR(50) NOT NULL,
|
||||
ExchangeRateToUSD DECIMAL(18,6) NOT NULL,
|
||||
IsBaseCurrency BIT NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE Payments (
|
||||
PaymentID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderID INT NOT NULL,
|
||||
PaymentMethodID INT NOT NULL,
|
||||
CurrencyID INT NOT NULL,
|
||||
Amount DECIMAL(18,2) NOT NULL,
|
||||
PaymentDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
TransactionID VARCHAR(100) NULL,
|
||||
PaymentStatus VARCHAR(30) NOT NULL DEFAULT 'Pending',
|
||||
GatewayResponse NVARCHAR(MAX) NULL,
|
||||
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
|
||||
FOREIGN KEY (PaymentMethodID) REFERENCES PaymentMethods(PaymentMethodID),
|
||||
FOREIGN KEY (CurrencyID) REFERENCES Currencies(CurrencyID)
|
||||
);
|
||||
|
||||
CREATE TABLE Refunds (
|
||||
RefundID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
PaymentID INT NOT NULL,
|
||||
RefundAmount DECIMAL(18,2) NOT NULL,
|
||||
RefundDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
Reason NVARCHAR(500) NULL,
|
||||
ApprovalStatus VARCHAR(30) NOT NULL DEFAULT 'Pending',
|
||||
ProcessedBy INT NULL,
|
||||
FOREIGN KEY (PaymentID) REFERENCES Payments(PaymentID)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 6. Shipping & Logistics Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE ShippingMethods (
|
||||
ShippingMethodID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
MethodName VARCHAR(100) NOT NULL UNIQUE, -- Standard, Express, Overnight
|
||||
EstimatedDeliveryDays INT NOT NULL,
|
||||
BaseCost DECIMAL(18,2) NOT NULL,
|
||||
CostPerKg DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||
IsActive BIT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
CREATE TABLE Shipments (
|
||||
ShipmentID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
OrderID INT NOT NULL,
|
||||
ShippingMethodID INT NOT NULL,
|
||||
TrackingNumber VARCHAR(100) NULL,
|
||||
CarrierName VARCHAR(100) NOT NULL,
|
||||
ShippedDate DATETIME2 NULL,
|
||||
DeliveredDate DATETIME2 NULL,
|
||||
ShippingCost DECIMAL(18,2) NOT NULL,
|
||||
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
|
||||
FOREIGN KEY (ShippingMethodID) REFERENCES ShippingMethods(ShippingMethodID)
|
||||
);
|
||||
|
||||
CREATE TABLE Warehouses (
|
||||
WarehouseID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
WarehouseCode VARCHAR(20) NOT NULL UNIQUE,
|
||||
WarehouseName NVARCHAR(100) NOT NULL,
|
||||
LocationAddress NVARCHAR(500) NOT NULL,
|
||||
City NVARCHAR(100) NOT NULL,
|
||||
CountryCode CHAR(2) NOT NULL,
|
||||
IsActive BIT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- Add WarehouseCode foreign key to Inventory
|
||||
ALTER TABLE Inventory ADD WarehouseID INT NULL;
|
||||
ALTER TABLE Inventory ADD FOREIGN KEY (WarehouseID) REFERENCES Warehouses(WarehouseID);
|
||||
ALTER TABLE Inventory ALTER COLUMN WarehouseCode VARCHAR(20) NULL;
|
||||
|
||||
-- =============================================
|
||||
-- 7. Reviews & Ratings Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE ProductReviews (
|
||||
ReviewID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ProductID INT NOT NULL,
|
||||
CustomerID INT NOT NULL,
|
||||
Rating INT NOT NULL, -- 1 to 5 stars
|
||||
Title NVARCHAR(200) NULL,
|
||||
ReviewText NVARCHAR(MAX) NULL,
|
||||
IsVerifiedPurchase BIT NOT NULL DEFAULT 0,
|
||||
HelpfulCount INT NOT NULL DEFAULT 0,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
IsApproved BIT NOT NULL DEFAULT 0,
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID)
|
||||
);
|
||||
|
||||
CREATE TABLE ReviewHelpfulness (
|
||||
HelpfulnessID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
ReviewID INT NOT NULL,
|
||||
CustomerID INT NOT NULL,
|
||||
IsHelpful BIT NOT NULL,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (ReviewID) REFERENCES ProductReviews(ReviewID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID),
|
||||
CONSTRAINT UQ_Review_Customer UNIQUE (ReviewID, CustomerID)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- 8. Shopping Cart Schema
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE ShoppingCarts (
|
||||
CartID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CustomerID INT NOT NULL UNIQUE,
|
||||
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
LastModified DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE CartItems (
|
||||
CartItemID INT IDENTITY(1,1) PRIMARY KEY,
|
||||
CartID INT NOT NULL,
|
||||
ProductID INT NOT NULL,
|
||||
Quantity INT NOT NULL,
|
||||
AddedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||
FOREIGN KEY (CartID) REFERENCES ShoppingCarts(CartID) ON DELETE CASCADE,
|
||||
FOREIGN KEY (ProductID) REFERENCES Products(ProductID)
|
||||
);
|
||||
|
||||
-- =============================================
|
||||
-- INDEXES for Performance
|
||||
-- =============================================
|
||||
|
||||
-- Customers indexes
|
||||
CREATE INDEX IX_Customers_Email ON Customers(Email);
|
||||
CREATE INDEX IX_Customers_LastName ON Customers(LastName);
|
||||
CREATE INDEX IX_Customers_CreatedAt ON Customers(CreatedAt);
|
||||
|
||||
-- CustomerAddresses indexes
|
||||
CREATE INDEX IX_CustomerAddresses_CustomerID ON CustomerAddresses(CustomerID);
|
||||
CREATE INDEX IX_CustomerAddresses_CountryCode ON CustomerAddresses(CountryCode);
|
||||
|
||||
-- Products indexes
|
||||
CREATE INDEX IX_Products_CategoryID ON Products(CategoryID);
|
||||
CREATE INDEX IX_Products_BrandID ON Products(BrandID);
|
||||
CREATE INDEX IX_Products_SKU ON Products(SKU);
|
||||
CREATE INDEX IX_Products_UnitPrice ON Products(UnitPrice);
|
||||
CREATE INDEX IX_Products_IsActive ON Products(IsActive);
|
||||
|
||||
-- Inventory indexes
|
||||
CREATE INDEX IX_Inventory_ProductID ON Inventory(ProductID);
|
||||
CREATE INDEX IX_Inventory_WarehouseID ON Inventory(WarehouseID);
|
||||
CREATE INDEX IX_Inventory_QuantityOnHand ON Inventory(QuantityOnHand);
|
||||
|
||||
-- Orders indexes
|
||||
CREATE INDEX IX_Orders_CustomerID ON Orders(CustomerID);
|
||||
CREATE INDEX IX_Orders_OrderDate ON Orders(OrderDate);
|
||||
CREATE INDEX IX_Orders_StatusID ON Orders(StatusID);
|
||||
CREATE INDEX IX_Orders_PaymentStatus ON Orders(PaymentStatus);
|
||||
CREATE INDEX IX_Orders_TotalAmount ON Orders(TotalAmount);
|
||||
|
||||
-- OrderItems indexes
|
||||
CREATE INDEX IX_OrderItems_OrderID ON OrderItems(OrderID);
|
||||
CREATE INDEX IX_OrderItems_ProductID ON OrderItems(ProductID);
|
||||
|
||||
-- Payments indexes
|
||||
CREATE INDEX IX_Payments_OrderID ON Payments(OrderID);
|
||||
CREATE INDEX IX_Payments_PaymentStatus ON Payments(PaymentStatus);
|
||||
CREATE INDEX IX_Payments_PaymentDate ON Payments(PaymentDate);
|
||||
|
||||
-- Shipments indexes
|
||||
CREATE INDEX IX_Shipments_OrderID ON Shipments(OrderID);
|
||||
CREATE INDEX IX_Shipments_TrackingNumber ON Shipments(TrackingNumber);
|
||||
CREATE INDEX IX_Shipments_ShippedDate ON Shipments(ShippedDate);
|
||||
|
||||
-- ProductReviews indexes
|
||||
CREATE INDEX IX_ProductReviews_ProductID ON ProductReviews(ProductID);
|
||||
CREATE INDEX IX_ProductReviews_CustomerID ON ProductReviews(CustomerID);
|
||||
CREATE INDEX IX_ProductReviews_Rating ON ProductReviews(Rating);
|
||||
CREATE INDEX IX_ProductReviews_CreatedAt ON ProductReviews(CreatedAt);
|
||||
|
||||
-- Promotions indexes
|
||||
CREATE INDEX IX_Promotions_PromotionCode ON Promotions(PromotionCode);
|
||||
CREATE INDEX IX_Promotions_StartDate_EndDate ON Promotions(StartDate, EndDate);
|
||||
|
||||
-- OrderTracking indexes
|
||||
CREATE INDEX IX_OrderTracking_OrderID ON OrderTracking(OrderID);
|
||||
CREATE INDEX IX_OrderTracking_StatusID ON OrderTracking(StatusID);
|
||||
CREATE INDEX IX_OrderTracking_StatusDate ON OrderTracking(StatusDate);
|
||||
|
||||
-- CartItems indexes
|
||||
CREATE INDEX IX_CartItems_CartID ON CartItems(CartID);
|
||||
CREATE INDEX IX_CartItems_ProductID ON CartItems(ProductID);
|
||||
|
||||
-- Composite indexes for common query patterns
|
||||
CREATE INDEX IX_Orders_Customer_Date ON Orders(CustomerID, OrderDate DESC);
|
||||
CREATE INDEX IX_OrderItems_Product_Price ON OrderItems(ProductID, UnitPrice);
|
||||
CREATE INDEX IX_Products_Category_Price ON Products(CategoryID, UnitPrice);
|
||||
|
||||
-- =============================================
|
||||
-- Insert initial reference data
|
||||
-- =============================================
|
||||
|
||||
-- Order Status
|
||||
INSERT INTO OrderStatus (StatusName, StatusDescription, IsFinalState) VALUES
|
||||
('Pending', 'Order placed but not processed', 0),
|
||||
('PaymentReceived', 'Payment has been confirmed', 0),
|
||||
('Processing', 'Order is being prepared', 0),
|
||||
('Shipped', 'Order has been shipped', 0),
|
||||
('Delivered', 'Order delivered to customer', 1),
|
||||
('Cancelled', 'Order cancelled', 1),
|
||||
('Refunded', 'Order refunded', 1);
|
||||
|
||||
-- Payment Methods
|
||||
INSERT INTO PaymentMethods (MethodName, ProcessingFee) VALUES
|
||||
('CreditCard', 2.9),
|
||||
('PayPal', 3.5),
|
||||
('BankTransfer', 0.5),
|
||||
('CashOnDelivery', 0.0);
|
||||
|
||||
-- Currencies
|
||||
INSERT INTO Currencies (CurrencyCode, CurrencyName, ExchangeRateToUSD, IsBaseCurrency) VALUES
|
||||
('USD', 'US Dollar', 1.0, 1),
|
||||
('EUR', 'Euro', 1.09, 0),
|
||||
('GBP', 'British Pound', 1.27, 0),
|
||||
('JPY', 'Japanese Yen', 0.0067, 0);
|
||||
|
||||
-- Shipping Methods
|
||||
INSERT INTO ShippingMethods (MethodName, EstimatedDeliveryDays, BaseCost, CostPerKg) VALUES
|
||||
('Standard Shipping', 5, 5.99, 1.5),
|
||||
('Express Shipping', 2, 12.99, 2.5),
|
||||
('Overnight', 1, 24.99, 4.0);
|
||||
|
||||
-- Price Tiers
|
||||
INSERT INTO PriceTiers (TierName, MinQuantity, DiscountPercentage) VALUES
|
||||
('Regular', 1, 0),
|
||||
('Wholesale', 10, 10),
|
||||
('Bulk', 50, 15),
|
||||
('Enterprise', 100, 20);
|
||||
|
||||
-- Sample Categories
|
||||
INSERT INTO Categories (CategoryName, CategoryCode, DisplayOrder) VALUES
|
||||
('Electronics', 'ELEC', 1),
|
||||
('Clothing', 'CLOTH', 2),
|
||||
('Books', 'BOOKS', 3),
|
||||
('Home & Garden', 'HOME', 4);
|
||||
|
||||
-- Sample Brands
|
||||
INSERT INTO Brands (BrandName, BrandCode) VALUES
|
||||
('TechPro', 'TP001'),
|
||||
('FashionHub', 'FH002'),
|
||||
('ReadWell', 'RW003');
|
||||
*/
|
||||
@@ -0,0 +1,97 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { BaseSqlImporter, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { Modifiers, MySQLCharset, MySQLCollation } from "@/lib/field";
|
||||
|
||||
export class MysqlImporter extends BaseSqlImporter {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.MYSQL;
|
||||
}
|
||||
|
||||
protected astToField(ast: any, sequence: number): ParsedField {
|
||||
try {
|
||||
const { field, fk_constraint } = super.astToField(ast, sequence)
|
||||
// get the data type
|
||||
const dataType: DataType = this.data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType;
|
||||
// if data type is of type set then extract it's values
|
||||
if (dataType.name == "set") {
|
||||
if (ast.data_type["Set"] && Array.isArray(ast.data_type["Set"])) {
|
||||
try {
|
||||
field.values = JSON.stringify(ast.data_type["Set"])
|
||||
} catch (error) {
|
||||
console.error("faield to parse Mysql Set values")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
// get data type modifiers
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
const options: any | undefined = [] = ast.options;
|
||||
|
||||
for (const option of options) {
|
||||
if (typeof option.option === "object") {
|
||||
if (option.option?.CharacterSet?.[0]?.Identifier?.value && modifiers.includes(Modifiers.CHARSET)) {
|
||||
const charset: string = option.option.CharacterSet[0].Identifier.value;
|
||||
|
||||
if (Object.values(MySQLCharset).includes(charset as MySQLCharset)) {
|
||||
|
||||
field.charset = charset;
|
||||
}
|
||||
}
|
||||
if (option.option?.Collation?.[0]?.Identifier?.value && modifiers.includes(Modifiers.COLLATE)) {
|
||||
|
||||
const collate: string = option.option.Collation[0].Identifier.value;
|
||||
|
||||
if (Object.values(MySQLCollation).includes(collate as MySQLCollation)) {
|
||||
|
||||
field.collate = collate;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
return { field, fk_constraint };
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected cleanSql(sql: string): string {
|
||||
let cleaned: string = super.cleanSql(sql);
|
||||
return cleaned.replace(/\b(?:unsigned|zerofill)\b/gi, '');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
CREATE TABLE employees (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
first_name VARCHAR(100),
|
||||
last_name VARCHAR(100),
|
||||
department_id INT,
|
||||
|
||||
CONSTRAINT uq_employee_name_dept
|
||||
UNIQUE (first_name, last_name, department_id)
|
||||
);
|
||||
|
||||
CREATE TABLE products (
|
||||
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||
sku VARCHAR(100),
|
||||
name VARCHAR(255)
|
||||
);
|
||||
|
||||
ALTER TABLE products
|
||||
ADD CONSTRAINT uq_products_sku
|
||||
UNIQUE (sku);
|
||||
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,725 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||
|
||||
import { DataTypes, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||
|
||||
export class OracleImporter extends BaseSqlImporter {
|
||||
|
||||
private alterNullabilityStatments: string[] = [];
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.ORACLE;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public parseSql(sql: string) {
|
||||
try {
|
||||
this.alterNullabilityStatments = [];
|
||||
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||
for (const statment of this.alterNullabilityStatments) {
|
||||
try {
|
||||
|
||||
this.processAlterColumnNullabilityStatment(statment, parsedDatabaase.tables);
|
||||
} catch (error) {
|
||||
parsedDatabaase.errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
return parsedDatabaase;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected astToField(ast: any, sequence: number): ParsedField {
|
||||
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||
|
||||
const dataType: DataType = this.data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType;
|
||||
// get the modifiers to test if the data type really support auto increment or not
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
if (ast.data_type.Custom && Array.isArray(ast.data_type.Custom)) {
|
||||
if (ast.data_type.Custom.length == 2 && ast.data_type.Custom[1]?.length == 2 && !isNaN(Number(ast.data_type.Custom[1]?.[1] && modifiers.includes(Modifiers.SCALE))))
|
||||
field.scale = Number(ast.data_type.Custom[1]?.[1]);
|
||||
}
|
||||
// get field defintion options
|
||||
const options: any | undefined = [] = ast.options;
|
||||
for (const option of options) {
|
||||
if (option.option?.Generated?.generated_as == "ByDefault") {
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT)) {
|
||||
field.autoIncrement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { field, fk_constraint };
|
||||
|
||||
}
|
||||
|
||||
|
||||
private processAlterColumnNullabilityStatment(statement: string, tables: TableInsertType[]): void {
|
||||
|
||||
|
||||
const regex = /ALTER\s+TABLE\s+"(?<tableName>[^"]+)"\s+MODIFY\s*\(\s*"(?<columnName>[^"]+)"\s+(?<nullability>NOT\s+NULL|NULL)(?:\s+ENABLE)?\s*\)/i;
|
||||
const match = statement.match(regex);
|
||||
|
||||
|
||||
|
||||
if (match?.groups) {
|
||||
const { tableName, columnName, nullability } = match.groups;
|
||||
|
||||
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name.toUpperCase() == tableName.toUpperCase());
|
||||
if (!table) {
|
||||
throw Error("Altered Table not found in : " + statement);
|
||||
}
|
||||
const field : FieldInsertType | undefined = table.fields?.find((field: FieldInsertType) => field.name.toUpperCase() == columnName.toUpperCase())
|
||||
if (!field)
|
||||
throw Error("Altered field not found in : " + statement);
|
||||
|
||||
|
||||
field.nullable = !(nullability.toUpperCase() == "NOT NULL") ;
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
const defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||
|
||||
if (!defaultValue && ast.Identifier && ast.Identifier.value && dataType.type == DataTypes.TIME) {
|
||||
|
||||
if (this.isCurrentTimesTampFunction(ast.Identifier.value))
|
||||
return TimeDefaultValues.NOW;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
|
||||
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||
const upper: string = value.toUpperCase();
|
||||
return super.isCurrentTimesTampFunction(value) || upper == "SYSDATE" || upper == "SYSTIMESTAMP";
|
||||
}
|
||||
|
||||
protected processDataType(typeName: string): DataType | undefined {
|
||||
let dataType: DataType | undefined = super.processDataType(typeName);
|
||||
|
||||
if (!dataType) {
|
||||
if (typeName == "year") {
|
||||
return this.data_types.find((dataType: DataType) => dataType.name == "interval year to month");
|
||||
}
|
||||
else if (typeName == "day") {
|
||||
return this.data_types.find((dataType: DataType) => dataType.name == "interval day to second");
|
||||
}
|
||||
}
|
||||
return dataType;
|
||||
}
|
||||
|
||||
|
||||
protected cleanSql(sql: string): string {
|
||||
let cleaned: string = super.cleanSql(sql);
|
||||
cleaned = cleaned.replace(/\bWITH\s+LOCAL\s+TIME\b/gi, 'WITH TIME');
|
||||
// remove all ENABLE keywords in add foriegn key constraint
|
||||
cleaned = cleaned.replace(
|
||||
/(ALTER\s+TABLE[\s\S]*?FOREIGN\s+KEY[\s\S]*?REFERENCES[\s\S]*?)\s+ENABLE(?=\s*;)/gi,
|
||||
'$1'
|
||||
);
|
||||
// remove MINVALUE [X] MAXVALUE [Y] INCREMENT BY [N] START WITH [M] CACHE [C] NOORDER NOCYCLE NOKEEP NOSCALE .
|
||||
cleaned = cleaned.replace(
|
||||
/(GENERATED\s+(?:ALWAYS|BY\s+DEFAULT(?:\s+ON\s+NULL)?)\s+AS\s+IDENTITY)([\s\S]*?)(?=,|\s+NOT\s+NULL|\s+PRIMARY\s+KEY)/gi,
|
||||
'$1'
|
||||
);
|
||||
// remove all USING INDEX ENBALE in add Unique or Primary Key constraints .
|
||||
cleaned = cleaned.replace(/\s+USING\s+INDEX\s+ENABLE(?=\s*;)/gi, '');
|
||||
// remove START WITH X INCREMENT BY Y
|
||||
cleaned = cleaned.replace(/\s+START\s+WITH\s+\d+(?:\s+INCREMENT\s+BY\s+\d+)?/gi, '');
|
||||
cleaned = cleaned.replace(
|
||||
/\bINTERVAL\s+YEAR(\s*\(\s*\d+\s*\))?\s+TO\s+MONTH(?:\s*\(\s*\d+\s*\))?/gi,
|
||||
'YEAR$1'
|
||||
);
|
||||
|
||||
cleaned = cleaned.replace(
|
||||
/\bINTERVAL\s+DAY(\s*\(\s*\d+\s*\))?\s+TO\s+SECOND(?:\s*\(\s*\d+\s*\))?/gi,
|
||||
'DAY$1'
|
||||
);
|
||||
|
||||
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
protected extractStatments(sql: string): ExtractedStatments {
|
||||
|
||||
const statments: ExtractedStatments = super.extractStatments(sql);
|
||||
|
||||
statments.alterTableStatements = statments.alterTableStatements.filter((statment: string) => {
|
||||
if (/ALTER\s+TABLE\s+.+?\s+MODIFY\s*\(\s*".+?"\s+(?:NULL|NOT\s+NULL)(?:\s+ENABLE)?\s*\)/i.test(statment)) {
|
||||
|
||||
this.alterNullabilityStatments.push(statment);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
return statment;
|
||||
}
|
||||
})
|
||||
return statments
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
-- =============================================
|
||||
-- Creative Enterprise Platform Database for Oracle
|
||||
-- Theme: Multi-Tenant Project Management & Resource Planning
|
||||
-- Version: 3.0 (Oracle Edition)
|
||||
-- =============================================
|
||||
|
||||
-- Drop user/schema if needed (optional - run separately)
|
||||
-- DROP USER creative_enterprise CASCADE;
|
||||
-- CREATE USER creative_enterprise IDENTIFIED BY password;
|
||||
-- GRANT CONNECT, RESOURCE, UNLIMITED TABLESPACE TO creative_enterprise;
|
||||
-- CONNECT creative_enterprise/password;
|
||||
|
||||
-- =============================================
|
||||
-- 1. TENANT & ORGANIZATION SCHEMA
|
||||
-- =============================================
|
||||
|
||||
-- Inline primary key, inline unique, default as expression
|
||||
CREATE TABLE Tenants (
|
||||
TenantID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 100 INCREMENT BY 1 PRIMARY KEY,
|
||||
TenantCode VARCHAR2(20) NOT NULL UNIQUE,
|
||||
TenantName NVARCHAR2(200) NOT NULL,
|
||||
SubscriptionTier VARCHAR2(30) DEFAULT 'Trial' NOT NULL,
|
||||
CreatedDate DATE DEFAULT SYSDATE NOT NULL,
|
||||
IsActive CHAR(1) DEFAULT '1' NOT NULL,
|
||||
MaxUsers NUMBER(5) DEFAULT 10 NOT NULL,
|
||||
CONSTRAINT UQ_Tenant_Name UNIQUE (TenantName)
|
||||
);
|
||||
|
||||
-- Constraints at end of table
|
||||
CREATE TABLE Departments (
|
||||
DepartmentID NUMBER(10) NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
DeptCode VARCHAR2(20) NOT NULL,
|
||||
DeptName NVARCHAR2(100) NOT NULL,
|
||||
Budget NUMBER(18,2) DEFAULT 0 NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT PK_Departments PRIMARY KEY (DepartmentID, TenantID),
|
||||
CONSTRAINT UQ_DeptCode_Tenant UNIQUE (DeptCode, TenantID)
|
||||
);
|
||||
|
||||
-- Foreign key added via ALTER after creation
|
||||
CREATE TABLE Companies (
|
||||
CompanyID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 500 INCREMENT BY 5 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
CompanyName NVARCHAR2(150) NOT NULL,
|
||||
LegalName NVARCHAR2(200) NOT NULL,
|
||||
TaxNumber VARCHAR2(50) NOT NULL,
|
||||
FoundedYear NUMBER(4) DEFAULT EXTRACT(YEAR FROM SYSDATE) NOT NULL,
|
||||
IsHeadquarters CHAR(1) DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
-- Adding constraint via ALTER
|
||||
ALTER TABLE Companies ADD CONSTRAINT PK_Companies PRIMARY KEY (CompanyID);
|
||||
ALTER TABLE Companies ADD CONSTRAINT UQ_Company_TaxNumber UNIQUE (TaxNumber);
|
||||
ALTER TABLE Companies ADD CONSTRAINT UQ_Company_Name_Tenant UNIQUE (CompanyName, TenantID);
|
||||
|
||||
-- =============================================
|
||||
-- 2. USER & ROLE MANAGEMENT
|
||||
-- =============================================
|
||||
|
||||
-- Mix of inline and named constraints
|
||||
CREATE TABLE Roles (
|
||||
RoleID RAW(16) DEFAULT SYS_GUID() PRIMARY KEY,
|
||||
RoleName VARCHAR2(50) NOT NULL CONSTRAINT AK_RoleName UNIQUE,
|
||||
PriorityLevel NUMBER(3) DEFAULT 5 NOT NULL,
|
||||
IsSystemRole CHAR(1) DEFAULT '0' NOT NULL
|
||||
);
|
||||
|
||||
-- Primary key inline, foreign keys at end
|
||||
CREATE TABLE Users (
|
||||
UserID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
RoleID RAW(16) NOT NULL,
|
||||
Email VARCHAR2(255) NOT NULL,
|
||||
Username VARCHAR2(100) NOT NULL,
|
||||
PasswordHash CHAR(64) NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
LastLogin TIMESTAMP NULL,
|
||||
IsLocked CHAR(1) DEFAULT '0' NOT NULL,
|
||||
LoginAttempts NUMBER(3) DEFAULT 0 NOT NULL,
|
||||
CONSTRAINT PK_Users PRIMARY KEY (UserID),
|
||||
CONSTRAINT UQ_User_Email UNIQUE (Email),
|
||||
CONSTRAINT UQ_User_Username_Tenant UNIQUE (Username, TenantID)
|
||||
);
|
||||
|
||||
-- Foreign keys added via ALTER
|
||||
ALTER TABLE Users ADD CONSTRAINT FK_Users_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
ALTER TABLE Users ADD CONSTRAINT FK_Users_Roles FOREIGN KEY (RoleID) REFERENCES Roles(RoleID);
|
||||
|
||||
-- Foreign key added via ALTER
|
||||
CREATE TABLE UserProfiles (
|
||||
ProfileID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
UserID NUMBER(10) NOT NULL,
|
||||
FirstName NVARCHAR2(50) NOT NULL,
|
||||
LastName NVARCHAR2(50) NOT NULL,
|
||||
PhoneNumber VARCHAR2(20) NULL,
|
||||
DateOfBirth DATE NULL,
|
||||
ProfileImageURL VARCHAR2(500) NULL,
|
||||
LastUpdated TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE UserProfiles ADD CONSTRAINT PK_UserProfiles PRIMARY KEY (ProfileID);
|
||||
ALTER TABLE UserProfiles ADD CONSTRAINT UQ_UserProfiles_UserID UNIQUE (UserID);
|
||||
ALTER TABLE UserProfiles ADD CONSTRAINT FK_UserProfiles_Users FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE;
|
||||
|
||||
-- =============================================
|
||||
-- 3. PROJECT & TASK MANAGEMENT
|
||||
-- =============================================
|
||||
|
||||
-- Primary key as named constraint at end
|
||||
CREATE TABLE Projects (
|
||||
ProjectID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1000 INCREMENT BY 1 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
DepartmentID NUMBER(10) NOT NULL,
|
||||
CompanyID NUMBER(10) NULL,
|
||||
ProjectName NVARCHAR2(200) NOT NULL,
|
||||
ProjectCode VARCHAR2(30) NOT NULL,
|
||||
StartDate DATE DEFAULT SYSDATE NOT NULL,
|
||||
EndDate DATE NULL,
|
||||
Status VARCHAR2(20) DEFAULT 'Planning' NOT NULL,
|
||||
Priority VARCHAR2(20) DEFAULT 'Medium' NOT NULL,
|
||||
CompletionPercent NUMBER(5,2) DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Projects ADD CONSTRAINT PK_Projects PRIMARY KEY (ProjectID);
|
||||
ALTER TABLE Projects ADD CONSTRAINT UQ_ProjectCode_Tenant UNIQUE (ProjectCode, TenantID);
|
||||
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Departments FOREIGN KEY (DepartmentID, TenantID) REFERENCES Departments(DepartmentID, TenantID);
|
||||
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Companies FOREIGN KEY (CompanyID) REFERENCES Companies(CompanyID);
|
||||
|
||||
-- All constraints inline
|
||||
CREATE TABLE Tasks (
|
||||
TaskID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 50000 INCREMENT BY 1 PRIMARY KEY,
|
||||
ProjectID NUMBER(10) NOT NULL,
|
||||
ParentTaskID NUMBER(15) NULL,
|
||||
TaskTitle NVARCHAR2(200) NOT NULL,
|
||||
TaskDescription NCLOB NULL,
|
||||
AssignedToUserID NUMBER(10) NULL,
|
||||
Status VARCHAR2(20) DEFAULT 'ToDo' NOT NULL,
|
||||
StoryPoints NUMBER(3) DEFAULT 1 NOT NULL,
|
||||
DueDate DATE NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
CONSTRAINT FK_Tasks_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID) ON DELETE CASCADE,
|
||||
CONSTRAINT FK_Tasks_ParentTask FOREIGN KEY (ParentTaskID) REFERENCES Tasks(TaskID),
|
||||
CONSTRAINT FK_Tasks_AssignedUser FOREIGN KEY (AssignedToUserID) REFERENCES Users(UserID),
|
||||
CONSTRAINT UQ_Task_Project_Title UNIQUE (ProjectID, TaskTitle)
|
||||
);
|
||||
|
||||
-- ALTER for composite foreign key
|
||||
CREATE TABLE TaskDependencies (
|
||||
DependencyID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
TaskID NUMBER(15) NOT NULL,
|
||||
DependsOnTaskID NUMBER(15) NOT NULL,
|
||||
DependencyType VARCHAR2(20) DEFAULT 'FS' NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE TaskDependencies ADD CONSTRAINT PK_TaskDependencies PRIMARY KEY (DependencyID);
|
||||
ALTER TABLE TaskDependencies ADD CONSTRAINT UQ_Task_Dependency UNIQUE (TaskID, DependsOnTaskID);
|
||||
ALTER TABLE TaskDependencies ADD CONSTRAINT FK_TaskDependencies_Task FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||
ALTER TABLE TaskDependencies ADD CONSTRAINT FK_TaskDependencies_DependsOn FOREIGN KEY (DependsOnTaskID) REFERENCES Tasks(TaskID);
|
||||
|
||||
-- =============================================
|
||||
-- 4. RESOURCE & ALLOCATION
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE ResourceTypes (
|
||||
ResourceTypeID NUMBER(3) PRIMARY KEY,
|
||||
TypeName VARCHAR2(50) NOT NULL UNIQUE,
|
||||
IsBillable CHAR(1) DEFAULT '1' NOT NULL,
|
||||
HourlyRate NUMBER(10,2) DEFAULT 0 NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE Resources (
|
||||
ResourceID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
ResourceTypeID NUMBER(3) NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
ResourceName NVARCHAR2(100) NOT NULL,
|
||||
ResourceCode VARCHAR2(30) NOT NULL,
|
||||
IsAvailable CHAR(1) DEFAULT '1' NOT NULL,
|
||||
DailyCapacity NUMBER(8,2) DEFAULT 8 NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Resources ADD CONSTRAINT PK_Resources PRIMARY KEY (ResourceID);
|
||||
ALTER TABLE Resources ADD CONSTRAINT UQ_ResourceCode_Tenant UNIQUE (ResourceCode, TenantID);
|
||||
ALTER TABLE Resources ADD CONSTRAINT FK_Resources_ResourceTypes FOREIGN KEY (ResourceTypeID) REFERENCES ResourceTypes(ResourceTypeID);
|
||||
ALTER TABLE Resources ADD CONSTRAINT FK_Resources_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
|
||||
-- Foreign key added via ALTER (different style)
|
||||
CREATE TABLE ResourceAllocations (
|
||||
AllocationID RAW(16) DEFAULT SYS_GUID() NOT NULL,
|
||||
ResourceID NUMBER(10) NOT NULL,
|
||||
TaskID NUMBER(15) NOT NULL,
|
||||
AllocatedHours NUMBER(8,2) NOT NULL,
|
||||
AllocationDate DATE DEFAULT SYSDATE NOT NULL,
|
||||
IsConfirmed CHAR(1) DEFAULT '0' NOT NULL,
|
||||
Notes NVARCHAR2(500) NULL
|
||||
);
|
||||
|
||||
ALTER TABLE ResourceAllocations ADD CONSTRAINT PK_ResourceAllocations PRIMARY KEY (AllocationID);
|
||||
ALTER TABLE ResourceAllocations ADD CONSTRAINT UQ_Resource_Task_Date UNIQUE (ResourceID, TaskID, AllocationDate);
|
||||
ALTER TABLE ResourceAllocations ADD CONSTRAINT FK_ResourceAllocations_Resources FOREIGN KEY (ResourceID) REFERENCES Resources(ResourceID);
|
||||
ALTER TABLE ResourceAllocations ADD CONSTRAINT FK_ResourceAllocations_Tasks FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||
|
||||
-- =============================================
|
||||
-- 5. TIME TRACKING
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE TimeEntries (
|
||||
TimeEntryID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
UserID NUMBER(10) NOT NULL,
|
||||
TaskID NUMBER(15) NOT NULL,
|
||||
EntryDate DATE DEFAULT SYSDATE NOT NULL,
|
||||
HoursWorked NUMBER(5,2) NOT NULL,
|
||||
Description NVARCHAR2(500) NULL,
|
||||
IsBilled CHAR(1) DEFAULT '0' NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
ALTER TABLE TimeEntries ADD CONSTRAINT PK_TimeEntries PRIMARY KEY (TimeEntryID);
|
||||
ALTER TABLE TimeEntries ADD CONSTRAINT FK_TimeEntries_Users FOREIGN KEY (UserID) REFERENCES Users(UserID);
|
||||
ALTER TABLE TimeEntries ADD CONSTRAINT FK_TimeEntries_Tasks FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||
ALTER TABLE TimeEntries ADD CONSTRAINT UQ_TimeEntry_User_Task_Date UNIQUE (UserID, TaskID, EntryDate);
|
||||
|
||||
-- =============================================
|
||||
-- 6. INVOICING & BILLING
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE BillingRates (
|
||||
RateID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
ResourceTypeID NUMBER(3) NOT NULL,
|
||||
HourlyRate NUMBER(10,2) NOT NULL,
|
||||
EffectiveFrom DATE DEFAULT SYSDATE NOT NULL,
|
||||
EffectiveTo DATE NULL
|
||||
);
|
||||
|
||||
ALTER TABLE BillingRates ADD CONSTRAINT PK_BillingRates PRIMARY KEY (RateID);
|
||||
ALTER TABLE BillingRates ADD CONSTRAINT UQ_Rate_Type_Tenant_Date UNIQUE (TenantID, ResourceTypeID, EffectiveFrom);
|
||||
ALTER TABLE BillingRates ADD CONSTRAINT FK_BillingRates_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
ALTER TABLE BillingRates ADD CONSTRAINT FK_BillingRates_ResourceTypes FOREIGN KEY (ResourceTypeID) REFERENCES ResourceTypes(ResourceTypeID);
|
||||
|
||||
CREATE TABLE Invoices (
|
||||
InvoiceID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 10000 INCREMENT BY 1 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
ProjectID NUMBER(10) NOT NULL,
|
||||
InvoiceNumber VARCHAR2(50) NOT NULL,
|
||||
IssueDate DATE DEFAULT SYSDATE NOT NULL,
|
||||
DueDate DATE NOT NULL,
|
||||
TotalAmount NUMBER(18,2) NOT NULL,
|
||||
TaxAmount NUMBER(18,2) DEFAULT 0 NOT NULL,
|
||||
Status VARCHAR2(20) DEFAULT 'Draft' NOT NULL,
|
||||
PaidDate DATE NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Invoices ADD CONSTRAINT PK_Invoices PRIMARY KEY (InvoiceID);
|
||||
ALTER TABLE Invoices ADD CONSTRAINT UQ_InvoiceNumber_Tenant UNIQUE (InvoiceNumber, TenantID);
|
||||
ALTER TABLE Invoices ADD CONSTRAINT FK_Invoices_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
ALTER TABLE Invoices ADD CONSTRAINT FK_Invoices_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID);
|
||||
|
||||
-- Foreign key with ON DELETE SET NULL
|
||||
CREATE TABLE InvoiceLineItems (
|
||||
LineItemID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
InvoiceID NUMBER(10) NOT NULL,
|
||||
TimeEntryID NUMBER(15) NULL,
|
||||
Description NVARCHAR2(255) NOT NULL,
|
||||
Quantity NUMBER(10,2) DEFAULT 1 NOT NULL,
|
||||
UnitPrice NUMBER(18,2) NOT NULL,
|
||||
LineTotal NUMBER(18,2) GENERATED ALWAYS AS (Quantity * UnitPrice) VIRTUAL
|
||||
);
|
||||
|
||||
ALTER TABLE InvoiceLineItems ADD CONSTRAINT PK_InvoiceLineItems PRIMARY KEY (LineItemID);
|
||||
ALTER TABLE InvoiceLineItems ADD CONSTRAINT FK_InvoiceLineItems_Invoices FOREIGN KEY (InvoiceID) REFERENCES Invoices(InvoiceID) ON DELETE CASCADE;
|
||||
ALTER TABLE InvoiceLineItems ADD CONSTRAINT FK_InvoiceLineItems_TimeEntries FOREIGN KEY (TimeEntryID) REFERENCES TimeEntries(TimeEntryID) ON DELETE SET NULL;
|
||||
|
||||
-- =============================================
|
||||
-- 7. NOTIFICATIONS & AUDIT
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE NotificationTypes (
|
||||
NotificationTypeID NUMBER(5) NOT NULL,
|
||||
TypeName VARCHAR2(50) NOT NULL,
|
||||
DefaultTemplate NVARCHAR2(500) NULL
|
||||
);
|
||||
|
||||
ALTER TABLE NotificationTypes ADD CONSTRAINT PK_NotificationTypes PRIMARY KEY (NotificationTypeID);
|
||||
ALTER TABLE NotificationTypes ADD CONSTRAINT UQ_NotificationTypeName UNIQUE (TypeName);
|
||||
|
||||
CREATE TABLE Notifications (
|
||||
NotificationID RAW(16) DEFAULT SYS_GUID() NOT NULL,
|
||||
UserID NUMBER(10) NOT NULL,
|
||||
NotificationTypeID NUMBER(5) NOT NULL,
|
||||
Title NVARCHAR2(200) NOT NULL,
|
||||
Message NCLOB NOT NULL,
|
||||
IsRead CHAR(1) DEFAULT '0' NOT NULL,
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
ReadAt TIMESTAMP NULL,
|
||||
MetadataJSON CLOB NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Notifications ADD CONSTRAINT PK_Notifications PRIMARY KEY (NotificationID);
|
||||
ALTER TABLE Notifications ADD CONSTRAINT FK_Notifications_Users FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE;
|
||||
ALTER TABLE Notifications ADD CONSTRAINT FK_Notifications_NotificationTypes FOREIGN KEY (NotificationTypeID) REFERENCES NotificationTypes(NotificationTypeID);
|
||||
|
||||
-- Audit log with all constraints via ALTER
|
||||
CREATE TABLE AuditLog (
|
||||
AuditID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
TenantID NUMBER(10) NOT NULL,
|
||||
UserID NUMBER(10) NULL,
|
||||
TableName VARCHAR2(100) NOT NULL,
|
||||
RecordID VARCHAR2(100) NOT NULL,
|
||||
ActionType VARCHAR2(20) NOT NULL,
|
||||
OldValueXML XMLTYPE NULL,
|
||||
NewValueXML XMLTYPE NULL,
|
||||
ChangedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||
IPAddress VARCHAR2(45) NULL
|
||||
);
|
||||
|
||||
ALTER TABLE AuditLog ADD CONSTRAINT PK_AuditLog PRIMARY KEY (AuditID);
|
||||
ALTER TABLE AuditLog ADD CONSTRAINT FK_AuditLog_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||
ALTER TABLE AuditLog ADD CONSTRAINT FK_AuditLog_Users FOREIGN KEY (UserID) REFERENCES Users(UserID);
|
||||
ALTER TABLE AuditLog MODIFY UserID DEFAULT USER;
|
||||
|
||||
-- =============================================
|
||||
-- 8. MILESTONES & DELIVERABLES
|
||||
-- =============================================
|
||||
|
||||
CREATE TABLE Milestones (
|
||||
MilestoneID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
ProjectID NUMBER(10) NOT NULL,
|
||||
MilestoneName NVARCHAR2(200) NOT NULL,
|
||||
TargetDate DATE NOT NULL,
|
||||
ActualCompletionDate DATE NULL,
|
||||
Status VARCHAR2(20) DEFAULT 'Pending' NOT NULL,
|
||||
Budget NUMBER(18,2) NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Milestones ADD CONSTRAINT PK_Milestones PRIMARY KEY (MilestoneID);
|
||||
ALTER TABLE Milestones ADD CONSTRAINT FK_Milestones_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID);
|
||||
ALTER TABLE Milestones ADD CONSTRAINT UQ_Milestone_Project_Name UNIQUE (ProjectID, MilestoneName);
|
||||
|
||||
CREATE TABLE Deliverables (
|
||||
DeliverableID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||
MilestoneID NUMBER(10) NOT NULL,
|
||||
DeliverableName NVARCHAR2(200) NOT NULL,
|
||||
Description NVARCHAR2(500) NULL,
|
||||
ExpectedOutput NVARCHAR2(500) NOT NULL,
|
||||
IsCompleted CHAR(1) DEFAULT '0' NOT NULL,
|
||||
CompletedAt TIMESTAMP NULL
|
||||
);
|
||||
|
||||
ALTER TABLE Deliverables ADD CONSTRAINT PK_Deliverables PRIMARY KEY (DeliverableID);
|
||||
ALTER TABLE Deliverables ADD CONSTRAINT FK_Deliverables_Milestones FOREIGN KEY (MilestoneID) REFERENCES Milestones(MilestoneID);
|
||||
ALTER TABLE Deliverables ADD CONSTRAINT UQ_Deliverable_Milestone_Name UNIQUE (MilestoneID, DeliverableName);
|
||||
|
||||
-- =============================================
|
||||
-- INDEXES for Performance Optimization
|
||||
-- =============================================
|
||||
|
||||
-- Single column indexes
|
||||
CREATE INDEX IX_Users_TenantID ON Users(TenantID);
|
||||
CREATE INDEX IX_Users_RoleID ON Users(RoleID);
|
||||
CREATE INDEX IX_Users_Email ON Users(Email);
|
||||
CREATE INDEX IX_Users_Username ON Users(Username);
|
||||
|
||||
CREATE INDEX IX_Projects_TenantID ON Projects(TenantID);
|
||||
CREATE INDEX IX_Projects_DepartmentID ON Projects(DepartmentID);
|
||||
CREATE INDEX IX_Projects_Status ON Projects(Status);
|
||||
CREATE INDEX IX_Projects_StartDate ON Projects(StartDate);
|
||||
|
||||
CREATE INDEX IX_Tasks_ProjectID ON Tasks(ProjectID);
|
||||
CREATE INDEX IX_Tasks_AssignedToUserID ON Tasks(AssignedToUserID);
|
||||
CREATE INDEX IX_Tasks_Status ON Tasks(Status);
|
||||
CREATE INDEX IX_Tasks_DueDate ON Tasks(DueDate);
|
||||
|
||||
CREATE INDEX IX_TimeEntries_UserID ON TimeEntries(UserID);
|
||||
CREATE INDEX IX_TimeEntries_TaskID ON TimeEntries(TaskID);
|
||||
CREATE INDEX IX_TimeEntries_EntryDate ON TimeEntries(EntryDate);
|
||||
CREATE INDEX IX_TimeEntries_IsBilled ON TimeEntries(IsBilled);
|
||||
|
||||
CREATE INDEX IX_ResourceAllocations_ResourceID ON ResourceAllocations(ResourceID);
|
||||
CREATE INDEX IX_ResourceAllocations_TaskID ON ResourceAllocations(TaskID);
|
||||
CREATE INDEX IX_ResourceAllocations_AllocationDate ON ResourceAllocations(AllocationDate);
|
||||
|
||||
CREATE INDEX IX_Invoices_TenantID ON Invoices(TenantID);
|
||||
CREATE INDEX IX_Invoices_ProjectID ON Invoices(ProjectID);
|
||||
CREATE INDEX IX_Invoices_Status ON Invoices(Status);
|
||||
CREATE INDEX IX_Invoices_DueDate ON Invoices(DueDate);
|
||||
|
||||
CREATE INDEX IX_InvoiceLineItems_InvoiceID ON InvoiceLineItems(InvoiceID);
|
||||
CREATE INDEX IX_InvoiceLineItems_TimeEntryID ON InvoiceLineItems(TimeEntryID);
|
||||
|
||||
CREATE INDEX IX_Notifications_UserID ON Notifications(UserID);
|
||||
CREATE INDEX IX_Notifications_IsRead ON Notifications(IsRead);
|
||||
CREATE INDEX IX_Notifications_CreatedAt ON Notifications(CreatedAt);
|
||||
|
||||
CREATE INDEX IX_AuditLog_TenantID ON AuditLog(TenantID);
|
||||
CREATE INDEX IX_AuditLog_TableName ON AuditLog(TableName);
|
||||
CREATE INDEX IX_AuditLog_ChangedAt ON AuditLog(ChangedAt);
|
||||
CREATE INDEX IX_AuditLog_ActionType ON AuditLog(ActionType);
|
||||
|
||||
-- Composite indexes for common query patterns (Oracle specific)
|
||||
CREATE INDEX IX_Tasks_Project_Status ON Tasks(ProjectID, Status);
|
||||
CREATE INDEX IX_TimeEntries_User_Date ON TimeEntries(UserID, EntryDate);
|
||||
CREATE INDEX IX_ResourceAllocations_Resource_Date ON ResourceAllocations(ResourceID, AllocationDate);
|
||||
CREATE INDEX IX_Invoices_Tenant_Status ON Invoices(TenantID, Status);
|
||||
CREATE INDEX IX_Notifications_User_Read ON Notifications(UserID, IsRead, CreatedAt DESC);
|
||||
|
||||
-- Bitmap indexes for low-cardinality columns (Oracle feature)
|
||||
CREATE BITMAP INDEX IX_Tasks_Status_BMP ON Tasks(Status);
|
||||
CREATE BITMAP INDEX IX_Projects_Status_BMP ON Projects(Status);
|
||||
CREATE BITMAP INDEX IX_Invoices_Status_BMP ON Invoices(Status);
|
||||
CREATE BITMAP INDEX IX_Users_IsLocked_BMP ON Users(IsLocked);
|
||||
|
||||
-- Function-based index (Oracle specific)
|
||||
CREATE INDEX IX_Users_Email_Upper ON Users(UPPER(Email));
|
||||
CREATE INDEX IX_Projects_Code_Upper ON Projects(UPPER(ProjectCode));
|
||||
|
||||
-- =============================================
|
||||
-- Comments for documentation (Oracle style)
|
||||
-- =============================================
|
||||
|
||||
COMMENT ON TABLE Tenants IS 'Multi-tenant organizations using the platform';
|
||||
COMMENT ON TABLE Projects IS 'Projects belonging to specific tenants and departments';
|
||||
COMMENT ON TABLE Tasks IS 'Work items associated with projects';
|
||||
COMMENT ON TABLE TimeEntries IS 'Time logged by users against tasks';
|
||||
COMMENT ON TABLE Invoices IS 'Billing documents for projects and time entries';
|
||||
COMMENT ON COLUMN Users.PasswordHash IS 'SHA-256 hash of user password';
|
||||
COMMENT ON COLUMN Tasks.StoryPoints IS 'Agile estimation points for task complexity';
|
||||
COMMENT ON COLUMN ResourceAllocations.AllocationID IS 'Globally unique identifier using SYS_GUID';
|
||||
|
||||
|
||||
|
||||
|
||||
CREATE TABLE ORACLE_ALL_DATATYPES_TEST (
|
||||
-- Identity / numeric types
|
||||
ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
NumberCol NUMBER(18,4) NOT NULL UNIQUE DEFAULT 12345.6789,
|
||||
IntegerCol INTEGER NULL DEFAULT 100,
|
||||
IntCol INT NOT NULL DEFAULT 200,
|
||||
SmallIntCol SMALLINT NULL UNIQUE DEFAULT 50,
|
||||
FloatCol FLOAT(20) NOT NULL DEFAULT 123.45,
|
||||
BinaryFloatCol BINARY_FLOAT NULL DEFAULT 10.5,
|
||||
BinaryDoubleCol BINARY_DOUBLE NOT NULL DEFAULT 99.999,
|
||||
|
||||
-- Character types
|
||||
CharCol CHAR(10) NOT NULL DEFAULT 'CHARVAL',
|
||||
NCharCol NCHAR(20) NULL DEFAULT N'NCHARVAL',
|
||||
VarChar2Col VARCHAR2(255) NOT NULL UNIQUE DEFAULT 'varchar2 text',
|
||||
NVarChar2Col NVARCHAR2(200) NULL DEFAULT N'nvarchar2 text',
|
||||
ClobCol CLOB NULL,
|
||||
NClobCol NCLOB NULL,
|
||||
|
||||
-- RAW / binary
|
||||
RawCol RAW(100) NOT NULL ,
|
||||
|
||||
BlobCol BLOB NULL,
|
||||
|
||||
-- Date & time
|
||||
DateCol DATE NOT NULL DEFAULT SYSDATE,
|
||||
TimestampCol TIMESTAMP(6) NULL DEFAULT SYSTIMESTAMP,
|
||||
TimestampTZCol TIMESTAMP(6) WITH TIME ZONE NOT NULL DEFAULT SYSTIMESTAMP,
|
||||
TimestampLTZCol TIMESTAMP(6) WITH LOCAL TIME ZONE NULL DEFAULT SYSTIMESTAMP,
|
||||
IntervalYearMonthCol INTERVAL YEAR(4) TO MONTH DEFAULT INTERVAL '2-6' YEAR TO MONTH,
|
||||
IntervalDaySecondCol INTERVAL DAY(3) TO SECOND(6)
|
||||
DEFAULT INTERVAL '5 12:30:45.123456' DAY TO SECOND,
|
||||
|
||||
-- XML
|
||||
XmlTypeCol XMLTYPE NULL,
|
||||
|
||||
-- Row identifier
|
||||
RowIdCol ROWID NULL,
|
||||
URowIdCol UROWID NULL,
|
||||
|
||||
-- JSON (stored as CLOB/VARCHAR2 in many Oracle versions)
|
||||
JsonCol CLOB NULL,
|
||||
|
||||
|
||||
|
||||
-- Default expressions
|
||||
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE all_oracle_types (
|
||||
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
|
||||
-- Numeric types
|
||||
col_number_int NUMBER(10) NOT NULL DEFAULT 1,
|
||||
col_number_decimal NUMBER(10,2) NULL DEFAULT 99.99,
|
||||
col_numeric NUMERIC(8,3) NOT NULL DEFAULT 123.456,
|
||||
col_decimal DECIMAL(12,4) NULL DEFAULT 456.7890,
|
||||
col_float FLOAT(10) NULL DEFAULT 1.23,
|
||||
col_binary_float BINARY_FLOAT NOT NULL DEFAULT 3.14,
|
||||
col_binary_double BINARY_DOUBLE NULL DEFAULT 6.28318,
|
||||
|
||||
-- Character types
|
||||
col_char CHAR(10) NOT NULL DEFAULT 'CHARVAL',
|
||||
col_nchar NCHAR(10) NULL DEFAULT 'NCHARVAL',
|
||||
|
||||
col_varchar2 VARCHAR2(255) NULL UNIQUE DEFAULT 'varchar2 value',
|
||||
col_nvarchar2 NVARCHAR2(100) NOT NULL DEFAULT 'nvarchar2 value',
|
||||
|
||||
col_clob CLOB NULL,
|
||||
col_nclob NCLOB NULL,
|
||||
|
||||
-- Raw / binary
|
||||
col_raw RAW(16) NULL,
|
||||
col_blob BLOB NULL,
|
||||
|
||||
-- Date & time types
|
||||
col_date DATE NOT NULL DEFAULT DATE '2024-01-01',
|
||||
|
||||
col_timestamp TIMESTAMP(6)
|
||||
NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
col_timestamp_tz TIMESTAMP(6) WITH TIME ZONE
|
||||
NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
col_timestamp_ltz TIMESTAMP(6) WITH LOCAL TIME ZONE
|
||||
NULL,
|
||||
|
||||
col_interval_ym INTERVAL YEAR(2) TO MONTH
|
||||
NULL DEFAULT INTERVAL '1-2' YEAR TO MONTH,
|
||||
|
||||
col_interval_ds INTERVAL DAY(2) TO SECOND(6)
|
||||
NULL DEFAULT INTERVAL '3 12:30:45.123456'
|
||||
DAY TO SECOND,
|
||||
|
||||
-- XML
|
||||
col_xml XMLTYPE NULL,
|
||||
|
||||
-- JSON (Oracle 21c+ native JSON type)
|
||||
col_json JSON NULL,
|
||||
|
||||
-- Row identifier
|
||||
col_urowid UROWID NULL,
|
||||
|
||||
-- Some extra inline UNIQUE columns
|
||||
col_unique_number NUMBER(5) UNIQUE DEFAULT 500,
|
||||
col_unique_text VARCHAR2(50) UNIQUE NULL
|
||||
);
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,284 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||
import { init, parse, format, validate, Statement } from '@guanmingchiu/sqlparser-ts';
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||
|
||||
interface PostgreSQLType {
|
||||
name: string;
|
||||
values: string[]
|
||||
}
|
||||
|
||||
export class PostgreSqlImporter extends BaseSqlImporter {
|
||||
|
||||
private enums: PostgreSQLType[] = [];
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.POSTGRES;
|
||||
}
|
||||
|
||||
public parseSql(sql: string) {
|
||||
try {
|
||||
|
||||
this.enums = [];
|
||||
|
||||
const cleaned: string = this.cleanSql(sql);
|
||||
|
||||
const { createPostgresTypesStatements, alterSequenceStatments } = this.extractStatments(cleaned);
|
||||
if (createPostgresTypesStatements)
|
||||
for (const statment of createPostgresTypesStatements) {
|
||||
const astStatment: any = parse(statment, this.dialect as any)?.pop();
|
||||
if (astStatment?.CreateType) {
|
||||
this.enums.push(this.astToEnum(astStatment.CreateType));
|
||||
}
|
||||
}
|
||||
|
||||
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||
|
||||
if (alterSequenceStatments) {
|
||||
for (const statment of alterSequenceStatments) {
|
||||
try {
|
||||
this.processAlterSequenceStatements(statment, parsedDatabaase.tables);
|
||||
} catch (error) {
|
||||
parsedDatabaase.errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return parsedDatabaase;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected processAlterSequenceStatements(statment: string, tables: TableInsertType[]) {
|
||||
|
||||
const regex =
|
||||
/ALTER\s+SEQUENCE\s+[\w"]+\.[\w"]+\s+OWNED\s+BY\s+("?[\w]+"?)\.("?[\w]+"?)\.("?[\w]+"?)/i;
|
||||
|
||||
const match = statment.match(regex);
|
||||
|
||||
if (!match) {
|
||||
return;
|
||||
}
|
||||
|
||||
const schema = match[1].replace(/"/g, '');
|
||||
const tableName = match[2].replace(/"/g, '');
|
||||
const columnName = match[3].replace(/"/g, '');
|
||||
|
||||
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||
if (!table) {
|
||||
throw Error(`table ${tableName} not found in : ${statment}`);
|
||||
}
|
||||
|
||||
const index = table.fields?.findIndex((field: FieldInsertType) => field.name == columnName) ?? -1;
|
||||
if (index < 0) {
|
||||
throw Error(`field ${columnName} not found in : ${statment}`);
|
||||
}
|
||||
|
||||
(table.fields as FieldInsertType[])[index].autoIncrement = true ;
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
protected astToField(ast: any, sequence: number): ParsedField {
|
||||
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||
|
||||
if (typeof ast.data_type === "object" && ast.data_type.Custom) {
|
||||
const typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
|
||||
// it's a serial type
|
||||
if (typeName?.toLowerCase().includes("serial")) {
|
||||
|
||||
field.autoIncrement = true;
|
||||
}
|
||||
else {
|
||||
const postgreSqlType: PostgreSQLType | undefined = this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName);
|
||||
if (postgreSqlType && postgreSqlType.values.length > 0) {
|
||||
try {
|
||||
field.values = JSON.stringify(postgreSqlType.values);
|
||||
} catch (error) {
|
||||
console.error("failed to parse postgresql type values")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
return { field, fk_constraint };
|
||||
}
|
||||
|
||||
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||
|
||||
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||
|
||||
if (!defaultValue) {
|
||||
if (ast.Function) {
|
||||
if (ast.Function.name?.[0]?.Identifier?.value == "gen_random_uuid") {
|
||||
return "random";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return defaultValue
|
||||
}
|
||||
|
||||
|
||||
protected processDataType(typeName: string): DataType | undefined {
|
||||
let dataType: DataType | undefined = super.processDataType(typeName);
|
||||
|
||||
if (!dataType) {
|
||||
if (typeName?.toLowerCase().includes("serial")) {
|
||||
// if it's a serial type , then get the base integer type (integer , smallint , bigint)
|
||||
let baseType: string = typeName.toLowerCase() == "serial" ? "integer" : typeName.toLowerCase().replace("serial", "int");
|
||||
return super.processDataType(baseType);
|
||||
}
|
||||
else {
|
||||
const isEnum: boolean = Boolean(this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName));
|
||||
if (isEnum) {
|
||||
return this.data_types.find((dataType: DataType) => dataType.name == "enum")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return dataType
|
||||
}
|
||||
|
||||
private astToEnum(ast: any): PostgreSQLType {
|
||||
|
||||
const values: string[] | undefined = ast.representation?.Enum?.labels?.map((label: any) => label.value);
|
||||
|
||||
return {
|
||||
name: ast.name?.[0]?.Identifier?.value,
|
||||
values
|
||||
} as PostgreSQLType;
|
||||
}
|
||||
|
||||
protected extractStatments(sql: string): ExtractedStatments {
|
||||
|
||||
const createPostgresTypesStatements: string[] = [];
|
||||
const alterSequenceStatments: string[] = [];
|
||||
const { createIndexStatements, createTableStatements, alterTableStatements } = super.extractStatments(sql);
|
||||
|
||||
// Split into individual statements
|
||||
const statements = sql
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const stmt of statements) {
|
||||
const upper = stmt.toUpperCase();
|
||||
if (upper.startsWith('CREATE TYPE')) {
|
||||
createPostgresTypesStatements.push(stmt);
|
||||
}
|
||||
if (upper.startsWith("ALTER SEQUENCE")) {
|
||||
alterSequenceStatments.push(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createIndexStatements,
|
||||
createTableStatements,
|
||||
alterTableStatements,
|
||||
createPostgresTypesStatements,
|
||||
alterSequenceStatments
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
|
||||
|
||||
CREATE TYPE "users_user_type_enum" AS ENUM (
|
||||
'individual',
|
||||
'agency',
|
||||
'developer',
|
||||
'admin',
|
||||
'employee'
|
||||
);
|
||||
|
||||
CREATE TYPE "users_provider_enum" AS ENUM (
|
||||
'google.com',
|
||||
'facebook.com',
|
||||
'password',
|
||||
'anonymous'
|
||||
);
|
||||
|
||||
CREATE TABLE "users" (
|
||||
id INTEGER NOT NULL PRIMARY KEY,
|
||||
full_name VARCHAR NOT NULL,
|
||||
email VARCHAR NOT NULL,
|
||||
picture_url VARCHAR NULL,
|
||||
phone_number VARCHAR NULL,
|
||||
password_hash VARCHAR NOT NULL,
|
||||
user_type users_user_type_enum NOT NULL DEFAULT 'individual',
|
||||
provider users_provider_enum NOT NULL DEFAULT 'password',
|
||||
birthday TIMESTAMP(3) NULL,
|
||||
gender BOOLEAN NULL,
|
||||
created_at TIMESTAMPTZ (6) NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMPTZ (6) NULL,
|
||||
is_email_verified BOOLEAN NOT NULL DEFAULT FALSE
|
||||
);
|
||||
|
||||
CREATE TABLE all_postgres_types (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
|
||||
-- Numeric types
|
||||
col_smallint SMALLINT NOT NULL DEFAULT 1,
|
||||
col_integer INTEGER NOT NULL UNIQUE DEFAULT 1000,
|
||||
col_bigint BIGINT NULL DEFAULT 10000,
|
||||
|
||||
col_decimal DECIMAL(10,2) NOT NULL DEFAULT 99.99,
|
||||
col_numeric NUMERIC(8,3) NULL DEFAULT 123.456,
|
||||
|
||||
col_real REAL NULL DEFAULT 1.23,
|
||||
col_double DOUBLE PRECISION NOT NULL DEFAULT 2.3456,
|
||||
|
||||
col_boolean BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
|
||||
-- Date & time types
|
||||
col_date DATE NOT NULL DEFAULT DATE '2024-01-01',
|
||||
col_timestamp TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
col_timestamptz TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
col_time TIME(6) NULL DEFAULT TIME '12:34:56.123456',
|
||||
|
||||
-- String types
|
||||
col_char CHAR(10) NOT NULL DEFAULT 'charval',
|
||||
col_varchar VARCHAR(255) NULL UNIQUE DEFAULT 'varchar value',
|
||||
col_text TEXT NULL DEFAULT 'some text',
|
||||
|
||||
-- Binary
|
||||
col_bytea BYTEA NULL,
|
||||
|
||||
-- Enum (PostgreSQL requires type creation first)
|
||||
col_enum TEXT NOT NULL DEFAULT 'A',
|
||||
|
||||
-- JSON
|
||||
col_json JSON NULL,
|
||||
col_jsonb JSONB NULL,
|
||||
|
||||
-- UUID
|
||||
col_uuid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||
|
||||
-- Array
|
||||
col_text_array TEXT[] NULL DEFAULT ARRAY['a','b','c'],
|
||||
|
||||
-- Range types
|
||||
col_int_range INT4RANGE NULL,
|
||||
col_ts_range TSRANGE NULL,
|
||||
|
||||
-- XML
|
||||
col_xml XML NULL,
|
||||
|
||||
-- Some extra UNIQUE inline
|
||||
col_unique_text TEXT UNIQUE
|
||||
);
|
||||
|
||||
*/
|
||||
@@ -0,0 +1,60 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { BaseSqlImporter } from "./base-sql-importer";
|
||||
|
||||
export class SqliteImporter extends BaseSqlImporter {
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(data_types);
|
||||
this.dialect = DatabaseDialect.SQLITE;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
/*
|
||||
CREATE TABLE "users" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
username TEXT NOT NULL UNIQUE,
|
||||
email TEXT NOT NULL UNIQUE,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE TABLE "projects" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT NULL DEFAULT 'Hello world'
|
||||
);
|
||||
|
||||
CREATE TABLE "tasks" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
project_id INTEGER NOT NULL,
|
||||
title TEXT NOT NULL,
|
||||
description TEXT NULL,
|
||||
due_date DATETIME NULL,
|
||||
priority TEXT NULL,
|
||||
status TEXT NULL,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("project_id") REFERENCES "projects" ("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "comments" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
task_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
comment_text TEXT NOT NULL,
|
||||
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE "task_assignments" (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
task_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
assigned_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE,
|
||||
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||
)
|
||||
*/
|
||||
@@ -0,0 +1,153 @@
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { decomposeManyToMany, getDefaultRelationshipName } from "./relationship";
|
||||
import _ from "lodash";
|
||||
import { DBDiffOperation } from "./database";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
|
||||
export const prepareForMigration = (db: DatabaseType) => {
|
||||
|
||||
let database = _.cloneDeep(db);
|
||||
|
||||
database.tables = database.tables.map((table: any) => {
|
||||
delete table.sequence;
|
||||
table.fields = table.fields.map((field: any) => {
|
||||
delete field.sequence;
|
||||
delete field.note;
|
||||
return field;
|
||||
})
|
||||
return table;
|
||||
});
|
||||
|
||||
database.relationships = database.relationships.map((relationship: RelationshipType) => {
|
||||
|
||||
if (relationship.name)
|
||||
return relationship;
|
||||
|
||||
relationship.sourceTable = database.tables.find((table: TableType) => table.id == relationship.sourceTableId) as TableType;
|
||||
relationship.targetTable = database.tables.find((table: TableType) => table.id == relationship.targetTableId) as TableType;
|
||||
|
||||
relationship.sourceField = relationship.sourceTable.fields.find((field: FieldType) => field.id == relationship.sourceFieldId) as FieldType;
|
||||
relationship.targetField = relationship.targetTable.fields.find((field: FieldType) => field.id == relationship.targetFieldId) as FieldType;
|
||||
|
||||
relationship.name = getDefaultRelationshipName(relationship)
|
||||
return relationship;
|
||||
});
|
||||
|
||||
database = decomposeManyToMany(database);
|
||||
|
||||
return database;
|
||||
}
|
||||
|
||||
// enrich a database operations is :
|
||||
// - adding the type to the fields .
|
||||
// - adding sourceField , targetField , sourceTable , targetTable to the relationships
|
||||
export const optimizeOps = (operations: DBDiffOperation[], currentDatabase: DatabaseType, previousDatabase: DatabaseType, data_types: DataType[]): DBDiffOperation[] => {
|
||||
let ops: DBDiffOperation[] = [];
|
||||
|
||||
|
||||
|
||||
const schemaOperations = operations.filter((operation: DBDiffOperation) => (operation.type != "UPDATE_FIELD_INDICES") && !(
|
||||
operation.type == "UPDATE_FIELD" && operation.changes.isPrimary !== undefined && Object.keys(operation.changes).length == 1
|
||||
));
|
||||
|
||||
|
||||
|
||||
for (let index = 0; index < schemaOperations.length; index++) {
|
||||
|
||||
const operation: DBDiffOperation = _.cloneDeep(schemaOperations[index]);
|
||||
|
||||
if (operation.type == "UPDATE_NUM_TABLES")
|
||||
continue;
|
||||
if (operation.type == "RENAME_DATABASE")
|
||||
continue;
|
||||
|
||||
|
||||
|
||||
if (operation.type == "CREATE_TABLE") {
|
||||
operation.table.fields = Object.values(operation.table.fields).map((field: FieldType) => ({
|
||||
...field,
|
||||
type: data_types.find((type: DataType) => type.id == field.typeId)
|
||||
})) as FieldType[];
|
||||
|
||||
operation.table.indices = Object.values(operation.table.indices).map((indice: IndexType) => ({
|
||||
...indice,
|
||||
fieldIndices: Object.values(indice.fieldIndices),
|
||||
|
||||
}))
|
||||
operation.table.indices = operation.table.indices.map((index: IndexType) => ({
|
||||
...index,
|
||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) => operation.table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)) as FieldType[]
|
||||
}))
|
||||
}
|
||||
|
||||
if (operation.type == "CREATE_RELATIONSHIP") {
|
||||
|
||||
// try to get the sourceTable from the previous database
|
||||
let sourceTable: TableType | undefined = previousDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId);
|
||||
if (!sourceTable) {
|
||||
// if the source table is not found that mean it's a new table created in this migration
|
||||
// so we need to get it from the currentDatabase
|
||||
sourceTable = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId) as TableType;
|
||||
}
|
||||
|
||||
let sourceField: FieldType | undefined = sourceTable.fields.find((field: FieldType) => field.id == operation.relationship.sourceFieldId);
|
||||
if (!sourceField) {
|
||||
// if the source field is not found , then it could be a new field created in this new migration
|
||||
// so what we need to do it is we need to get it from the current Database ,
|
||||
const currentSourceTable: TableType = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId) as TableType;
|
||||
sourceField = currentSourceTable.fields.find((field: FieldType) => field.id == operation.relationship.sourceFieldId);
|
||||
}
|
||||
|
||||
// now we do the same thing for the target table and field .
|
||||
let targetTable: TableType | undefined = previousDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||
// if not found , the target table culd be a new table created in this migration
|
||||
if (!targetTable) {
|
||||
targetTable = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||
}
|
||||
let targetField: FieldType | undefined = targetTable.fields.find((field: FieldType) => field.id == operation.relationship.targetFieldId);
|
||||
if (!targetField) {
|
||||
// it can be a new field , so we need to get it from the current migration .
|
||||
const currentTargetTable: TableType = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||
targetField = currentTargetTable.fields.find((field: FieldType) => field.id == operation.relationship.targetFieldId) as FieldType
|
||||
}
|
||||
operation.relationship.sourceTable = sourceTable;
|
||||
operation.relationship.targetTable = targetTable;
|
||||
|
||||
operation.relationship.sourceField = sourceField as FieldType;
|
||||
operation.relationship.targetField = targetField as FieldType;
|
||||
}
|
||||
|
||||
ops.push(operation)
|
||||
}
|
||||
|
||||
ops = orderOperations(ops)
|
||||
|
||||
return ops;
|
||||
}
|
||||
|
||||
|
||||
const orderOperations = (ops: DBDiffOperation[]): DBDiffOperation[] => {
|
||||
const OPERATION_ORDER = [
|
||||
// Create
|
||||
'CREATE_TABLE',
|
||||
'CREATE_RELATIONSHIP',
|
||||
];
|
||||
return [...ops].sort(
|
||||
(a, b) =>
|
||||
OPERATION_ORDER.indexOf(a.type) - OPERATION_ORDER.indexOf(b.type)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,389 @@
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataTypes, ForeignKeyActions, 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";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { DBDiffOperation, mapDiffToDBDiffOperation, normalizeDatabase } from "@/utils/database";
|
||||
import { optimizeOps, prepareForMigration } from "@/utils/migration";
|
||||
import { compare } from "fast-json-patch";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { Statement } from "pgsql-ast-parser";
|
||||
import { getPostgresEnumName } from "../render-uttils";
|
||||
import { emptyDb } from "@/lib/database";
|
||||
|
||||
export default abstract class BaseDatabaseRenderer {
|
||||
|
||||
protected dialect: DatabaseDialect;
|
||||
protected data_types: DataType[];
|
||||
protected schema?: DatabaseType | undefined;
|
||||
protected parser: any;
|
||||
protected readyPromise: Promise<void>;
|
||||
|
||||
public constructor(dialect: DatabaseDialect, data_types: DataType[]) {
|
||||
this.dialect = dialect;
|
||||
this.data_types = data_types;
|
||||
|
||||
this.readyPromise = import("node-sql-parser").then((sqlParser) => {
|
||||
this.parser = new sqlParser.Parser();
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
public getDialect(): DatabaseDialect {
|
||||
return this.dialect;
|
||||
}
|
||||
|
||||
public async renderDDL(database: DatabaseType): Promise<string> {
|
||||
|
||||
const previousDatabase = emptyDb(database);
|
||||
|
||||
// we prepare both databases by enriching them and removing some UI elements that can interfear with json fast path
|
||||
const preparedCurrentDatabase = prepareForMigration(database);
|
||||
const preparedPreviousDatabse = prepareForMigration(previousDatabase);
|
||||
|
||||
// notmalize both just ignore array elements order diff
|
||||
const normalizedPreviousDatabase = normalizeDatabase(preparedPreviousDatabse);
|
||||
const normalizedCurrentDatabase = normalizeDatabase(preparedCurrentDatabase);
|
||||
|
||||
// perform a comparison to extract the diff between the previous database and the current one .
|
||||
const diff_json = compare(normalizedPreviousDatabase, normalizedCurrentDatabase);
|
||||
// map the diff to DB Diff operations to facilate the rendering
|
||||
// optimize the operations by enriching each operation
|
||||
let operations: DBDiffOperation[] = mapDiffToDBDiffOperation(diff_json)
|
||||
operations = optimizeOps(operations, preparedCurrentDatabase, preparedPreviousDatabse, this.data_types);
|
||||
|
||||
// extract the ast based on the dialect
|
||||
const ast: ASTStatment[] = this.operationsToAst(operations, database);
|
||||
// render ast from sql
|
||||
const sql = await this.astToSQL(ast);
|
||||
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||
const ast: ASTStatment[] = [];
|
||||
// we basiclly loop over all operations , and turn them into ast .
|
||||
// some operation can return multiple statments .
|
||||
// so for that we need to check our input if its one AST or an object AST
|
||||
for (const operation of operations) {
|
||||
const statmentAst = this.operationToAst(operation);
|
||||
if (!statmentAst)
|
||||
continue;
|
||||
else if (Array.isArray(statmentAst))
|
||||
ast.push(...statmentAst);
|
||||
else
|
||||
ast.push(statmentAst);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected operationToAst(operation: DBDiffOperation): ASTStatment | ASTStatment[] | null {
|
||||
switch (operation.type) {
|
||||
case "CREATE_TABLE":
|
||||
return this.createTableAst(
|
||||
operation.table
|
||||
);
|
||||
|
||||
case "CREATE_RELATIONSHIP":
|
||||
if (operation.relationship.cardinality == Cardinality.many_to_many)
|
||||
break;
|
||||
return this.createRelationshipAst(
|
||||
operation.relationship,
|
||||
);
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected abstract astToSQL(ast: ASTStatment[]): Promise<string>;
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
let indices_ast: ASTStatment[] = [];
|
||||
// get primary keys and check if we hame more than one
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
||||
// check if we have multiple primary keys if so we need to create a contraint
|
||||
const constraints: ASTStatment[] = multiPrimaryKeys ? [this.getPrimaryKeyContraint(primaryKeys)] : [];
|
||||
// get field dification
|
||||
const field_definitions = table.fields.map((field: FieldType) => this.getFieldDefinition(field, table, multiPrimaryKeys))
|
||||
// the base table ast contain only the field dification and the contraints , then child renderer turn it to ast
|
||||
// check if the table have indices
|
||||
if (table.indices && table.indices.length > 0) {
|
||||
// if so loop over it indices and ignore the index with no columns since that will cause an SQl Syntax error .
|
||||
// finally get the ast of the index and push it to the indices_ast
|
||||
for (const index of table.indices) {
|
||||
if (index.fieldIndices.length == 0)
|
||||
continue;
|
||||
indices_ast.push(this.createIndexAst(
|
||||
table,
|
||||
index
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
field_definitions,
|
||||
constraints,
|
||||
indices_ast
|
||||
} as any;
|
||||
}
|
||||
|
||||
|
||||
protected updateFieldAst(table: TableType, field: FieldType, changes: FieldType): ASTStatment | ASTStatment[] {
|
||||
// get the previous type
|
||||
let type = field.type;
|
||||
|
||||
const ast: any = {
|
||||
length: null,
|
||||
scale: null,
|
||||
dataType: null,
|
||||
auto_increment: null,
|
||||
}
|
||||
// if there is changes in the type , max length , scale or precision or auto incrrement .
|
||||
// in this case SQL Database treat this as a type changes
|
||||
if (changes.typeId || changes.maxLength || changes.scale || changes.precision || changes.autoIncrement !== undefined || changes.values) {
|
||||
// if the type changes , get the new type
|
||||
if (changes.typeId)
|
||||
type = changes.type;
|
||||
|
||||
// get the new or the old type name
|
||||
ast.dataType = (this.dialect == DatabaseDialect.POSTGRES && type.name == "enum") ? getPostgresEnumName(table, field) : type.name?.toLocaleUpperCase();
|
||||
// get the new or old type modifiers
|
||||
const modifiers: string[] = type.modifiers ? JSON.parse(type.modifiers) : [];
|
||||
// if the type support length as VARCHAR(length) and we either have an old max length , or new max length , then set it in the ast
|
||||
if (modifiers.includes(Modifiers.LENGTH) && (changes.maxLength || field.maxLength)) {
|
||||
if (changes.maxLength)
|
||||
ast.length = changes.maxLength
|
||||
else
|
||||
ast.length = field.maxLength;
|
||||
}
|
||||
// if the field support precision and we either have a previous precision or the user add new one .
|
||||
// then set the precision in the AST .
|
||||
if (modifiers.includes(Modifiers.PRECISION) && (changes.precision || field.precision)) {
|
||||
if (changes.precision) {
|
||||
ast.length = changes.precision;
|
||||
} else {
|
||||
ast.length = field.precision;
|
||||
}
|
||||
if (modifiers.includes(Modifiers.SCALE) && !(changes.scale || field.scale)) {
|
||||
// if the scale is not set , we have to set it as O
|
||||
ast.scale = "0";
|
||||
}
|
||||
}
|
||||
// if the type support scale , and scale is set then add it to the ast
|
||||
if (modifiers.includes(Modifiers.SCALE) && (changes.scale || field.scale)) {
|
||||
if (changes.scale) {
|
||||
ast.scale = changes.scale;
|
||||
} else {
|
||||
ast.scale = field.scale;
|
||||
}
|
||||
}
|
||||
// okay if the field is auto increment in postgres we need to switch the type ro serial type .
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && (field.autoIncrement || changes.autoIncrement) && this.dialect == DatabaseDialect.POSTGRES) {
|
||||
if (changes.autoIncrement !== false) {
|
||||
if (ast.dataType == "INTEGER")
|
||||
ast.dataType = "SERIAL";
|
||||
else
|
||||
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||
}
|
||||
}
|
||||
// if there changes in the values
|
||||
if (modifiers.includes(Modifiers.VALUES) && changes.values && this.dialect != DatabaseDialect.POSTGRES) {
|
||||
try {
|
||||
ast.values = JSON.parse(changes.values);
|
||||
} catch (error) {
|
||||
ast.value = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast as AST;
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
|
||||
let primaryKey: FieldType = relationship.sourceField;
|
||||
let foreignKey: FieldType = relationship.targetField;
|
||||
|
||||
let sourceTable: TableType = relationship.sourceTable;
|
||||
let targetTable: TableType = relationship.targetTable;
|
||||
|
||||
if (relationship.cardinality == Cardinality.many_to_one) {
|
||||
primaryKey = relationship.targetField;
|
||||
foreignKey = relationship.sourceField;
|
||||
sourceTable = relationship.targetTable;
|
||||
targetTable = relationship.sourceTable;
|
||||
}
|
||||
return {
|
||||
primaryKey,
|
||||
foreignKey,
|
||||
sourceTable,
|
||||
targetTable
|
||||
} as any
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract createIndexAst(table: TableType, index: IndexType): ASTStatment;
|
||||
|
||||
protected abstract getPrimaryKeyContraint(field: FieldType[], table?: TableType): ASTStatment;
|
||||
|
||||
protected abstract startTransaction(): string;
|
||||
|
||||
protected abstract commit(): string;
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): ASTStatment {
|
||||
// get the modifiers based on the field data type
|
||||
const modifiers: string[] = field?.type?.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||
|
||||
const ast: any = {
|
||||
length: null,
|
||||
scale: null,
|
||||
default_value: null,
|
||||
dataType: null,
|
||||
values: null,
|
||||
default_value_type: null,
|
||||
auto_increment: null,
|
||||
primary_key: ignorePkContraint ? false : field.isPrimary,
|
||||
modifiers: null
|
||||
}
|
||||
|
||||
|
||||
|
||||
// data types by the defaut the field data type name , except for Postgres types needs to be handled diffrenet
|
||||
ast.dataType = field.type?.name?.toLocaleUpperCase();
|
||||
|
||||
// if the field support PRECISION & SCALE modifiers set the length to precision
|
||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
||||
ast.length = field.precision;
|
||||
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
||||
ast.scale = "0";
|
||||
}
|
||||
// if the field support sclae and sclae is set then set the scale
|
||||
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
||||
ast.scale = field.scale;
|
||||
|
||||
// the field support dynamic length , set the length ast
|
||||
// but in case the max length is not set, and the dielect (Mysql , MariaDb ) require a length , then set the default length
|
||||
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
||||
ast.length = field.maxLength;
|
||||
|
||||
else if (
|
||||
modifiers.includes(Modifiers.LENGTH) &&
|
||||
(field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) &&
|
||||
field.type.name?.startsWith('var') &&
|
||||
!field.maxLength
|
||||
)
|
||||
ast.length = MYSQL_MAX_VAR_LENGTH;
|
||||
|
||||
// if the field is an enum type just parse the enum values
|
||||
if (modifiers.includes(Modifiers.VALUES) && field.values) {
|
||||
try {
|
||||
ast.values = JSON.parse(field.values);
|
||||
} catch (error) {
|
||||
ast.value = [];
|
||||
}
|
||||
}
|
||||
const default_value_defintition = this.processDefaultValue(field) as any
|
||||
|
||||
if (default_value_defintition) {
|
||||
const { default_value_type, default_value } = default_value_defintition;
|
||||
ast.default_value_type = default_value_type;
|
||||
ast.default_value = default_value;
|
||||
|
||||
}
|
||||
|
||||
// if the field support auto increment and if the field it is , then set aut increment ast to true .
|
||||
// but if the dialect is Postgres , then we have to change the type from INTEGERS to SERIALS
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) {
|
||||
ast.auto_increment = true
|
||||
} else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) {
|
||||
ast.auto_increment = true;
|
||||
if (ast.dataType == "INTEGER")
|
||||
ast.dataType = "SERIAL";
|
||||
else
|
||||
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||
} else {
|
||||
ast.auto_increment = false;
|
||||
}
|
||||
|
||||
ast.modifiers = modifiers;
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
// if the field have a default value then we need to proccess it based on it type
|
||||
if (field.defaultValue && field.defaultValue.trim().length > 0) {
|
||||
|
||||
const ast: any = {
|
||||
default_value_type: null,
|
||||
default_value: null
|
||||
}
|
||||
|
||||
if ((field.type.type == DataTypes.INTEGER || field.type.type == DataTypes.NUMERIC) && !isNaN(Number(field.defaultValue))) {
|
||||
ast.default_value_type = "number"
|
||||
if (field.type.type == DataTypes.NUMERIC)
|
||||
ast.default_value = parseFloat(field.defaultValue);
|
||||
else
|
||||
ast.default_value = parseInt(field.defaultValue);
|
||||
}
|
||||
else if (field.type.type == DataTypes.TEXT || field.type.type == DataTypes.ENUM) {
|
||||
if (field.type.type == DataTypes.ENUM) {
|
||||
const values = field.values ? JSON.parse(field.values) : [];
|
||||
|
||||
if (field.type.name == "set") {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
|
||||
}
|
||||
else if (values.includes(field.defaultValue)) {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
}
|
||||
else {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
|
||||
}
|
||||
// if we have a field of type time and the default value is Now , then the default value is the fucntion CURRENT_TIME or NOW()
|
||||
else if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW) {
|
||||
ast.default_value_type = "function";
|
||||
ast.default_value = "CURRENT_TIMESTAMP";
|
||||
}
|
||||
|
||||
else if ((field.type.name == "uuid" || field.type.name == "uniqueidentifier") && field.defaultValue == "random") {
|
||||
ast.default_value_type = "function";
|
||||
ast.default_value = "random";
|
||||
|
||||
}
|
||||
else if (field.type.type == DataTypes.TIME) {
|
||||
ast.default_value_type = "single_quote_string";
|
||||
ast.default_value = field.defaultValue;
|
||||
}
|
||||
// the field have a boolean value
|
||||
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||
ast.default_value_type = "bool"
|
||||
ast.default_value = field.defaultValue == "true" ? true : false;
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export interface DBRenderOutput {
|
||||
schema: DatabaseType,
|
||||
diff_json: any[],
|
||||
sql: string;
|
||||
operations: DBDiffOperation[];
|
||||
}
|
||||
|
||||
|
||||
export type ASTStatment = AST | Statement | string | null
|
||||
@@ -0,0 +1,306 @@
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { format } from "sql-formatter";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
|
||||
|
||||
|
||||
export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
// in the base SQL Render we use node-sql-parser .
|
||||
// by simly passing the AST and formating the code .
|
||||
// other renderers can override this method to handle the parsing based on the dielact
|
||||
await this.readyPromise ;
|
||||
|
||||
let sql: string = this.parser.sqlify(ast as AST[], {
|
||||
database: getDatabaseByDialect(this.dialect).name
|
||||
});
|
||||
return format(sql, { language: 'sql' });
|
||||
}
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
// get the tale defintion from parent render that contain only the field defintion and primary keys contraints
|
||||
// this definition is commun amount (Mysql , MariaDB , Sqlite) , for postgres we need to override it and handle enums
|
||||
const { field_definitions, constraints, indices_ast } = super.createTableAst(table) as any
|
||||
// wrape those defintion with create table AST
|
||||
return [{
|
||||
keyword: "table",
|
||||
type: "create",
|
||||
table: [{
|
||||
table: table.name
|
||||
}],
|
||||
create_definitions: [...field_definitions, ...constraints]
|
||||
}, ...indices_ast] as any
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||
// also we need default value expression that it is compatible with node-sql-parser
|
||||
let valuesExpr: any | null;
|
||||
// if the definition extracted values os this field is an enum .
|
||||
// and for that we generate the values expression that's compatible with out parser
|
||||
if (definition.values && definition.values.length > 0) {
|
||||
const values = definition.values.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}));
|
||||
|
||||
valuesExpr = {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: values
|
||||
}
|
||||
}
|
||||
|
||||
const default_val = !definition.modifiers.includes(Modifiers.NO_DEFAULT) ? this.processDefaultValue(field) : undefined;
|
||||
const unique: string | null = !definition.modifiers.includes(Modifiers.NO_UNIQUE) ? (field.unique ? "unique" : null) : null;
|
||||
// return an AST statment for a field definition
|
||||
return {
|
||||
column: {
|
||||
type: "column_ref",
|
||||
column: {
|
||||
expr: {
|
||||
type: "default", value: field.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
default_val,
|
||||
unique,
|
||||
auto_increment: definition.auto_increment ? "auto_increment" : undefined,
|
||||
nullable: {
|
||||
type: field.nullable ? "null" : "not null",
|
||||
value: field.nullable ? "null" : "not null",
|
||||
},
|
||||
definition: {
|
||||
dataType: definition.dataType,
|
||||
length: definition.length,
|
||||
scale: definition.scale,
|
||||
expr: valuesExpr
|
||||
},
|
||||
primary_key: definition.primary_key ? "primary key" : null,
|
||||
resource: "column"
|
||||
} as any
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
// get the foriegn key , primary key source table and target table from the parent renderer
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
let on_action: any[] = [];
|
||||
// get the foriegn key actions for on delete and on cascade
|
||||
if (relationship.onDelete) {
|
||||
const value: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on delete",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
if (relationship.onUpdate) {
|
||||
const value: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on update",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
// return create foriegn key contraint for Mysql Dialect
|
||||
return {
|
||||
type: "alter",
|
||||
keyword: "table",
|
||||
table: [
|
||||
{
|
||||
|
||||
table: targetTable.name
|
||||
}
|
||||
],
|
||||
expr: [{
|
||||
action: "add",
|
||||
create_definitions: {
|
||||
constraint: relationship.name,
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: foreignKey.name
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
constraint_type: "FOREIGN KEY",
|
||||
keyword: "constraint",
|
||||
resource: "constraint",
|
||||
reference_definition: {
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: primaryKey.name
|
||||
}
|
||||
},
|
||||
}
|
||||
],
|
||||
table: [
|
||||
{
|
||||
db: null,
|
||||
table: sourceTable.name
|
||||
}
|
||||
],
|
||||
keyword: "references",
|
||||
on_action
|
||||
}
|
||||
},
|
||||
|
||||
resource: "constraint",
|
||||
type: "alter"
|
||||
}]
|
||||
} as any
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "cascade";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "set null";
|
||||
|
||||
case ForeignKeyActions.RESTRICT:
|
||||
return "restrict";
|
||||
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "set default";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(primaryKeys: FieldType[]): AST {
|
||||
// primary key contraint
|
||||
return {
|
||||
constraint_type: "primary key",
|
||||
resource: "constraint",
|
||||
definition: primaryKeys.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
} as any
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): AST {
|
||||
// create index AST for Mysql Dielect
|
||||
return {
|
||||
index: index.name,
|
||||
type: "create",
|
||||
table: { table: table.name },
|
||||
keyword: "index",
|
||||
on_kw: "on",
|
||||
index_type: index.unique ? "unique" : null,
|
||||
index_columns: index.fields.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
} as AST
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
const definition = super.processDefaultValue(field) as any;
|
||||
// if the field have a default value , then we generate an expression based on the type of the default value
|
||||
if (definition && definition.default_value !== null) {
|
||||
let default_val: any | null = null
|
||||
if (definition.default_value_type == "string")
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "single_quote_string",
|
||||
value: field.defaultValue
|
||||
}
|
||||
}
|
||||
else if (definition.default_value_type == "function" && field.defaultValue == TimeDefaultValues.NOW)
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "origin",
|
||||
value: "CURRENT_TIMESTAMP"
|
||||
}
|
||||
]
|
||||
},
|
||||
over: null
|
||||
}
|
||||
}
|
||||
else if (definition.default_value_type == "function" && field.defaultValue == "random") {
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: "function",
|
||||
name: {
|
||||
name: [
|
||||
{
|
||||
type: "default",
|
||||
value: this.dialect == DatabaseDialect.POSTGRES ? "gen_random_uuid" : "UUID"
|
||||
}
|
||||
]
|
||||
},
|
||||
args: {
|
||||
type: "expr_list",
|
||||
value: []
|
||||
},
|
||||
parentheses: this.dialect == DatabaseDialect.MARIADB
|
||||
}
|
||||
}
|
||||
}
|
||||
else {
|
||||
default_val = {
|
||||
type: "default",
|
||||
value: {
|
||||
type: definition.default_value_type,
|
||||
value: definition.default_value
|
||||
}
|
||||
}
|
||||
}
|
||||
return default_val;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN TRANSACTION;" ;
|
||||
}
|
||||
|
||||
protected commit(): string {
|
||||
return "COMMIT;"
|
||||
}
|
||||
|
||||
|
||||
|
||||
protected abstract getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import MysqlRenderer from "./mysql-renderer";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
|
||||
export default class MariaDbRenderer extends MysqlRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super( data_types , DatabaseDialect.MARIADB)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { format } from 'sql-formatter';
|
||||
import { AST } from "node-sql-parser";
|
||||
|
||||
export default class MSSqlRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.MSSQL, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
let sql = ast.join("\n");
|
||||
sql = format(sql, { language: 'sql' });
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql}
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
}
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
const ast: any = super.createTableAst(table);
|
||||
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
|
||||
// get primary keys and check if we hame more than one
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
let constraints: ASTStatment = "";
|
||||
if (primaryKeys.length > 0) {
|
||||
constraints = this.getPrimaryKeyContraint(primaryKeys, table)
|
||||
}
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${constraints ? ',\n' + constraints : ""}
|
||||
);
|
||||
${indexes}
|
||||
` ;
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||
|
||||
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "IDENTITY(1,1)" : "") : ""
|
||||
|
||||
let nullable: string = ""
|
||||
let unique: string = "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||
|
||||
let options: number[] | string = [];
|
||||
|
||||
if (ast.length)
|
||||
options.push(ast.length);
|
||||
|
||||
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||
options.push(DEFAULT_LENGTH_PARAM);
|
||||
|
||||
if (ast.scale && ast.scale != "0")
|
||||
options.push(ast.scale);
|
||||
|
||||
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||
|
||||
let defaultValue: any = "";
|
||||
|
||||
if (ast.default_value !== undefined && ast.default_value !== null && !dropDefaultValue) {
|
||||
defaultValue = `CONSTRAINT DF_${table.name}_${field.name} DEFAULT ${ast.default_value}`;
|
||||
}
|
||||
if (!ast.primary_key) {
|
||||
unique = field.unique ? `CONSTRAINT UQ_${table.name}_${field.name} UNIQUE` : "";
|
||||
}
|
||||
|
||||
const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
|
||||
}
|
||||
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
const ast: any = super.processDefaultValue(field);
|
||||
|
||||
if (ast && ast.default_value_type) {
|
||||
if (ast.default_value_type == "single_quote_string") {
|
||||
ast.default_value = `'${ast.default_value}'`
|
||||
}
|
||||
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||
ast.default_value = ast.default_value;
|
||||
|
||||
else if (ast.default_value_type == "function") {
|
||||
|
||||
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||
|
||||
switch (field.type.name?.toUpperCase()) {
|
||||
case "DATE":
|
||||
ast.default_value = "CAST(GETDATE() AS DATE)"
|
||||
break;
|
||||
case "DATETIME":
|
||||
ast.default_value = "GETDATE()"
|
||||
break;
|
||||
case "SMALLDATETIME":
|
||||
ast.default_value = "GETDATE()"
|
||||
break;
|
||||
case "DATETIME2":
|
||||
ast.default_value = "SYSDATETIME()"
|
||||
break;
|
||||
case "DATETIMEOFFSET":
|
||||
ast.default_value = "SYSDATETIMEOFFSET()"
|
||||
break;
|
||||
}
|
||||
} else if (ast.default_value == "random") {
|
||||
if (field.isPrimary)
|
||||
ast.default_value = "NEWSEQUENTIALID()"
|
||||
else
|
||||
ast.default_value = "NEWID()"
|
||||
}
|
||||
|
||||
}
|
||||
if ( ast.default_value_type == "bool") {
|
||||
if (ast.default_value) {
|
||||
ast.default_value = 1 ;
|
||||
}else {
|
||||
ast.default_value = 0 ;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
|
||||
const onDeleteFKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onUpdateFKAction: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
|
||||
const onDeleteAction: string = onDeleteFKAction ? `ON DELETE ${onDeleteFKAction}` : ""
|
||||
const onUpdateAction: string = onUpdateFKAction ? `ON UPDATE ${onUpdateFKAction}` : ""
|
||||
|
||||
const FKActions: string = [onDeleteAction, onUpdateAction].join(" ");
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${FKActions};
|
||||
`
|
||||
}
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "CASCADE";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "SET NULL";
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "SET DEFAULT";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[], table?: TableType): ASTStatment {
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
return `CONSTRAINT PK_${table?.name} PRIMARY KEY (${pks.join(",")})`
|
||||
}
|
||||
|
||||
protected startTransaction(): string {
|
||||
return `BEGIN TRANSACTION;`
|
||||
}
|
||||
protected commit(): string {
|
||||
return "COMMIT;"
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { fixCharsetPlacement } from "../render-uttils";
|
||||
import { format } from 'sql-formatter';
|
||||
import { Modifiers } from "@/lib/field";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
export default class MysqlRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[], dialect: DatabaseDialect = DatabaseDialect.MYSQL) {
|
||||
super(dialect, data_types)
|
||||
}
|
||||
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||
// for Mysql , MariaDB , we use node-sql-parser to turn AST to SQL ,
|
||||
// the package have a bug which is CHARSET and COLLATION missplacementts
|
||||
// so we patch it using fixCharsetPlacement function
|
||||
// and finally we format the code
|
||||
try {
|
||||
let sql: string = await super.astToSQL(ast);
|
||||
sql = fixCharsetPlacement(format(sql, { language: "sql" }));
|
||||
sql = format(sql, { language: 'sql' });
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql};
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// get the modifiers based on the field data type
|
||||
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||
// also we need default value expression that it is compatible with node-sql-parser
|
||||
let suffix: string[] = [];
|
||||
|
||||
// check if the field support ZEROFILL and if it set , and the same thing for unsigned
|
||||
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
|
||||
suffix.push(Modifiers.ZEROFILL.toUpperCase());
|
||||
}
|
||||
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
|
||||
suffix.push(Modifiers.UNSIGNED.toUpperCase());
|
||||
}
|
||||
// if it support charset and charset is set , then generate it expression
|
||||
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
|
||||
|
||||
definition.character_set = {
|
||||
type: "CHARACTER SET",
|
||||
value: {
|
||||
type: "default",
|
||||
value: field.charset
|
||||
}
|
||||
}
|
||||
// if it support collate and collate is set , then generate it expression
|
||||
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
|
||||
|
||||
definition.collate = {
|
||||
keyword: "collate",
|
||||
type: "collate",
|
||||
collate: {
|
||||
name: field.collate,
|
||||
}
|
||||
}
|
||||
|
||||
if (suffix.length > 0) {
|
||||
definition.definition.suffix = suffix
|
||||
}
|
||||
return definition;
|
||||
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { AST } from "node-sql-parser";
|
||||
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { format } from 'sql-formatter';
|
||||
|
||||
export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.ORACLE, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
let sql = ast.join("\n");
|
||||
sql = format(sql, { language: 'sql' });
|
||||
return sql;
|
||||
}
|
||||
|
||||
|
||||
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||
const ast: any = super.createTableAst(table);
|
||||
let pk_constraint: string = ast.constraints?.pop();
|
||||
if (pk_constraint)
|
||||
pk_constraint = "," + pk_constraint
|
||||
else
|
||||
pk_constraint = "";
|
||||
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||
return `
|
||||
CREATE TABLE ${table.name} (
|
||||
${(ast as any).field_definitions.join(",\n")}
|
||||
${pk_constraint}
|
||||
);
|
||||
${indexes}
|
||||
` ;
|
||||
}
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||
|
||||
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
let primaryKey: string = ast.primary_key ? "PRIMARY KEY" : "";
|
||||
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "GENERATED BY DEFAULT AS IDENTITY" : "") : ""
|
||||
|
||||
let nullable: string = ""
|
||||
let unique: string = "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||
|
||||
if (!ast.primary_key)
|
||||
unique = field.unique ? "UNIQUE" : "";
|
||||
|
||||
let options: number[] | string = [];
|
||||
|
||||
if (ast.length)
|
||||
options.push(ast.length);
|
||||
|
||||
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||
options.push(DEFAULT_LENGTH_PARAM);
|
||||
|
||||
if (ast.scale && ast.scale != "0")
|
||||
options.push(ast.scale);
|
||||
|
||||
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||
|
||||
let defaultValue: any = "";
|
||||
|
||||
if (ast.default_value !== undefined && ast.default_value !== null) {
|
||||
defaultValue = "DEFAULT " + ast.default_value
|
||||
}
|
||||
if (dropDefaultValue) {
|
||||
defaultValue = "DEFAULT NULL"
|
||||
}
|
||||
|
||||
let dataType = ast.dataType;
|
||||
if (dataType === "TIMESTAMP WITH LOCAL TIME ZONE" && options) {
|
||||
dataType = `TIMESTAMP${options} WITH LOCAL TIME ZONE`
|
||||
}
|
||||
else if (dataType === "TIMESTAMP WITH TIME ZONE" && options) {
|
||||
dataType = `TIMESTAMP${options} WITH TIME ZONE`
|
||||
}
|
||||
else if (dataType === "INTERVAL DAY TO SECOND" && options) {
|
||||
dataType = `INTERVAL DAY${options} TO SECOND`
|
||||
}
|
||||
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
|
||||
dataType = `INTERVAL YEAR${options} TO MONTH`
|
||||
|
||||
}
|
||||
|
||||
else
|
||||
dataType = dataType + options
|
||||
|
||||
|
||||
//const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||
|
||||
return `${field.name} ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
|
||||
}
|
||||
protected processDefaultValue(field: FieldType): AST | null {
|
||||
|
||||
const ast: any = super.processDefaultValue(field);
|
||||
|
||||
if (ast && ast.default_value !== undefined && ast.default_value !== null) {
|
||||
|
||||
|
||||
if (ast.default_value_type == "single_quote_string") {
|
||||
ast.default_value = `'${ast.default_value}'`
|
||||
}
|
||||
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||
ast.default_value = ast.default_value;
|
||||
|
||||
else if (ast.default_value_type == "function") {
|
||||
|
||||
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||
|
||||
switch (field.type.name?.toUpperCase()) {
|
||||
case "DATE":
|
||||
ast.default_value = "SYSDATE"
|
||||
break;
|
||||
case "TIMESTAMP":
|
||||
ast.default_value = "CURRENT_TIMESTAMP"
|
||||
break;
|
||||
case "TIMESTAMP WITH TIME ZONE":
|
||||
ast.default_value = "SYSTIMESTAMP"
|
||||
break;
|
||||
case "TIMESTAMP WITH LOCAL TIME ZONE":
|
||||
ast.default_value = "SYSTIMESTAMP"
|
||||
break;
|
||||
}
|
||||
} else if (ast.default_value == "random") {
|
||||
ast.default_value = "SYS_GUID()"
|
||||
}
|
||||
}
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||
const unique: string = index.unique ? " UNIQUE" : "";
|
||||
|
||||
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||
if (columns.length == 0)
|
||||
return null;
|
||||
columns = `(${columns.join(",")})`
|
||||
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||
|
||||
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||
|
||||
const FKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
const onDeleteAction: string = FKAction ? `ON DELETE ${FKAction}` : ""
|
||||
|
||||
return `
|
||||
ALTER TABLE ${targetTable.name}
|
||||
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${onDeleteAction};
|
||||
`
|
||||
}
|
||||
|
||||
|
||||
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "CASCADE";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "SET NULL";
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
|
||||
//throw new Error("Method not implemented.");
|
||||
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||
return `
|
||||
PRIMARY KEY (${pks.join(",")})
|
||||
`
|
||||
}
|
||||
protected startTransaction(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected commit(): string {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,157 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { getPostgresEnumName } from "../render-uttils";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Statement, toSql } from "pgsql-ast-parser";
|
||||
import { ASTStatment } from "./base-database-renderer";
|
||||
import { format } from "sql-formatter";
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { DataTypes } from "@/lib/field";
|
||||
|
||||
|
||||
export default class PostgresqlRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.POSTGRES, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||
// postgres renderer is different , we use a different parser pgsql-ast-parser for index , enum renaming .
|
||||
// those kind of operations return Statment not AST .
|
||||
// so the we pass AST to the default parser and Statment to Postgres parser
|
||||
|
||||
await this.readyPromise ;
|
||||
let sql: string[] = [];
|
||||
|
||||
for (const statment of ast) {
|
||||
if ((statment as Statement)?.type == "alter index" || (statment as Statement)?.type == "alter enum") {
|
||||
// index renaming
|
||||
sql.push(toSql.statement(statment as Statement) + ";");
|
||||
}
|
||||
else {
|
||||
const statment_sql = this.parser.sqlify(statment as AST, {
|
||||
database: getDatabaseByDialect(this.dialect).name
|
||||
});
|
||||
sql.push(statment_sql + ";");
|
||||
}
|
||||
}
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = [this.startTransaction( ) , ...sql , this.commit()]
|
||||
}
|
||||
return format(sql.join(""), { language: 'postgresql' });
|
||||
}
|
||||
|
||||
|
||||
protected createTableAst(table: TableType): AST[] | AST {
|
||||
// the only difference in create table statment in postgres is we need to get declare all enums before creating the table
|
||||
const definition: AST[] = super.createTableAst(table) as any
|
||||
// so we need an enum ast
|
||||
const enumsAst: AST[] = [];
|
||||
// get fields to type enum
|
||||
let postgresEnums: FieldType[] = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||
if (postgresEnums.length > 0) {
|
||||
// loop over them and intiat them one by one
|
||||
for (const postgresEnum of postgresEnums) {
|
||||
(postgresEnum as any).table = table;
|
||||
enumsAst.push(this.createEnumAst(postgresEnum));
|
||||
}
|
||||
}
|
||||
definition.splice(0, 0, ...enumsAst);
|
||||
return definition;
|
||||
}
|
||||
|
||||
|
||||
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||
// we get the base field definition from the parent renderer
|
||||
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||
|
||||
// since postrges uses the serial data types , then there is not need for auto incrmenet attribute
|
||||
definition.auto_increment = undefined;
|
||||
// postgres handle enums diffrently , we need to create the enum first then reference to it in the column line
|
||||
if (definition.definition.dataType == "ENUM") {
|
||||
definition.definition.dataType = getPostgresEnumName(table, field);
|
||||
definition.definition.expr = undefined;
|
||||
}
|
||||
return definition;
|
||||
}
|
||||
|
||||
|
||||
|
||||
private createEnumAst(field: FieldType): AST {
|
||||
// get the enum values , and cast it into an array
|
||||
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
||||
// return the ast of creating a table
|
||||
return {
|
||||
as: "as",
|
||||
type: "create",
|
||||
resource: "enum",
|
||||
name: {
|
||||
schema: null,
|
||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||
},
|
||||
keyword: "type",
|
||||
create_definitions: {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: jsonValues.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}))
|
||||
}
|
||||
} as any
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, newDataType: DataType): AST | null {
|
||||
|
||||
const oldType: DataTypes = field.type.type as DataTypes;
|
||||
const newType: DataTypes = newDataType.type as DataTypes;
|
||||
|
||||
if (oldType === newType && oldType != DataTypes.ENUM) return null;
|
||||
|
||||
if (
|
||||
(oldType === DataTypes.INTEGER && newType === DataTypes.NUMERIC) ||
|
||||
(oldType === DataTypes.NUMERIC && newType === DataTypes.INTEGER)
|
||||
) return null;
|
||||
|
||||
|
||||
let dataType = newDataType.name?.toUpperCase();
|
||||
|
||||
if (newDataType.name == "enum")
|
||||
dataType = "TEXT::" + getPostgresEnumName(table, field);
|
||||
else if (newDataType.name != "text")
|
||||
dataType = "TEXT::" + newDataType.name?.toUpperCase();
|
||||
|
||||
return {
|
||||
as: null,
|
||||
symbol: "::",
|
||||
target: [
|
||||
{
|
||||
dataType
|
||||
}
|
||||
],
|
||||
type: "cast",
|
||||
keyword: "cast",
|
||||
expr: {
|
||||
type: "column_ref",
|
||||
table: null,
|
||||
column: {
|
||||
expr: {
|
||||
type: "default",
|
||||
value: field.name
|
||||
}
|
||||
},
|
||||
collate: null
|
||||
}
|
||||
} as any;
|
||||
}
|
||||
|
||||
protected startTransaction(): string {
|
||||
return "BEGIN;" ;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { AST } from "node-sql-parser";
|
||||
import { fixSQLiteColumnOrder } from "../render-uttils";
|
||||
import { format } from 'sql-formatter';
|
||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { ASTStatment } from "./base-database-renderer";
|
||||
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { DBDiffOperation } from "@/utils/database";
|
||||
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||
import { orderTables } from "@/utils/tables";
|
||||
|
||||
export default class SqliteRenderer extends BaseSQLRenderer {
|
||||
|
||||
public constructor(data_types: DataType[]) {
|
||||
super(DatabaseDialect.SQLITE, data_types)
|
||||
}
|
||||
|
||||
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||
try {
|
||||
let sql: string = await super.astToSQL(ast);
|
||||
|
||||
sql = fixSQLiteColumnOrder(format(sql, { language: "sql" }));
|
||||
sql = format(sql, { language: 'sql' });
|
||||
|
||||
if (sql.length > 0) {
|
||||
sql = `${this.startTransaction()}
|
||||
|
||||
${sql};
|
||||
|
||||
${this.commit()}
|
||||
`
|
||||
}
|
||||
return sql;
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||
|
||||
const ast: ASTStatment[] = [];
|
||||
|
||||
let sortedTables: TableType[] | RenderableTable = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_TABLE").map((operation: DBDiffOperation) => (operation as any).table)
|
||||
|
||||
if (database) {
|
||||
let renderableTables: RenderableTable[] = sortedTables.map((table: TableType) => toRenderableTable(table, database));
|
||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||
|
||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||
|
||||
sortedTables = sortedTablesIds.map((id: string) =>
|
||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||
);
|
||||
}
|
||||
|
||||
const relationshipOperations: DBDiffOperation[] = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_RELATIONSHIP");
|
||||
|
||||
// we basiclly loop over all operations , and turn them into ast .
|
||||
// some operation can return multiple statments .
|
||||
// so for that we need to check our input if its one AST or an object AST
|
||||
for (const table of sortedTables) {
|
||||
const statmentAst = this.createTableAst(table);
|
||||
|
||||
if (!statmentAst)
|
||||
continue;
|
||||
|
||||
|
||||
if ((table as RenderableTable).foreignRelationships?.length > 0) {
|
||||
const relationshipsAst: ASTStatment[] = (table as RenderableTable).foreignRelationships.map((relationship: RelationshipType) => {
|
||||
const operation: DBDiffOperation = relationshipOperations.find((operation: DBDiffOperation) => (operation as any).relationship?.id == relationship.id) as DBDiffOperation;
|
||||
return this.createRelationshipAst((operation as any).relationship)
|
||||
})
|
||||
|
||||
if (Array.isArray(statmentAst)) {
|
||||
|
||||
const index = statmentAst.findIndex((statment: any) => statment.type == "create" && statment.keyword == "table");
|
||||
|
||||
if (index >= 0) {
|
||||
(statmentAst[index] as any).create_definitions.push(...relationshipsAst)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Array.isArray(statmentAst))
|
||||
ast.push(...statmentAst);
|
||||
else
|
||||
ast.push(statmentAst);
|
||||
}
|
||||
return ast;
|
||||
}
|
||||
|
||||
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||
const relationshipAst: any = super.createRelationshipAst(relationship) as any;
|
||||
return relationshipAst.expr[0].create_definitions;
|
||||
}
|
||||
|
||||
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected dropPrimaryKeyConstraintExpr(table: TableType): ASTStatment {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
protected createPrimaryKeyConstraintExpr(table: TableType, fields: FieldType[]): ASTStatment {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,395 +0,0 @@
|
||||
|
||||
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataTypes, ForeignKeyActions, 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";
|
||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||
import { IndexType } from "@/lib/schemas/index-schema";
|
||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
||||
import { getForeignRelationships } from "@/utils/relationship";
|
||||
import { orderTables } from "@/utils/tables";
|
||||
import _ from "lodash" ;
|
||||
|
||||
export const DatabaseToAst = (db: DatabaseType, data_types: DataType[]) => {
|
||||
let dbAst: any = [];
|
||||
if (!data_types || data_types.length == 0)
|
||||
return dbAst;
|
||||
const database = _.cloneDeep(db) ;
|
||||
let renderableTables: RenderableTable[] = database.tables.map((table: TableType) => toRenderableTable(table, database));
|
||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||
|
||||
try {
|
||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||
|
||||
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
|
||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||
);
|
||||
for (const table of sortedTables) {
|
||||
let postgresEnums: FieldType[];
|
||||
if (database.dialect == DatabaseDialect.POSTGRES) {
|
||||
postgresEnums = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||
if (postgresEnums.length > 0) {
|
||||
for (const postgresEnum of postgresEnums) {
|
||||
(postgresEnum as any).table = table;
|
||||
dbAst.push(PotgresEnumToAst(postgresEnum));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dbAst.push(TableToAst(table, data_types, database.dialect == DatabaseDialect.POSTGRES));
|
||||
|
||||
if (table.indices && table.indices.length > 0)
|
||||
for (const index of table.indices)
|
||||
if (index.fieldIndices?.length > 0)
|
||||
dbAst.push(IndexToAst({
|
||||
...index,
|
||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
|
||||
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
|
||||
) as FieldType[],
|
||||
}, table));
|
||||
};
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
return dbAst;
|
||||
}
|
||||
|
||||
|
||||
export const TableToAst = (table: TableType, data_types: DataType[], isPostgresDialect: boolean = false) => {
|
||||
|
||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
||||
|
||||
const primaryKeysConstraints: any[] | undefined = multiPrimaryKeys ? [{
|
||||
constraint_type: "primary key",
|
||||
resource: "constraint",
|
||||
definition: primaryKeys.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
}] : [];
|
||||
|
||||
let foreignRelationships = getForeignRelationships(table);
|
||||
const foreignKeysConstraints = foreignRelationships.map((relationship: RelationshipType) => relationshipToAst(relationship));
|
||||
const constraints: any[] = [
|
||||
...primaryKeysConstraints,
|
||||
...foreignKeysConstraints
|
||||
];
|
||||
|
||||
const filed_definitions = table.fields.filter((field: FieldType) => field.type).map((field: FieldType) => {
|
||||
const isPostgresEnum: boolean = isPostgresDialect && field.type.name == "enum";
|
||||
|
||||
return FieldToAst({
|
||||
...field,
|
||||
type: !(isPostgresEnum) ?
|
||||
data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType :
|
||||
{ name: `${table.name}_${field.name.toLowerCase()}_enum` } as DataType,
|
||||
}, multiPrimaryKeys, !isPostgresEnum)
|
||||
})
|
||||
|
||||
return {
|
||||
keyword: "table",
|
||||
type: "create",
|
||||
table: [{
|
||||
table: table.name
|
||||
}],
|
||||
create_definitions: [...filed_definitions, ...constraints]
|
||||
}
|
||||
}
|
||||
export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, upperCaseType: boolean = true) => {
|
||||
|
||||
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 | string | 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;
|
||||
let dataType: string | undefined = upperCaseType ? field.type?.name?.toLocaleUpperCase() : field.type?.name as string | undefined;
|
||||
|
||||
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;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
||||
length = field.maxLength;
|
||||
|
||||
else if (modifiers.includes(Modifiers.LENGTH) && (field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) && field.type.name?.startsWith('var') && !field.maxLength)
|
||||
length = MYSQL_MAX_VAR_LENGTH;
|
||||
|
||||
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.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 = field.defaultValue == "true" ? true : false;
|
||||
}
|
||||
|
||||
// Check if it's a number (but not empty string or just whitespace)
|
||||
else if (!isNaN(Number(field.defaultValue))) {
|
||||
default_val.value.type = "number"
|
||||
default_val.value.value = Number(field.defaultValue);
|
||||
}
|
||||
}
|
||||
|
||||
let auto_increment: string | undefined;
|
||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) {
|
||||
auto_increment = "auto_increment"
|
||||
} else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) {
|
||||
if (dataType == "INTEGER")
|
||||
dataType = "SERIAL";
|
||||
else
|
||||
dataType = dataType?.replace("INT", "SERIAL");
|
||||
|
||||
} else {
|
||||
auto_increment = undefined;
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
column: {
|
||||
type: "column_ref",
|
||||
column: {
|
||||
expr: {
|
||||
type: "default", value: field.name,
|
||||
}
|
||||
},
|
||||
},
|
||||
collate,
|
||||
character_set,
|
||||
default_val,
|
||||
unique: field.unique ? "unique" : null,
|
||||
auto_increment,
|
||||
nullable: {
|
||||
type: field.nullable ? "null" : "not null",
|
||||
value: field.nullable ? "null" : "not null",
|
||||
},
|
||||
definition: {
|
||||
dataType,
|
||||
length,
|
||||
scale,
|
||||
suffix,
|
||||
expr: valuesExpr
|
||||
},
|
||||
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
|
||||
resource: "column"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const IndexToAst = (index: IndexType, table: TableType) => {
|
||||
|
||||
return {
|
||||
index: index.name,
|
||||
type: "create",
|
||||
table: { table: table.name },
|
||||
keyword: "index",
|
||||
on_kw: "on",
|
||||
index_type: index.unique ? "unique" : null,
|
||||
index_columns: index.fields.map((field: FieldType) => ({
|
||||
column: field.name,
|
||||
type: "column_ref"
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
export const PotgresEnumToAst = (field: FieldType) => {
|
||||
|
||||
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
||||
|
||||
return {
|
||||
as: "as",
|
||||
type: "create",
|
||||
resource: "enum",
|
||||
name: {
|
||||
schema: null,
|
||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||
},
|
||||
keyword: "type",
|
||||
create_definitions: {
|
||||
parentheses: true,
|
||||
type: "expr_list",
|
||||
value: jsonValues.map((value: string) => ({
|
||||
type: "single_quote_string",
|
||||
value
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const foreignKeyActionToAst = (action: ForeignKeyActions): string | null => {
|
||||
switch (action) {
|
||||
case ForeignKeyActions.CASCADE:
|
||||
return "cascade";
|
||||
|
||||
case ForeignKeyActions.SET_NULL:
|
||||
return "set null";
|
||||
|
||||
case ForeignKeyActions.RESTRICT:
|
||||
return "restrict";
|
||||
|
||||
case ForeignKeyActions.SET_DEFAULT:
|
||||
return "set default";
|
||||
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const relationshipToAst = (relationship: RelationshipType) => {
|
||||
|
||||
let primaryKey: FieldType = relationship.sourceField;
|
||||
let foreignKey: FieldType = relationship.targetField;
|
||||
|
||||
let sourceTable: TableType = relationship.sourceTable;
|
||||
let targetTable: TableType = relationship.targetTable;
|
||||
|
||||
if (relationship.cardinality == Cardinality.many_to_one) {
|
||||
primaryKey = relationship.targetField;
|
||||
foreignKey = relationship.sourceField;
|
||||
sourceTable = relationship.targetTable;
|
||||
targetTable = relationship.sourceTable;
|
||||
}
|
||||
|
||||
let on_action: any[] = [];
|
||||
|
||||
if (relationship.onDelete) {
|
||||
const value: string | null = foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on delete",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
if (relationship.onUpdate) {
|
||||
const value: string | null = foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||
if (value)
|
||||
on_action.push({
|
||||
type: "on update",
|
||||
value: {
|
||||
type: "origin",
|
||||
value
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
constraint: null,
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
column: foreignKey.name,
|
||||
|
||||
}
|
||||
],
|
||||
constraint_type: "FOREIGN KEY",
|
||||
resource: "constraint",
|
||||
reference_definition: {
|
||||
definition: [
|
||||
{
|
||||
type: "column_ref",
|
||||
column: primaryKey.name,
|
||||
}
|
||||
],
|
||||
table: [
|
||||
{
|
||||
table: sourceTable.name,
|
||||
}
|
||||
],
|
||||
keyword: "references",
|
||||
on_action
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,722 +0,0 @@
|
||||
import { DataTypes, ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||
import { Parser } from "node-sql-parser";
|
||||
import { v4 } from "uuid";
|
||||
import { parse } from 'pgsql-ast-parser';
|
||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||
import { randomColor } from "@/lib/colors";
|
||||
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
||||
|
||||
|
||||
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
|
||||
const parser = new Parser();
|
||||
let errors: Error[] = [];
|
||||
|
||||
const createTableStatements: string[] = [];
|
||||
const alterTableStatements: string[] = [];
|
||||
const createIndexStatements: string[] = [];
|
||||
const createPostgresTypesStatements: string[] = [];
|
||||
|
||||
const tables: TableInsertType[] = [];
|
||||
let relationships: RelationshipInsertType[] = [];
|
||||
const indices: IndexInsertType[] = []
|
||||
|
||||
const postgresTypes: any[] = [];
|
||||
// Clean up SQL: remove comments and normalize
|
||||
const cleanedSql = sql
|
||||
.replace(/--.*$/gm, '') // remove single-line comments
|
||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
||||
.replace(/\s+/g, ' ') // normalize whitespace
|
||||
.replace(/;\s*/g, ';\n'); // separate statements
|
||||
|
||||
// Split into individual statements
|
||||
const statements = cleanedSql
|
||||
.split('\n')
|
||||
.map(s => s.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
for (const stmt of statements) {
|
||||
const upper = stmt.toUpperCase();
|
||||
|
||||
if (upper.startsWith('CREATE TABLE')) {
|
||||
createTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('ALTER TABLE')) {
|
||||
alterTableStatements.push(stmt);
|
||||
} else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) {
|
||||
createIndexStatements.push(stmt);
|
||||
} else if (upper.startsWith('CREATE TYPE')) {
|
||||
createPostgresTypesStatements.push(stmt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (dialect == DatabaseDialect.POSTGRES) {
|
||||
for (const postgresType of createPostgresTypesStatements) {
|
||||
try {
|
||||
const instructionAst = parse(postgresType);
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
postgresTypes.push(instructionAst[0]);
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for (const createTable of createTableStatements) {
|
||||
try {
|
||||
const instructionAst = parse(createTable);
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
const table: TableInsertType = postgresAstToTable(instructionAst[0], data_types, postgresTypes);
|
||||
tables.push(table);
|
||||
|
||||
for (const column of (instructionAst[0] as any).columns)
|
||||
if (column.constraints?.length > 0) {
|
||||
let referenceColumn: any | undefined = column.constraints.find((contraint: any) => contraint.type == "reference")
|
||||
|
||||
if (!referenceColumn)
|
||||
continue
|
||||
|
||||
referenceColumn = {
|
||||
...referenceColumn, localColumns: [
|
||||
{ name: column.name.name }
|
||||
]
|
||||
}
|
||||
|
||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst([referenceColumn], table);
|
||||
|
||||
try {
|
||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
||||
relationships = relationships.concat(newRelationships);
|
||||
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
|
||||
if ((instructionAst[0] as any).constraints && (instructionAst[0] as any).constraints.length > 0) {
|
||||
|
||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst((instructionAst[0] as any).constraints, table)
|
||||
try {
|
||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
||||
relationships = relationships.concat(newRelationships);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
|
||||
}
|
||||
}
|
||||
for (const createIndex of createIndexStatements) {
|
||||
try {
|
||||
const instructionAst = parse(createIndex);
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
indices.push(postgresAstToIndex(instructionAst[0], tables));
|
||||
}
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
}
|
||||
}
|
||||
for (const alterTable of alterTableStatements) {
|
||||
try {
|
||||
const instructionAst = parse(alterTable);
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
const extractedRelationships: RelationshipInsertType[] = postgresAstToRelationship((instructionAst[0] as any), tables);
|
||||
relationships = relationships.concat(extractedRelationships);
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let foreignKeyConstraints: any[] = [];
|
||||
let referenceDefinitions: any[] = [];
|
||||
|
||||
for (let createTable of createTableStatements) {
|
||||
|
||||
try {
|
||||
|
||||
if (dialect == DatabaseDialect.SQLITE)
|
||||
createTable = createTable.replace(/\btext\s*\(\s*\d+\s*\)/gi, 'TEXT');
|
||||
|
||||
|
||||
let instructionAst = parser.astify(createTable, {
|
||||
database: dialect == DatabaseDialect.MARIADB ? getDatabaseByDialect(DatabaseDialect.MYSQL).name : getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
if (instructionAst) {
|
||||
const table: TableInsertType = astToTable(instructionAst, data_types);
|
||||
tables.push(table);
|
||||
|
||||
const tableForeignKeyConstraints = (instructionAst as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
|
||||
const tableReferenceDefinitions = (instructionAst as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
|
||||
|
||||
foreignKeyConstraints = foreignKeyConstraints.concat(
|
||||
tableForeignKeyConstraints.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
referenceDefinitions = referenceDefinitions.concat(
|
||||
tableReferenceDefinitions.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
for (const referenceDefinition of referenceDefinitions) {
|
||||
try {
|
||||
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
|
||||
} catch (error) {
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
for (const alterTable of alterTableStatements) {
|
||||
try {
|
||||
let instructionAst: any = parser.astify(alterTable, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
|
||||
if (instructionAst) {
|
||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
|
||||
...instructionAst[0],
|
||||
table: instructionAst[0].table?.[0].table
|
||||
}) as RelationshipInsertType[];
|
||||
|
||||
relationships = relationships.concat(extractedRelationships)
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
|
||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
||||
relationships = relationships.concat((error as any).relationships);
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
for (const createIndex of createIndexStatements) {
|
||||
try {
|
||||
let instructionAst = parser.astify(createIndex, {
|
||||
database: getDatabaseByDialect(dialect).name
|
||||
});
|
||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
||||
instructionAst = instructionAst[0];
|
||||
}
|
||||
indices.push(astToIndex(instructionAst, tables));
|
||||
|
||||
} catch (error) {
|
||||
|
||||
errors.push(error as Error);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (tables.length == 0)
|
||||
throw Error("Error while Parsing")
|
||||
|
||||
|
||||
return { tables, relationships, indices, errors };
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const astToForiegnKeyAction = (ast: string): ForeignKeyActions => {
|
||||
switch (ast) {
|
||||
case "cascade":
|
||||
return ForeignKeyActions.CASCADE;
|
||||
case "set null":
|
||||
return ForeignKeyActions.SET_NULL;
|
||||
case 'restrict':
|
||||
return ForeignKeyActions.RESTRICT;
|
||||
case "set default":
|
||||
return ForeignKeyActions.SET_DEFAULT;
|
||||
}
|
||||
return ForeignKeyActions.NO_ACTION
|
||||
}
|
||||
|
||||
export const astToRelationship = (tables: TableInsertType[], constraintAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
|
||||
|
||||
let targetField: FieldInsertType | undefined;
|
||||
let sourceTable: TableInsertType | undefined;
|
||||
let sourceField: FieldInsertType | undefined;
|
||||
let targetTable: TableInsertType | undefined;
|
||||
|
||||
let relationships: RelationshipInsertType[] = [];
|
||||
|
||||
|
||||
let onDelete: ForeignKeyActions | undefined;
|
||||
let onUpdate: ForeignKeyActions | undefined;
|
||||
let on_action: any | undefined;
|
||||
|
||||
|
||||
|
||||
if (constraintAst) {
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == constraintAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.definition?.[0].column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == constraintAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.reference_definition?.definition?.[0].column);
|
||||
on_action = constraintAst.reference_definition.on_action;
|
||||
|
||||
}
|
||||
|
||||
if (columnAst) {
|
||||
|
||||
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == columnAst.table?.[0].table);
|
||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.column?.column);
|
||||
sourceTable = tables.find((table: TableInsertType) => table.name == columnAst.reference_definition?.table?.[0].table);
|
||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.reference_definition?.definition?.[0].column);
|
||||
on_action = columnAst.reference_definition.on_action;
|
||||
}
|
||||
|
||||
|
||||
|
||||
if (on_action) {
|
||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
||||
|
||||
if (onDeleteAst)
|
||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
||||
if (onUpdateAst)
|
||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
||||
}
|
||||
|
||||
if (alterTableAst) {
|
||||
|
||||
const expressions = alterTableAst.expr;
|
||||
const foreignKeyExpressions = expressions.filter((expression: any) => expression.resource == "constraint" && expression.create_definitions?.constraint_type == "FOREIGN KEY")
|
||||
targetTable = tables.find((table: TableInsertType) => table.name == alterTableAst.table);
|
||||
|
||||
|
||||
for (const expression of foreignKeyExpressions) {
|
||||
|
||||
const targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.definition?.[0].column);
|
||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == expression.create_definitions?.reference_definition?.table?.[0].table)
|
||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.reference_definition?.definition?.[0].column);
|
||||
|
||||
const on_action: any | undefined = expression.create_definitions?.reference_definition?.on_action;
|
||||
|
||||
let onDelete: ForeignKeyActions | undefined;
|
||||
let onUpdate: ForeignKeyActions | undefined;
|
||||
if (on_action) {
|
||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
||||
|
||||
if (onDeleteAst)
|
||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
||||
if (onUpdateAst)
|
||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
||||
}
|
||||
|
||||
|
||||
if (!targetField || !sourceField || !sourceTable)
|
||||
continue;
|
||||
|
||||
relationships.push({
|
||||
id: v4(),
|
||||
targetTableId: targetTable?.id,
|
||||
targetFieldId: targetField.id,
|
||||
sourceTableId: sourceTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete,
|
||||
onUpdate
|
||||
} as RelationshipInsertType)
|
||||
}
|
||||
|
||||
if (relationships.length == foreignKeyExpressions.length)
|
||||
return relationships;
|
||||
else
|
||||
throw Error({
|
||||
success: false,
|
||||
message: "Failed to Extract all relationships",
|
||||
relationships
|
||||
} as any)
|
||||
}
|
||||
|
||||
if (!targetField || !sourceField || !sourceTable)
|
||||
throw Error("Failed to extract relationship");
|
||||
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
targetTableId: targetTable?.id,
|
||||
targetFieldId: targetField.id,
|
||||
sourceTableId: sourceTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete,
|
||||
onUpdate
|
||||
} as RelationshipInsertType;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.table[0]?.table,
|
||||
fields: ast.create_definitions.filter((column: any) => column.resource == "column")
|
||||
.map((fieldAst: any, index: number) => astToField(fieldAst, data_types, index)),
|
||||
color: randomColor()
|
||||
} as TableInsertType;
|
||||
}
|
||||
|
||||
|
||||
export const astToField = (ast: any, data_types: DataType[], sequence: number): FieldInsertType => {
|
||||
|
||||
|
||||
|
||||
const dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
return dataType.name == ast.definition.dataType?.toLowerCase() || synonyms.includes(ast.definition.dataType?.toLowerCase())
|
||||
});
|
||||
|
||||
let values: string | undefined = undefined;
|
||||
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
const { length, scale } = ast.definition;
|
||||
const { character_set, collate: collation } = ast;
|
||||
|
||||
let charset: string | undefined;
|
||||
let collate: string | undefined;
|
||||
|
||||
let defaultValue: string | undefined = ast.default_val?.value?.value ? ast.default_val?.value?.value : undefined;
|
||||
|
||||
let maxLength: number | null = null;
|
||||
let precision: number | null = null;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
||||
maxLength = length;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
||||
precision = length;
|
||||
|
||||
|
||||
if (modifiers.includes(Modifiers.CHARSET) && character_set)
|
||||
charset = character_set.value?.value;
|
||||
|
||||
if (modifiers.includes(Modifiers.COLLATE) && collation)
|
||||
collate = collation.collate?.name;
|
||||
|
||||
if (modifiers.includes(Modifiers.VALUES)) {
|
||||
|
||||
|
||||
const value = ast.definition?.expr?.value.map((value: any) => value.value);
|
||||
if (value)
|
||||
values = JSON.stringify(value);
|
||||
}
|
||||
|
||||
if (dataType?.type == DataTypes.TIME &&
|
||||
ast.default_val?.value?.type == "function" &&
|
||||
ast.default_val?.value?.name?.name?.length > 0 &&
|
||||
ast.default_val?.value?.name?.name[0].value == "CURRENT_TIMESTAMP")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
const isPrimary: boolean = ast.primary_key == "primary key";
|
||||
const nullable: boolean = ast.nullable?.value ? ast.nullable?.value != "not null" : !isPrimary;
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.column.column,
|
||||
defaultValue,
|
||||
typeId: dataType?.id,
|
||||
nullable,
|
||||
unique: ast.unique == "unique",
|
||||
maxLength,
|
||||
precision,
|
||||
scale,
|
||||
sequence,
|
||||
autoIncrement: ast.auto_increment == "auto_increment",
|
||||
isPrimary,
|
||||
values,
|
||||
charset,
|
||||
collate,
|
||||
} as FieldInsertType;
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const postgresAstToTable = (ast: any, data_types: DataType[], postgresTypes: any[]): TableInsertType => {
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.name.name,
|
||||
fields: ast.columns.filter((column: any) => column.kind == "column")
|
||||
.map((fieldAst: any, index: number) => postgresAstToField(fieldAst, data_types, index, postgresTypes)),
|
||||
color: randomColor(),
|
||||
} as TableInsertType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
export const postgresAstToField = (ast: any, data_types: DataType[], sequence: number, postgresTypes: any[]): FieldInsertType => {
|
||||
|
||||
let dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||
return dataType.name == ast.dataType.name?.toLowerCase() || synonyms.includes(ast.dataType.name?.toLowerCase())
|
||||
});
|
||||
|
||||
let values: string | undefined;
|
||||
let autoIncrement: boolean = false;
|
||||
|
||||
if (!dataType && postgresTypes && postgresTypes.length > 0) {
|
||||
const postgresType: any = postgresTypes.find((type: any) => type.name.name == ast.dataType.name);
|
||||
if (postgresType) {
|
||||
dataType = data_types.find((dataType: DataType) => dataType.name == "enum") as DataType;
|
||||
values = JSON.stringify(postgresType.values.map((value: any) => value.value));
|
||||
};
|
||||
}
|
||||
|
||||
if (ast.dataType.name?.toLowerCase().includes("serial")) {
|
||||
|
||||
let baseType: string = ast.dataType.name?.toLowerCase() == "serial" ? "integer" : ast.dataType.name?.toLowerCase().replace("serial", "int");
|
||||
dataType = data_types.find((dataType: DataType) => {
|
||||
return dataType.name == baseType;
|
||||
});
|
||||
autoIncrement = true;
|
||||
}
|
||||
|
||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||
|
||||
let length: number | undefined;
|
||||
let scale: number | undefined;
|
||||
let unique: boolean = false;
|
||||
let primaryKey: boolean = false;
|
||||
|
||||
if (ast.dataType.config && ast.dataType.config.length > 0) {
|
||||
if (ast.dataType.config.length >= 1)
|
||||
length = ast.dataType.config[0];
|
||||
|
||||
if (ast.dataType.config.length == 2)
|
||||
scale = ast.dataType.config[1];
|
||||
}
|
||||
|
||||
|
||||
let maxLength: number | null = null;
|
||||
let precision: number | null = null;
|
||||
|
||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
||||
maxLength = length;
|
||||
|
||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
||||
precision = length;
|
||||
|
||||
let defaultValue: string | undefined;
|
||||
let nullable: boolean = true;
|
||||
|
||||
const constraints: any[] | undefined = ast.constraints;
|
||||
if (constraints && constraints.length > 0) {
|
||||
|
||||
const nullableConstraints: any | undefined = constraints.find((c: any) => c.type == "not null");
|
||||
const defaultValueConstraints: any | undefined = constraints.find((c: any) => c.type == "default");
|
||||
|
||||
if (defaultValueConstraints) {
|
||||
if (defaultValueConstraints.default.type == "keyword" && defaultValueConstraints.default.keyword == "current_timestamp")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "call" && defaultValueConstraints.default.function?.name == "now")
|
||||
defaultValue = TimeDefaultValues.NOW;
|
||||
|
||||
else if (defaultValueConstraints.default.type == "cast" && defaultValueConstraints.default.operand)
|
||||
defaultValue = String(defaultValueConstraints.default.operand.value)
|
||||
else
|
||||
defaultValue = String(defaultValueConstraints.default.value);
|
||||
}
|
||||
const uniqueConstraints: any | undefined = constraints.find((c: any) => c.type == "unique");
|
||||
const primryKeyConstraints: any | undefined = constraints.find((c: any) => c.type == "primary key");
|
||||
|
||||
if (uniqueConstraints)
|
||||
unique = true;
|
||||
if (primryKeyConstraints)
|
||||
primaryKey = true;
|
||||
|
||||
if (nullableConstraints || primaryKey)
|
||||
nullable = false;
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.name.name,
|
||||
defaultValue,
|
||||
typeId: dataType?.id,
|
||||
nullable,
|
||||
unique,
|
||||
maxLength,
|
||||
precision,
|
||||
scale,
|
||||
isPrimary: primaryKey,
|
||||
sequence,
|
||||
values,
|
||||
autoIncrement
|
||||
} as FieldInsertType;
|
||||
}
|
||||
|
||||
export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]): RelationshipInsertType[] => {
|
||||
|
||||
const relationships: RelationshipInsertType[] = [];
|
||||
const changes = ast.changes;
|
||||
|
||||
const targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name);
|
||||
|
||||
if (!targetTable)
|
||||
throw Error("source table not found");
|
||||
|
||||
const foreignKeyConstraints = changes.filter((change: any) => change.type == 'add constraint' && change.constraint && change.constraint.type == "foreign key").map((change: any) => change.constraint);
|
||||
|
||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
||||
|
||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == foreignKeyConstraint.foreignTable.name);
|
||||
const targetField: FieldInsertType | undefined = targetTable.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.localColumns[0]?.name)
|
||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.foreignColumns[0]?.name)
|
||||
|
||||
const onDelete = foreignKeyConstraint.onDelete ? astToForiegnKeyAction(foreignKeyConstraint.onDelete) : undefined;
|
||||
const onUpdate = foreignKeyConstraint.onUpdate ? astToForiegnKeyAction(foreignKeyConstraint.onUpdate) : undefined;
|
||||
|
||||
if (!sourceField || !targetField || !sourceTable)
|
||||
continue;
|
||||
|
||||
relationships.push({
|
||||
id: v4(),
|
||||
sourceTableId: sourceTable.id,
|
||||
targetTableId: targetTable.id,
|
||||
sourceFieldId: sourceField.id,
|
||||
targetFieldId: targetField.id,
|
||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||
onDelete, onUpdate
|
||||
} as RelationshipInsertType)
|
||||
}
|
||||
|
||||
if (relationships.length == foreignKeyConstraints.length)
|
||||
return relationships;
|
||||
else
|
||||
throw Error({
|
||||
success: false,
|
||||
message: "Failed to Extract all relationships",
|
||||
relationships
|
||||
} as any)
|
||||
}
|
||||
|
||||
|
||||
const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableInsertType) => {
|
||||
|
||||
const changes: any[] = constraints.filter((constraint: any) => constraint.type == "foreign key" || constraint.type == "reference").map((constraint: any) => ({
|
||||
|
||||
type: "add constraint",
|
||||
constraint: {
|
||||
type: "foreign key",
|
||||
localColumns: [
|
||||
{
|
||||
name: constraint.localColumns?.[0].name
|
||||
}
|
||||
],
|
||||
foreignTable: {
|
||||
name: constraint.foreignTable.name,
|
||||
},
|
||||
foreignColumns: [
|
||||
{
|
||||
name: constraint.foreignColumns?.[0].name
|
||||
}
|
||||
],
|
||||
onDelete: constraint.onDelete,
|
||||
onUpdate: constraint.onUpdate,
|
||||
}
|
||||
}))
|
||||
|
||||
return {
|
||||
type: "alter table",
|
||||
only: true,
|
||||
table: {
|
||||
name: table.name
|
||||
},
|
||||
changes
|
||||
}
|
||||
}
|
||||
|
||||
export const astToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType => {
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.table);
|
||||
|
||||
if (!table)
|
||||
throw Error("table not found");
|
||||
|
||||
const fieldNames: string[] = ast.index_columns.map((column: any) => column.column);
|
||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
||||
.map((field: FieldInsertType) => field.id);
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.index,
|
||||
tableId: table.id,
|
||||
unique: ast.index_type == "unique",
|
||||
fieldIndices: fieldIds?.map((id: string) => ({
|
||||
id: v4(),
|
||||
fieldId: id
|
||||
}))
|
||||
} as IndexInsertType
|
||||
}
|
||||
|
||||
export const postgresAstToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType => {
|
||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name);
|
||||
|
||||
if (!table)
|
||||
throw Error("table not found");
|
||||
|
||||
const fieldNames: string[] = ast.expressions.map((expression: any) => expression.expression.name);
|
||||
|
||||
|
||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
||||
.map((field: FieldInsertType) => field.id);
|
||||
|
||||
return {
|
||||
id: v4(),
|
||||
name: ast.indexName.name,
|
||||
tableId: table.id,
|
||||
unique: ast.unique,
|
||||
fieldIndices: fieldIds?.map((id: string) => ({
|
||||
id: v4(),
|
||||
fieldId: id
|
||||
}))
|
||||
} as IndexInsertType
|
||||
}
|
||||
|
||||
@@ -1,3 +1,14 @@
|
||||
import { DatabaseDialect } from "@/lib/database";
|
||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||
import MysqlRenderer from "./database/mysql-renderer";
|
||||
import PostgresqlRenderer from "./database/postgresql-renderer";
|
||||
import MariaDbRenderer from "./database/mariadb-renderer";
|
||||
import OracleRenderer from "./database/oracle-renderer";
|
||||
import MSSqlRenderer from "./database/mssql-database-renderer";
|
||||
import SqliteRenderer from "./database/sqlite-database-renderer";
|
||||
import { TableType } from "@/lib/schemas/table-schema";
|
||||
import { FieldType } from "@/lib/schemas/field-schema";
|
||||
|
||||
/**
|
||||
* Fixes charset/collation placement in SQL CREATE TABLE statements
|
||||
* @param sql The SQL string to process
|
||||
@@ -151,4 +162,32 @@ export interface CircularDependencyError {
|
||||
cycle: string[];
|
||||
success: boolean;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export const getPostgresEnumName = (table: TableType, field: FieldType): string => {
|
||||
|
||||
return `${table.name}_${field.name.toLowerCase()}_enum`
|
||||
}
|
||||
|
||||
export const getRenderer = (dialect: DatabaseDialect, data_types: DataType[]) => {
|
||||
|
||||
switch (dialect) {
|
||||
case DatabaseDialect.MYSQL:
|
||||
return new MysqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.POSTGRES:
|
||||
return new PostgresqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.MARIADB:
|
||||
return new MariaDbRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.ORACLE:
|
||||
return new OracleRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.MSSQL:
|
||||
return new MSSqlRenderer(data_types);
|
||||
|
||||
case DatabaseDialect.SQLITE:
|
||||
return new SqliteRenderer(data_types);
|
||||
}
|
||||
}
|
||||
+5
-1
@@ -12,11 +12,15 @@ export default defineConfig({
|
||||
optimizeDeps: {
|
||||
// Don't optimize these packages as they contain web workers and WASM files.
|
||||
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
|
||||
exclude: ['@journeyapps/wa-sqlite', '@powersync/web'],
|
||||
exclude: ['@journeyapps/wa-sqlite', '@powersync/web' , '@guanmingchiu/sqlparser-ts'],
|
||||
include: ['@powersync/web > js-logger']
|
||||
},
|
||||
worker: {
|
||||
format: 'es',
|
||||
plugins: () => [ topLevelAwait()]
|
||||
} ,
|
||||
|
||||
build: {
|
||||
target: "esnext"
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user