5 Commits

Author SHA1 Message Date
KarimTamani caa277577a Improved UI/UX + Lazy Loading 2026-06-10 21:43:46 +01:00
KarimTamani 6c3b431ee2 Add Oracle & MSSQL Support 2026-06-10 18:09:54 +01:00
dependabot[bot] 3985e53886 Bump rollup from 4.52.2 to 4.60.1
Bumps [rollup](https://github.com/rollup/rollup) from 4.52.2 to 4.60.1.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.52.2...v4.60.1)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 4.60.1
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 21:33:30 +01:00
dependabot[bot] ef27393d9a Bump the npm_and_yarn group across 1 directory with 9 updates
Bumps the npm_and_yarn group with 9 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [lodash](https://github.com/lodash/lodash) | `4.17.21` | `4.18.1` |
| [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) | `5.4.20` | `5.4.21` |
| [minimatch](https://github.com/isaacs/minimatch) | `3.1.2` | `3.1.5` |
| [esbuild](https://github.com/evanw/esbuild) | `0.21.5` | `removed` |
| [flatted](https://github.com/WebReflection/flatted) | `3.3.3` | `3.4.2` |
| [js-yaml](https://github.com/nodeca/js-yaml) | `4.1.0` | `4.1.1` |
| [picomatch](https://github.com/micromatch/picomatch) | `2.3.1` | `2.3.2` |
| [react-router](https://github.com/remix-run/react-router/tree/HEAD/packages/react-router) | `6.23.0` | `6.30.3` |
| [tar](https://github.com/isaacs/node-tar) | `7.5.1` | `7.5.13` |



Updates `lodash` from 4.17.21 to 4.18.1
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.21...4.18.1)

Updates `vite` from 5.4.20 to 5.4.21
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.21/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.21/packages/vite)

Updates `minimatch` from 3.1.2 to 3.1.5
- [Changelog](https://github.com/isaacs/minimatch/blob/main/changelog.md)
- [Commits](https://github.com/isaacs/minimatch/compare/v3.1.2...v3.1.5)

Removes `esbuild`

Updates `flatted` from 3.3.3 to 3.4.2
- [Commits](https://github.com/WebReflection/flatted/compare/v3.3.3...v3.4.2)

Updates `js-yaml` from 4.1.0 to 4.1.1
- [Changelog](https://github.com/nodeca/js-yaml/blob/master/CHANGELOG.md)
- [Commits](https://github.com/nodeca/js-yaml/compare/4.1.0...4.1.1)

Updates `picomatch` from 2.3.1 to 2.3.2
- [Release notes](https://github.com/micromatch/picomatch/releases)
- [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md)
- [Commits](https://github.com/micromatch/picomatch/compare/2.3.1...2.3.2)

Updates `react-router` from 6.23.0 to 6.30.3
- [Release notes](https://github.com/remix-run/react-router/releases)
- [Changelog](https://github.com/remix-run/react-router/blob/react-router@6.30.3/packages/react-router/CHANGELOG.md)
- [Commits](https://github.com/remix-run/react-router/commits/react-router@6.30.3/packages/react-router)

Updates `tar` from 7.5.1 to 7.5.13
- [Release notes](https://github.com/isaacs/node-tar/releases)
- [Changelog](https://github.com/isaacs/node-tar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/isaacs/node-tar/compare/v7.5.1...v7.5.13)

---
updated-dependencies:
- dependency-name: lodash
  dependency-version: 4.18.1
  dependency-type: direct:production
  dependency-group: npm_and_yarn
- dependency-name: vite
  dependency-version: 5.4.21
  dependency-type: direct:development
  dependency-group: npm_and_yarn
- dependency-name: minimatch
  dependency-version: 3.1.5
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: esbuild
  dependency-version: 
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: flatted
  dependency-version: 3.4.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: js-yaml
  dependency-version: 4.1.1
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: picomatch
  dependency-version: 2.3.2
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: react-router
  dependency-version: 6.30.3
  dependency-type: indirect
  dependency-group: npm_and_yarn
- dependency-name: tar
  dependency-version: 7.5.13
  dependency-type: indirect
  dependency-group: npm_and_yarn
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-09 21:28:31 +01:00
Tamani Karim 9feb0397bc Merge pull request #4 from stackrender/copilot/dockerize-the-application
Copilot/dockerize the application

“Merge Docker build improvements and memory fix into main
2026-03-29 16:18:18 +01:00
63 changed files with 7970 additions and 3801 deletions
+2512 -2199
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -1,7 +1,7 @@
{
"name": "StackRender",
"private": true,
"version": "1.0.2",
"version": "1.3.2",
"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,10 +99,11 @@
"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",
"vite-plugin-top-level-await": "^1.5.0",
"vite-tsconfig-paths": "^4.3.2"
}
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

-4
View File
@@ -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}
+2 -3
View File
@@ -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}
>
{
+130
View File
@@ -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 '>
+16 -21
View File
@@ -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} -&gt; {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>
}
+16 -10
View File
@@ -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) {
+18 -15
View File
@@ -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>
+75
View File
@@ -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;
-62
View File
@@ -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
View File
@@ -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
View File
@@ -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."
}
}
}
+17
View File
@@ -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."
}
}
+16
View File
@@ -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."
}
}
+15
View File
@@ -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 lemplacement où enregistrer le fichier .sql.",
"step5": "Terminez lassistant, 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 dexportation, 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 lemplacement du fichier dexportation, puis passez à l’étape suivante.",
"step5": "Sous Types dobjets standards, cochez uniquement Tables, Index, Contraintes et Contraintes référentielles. Laissez toutes les autres options décochées.",
"step6": "Terminez lassistant, puis ouvrez le fichier .sql généré et copiez son contenu dans l’éditeur de code ci-dessous."
}
}
}
+15
View File
@@ -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 फ़ाइल खोलें और उसकी सामग्री को नीचे दिए गए कोड एडिटर में कॉपी करें।"
}
}
}
+15
View File
@@ -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."
}
}
}
+15
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
},
});
+175
View File
@@ -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'
}
]
+151
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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;
+1 -1
View File
@@ -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(),
-3
View File
@@ -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
View File
@@ -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[]) => {
+778
View File
@@ -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
);
*/
+40
View File
@@ -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;
}
+37
View File
@@ -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
}
}
+702
View File
@@ -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');
*/
+97
View File
@@ -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);
*/
+725
View File
@@ -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
);
*/
+284
View File
@@ -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
);
*/
+60
View File
@@ -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
)
*/
+153
View File
@@ -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.");
}
}
-395
View File
@@ -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
}
}
}
-722
View File
@@ -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
}
+39
View File
@@ -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
View File
@@ -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"
}
})