mirror of
https://github.com/stackrender/stackrender.git
synced 2026-09-11 03:35:42 +00:00
Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6c3b431ee2 | |||
| 3985e53886 | |||
| ef27393d9a | |||
| 9feb0397bc |
Generated
+2481
-2542
File diff suppressed because it is too large
Load Diff
+8
-5
@@ -13,6 +13,7 @@
|
|||||||
"@codemirror/lang-sql": "^6.9.0",
|
"@codemirror/lang-sql": "^6.9.0",
|
||||||
"@dnd-kit/core": "^6.3.1",
|
"@dnd-kit/core": "^6.3.1",
|
||||||
"@dnd-kit/sortable": "^10.0.0",
|
"@dnd-kit/sortable": "^10.0.0",
|
||||||
|
"@guanmingchiu/sqlparser-ts": "^0.61.1",
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.8.2",
|
||||||
"@nextui-org/react": "^2.6.11",
|
"@nextui-org/react": "^2.6.11",
|
||||||
"@powersync/drizzle-driver": "^0.4.0",
|
"@powersync/drizzle-driver": "^0.4.0",
|
||||||
@@ -53,7 +54,7 @@
|
|||||||
"fast-json-patch": "^3.1.1",
|
"fast-json-patch": "^3.1.1",
|
||||||
"framer-motion": "11.15.0",
|
"framer-motion": "11.15.0",
|
||||||
"i18next-browser-languagedetector": "^8.0.5",
|
"i18next-browser-languagedetector": "^8.0.5",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.18.1",
|
||||||
"lucide-react": "^0.501.0",
|
"lucide-react": "^0.501.0",
|
||||||
"node-sql-parser": "^5.3.9",
|
"node-sql-parser": "^5.3.9",
|
||||||
"object-hash": "^3.0.0",
|
"object-hash": "^3.0.0",
|
||||||
@@ -64,7 +65,7 @@
|
|||||||
"react-dom": "18.3.1",
|
"react-dom": "18.3.1",
|
||||||
"react-i18next": "^15.5.1",
|
"react-i18next": "^15.5.1",
|
||||||
"react-resizable-panels": "^2.1.9",
|
"react-resizable-panels": "^2.1.9",
|
||||||
"react-router-dom": "6.23.0",
|
"react-router-dom": "6.30.3",
|
||||||
"react-tag-input": "^6.10.6",
|
"react-tag-input": "^6.10.6",
|
||||||
"sonner": "^2.0.7",
|
"sonner": "^2.0.7",
|
||||||
"sql-formatter": "^15.6.3",
|
"sql-formatter": "^15.6.3",
|
||||||
@@ -78,15 +79,16 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
"@types/better-sqlite3": "^7.6.13",
|
||||||
"@types/node": "20.5.7",
|
"@types/node": "20.19.0",
|
||||||
"@types/object-hash": "^3.0.6",
|
"@types/object-hash": "^3.0.6",
|
||||||
"@types/pluralize": "^0.0.33",
|
"@types/pluralize": "^0.0.33",
|
||||||
"@types/react": "18.3.3",
|
"@types/react": "18.3.3",
|
||||||
"@types/react-dom": "18.3.0",
|
"@types/react-dom": "18.3.0",
|
||||||
"@typescript-eslint/eslint-plugin": "8.11.0",
|
"@typescript-eslint/eslint-plugin": "8.11.0",
|
||||||
"@typescript-eslint/parser": "8.11.0",
|
"@typescript-eslint/parser": "8.11.0",
|
||||||
"@vitejs/plugin-react": "^4.2.1",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"autoprefixer": "10.4.19",
|
"autoprefixer": "10.4.19",
|
||||||
|
"esbuild": "^0.28.0",
|
||||||
"eslint": "^8.57.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-config-prettier": "9.1.0",
|
"eslint-config-prettier": "9.1.0",
|
||||||
"eslint-plugin-import": "^2.26.0",
|
"eslint-plugin-import": "^2.26.0",
|
||||||
@@ -97,9 +99,10 @@
|
|||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-unused-imports": "4.1.4",
|
"eslint-plugin-unused-imports": "4.1.4",
|
||||||
"prettier": "3.3.3",
|
"prettier": "3.3.3",
|
||||||
|
"rollup": "^4.61.1",
|
||||||
"tw-animate-css": "^1.3.6",
|
"tw-animate-css": "^1.3.6",
|
||||||
"typescript": "5.6.3",
|
"typescript": "5.6.3",
|
||||||
"vite": "^5.2.0",
|
"vite": "^8.0.3",
|
||||||
"vite-plugin-top-level-await": "^1.5.0",
|
"vite-plugin-top-level-await": "^1.5.0",
|
||||||
"vite-tsconfig-paths": "^4.3.2"
|
"vite-tsconfig-paths": "^4.3.2"
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 94 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 66 KiB |
@@ -1,7 +1,6 @@
|
|||||||
|
|
||||||
|
|
||||||
import "@/styles/globals.css"
|
import "@/styles/globals.css"
|
||||||
|
|
||||||
import { ReactFlowProvider } from "@xyflow/react";
|
import { ReactFlowProvider } from "@xyflow/react";
|
||||||
import useAppRoutes from "./routes/app-route";
|
import useAppRoutes from "./routes/app-route";
|
||||||
import { SyncProvider } from "./providers/sync-provider/sync-provider";
|
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 { ModalProvider } from "./providers/modal-provider/modal-provider";
|
||||||
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
import DatabaseHotkeysProvider from "./providers/database-hotkeys/database-hotkeys-provider";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
|
|
||||||
const appRoutes = useAppRoutes();
|
const appRoutes = useAppRoutes();
|
||||||
@@ -21,7 +18,6 @@ function App() {
|
|||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<DatabaseProvider>
|
<DatabaseProvider>
|
||||||
<DiagramProvider>
|
<DiagramProvider>
|
||||||
|
|
||||||
<ModalProvider>
|
<ModalProvider>
|
||||||
<DatabaseHotkeysProvider>
|
<DatabaseHotkeysProvider>
|
||||||
{appRoutes}
|
{appRoutes}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
import { useRenderSql } from "@/hooks/user-render-sql";
|
|
||||||
import { useDatabase } from "@/providers/database-provider/database-provider";
|
import { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
import React, { useEffect, useMemo } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
|
|
||||||
import CodeMirror from '@uiw/react-codemirror';
|
import CodeMirror from '@uiw/react-codemirror';
|
||||||
import { sql } from '@codemirror/lang-sql';
|
import { sql } from '@codemirror/lang-sql';
|
||||||
import { oneDark } from '@codemirror/theme-one-dark';
|
import { oneDark } from '@codemirror/theme-one-dark';
|
||||||
|
|
||||||
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
||||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
import CircularDependencyAlert from "./circular-dependecy-alert";
|
import CircularDependencyAlert from "./circular-dependecy-alert";
|
||||||
@@ -13,21 +11,26 @@ import { useTranslation } from "react-i18next";
|
|||||||
import Clipboard from "@/components/clipboard";
|
import Clipboard from "@/components/clipboard";
|
||||||
import { TableType } from "@/lib/schemas/table-schema";
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
|
||||||
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
import { useTheme } from "@/providers/theme-provider/theme-provider";
|
||||||
import { toast } from "sonner";
|
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";
|
||||||
|
|
||||||
|
|
||||||
interface SqlPreviewProps {
|
interface SqlPreviewProps {
|
||||||
tableFilterIds?: string[]
|
tableFilterIds?: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
||||||
|
|
||||||
|
|
||||||
const { database: currentDatabase } = useDatabase();
|
const { database: currentDatabase } = useDatabase();
|
||||||
|
const { data_types } = useDatabaseOperations();
|
||||||
|
const [sqlCode, setSqlCode] = useState<string>("");
|
||||||
|
|
||||||
|
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined);
|
||||||
|
|
||||||
const database = useMemo(() => {
|
const database = useMemo(() => {
|
||||||
if (!tableFilterIds)
|
if (!tableFilterIds)
|
||||||
return currentDatabase;
|
return currentDatabase;
|
||||||
@@ -40,10 +43,39 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
|||||||
} as DatabaseType;
|
} as DatabaseType;
|
||||||
}, [currentDatabase, tableFilterIds])
|
}, [currentDatabase, tableFilterIds])
|
||||||
|
|
||||||
const { sql: sqlCode, circularDependency } = useRenderSql(database as DatabaseType);
|
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
const { t } = useTranslation();
|
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(() => {
|
useEffect(() => {
|
||||||
if (circularDependency)
|
if (circularDependency)
|
||||||
toast(t("db_controller.circular_dependency.title"), {
|
toast(t("db_controller.circular_dependency.title"), {
|
||||||
@@ -66,7 +98,6 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
|||||||
text={sqlCode}
|
text={sqlCode}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
defaultValue={sqlCode}
|
defaultValue={sqlCode}
|
||||||
value={sqlCode}
|
value={sqlCode}
|
||||||
@@ -74,9 +105,7 @@ const SqlPreview: React.FC<SqlPreviewProps> = ({ tableFilterIds }) => {
|
|||||||
extensions={[sql()]}
|
extensions={[sql()]}
|
||||||
readOnly
|
readOnly
|
||||||
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
theme={theme != "dark" ? overrideLightTheme : [oneDark, overrideDarkTheme]}
|
||||||
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,11 @@ import { overrideDarkTheme, overrideLightTheme } from "@/lib/colors";
|
|||||||
import { sql } from '@codemirror/lang-sql';
|
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 { 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 { useDatabase, useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
||||||
|
|
||||||
import { AlertCircleIcon, Code, HelpCircle } from "lucide-react";
|
import { AlertCircleIcon, Code, HelpCircle } from "lucide-react";
|
||||||
import { SqlToDatabase } from "@/utils/render/parsers/sql_to_database";
|
|
||||||
import Clipboard from "@/components/clipboard";
|
import Clipboard from "@/components/clipboard";
|
||||||
import { Trans } from 'react-i18next';
|
import { Trans } from 'react-i18next';
|
||||||
import { Node, useReactFlow } from "@xyflow/react";
|
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 { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
@@ -20,6 +18,8 @@ import { useTheme } from "@/providers/theme-provider/theme-provider";
|
|||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { buttonVariants } from "@/components/ui/button";
|
import { buttonVariants } from "@/components/ui/button";
|
||||||
|
import { BaseSqlImporter } from "@/utils/import/base-sql-importer";
|
||||||
|
import { getImporter } from "@/utils/import/import-utils";
|
||||||
|
|
||||||
|
|
||||||
const options: ImportDatabaseOption[] = [{
|
const options: ImportDatabaseOption[] = [{
|
||||||
@@ -107,6 +107,27 @@ const options: ImportDatabaseOption[] = [{
|
|||||||
type: ImportMethodType.DB_CLIENT,
|
type: ImportMethodType.DB_CLIENT,
|
||||||
numberOfInstructions: 5,
|
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 +135,6 @@ const options: ImportDatabaseOption[] = [{
|
|||||||
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
|
||||||
const { theme: resolvedTheme } = useTheme();
|
const { theme: resolvedTheme } = useTheme();
|
||||||
const { database, isLoading, isSwitchingDatabase } = useDatabase();
|
const { database, isLoading, isSwitchingDatabase } = useDatabase();
|
||||||
const { data_types, importDatabase } = useDatabaseOperations();
|
const { data_types, importDatabase } = useDatabaseOperations();
|
||||||
@@ -123,6 +143,17 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
|||||||
const [error, setError] = useState<boolean>(false);
|
const [error, setError] = useState<boolean>(false);
|
||||||
const { fitView } = useReactFlow();
|
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(() => {
|
let currentOption: ImportDatabaseOption | undefined = useMemo(() => {
|
||||||
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
|
return options.find((option: ImportDatabaseOption) => option.dialect == database?.dialect)
|
||||||
}, [database]);
|
}, [database]);
|
||||||
@@ -141,52 +172,63 @@ const ImportDatabaseModal: React.FC<ModalProps> = ({ isOpen, onOpenChange }) =>
|
|||||||
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId) as ImportDatabaseMethod;
|
return currentOption?.methods.find((method: ImportDatabaseMethod) => method.id == selectedMethodId) as ImportDatabaseMethod;
|
||||||
}, [selectedMethodId, currentOption])
|
}, [selectedMethodId, currentOption])
|
||||||
|
|
||||||
|
|
||||||
const validateSql = useCallback((code: string) => {
|
const validateSql = useCallback((code: string) => {
|
||||||
try {
|
if (importer)
|
||||||
const parsedDatabase = SqlToDatabase(code, data_types, database?.dialect as DatabaseDialect);
|
|
||||||
|
|
||||||
setParsedDatabase(parsedDatabase);
|
|
||||||
setError(false);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
setError(true);
|
|
||||||
setParsedDatabase(undefined)
|
|
||||||
}
|
|
||||||
setSqlCode(code)
|
|
||||||
|
|
||||||
}, [database?.dialect, data_types])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const onImport = useCallback(async () => {
|
|
||||||
return new Promise(async (res, rej) => {
|
|
||||||
try {
|
try {
|
||||||
const nodes: Node[] = parsedDatabase.tables.map((table: TableInsertType) => ({
|
const parsedDatabase = importer.parseSql(code);
|
||||||
id: table.id,
|
setParsedDatabase(parsedDatabase);
|
||||||
data: {
|
setError(false);
|
||||||
table
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
const adjustedTables = await adjustTablesPositions(nodes, parsedDatabase.relationships);
|
|
||||||
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indices);
|
|
||||||
|
|
||||||
onOpenChange && onOpenChange(false);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
fitView({
|
|
||||||
duration: 500
|
|
||||||
});
|
|
||||||
}, 300);
|
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
||||||
setError(true);
|
setError(true);
|
||||||
rej()
|
setParsedDatabase(undefined)
|
||||||
}
|
}
|
||||||
})
|
else {
|
||||||
}, [parsedDatabase]);
|
console.error("Importer not loaded");
|
||||||
|
}
|
||||||
|
setSqlCode(code)
|
||||||
|
|
||||||
|
}, [database?.dialect, data_types, importer])
|
||||||
|
|
||||||
|
const onImport = useCallback(async () => {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
await importDatabase(adjustedTables, parsedDatabase.relationships, parsedDatabase.indexes);
|
||||||
|
|
||||||
|
onOpenChange && onOpenChange(false);
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
fitView({
|
||||||
|
duration: 500
|
||||||
|
});
|
||||||
|
}, 300);
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
|
||||||
|
setError(true);
|
||||||
|
rej()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [parsedDatabase, database]);
|
||||||
|
|
||||||
|
|
||||||
if (isLoading || isSwitchingDatabase)
|
if (isLoading || isSwitchingDatabase)
|
||||||
|
|||||||
@@ -1,62 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
import { useDatabaseOperations } from "@/providers/database-provider/database-provider";
|
|
||||||
import { DatabaseToAst } from "@/utils/render/parsers/database_to_ast";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import { Parser } from "node-sql-parser";
|
|
||||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
|
||||||
import { format } from 'sql-formatter';
|
|
||||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
|
||||||
import { CircularDependencyError, fixCharsetPlacement, fixSQLiteColumnOrder } from "@/utils/render/render-uttils";
|
|
||||||
import { areArraysEqual } from "@/utils/utils";
|
|
||||||
import { decomposeManyToMany } from "@/utils/relationship";
|
|
||||||
|
|
||||||
const parser = new Parser();
|
|
||||||
|
|
||||||
|
|
||||||
export const useRenderSql = (database: DatabaseType) => {
|
|
||||||
const [sql, setSql] = useState<string>("");
|
|
||||||
const { data_types } = useDatabaseOperations();
|
|
||||||
const [circularDependency, setCircularDependency] = useState<CircularDependencyError | undefined>(undefined) ;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
const decomposedDatabase = decomposeManyToMany(database) ;
|
|
||||||
|
|
||||||
const dbAst: any = DatabaseToAst(decomposedDatabase, data_types);
|
|
||||||
|
|
||||||
let sql: string = parser.sqlify(dbAst, {
|
|
||||||
database: getDatabaseByDialect(database.dialect).name
|
|
||||||
});
|
|
||||||
|
|
||||||
if (database.dialect != DatabaseDialect.SQLITE)
|
|
||||||
sql = fixCharsetPlacement(format(sql, { language: "sql" }));
|
|
||||||
else
|
|
||||||
sql = fixSQLiteColumnOrder(format(sql, { language: "sql" }));
|
|
||||||
|
|
||||||
const formattedSqlCode = format(sql, { language: 'sql' });
|
|
||||||
|
|
||||||
setSql(
|
|
||||||
(formattedSqlCode)
|
|
||||||
);
|
|
||||||
setCircularDependency(undefined);
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
if ((error as CircularDependencyError)?.cycle)
|
|
||||||
setCircularDependency((previousError) => {
|
|
||||||
if (!previousError)
|
|
||||||
return error as CircularDependencyError;
|
|
||||||
else if (Array.isArray(previousError.cycle) && Array.isArray((error as CircularDependencyError).cycle) && !(areArraysEqual(previousError.cycle, (error as CircularDependencyError).cycle)))
|
|
||||||
|
|
||||||
return error as CircularDependencyError;
|
|
||||||
return previousError;
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
}, [database , data_types]);
|
|
||||||
|
|
||||||
return { sql, circularDependency };
|
|
||||||
|
|
||||||
}
|
|
||||||
+17
-2
@@ -6,7 +6,7 @@ export const ar = {
|
|||||||
tables: "الجداول",
|
tables: "الجداول",
|
||||||
relationships: "العلاقات",
|
relationships: "العلاقات",
|
||||||
database: "قاعدة البيانات",
|
database: "قاعدة البيانات",
|
||||||
documentation : "الوثائق"
|
documentation: "الوثائق"
|
||||||
},
|
},
|
||||||
color_picker: {
|
color_picker: {
|
||||||
default_color: "اللون الافتراضي"
|
default_color: "اللون الافتراضي"
|
||||||
@@ -196,7 +196,7 @@ export const ar = {
|
|||||||
import_database: {
|
import_database: {
|
||||||
title: "استيراد قاعدة البيانات",
|
title: "استيراد قاعدة البيانات",
|
||||||
import: "استيراد",
|
import: "استيراد",
|
||||||
view_docs : "عرض الوثائق" ,
|
view_docs: "عرض الوثائق",
|
||||||
import_options: "هل ترغب في الاستيراد باستخدام:",
|
import_options: "هل ترغب في الاستيراد باستخدام:",
|
||||||
import_error: "خطأ في تحليل SQL",
|
import_error: "خطأ في تحليل SQL",
|
||||||
import_error_description: "تعذر استيراد SQL الخاص بك بسبب وجود صيغة غير صحيحة.",
|
import_error_description: "تعذر استيراد SQL الخاص بك بسبب وجود صيغة غير صحيحة.",
|
||||||
@@ -259,6 +259,21 @@ export const ar = {
|
|||||||
step3: "من القائمة، اختر <bold>File > Export > Database to SQL file</bold>.",
|
step3: "من القائمة، اختر <bold>File > Export > Database to SQL file</bold>.",
|
||||||
step4: "في النافذة، اختر <bold>Export schema only</bold> ثم انقر <bold>Save</bold>.",
|
step4: "في النافذة، اختر <bold>Export schema only</bold> ثم انقر <bold>Save</bold>.",
|
||||||
step5: "أخيرًا، انسخ محتوى ملف <code>.sql</code> إلى محرر الكود أدناه."
|
step5: "أخيرًا، انسخ محتوى ملف <code>.sql</code> إلى محرر الكود أدناه."
|
||||||
|
},
|
||||||
|
ssms: {
|
||||||
|
"step1": "افتح SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "انقر بزر الفأرة الأيمن على قاعدة البيانات، ثم اختر Tasks → Generate Scripts من القائمة.",
|
||||||
|
"step3": "في خطوة Choose Objects، اختر Choose specific database objects ثم قم بتحديد جميع الجداول.",
|
||||||
|
"step4": "في خطوة Set Scripting Options، اختر Save to file وحدد مكان حفظ ملف .sql.",
|
||||||
|
"step5": "أكمل المعالج، ثم افتح ملف .sql الذي تم إنشاؤه وانسخ محتواه إلى محرر الكود أدناه."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "افتح Oracle SQL Developer واتصل بقاعدة البيانات الخاصة بك.",
|
||||||
|
"step2": "من القائمة العلوية، اذهب إلى Tools → Database Export.",
|
||||||
|
"step3": "في نافذة التصدير، اختر الاتصال الخاص بك وتأكد من تفعيل Pretty Print و Terminator فقط، وإلغاء تحديد Export Data لضمان تصدير المخطط فقط.",
|
||||||
|
"step4": "اختر مكان حفظ ملف التصدير ثم تابع إلى الخطوة التالية.",
|
||||||
|
"step5": "تحت Standard Object Types، اختر فقط Tables و Indexes و Constraints و Referential Constraints، واترك باقي الخيارات بدون تحديد.",
|
||||||
|
"step6": "أكمل المعالج، ثم افتح ملف .sql الناتج وانسخ محتواه إلى محرر الكود أدناه."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+17
-2
@@ -6,7 +6,7 @@ export const de = {
|
|||||||
tables: "Tabellen",
|
tables: "Tabellen",
|
||||||
relationships: "Beziehungen",
|
relationships: "Beziehungen",
|
||||||
database: "Datenbank",
|
database: "Datenbank",
|
||||||
documentation : "Dokumentation"
|
documentation: "Dokumentation"
|
||||||
},
|
},
|
||||||
color_picker: {
|
color_picker: {
|
||||||
default_color: "Standardfarbe"
|
default_color: "Standardfarbe"
|
||||||
@@ -196,7 +196,7 @@ export const de = {
|
|||||||
import_database: {
|
import_database: {
|
||||||
title: "Datenbank importieren",
|
title: "Datenbank importieren",
|
||||||
import: "Importieren",
|
import: "Importieren",
|
||||||
view_docs : "Dokumentation anzeigen" ,
|
view_docs: "Dokumentation anzeigen",
|
||||||
import_options: "Möchtest du importieren mit:",
|
import_options: "Möchtest du importieren mit:",
|
||||||
import_error: "SQL-Parsing-Fehler",
|
import_error: "SQL-Parsing-Fehler",
|
||||||
import_error_description: "Wir konnten dein SQL nicht importieren, da es ungültige Syntax enthält.",
|
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>.",
|
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>.",
|
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."
|
step5: "Kopiere abschließend den Inhalt der <code>.sql</code>-Datei in den Editor."
|
||||||
|
},
|
||||||
|
ssms: {
|
||||||
|
"step1": "Öffnen Sie SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "Klicken Sie mit der rechten Maustaste auf Ihre Datenbank und wählen Sie im Kontextmenü Tasks → Skripte generieren.",
|
||||||
|
"step3": "Im Schritt Objekte auswählen wählen Sie Bestimmte Datenbankobjekte auswählen und markieren Sie alle Tabellen.",
|
||||||
|
"step4": "Im Schritt Skripting-Optionen festlegen wählen Sie In Datei speichern und wählen Sie den Speicherort für die .sql-Datei.",
|
||||||
|
"step5": "Schließen Sie den Assistenten ab, öffnen Sie anschließend die generierte .sql-Datei und kopieren Sie deren Inhalt in den Code-Editor unten."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Öffnen Sie Oracle SQL Developer und verbinden Sie sich mit Ihrer Datenbank.",
|
||||||
|
"step2": "Gehen Sie im oberen Menü zu Tools → Datenbank-Export.",
|
||||||
|
"step3": "Im Exportdialog wählen Sie Ihre Verbindung und stellen Sie sicher, dass nur Pretty Print und Terminator aktiviert sind und Export Data deaktiviert ist, um nur das Schema zu exportieren.",
|
||||||
|
"step4": "Wählen Sie den Speicherort der Exportdatei und fahren Sie mit dem nächsten Schritt fort.",
|
||||||
|
"step5": "Unter Standard-Objekttypen wählen Sie nur Tabellen, Indizes, Einschränkungen und referenzielle Einschränkungen aus. Lassen Sie alle anderen Optionen deaktiviert.",
|
||||||
|
"step6": "Schließen Sie den Assistenten ab, öffnen Sie anschließend die generierte .sql-Datei und kopieren Sie deren Inhalt in den Code-Editor unten."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -277,6 +277,23 @@ export const en = {
|
|||||||
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
step5: "Finally, copy the contents of the <code>.sql</code> file into the <bold>code editor</bold> below."
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ssms: {
|
||||||
|
"step1": "Open <bold>SQL Server Management Studio (SSMS)</bold>.",
|
||||||
|
"step2": "Right-click your database, then select <bold>Tasks → Generate Scripts</bold> from the context menu.",
|
||||||
|
"step3": "In the <bold>Choose Objects</bold> step, select <bold>Choose specific database objects</bold>, then check all tables.",
|
||||||
|
"step4": "In the <bold>Set Scripting Options</bold> step, select <bold>Save to file</bold> and choose where to save the <code>.sql</code> file.",
|
||||||
|
"step5": "Complete the wizard, then open the generated <code>.sql</code> file and copy its contents into the <bold>code editor</bold> below."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Open <bold>Oracle SQL Developer</bold> and connect to your database.",
|
||||||
|
"step2": "From the top menu, go to <bold>Tools → Database Export</bold>.",
|
||||||
|
"step3": "In the export dialog, select your connection, then make sure only <bold>Pretty Print</bold> and <bold>Terminator</bold> are checked, and <bold>Export Data</bold> is unchecked to ensure schema-only export.",
|
||||||
|
"step4": "Choose where to save the export file, then continue to the next step.",
|
||||||
|
"step5": "Under <bold>Standard Object Types</bold>, check only <bold>Tables</bold>, <bold>Indexes</bold>, <bold>Constraints</bold>, and <bold>Referential Constraints</bold>. Leave all other options unchecked.",
|
||||||
|
"step6": "Complete the wizard, then open the generated <code>.sql</code> file and copy its contents into the <bold>code editor</bold> below."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -273,6 +273,22 @@ export const es = {
|
|||||||
step5: "Finalmente, copia el contenido del archivo <code>.sql</code> en el <bold>editor de código</bold> a continuación."
|
step5: "Finalmente, copia el contenido del archivo <code>.sql</code> en el <bold>editor de código</bold> a continuación."
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ssms: {
|
||||||
|
"step1": "Abra SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "Haga clic derecho en su base de datos y luego seleccione Tareas → Generar scripts en el menú contextual.",
|
||||||
|
"step3": "En el paso Elegir objetos, seleccione Elegir objetos específicos de la base de datos y marque todas las tablas.",
|
||||||
|
"step4": "En el paso Establecer opciones de script, seleccione Guardar en archivo y elija dónde guardar el archivo .sql.",
|
||||||
|
"step5": "Complete el asistente, luego abra el archivo .sql generado y copie su contenido en el editor de código a continuación."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Abra Oracle SQL Developer y conéctese a su base de datos.",
|
||||||
|
"step2": "En el menú superior, vaya a Herramientas → Exportación de base de datos.",
|
||||||
|
"step3": "En el diálogo de exportación, seleccione su conexión y asegúrese de que solo estén marcadas Pretty Print y Terminator, y que Export Data esté desmarcado para asegurar exportación solo del esquema.",
|
||||||
|
"step4": "Elija dónde guardar el archivo de exportación y continúe al siguiente paso.",
|
||||||
|
"step5": "En Tipos de objetos estándar, marque solo Tablas, Índices, Restricciones y Restricciones referenciales. Deje todo lo demás desmarcado.",
|
||||||
|
"step6": "Complete el asistente, luego abra el archivo .sql generado y copie su contenido en el editor de código a continuación."
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -262,6 +262,21 @@ export const fr = {
|
|||||||
step3: "Allez à <bold>Fichier > Exporter > Base de données vers fichier SQL</bold>.",
|
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>.",
|
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."
|
step5: "Enfin, copiez le contenu du fichier <code>.sql</code> dans l’éditeur de code ci-dessous."
|
||||||
|
},
|
||||||
|
ssms: {
|
||||||
|
"step1": "Ouvrez SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "Faites un clic droit sur votre base de données, puis sélectionnez Tâches → Générer des scripts dans le menu contextuel.",
|
||||||
|
"step3": "Dans l’étape Choisir les objets, sélectionnez Choisir des objets spécifiques de la base de données, puis cochez toutes les tables.",
|
||||||
|
"step4": "Dans l’étape Définir les options de script, sélectionnez Enregistrer dans un fichier et choisissez l’emplacement où enregistrer le fichier .sql.",
|
||||||
|
"step5": "Terminez l’assistant, puis ouvrez le fichier .sql généré et copiez son contenu dans l’éditeur de code ci-dessous."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Ouvrez Oracle SQL Developer et connectez-vous à votre base de données.",
|
||||||
|
"step2": "Dans le menu supérieur, allez dans Outils → Exportation de base de données.",
|
||||||
|
"step3": "Dans la boîte de dialogue d’exportation, sélectionnez votre connexion, puis assurez-vous que seules les options Pretty Print et Terminator sont cochées, et que Export Data est décoché pour garantir une exportation du schéma uniquement.",
|
||||||
|
"step4": "Choisissez l’emplacement du fichier d’exportation, puis passez à l’étape suivante.",
|
||||||
|
"step5": "Sous Types d’objets standards, cochez uniquement Tables, Index, Contraintes et Contraintes référentielles. Laissez toutes les autres options décochées.",
|
||||||
|
"step6": "Terminez l’assistant, puis ouvrez le fichier .sql généré et copiez son contenu dans l’éditeur de code ci-dessous."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,21 @@ export const hi = {
|
|||||||
step3: "शीर्ष मेनू से <bold>File > Export > Database to SQL file</bold> पर जाएं।",
|
step3: "शीर्ष मेनू से <bold>File > Export > Database to SQL file</bold> पर जाएं।",
|
||||||
step4: "डायलॉग में <bold>Export schema only</bold> चुनें और <bold>Save</bold> पर क्लिक करें।",
|
step4: "डायलॉग में <bold>Export schema only</bold> चुनें और <bold>Save</bold> पर क्लिक करें।",
|
||||||
step5: "अंत में, <code>.sql</code> फ़ाइल की सामग्री को नीचे दिए गए <bold>code editor</bold> में कॉपी करें।"
|
step5: "अंत में, <code>.sql</code> फ़ाइल की सामग्री को नीचे दिए गए <bold>code editor</bold> में कॉपी करें।"
|
||||||
|
} ,
|
||||||
|
ssms: {
|
||||||
|
"step1": "SQL Server Management Studio (SSMS) खोलें।",
|
||||||
|
"step2": "अपना डेटाबेस पर राइट-क्लिक करें, फिर संदर्भ मेनू से Tasks → Generate Scripts चुनें।",
|
||||||
|
"step3": "Choose Objects चरण में, Choose specific database objects चुनें, फिर सभी टेबल्स को चेक करें।",
|
||||||
|
"step4": "Set Scripting Options चरण में, Save to file चुनें और .sql फ़ाइल को सेव करने का स्थान चुनें।",
|
||||||
|
"step5": "विज़ार्ड पूरा करें, फिर जनरेट की गई .sql फ़ाइल खोलें और उसकी सामग्री को नीचे दिए गए कोड एडिटर में कॉपी करें।"
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Oracle SQL Developer खोलें और अपने डेटाबेस से कनेक्ट करें।",
|
||||||
|
"step2": "ऊपरी मेनू से Tools → Database Export पर जाएँ।",
|
||||||
|
"step3": "Export डायलॉग में अपना कनेक्शन चुनें, फिर सुनिश्चित करें कि केवल Pretty Print और Terminator चुने गए हैं, और Export Data अनचेक है ताकि केवल schema export हो।",
|
||||||
|
"step4": "एक्सपोर्ट फ़ाइल को सेव करने का स्थान चुनें, फिर अगले चरण पर जाएँ।",
|
||||||
|
"step5": "Standard Object Types के अंतर्गत केवल Tables, Indexes, Constraints, और Referential Constraints चुनें। बाकी सभी विकल्प अनचेक रखें।",
|
||||||
|
"step6": "विज़ार्ड पूरा करें, फिर जनरेट की गई .sql फ़ाइल खोलें और उसकी सामग्री को नीचे दिए गए कोड एडिटर में कॉपी करें।"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -269,6 +269,21 @@ export const pt = {
|
|||||||
step3: "Vá para <bold>Arquivo > Exportar > Banco de Dados para Arquivo SQL</bold>.",
|
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>.",
|
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."
|
step5: "Finalmente, copie o conteúdo do arquivo <code>.sql</code> para o <bold>editor de código</bold> abaixo."
|
||||||
|
} ,
|
||||||
|
ssms: {
|
||||||
|
"step1": "Abra o SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "Clique com o botão direito no seu banco de dados e selecione Tarefas → Gerar Scripts no menu de contexto.",
|
||||||
|
"step3": "Na etapa Escolher Objetos, selecione Escolher objetos específicos do banco de dados e marque todas as tabelas.",
|
||||||
|
"step4": "Na etapa Definir opções de script, selecione Salvar em arquivo e escolha onde salvar o arquivo .sql.",
|
||||||
|
"step5": "Conclua o assistente e depois abra o arquivo .sql gerado e copie seu conteúdo para o editor de código abaixo."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Abra o Oracle SQL Developer e conecte-se ao seu banco de dados.",
|
||||||
|
"step2": "No menu superior, vá em Ferramentas → Exportação de Banco de Dados.",
|
||||||
|
"step3": "Na janela de exportação, selecione sua conexão e certifique-se de que apenas Pretty Print e Terminator estejam marcados, e que Export Data esteja desmarcado para garantir exportação apenas do schema.",
|
||||||
|
"step4": "Escolha onde salvar o arquivo de exportação e continue para a próxima etapa.",
|
||||||
|
"step5": "Em Tipos de Objetos Padrão, marque apenas Tabelas, Índices, Restrições e Restrições referenciais. Deixe todas as outras opções desmarcadas.",
|
||||||
|
"step6": "Conclua o assistente e depois abra o arquivo .sql gerado e copie seu conteúdo para o editor de código abaixo."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,21 @@ export const ru = {
|
|||||||
step3: "Выберите <bold>Файл > Экспорт > База данных в SQL файл</bold>.",
|
step3: "Выберите <bold>Файл > Экспорт > База данных в SQL файл</bold>.",
|
||||||
step4: "Выберите <bold>Экспортировать только схему</bold> и нажмите <bold>Сохранить</bold>.",
|
step4: "Выберите <bold>Экспортировать только схему</bold> и нажмите <bold>Сохранить</bold>.",
|
||||||
step5: "Скопируйте содержимое <code>.sql</code> файла в редактор ниже."
|
step5: "Скопируйте содержимое <code>.sql</code> файла в редактор ниже."
|
||||||
|
},
|
||||||
|
ssms: {
|
||||||
|
"step1": "Откройте SQL Server Management Studio (SSMS).",
|
||||||
|
"step2": "Щёлкните правой кнопкой мыши по вашей базе данных, затем выберите Tasks → Generate Scripts в контекстном меню.",
|
||||||
|
"step3": "В шаге Choose Objects выберите Select specific database objects и отметьте все таблицы.",
|
||||||
|
"step4": "В шаге Set Scripting Options выберите Save to file и укажите, куда сохранить файл .sql.",
|
||||||
|
"step5": "Завершите мастер, затем откройте созданный файл .sql и скопируйте его содержимое в редактор кода ниже."
|
||||||
|
},
|
||||||
|
sqldeveloper: {
|
||||||
|
"step1": "Откройте Oracle SQL Developer и подключитесь к базе данных.",
|
||||||
|
"step2": "В верхнем меню перейдите в Tools → Database Export.",
|
||||||
|
"step3": "В окне экспорта выберите соединение и убедитесь, что отмечены только Pretty Print и Terminator, а Export Data отключён, чтобы экспортировать только схему.",
|
||||||
|
"step4": "Выберите место сохранения файла экспорта и перейдите к следующему шагу.",
|
||||||
|
"step5": "В разделе Standard Object Types отметьте только Tables, Indexes, Constraints и Referential Constraints. Остальные параметры оставьте отключёнными.",
|
||||||
|
"step6": "Завершите мастер, затем откройте созданный файл .sql и скопируйте его содержимое в редактор кода ниже."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-1
@@ -210,7 +210,7 @@ export const zh = {
|
|||||||
import_database: {
|
import_database: {
|
||||||
title: "导入数据库",
|
title: "导入数据库",
|
||||||
import: "导入",
|
import: "导入",
|
||||||
view_docs : "查看文档" ,
|
view_docs: "查看文档",
|
||||||
import_options: "请选择导入方式:",
|
import_options: "请选择导入方式:",
|
||||||
import_error: "SQL 解析错误",
|
import_error: "SQL 解析错误",
|
||||||
import_error_description: "我们无法导入您的 SQL,因为它包含无效语法。",
|
import_error_description: "我们无法导入您的 SQL,因为它包含无效语法。",
|
||||||
@@ -276,6 +276,21 @@ export const zh = {
|
|||||||
step3: "顶部菜单选择 <bold>文件 > 导出 > 导出为 SQL 文件</bold>。",
|
step3: "顶部菜单选择 <bold>文件 > 导出 > 导出为 SQL 文件</bold>。",
|
||||||
step4: "选择 <bold>仅导出结构</bold> 并点击 <bold>保存</bold>。",
|
step4: "选择 <bold>仅导出结构</bold> 并点击 <bold>保存</bold>。",
|
||||||
step5: "将 .sql 文件内容复制到下方 <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 文件,并将其内容复制到下面的代码编辑器中。"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
import { DataInsertType } from "../schemas/data-type-schema";
|
||||||
|
|
||||||
|
export const MSSQLDataType: Partial<DataInsertType>[] = [
|
||||||
|
{
|
||||||
|
id: 'c7a6fd94-ecec-4b79-9ea6-8ca25bae8d9c',
|
||||||
|
name: 'tinyint',
|
||||||
|
type: 'integer',
|
||||||
|
modifiers: ['auto_increment']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '48bdd059-9b52-4f93-b5c1-42d9dc2b7d13',
|
||||||
|
name: 'smallint',
|
||||||
|
type: 'integer',
|
||||||
|
|
||||||
|
modifiers: ['auto_increment']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ae5e05da-21e0-4746-b972-6069ab628dc2',
|
||||||
|
name: 'int',
|
||||||
|
type: 'integer',
|
||||||
|
|
||||||
|
modifiers: ['auto_increment']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8a719f0c-0b0f-4be2-b660-61502575a5bd',
|
||||||
|
name: 'bigint',
|
||||||
|
type: 'integer',
|
||||||
|
|
||||||
|
modifiers: ['auto_increment']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '442c88ae-6d53-41d5-bdaf-cb95192ab2e7',
|
||||||
|
name: 'decimal',
|
||||||
|
type: 'numeric',
|
||||||
|
modifiers: ['precision', 'scale'],
|
||||||
|
synonyms: ['numeric']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c3102982-a9ee-48c9-b050-bc3e8318efda',
|
||||||
|
name: 'float',
|
||||||
|
type: 'numeric',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1858ca0e-ba61-45aa-9236-8028ab7d7570',
|
||||||
|
name: 'real',
|
||||||
|
type: 'numeric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3842960d-f0a9-411b-99d2-be3cfaf03fe7',
|
||||||
|
name: 'money',
|
||||||
|
type: 'numeric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '664aacc0-d646-4052-a616-d0b3d5d546df',
|
||||||
|
name: 'smallmoney',
|
||||||
|
type: 'numeric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fbdfd7d5-72d1-4c0b-9cfb-ca843ac444a4',
|
||||||
|
name: 'char',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1a6f261b-1abf-4f13-b803-a9533198f4c9',
|
||||||
|
name: 'varchar',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '0a27e11f-1af7-4a62-babd-cfc1ecdd5f1f',
|
||||||
|
name: 'text',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c8cd26da-a09e-4a18-8b2f-11a2e4fe049c',
|
||||||
|
name: 'nchar',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '53f552a2-b782-4084-a236-6928651b4f43',
|
||||||
|
name: 'nvarchar',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'e0071583-b97a-4f67-80b1-6ff25b8b8c37',
|
||||||
|
name: 'ntext',
|
||||||
|
type: 'text'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9df5d149-c9f1-4d26-a3ae-832accc08831',
|
||||||
|
name: 'binary',
|
||||||
|
type: 'binary',
|
||||||
|
modifiers: ['length', 'no_default']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3c351e8c-92ba-40aa-843c-df2d8409e95a',
|
||||||
|
name: 'varbinary',
|
||||||
|
type: 'binary',
|
||||||
|
modifiers: ['length', 'no_default']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '731051f0-050f-4eb2-a327-5e10dd6bf6ca',
|
||||||
|
name: 'image',
|
||||||
|
type: 'binary',
|
||||||
|
modifiers: ['no_unique', 'no_default']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'b412d41c-255b-413d-b866-4b652f291606',
|
||||||
|
name: 'date',
|
||||||
|
type: 'time'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'cc0f4f94-772c-4f4e-b1d3-f2fe0a18ee49',
|
||||||
|
name: 'time',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '83a37c4f-9501-485e-bc10-802673ca0594',
|
||||||
|
name: 'datetime',
|
||||||
|
type: 'time'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '85a466ba-b97a-4c47-a204-1a8f0e16d275',
|
||||||
|
name: 'smalldatetime',
|
||||||
|
type: 'time'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '562887bf-8d60-4e9e-b038-e760a6da05a2',
|
||||||
|
name: 'datetime2',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '1165eda1-6b45-4c45-82de-ddc90bb87f86',
|
||||||
|
name: 'datetimeoffset',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '00ea0771-47e0-4067-ba74-a153421c2d69',
|
||||||
|
name: 'uniqueidentifier',
|
||||||
|
type: 'uuid',
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '03db80b0-d2c0-47bc-879b-8d0c17a36a9e',
|
||||||
|
name: 'bit',
|
||||||
|
type: 'boolean'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '060d32ca-4e8a-496f-a29e-36d1947d0469',
|
||||||
|
name: 'json',
|
||||||
|
type: 'json'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '090ad5cd-43b6-4443-93ce-61a6f12c6f4e',
|
||||||
|
name: 'xml',
|
||||||
|
type: 'xml'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '9e9ddfc5-176b-4574-a097-3a1cbf927d9c',
|
||||||
|
name: 'geometry',
|
||||||
|
type: 'geometric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c6cdd823-5b8e-45ed-a489-3b68ce10b698',
|
||||||
|
name: 'geography',
|
||||||
|
type: 'geometric'
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
import { DataInsertType } from "../schemas/data-type-schema";
|
||||||
|
|
||||||
|
export const OracleDataType: Partial<DataInsertType>[] = [
|
||||||
|
{
|
||||||
|
id: '7b79fc89-38ae-4cf7-bdb8-1a270c2c4f0b',
|
||||||
|
name: 'number',
|
||||||
|
type: 'numeric',
|
||||||
|
modifiers: ['precision', 'scale', "auto_increment"],
|
||||||
|
synonyms: ['decimal', 'dec', 'numeric']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fc4ba819-cbf8-4550-9e78-6f7a05883f55',
|
||||||
|
name: 'integer',
|
||||||
|
type: 'integer',
|
||||||
|
modifiers: ["auto_increment"],
|
||||||
|
synonyms: ['int', 'smallint']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'd4a2a12b-1c72-443a-9c9b-b30a0a4e64d3',
|
||||||
|
name: 'float',
|
||||||
|
type: 'numeric',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'd2b819bd-028a-4790-8e32-a1e06c6cb82c',
|
||||||
|
name: 'binary_float',
|
||||||
|
type: 'numeric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6a04aad8-b97e-4a57-ae1a-b270f3885821',
|
||||||
|
name: 'binary_double',
|
||||||
|
type: 'numeric'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a15c03d8-e8f1-4285-8148-134311b403e4',
|
||||||
|
name: 'varchar2',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length'],
|
||||||
|
synonyms: ['varchar', 'character varying']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'f5932edf-41bc-4d52-ad1d-4501acbe77f1',
|
||||||
|
name: 'char',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length'],
|
||||||
|
synonyms: ['character']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'ac67bfe8-6c83-4729-b9b2-ac6fbca5699f',
|
||||||
|
name: 'nchar',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bd515b9f-1355-436a-9ff6-f007d61361f2',
|
||||||
|
name: 'nvarchar2',
|
||||||
|
type: 'text',
|
||||||
|
modifiers: ['length']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '16115bbb-a06e-4663-96c5-6169398aba60',
|
||||||
|
name: 'clob',
|
||||||
|
type: 'lob',
|
||||||
|
modifiers: ['no_default' , 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'a0e84fd7-dae8-4f1c-b989-acae67eedbf5',
|
||||||
|
name: 'nclob',
|
||||||
|
type: 'lob',
|
||||||
|
modifiers: ['no_default' , 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '18762e8d-8fa7-43cf-9e0a-4205f2b2f3b1',
|
||||||
|
name: 'blob',
|
||||||
|
type: 'lob',
|
||||||
|
modifiers: ['no_default' , 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c19f392c-6edc-439c-a65b-a52f1d616051',
|
||||||
|
name: 'bfile',
|
||||||
|
type: 'lob',
|
||||||
|
modifiers: ['no_default' , 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '699d85c7-080e-4ec9-ae3c-2dafdaf4cd27',
|
||||||
|
name: 'raw',
|
||||||
|
type: 'binary',
|
||||||
|
modifiers: ['length' , 'no_default' ]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8521fef1-0105-49ca-a927-cde3cf4224d3',
|
||||||
|
name: 'date',
|
||||||
|
type: 'time',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '5306e486-a2db-4095-9c4d-bc216e2b3b57',
|
||||||
|
name: 'timestamp',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision'],
|
||||||
|
synonyms: ['timestamp without time zone']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '8604a573-7472-4603-952e-316fe1ef3407',
|
||||||
|
name: 'timestamp with time zone',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6ec0d6d2-dc41-49b1-9e15-2ded2d75b5ef',
|
||||||
|
name: 'timestamp with local time zone',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '7076c729-ff35-4d5e-8f21-8c02bfc90d5c',
|
||||||
|
name: 'interval year to month',
|
||||||
|
type: 'time' ,
|
||||||
|
modifiers: ['precision' , "no_default"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '6e2ca449-5062-4feb-a5ce-19fcc751ff2b',
|
||||||
|
name: 'interval day to second',
|
||||||
|
type: 'time',
|
||||||
|
modifiers: ['precision' , "no_default"]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '3c3cd09e-a970-4c48-9f6e-6636757e3dd7',
|
||||||
|
name: 'rowid',
|
||||||
|
type: 'rowid',
|
||||||
|
modifiers: ['no_default' , 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'f8ca2603-b196-4044-baec-bdc02803f3b8',
|
||||||
|
name: 'urowid',
|
||||||
|
type: 'rowid',
|
||||||
|
modifiers: ['no_default', 'no_unique']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'c9abbfda-e2b3-4746-a536-36f34204b383',
|
||||||
|
name: 'xmltype',
|
||||||
|
type: 'xml',
|
||||||
|
modifiers: ['no_default']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '92fb0e59-b573-43a6-a7d3-f357867a3b00',
|
||||||
|
name: 'json',
|
||||||
|
type: 'json',
|
||||||
|
modifiers: ['no_default']
|
||||||
|
},
|
||||||
|
|
||||||
|
]
|
||||||
@@ -5,6 +5,8 @@ import { PostgresDataType } from "./postgres_data_types";
|
|||||||
import { SqliteDataTypes } from "./sqlite_data_types";
|
import { SqliteDataTypes } from "./sqlite_data_types";
|
||||||
import { MariaDbDataType } from "./mariadb_data_types";
|
import { MariaDbDataType } from "./mariadb_data_types";
|
||||||
import { DatabaseDialect } from "../database";
|
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(MysqlDataType , DatabaseDialect.MYSQL) ,
|
||||||
...mapToDataType(PostgresDataType , DatabaseDialect.POSTGRES) ,
|
...mapToDataType(PostgresDataType , DatabaseDialect.POSTGRES) ,
|
||||||
...mapToDataType(SqliteDataTypes , DatabaseDialect.SQLITE) ,
|
...mapToDataType(SqliteDataTypes , DatabaseDialect.SQLITE) ,
|
||||||
...mapToDataType(MariaDbDataType , DatabaseDialect.MARIADB)
|
...mapToDataType(MariaDbDataType , DatabaseDialect.MARIADB) ,
|
||||||
|
...mapToDataType(OracleDataType , DatabaseDialect.ORACLE) ,
|
||||||
|
...mapToDataType(MSSQLDataType , DatabaseDialect.MSSQL) ,
|
||||||
] as DataInsertType[])
|
] as DataInsertType[])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+17
-4
@@ -1,3 +1,5 @@
|
|||||||
|
import _ from "lodash";
|
||||||
|
import { DatabaseType as DatabaseSchemaType } from "./schemas/database-schema";
|
||||||
|
|
||||||
|
|
||||||
export interface DatabaseType {
|
export interface DatabaseType {
|
||||||
@@ -13,7 +15,7 @@ export enum DatabaseDialect {
|
|||||||
POSTGRES = "postgres",
|
POSTGRES = "postgres",
|
||||||
SQLITE = "sqlite",
|
SQLITE = "sqlite",
|
||||||
MARIADB = "mariadb",
|
MARIADB = "mariadb",
|
||||||
SQL_SERVER = "sql_server",
|
MSSQL = "mssql",
|
||||||
ORACLE = "oracle"
|
ORACLE = "oracle"
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,10 +47,10 @@ export const DBTypes: DatabaseType[] = [
|
|||||||
|
|
||||||
{
|
{
|
||||||
name: "SQL Server",
|
name: "SQL Server",
|
||||||
dialect: DatabaseDialect.SQL_SERVER,
|
dialect: DatabaseDialect.MSSQL,
|
||||||
logo: "/sql_server_logo_small.png",
|
logo: "/sql_server_logo_small.png",
|
||||||
small_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,
|
dialect: DatabaseDialect.ORACLE,
|
||||||
logo: "/oracle_logo_small.png",
|
logo: "/oracle_logo_small.png",
|
||||||
small_logo: "/oracle_logo_small.png" ,
|
small_logo: "/oracle_logo_small.png" ,
|
||||||
comming : true
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -115,3 +117,14 @@ export enum CardinalityStyle {
|
|||||||
SYMBOLIC = "SYMBOLIC"
|
SYMBOLIC = "SYMBOLIC"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export const emptyDb = (database: DatabaseSchemaType) : DatabaseSchemaType => {
|
||||||
|
|
||||||
|
let cloneDb : any = _.cloneDeep(database);
|
||||||
|
cloneDb.tables = [];
|
||||||
|
cloneDb.relationships = [];
|
||||||
|
cloneDb.numOfTables = 0;
|
||||||
|
|
||||||
|
return cloneDb as DatabaseSchemaType;
|
||||||
|
}
|
||||||
+2
-1
@@ -106,4 +106,5 @@ export enum ForeignKeyActions {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MYSQL_MAX_VAR_LENGTH = 255;
|
export const MYSQL_MAX_VAR_LENGTH = 255;
|
||||||
|
export const DEFAULT_LENGTH_PARAM = 100;
|
||||||
@@ -9,7 +9,7 @@ export const data_types = sqliteTable('data_types', {
|
|||||||
name: text('name'),
|
name: text('name'),
|
||||||
|
|
||||||
dialect: text("dialect", {
|
dialect: text("dialect", {
|
||||||
enum: ["postgres", "mysql", "sqlite", "mariadb"],
|
enum: ["postgres", "mysql", "sqlite", "mariadb" , "mssql" , "oracle"],
|
||||||
}).notNull().default("postgres"),
|
}).notNull().default("postgres"),
|
||||||
|
|
||||||
type: text('type').notNull(),
|
type: text('type').notNull(),
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { field_indices, FieldIndexInsertType } from "@/lib/schemas/field_index-s
|
|||||||
import { v4 } from "uuid";
|
import { v4 } from "uuid";
|
||||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
import { deleteFieldsWithCascade, deleteIndicesWithCascade, deleteTablesWithCascade } from "@/utils/cascade";
|
import { deleteFieldsWithCascade, deleteIndicesWithCascade, deleteTablesWithCascade } from "@/utils/cascade";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
|
||||||
const TYPE_ORDER = ["integer", "text", "boolean", "numeric", "time", "enum"] as const;
|
const TYPE_ORDER = ["integer", "text", "boolean", "numeric", "time", "enum"] as const;
|
||||||
|
|
||||||
@@ -431,7 +432,7 @@ const DatabaseProvider: React.FC<Props> = ({ children }) => {
|
|||||||
}, [db, currentDatabaseId]);
|
}, [db, currentDatabaseId]);
|
||||||
|
|
||||||
const getInteger = useCallback(() => {
|
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]);
|
}, [data_types]);
|
||||||
|
|
||||||
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
|
const importDatabase = useCallback(async (importedTables: TableInsertType[], importedRelationships: RelationshipInsertType[], importedIndices: IndexInsertType[]) => {
|
||||||
|
|||||||
@@ -0,0 +1,780 @@
|
|||||||
|
|
||||||
|
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) {
|
||||||
|
console.log (typeName , this.data_types)
|
||||||
|
throw Error("Data type not found");
|
||||||
|
}
|
||||||
|
// get data type modifiers
|
||||||
|
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||||
|
// the parser pass an extra param in case if the type is cutsom , that extra param can be either a length or precision
|
||||||
|
// to know which one we have to test what kind of modifier the data type support
|
||||||
|
if (extraParam && modifiers.includes(Modifiers.LENGTH))
|
||||||
|
maxLength = extraParam;
|
||||||
|
|
||||||
|
if (extraParam && modifiers.includes(Modifiers.PRECISION))
|
||||||
|
precision = extraParam;
|
||||||
|
|
||||||
|
if (params && params.length > 0 && Array.isArray(params[0]) && modifiers.includes(Modifiers.VALUES)) {
|
||||||
|
|
||||||
|
const paramValues = params[0].filter((value: any) => value.Name !== undefined).map((value: any) => value.Name);
|
||||||
|
if (paramValues.every(v => typeof v === "string")) {
|
||||||
|
values = JSON.stringify(paramValues);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modifiers.includes(Modifiers.PRECISION) && params && params.length > 0 && !isNaN(params[0]) && !precision) {
|
||||||
|
precision = params[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// get field defintion options
|
||||||
|
const options: any | undefined = [] = ast.options;
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const option of options) {
|
||||||
|
if (typeof option.option == "string") {
|
||||||
|
|
||||||
|
if (option.option == "Null")
|
||||||
|
nullable = true;
|
||||||
|
|
||||||
|
if (option.option == "NotNull")
|
||||||
|
nullable = false;
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
else if (typeof option.option === "object") {
|
||||||
|
|
||||||
|
if (option.option.PrimaryKey) {
|
||||||
|
isPrimary = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.option.Unique) {
|
||||||
|
unique = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.option.Default) {
|
||||||
|
|
||||||
|
defaultValue = this.processDefaultValue(option.option.Default, dataType);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (option.option.DialectSpecific && option.option.DialectSpecific.length > 0) {
|
||||||
|
for (const { Word } of option.option.DialectSpecific) {
|
||||||
|
if (Word?.keyword == "AUTO_INCREMENT") {
|
||||||
|
autoIncrement = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (option.option.ForeignKey) {
|
||||||
|
fk_constraint = option.option.ForeignKey;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
throw Error("Failed to extract column options")
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isPrimary) {
|
||||||
|
nullable = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
field: {
|
||||||
|
id: v4(),
|
||||||
|
name: ast.name.value,
|
||||||
|
typeId: dataType?.id,
|
||||||
|
nullable,
|
||||||
|
autoIncrement,
|
||||||
|
unique,
|
||||||
|
isPrimary,
|
||||||
|
maxLength,
|
||||||
|
precision,
|
||||||
|
scale,
|
||||||
|
defaultValue,
|
||||||
|
sequence,
|
||||||
|
values
|
||||||
|
} as FieldInsertType,
|
||||||
|
fk_constraint
|
||||||
|
};
|
||||||
|
|
||||||
|
}
|
||||||
|
protected astToRelationship(ast: any, tables: TableInsertType[]): RelationshipInsertType {
|
||||||
|
|
||||||
|
|
||||||
|
let targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.target_table);
|
||||||
|
let sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.foreign_table?.at(-1)?.Identifier?.value);
|
||||||
|
let sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == ast.referred_columns?.[0]?.value);
|
||||||
|
let targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == ast.columns?.[0]?.value);
|
||||||
|
|
||||||
|
if (!sourceTable || !sourceField || !targetField || !targetTable) {
|
||||||
|
throw Error("Inavlid FK Constraint")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const onDelete: ForeignKeyActions | undefined = ast.on_delete ? this.astToForiegnKeyAction(ast.on_delete) : undefined;
|
||||||
|
const onUpdate: ForeignKeyActions | undefined = ast.on_update ? this.astToForiegnKeyAction(ast.on_update) : undefined;
|
||||||
|
const name: string | undefined = ast.name?.value;
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: v4(),
|
||||||
|
name,
|
||||||
|
sourceFieldId: sourceField.id,
|
||||||
|
targetFieldId: targetField.id,
|
||||||
|
sourceTableId: sourceTable.id,
|
||||||
|
targetTableId: targetTable.id,
|
||||||
|
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
||||||
|
onDelete,
|
||||||
|
onUpdate
|
||||||
|
} as RelationshipInsertType
|
||||||
|
}
|
||||||
|
protected astToIndex(ast: any, tables: TableInsertType[]): ParsedIndex {
|
||||||
|
|
||||||
|
const tableName: string = ast.CreateIndex.table_name?.at(-1)?.Identifier?.value;
|
||||||
|
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||||
|
|
||||||
|
if (!table)
|
||||||
|
throw Error("index table not found");
|
||||||
|
|
||||||
|
const columnNames: string[] = ast.CreateIndex.columns.map(({ column }: any) => column.expr?.Identifier?.value)
|
||||||
|
.filter((column: string | undefined) => column != undefined);
|
||||||
|
|
||||||
|
const fieldIds: string[] = (table.fields?.filter((field: FieldInsertType) => columnNames.includes(field.name)) as FieldInsertType[])
|
||||||
|
.map((field: FieldInsertType) => field.id);
|
||||||
|
|
||||||
|
let indexName: string = ast.CreateIndex.name?.[0]?.Identifier?.value;
|
||||||
|
if (!indexName) {
|
||||||
|
indexName = `${tableName}_${columnNames.join("_")}_index`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
index: {
|
||||||
|
id: v4(),
|
||||||
|
name: indexName,
|
||||||
|
tableId: table.id,
|
||||||
|
unique: ast.CreateIndex.unique,
|
||||||
|
fieldIndices: fieldIds?.map((id: string) => ({
|
||||||
|
id: v4(),
|
||||||
|
fieldId: id
|
||||||
|
}))
|
||||||
|
} as IndexInsertType,
|
||||||
|
error: undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected astToForiegnKeyAction(ast: ReferentialAction): ForeignKeyActions {
|
||||||
|
switch (ast) {
|
||||||
|
case "Cascade":
|
||||||
|
return ForeignKeyActions.CASCADE;
|
||||||
|
case "SetNull":
|
||||||
|
return ForeignKeyActions.SET_NULL;
|
||||||
|
case 'Restrict':
|
||||||
|
return ForeignKeyActions.RESTRICT;
|
||||||
|
case "SetDefault":
|
||||||
|
return ForeignKeyActions.SET_DEFAULT;
|
||||||
|
}
|
||||||
|
return ForeignKeyActions.NO_ACTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDataType(typeName: string): DataType | undefined {
|
||||||
|
|
||||||
|
|
||||||
|
// get data type from the supported ones
|
||||||
|
return this.data_types.find((dataType: DataType) => {
|
||||||
|
let synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
||||||
|
synonyms = synonyms.map((synonym: string) => synonym.replace(/ /g, ''));
|
||||||
|
return (dataType.name as string).replace(/ /g, '') == typeName || synonyms.includes(typeName as string)
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||||
|
|
||||||
|
if (ast.Value && ast.Value.value) {
|
||||||
|
|
||||||
|
const value: any = ast.Value.value;
|
||||||
|
if (value.Number && Array.isArray(value.Number) && value.Number.length > 0 && !isNaN(Number(value.Number[0]))) {
|
||||||
|
return value.Number[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (value.SingleQuotedString || value.DoubleQuotedString) {
|
||||||
|
const valuesArray = Object.values(value);
|
||||||
|
if (valuesArray.length > 0)
|
||||||
|
return valuesArray[0] as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (value.Boolean) {
|
||||||
|
return "true";
|
||||||
|
}
|
||||||
|
|
||||||
|
else if (value.NationalStringLiteral) {
|
||||||
|
return value.NationalStringLiteral
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (ast.Function) {
|
||||||
|
if (dataType.type == DataTypes.TIME) {
|
||||||
|
if (ast.Function.name?.[0]?.Identifier?.value && this.isCurrentTimesTampFunction(ast.Function.name?.[0]?.Identifier?.value))
|
||||||
|
return TimeDefaultValues.NOW;
|
||||||
|
|
||||||
|
}
|
||||||
|
} else if (ast.TypedString) {
|
||||||
|
|
||||||
|
const value = ast.TypedString.value?.value;
|
||||||
|
|
||||||
|
if (value.SingleQuotedString || value.DoubleQuotedString) {
|
||||||
|
const valuesArray = Object.values(value);
|
||||||
|
if (valuesArray.length > 0)
|
||||||
|
return valuesArray[0] as string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||||
|
const upper: string = value.toUpperCase();
|
||||||
|
|
||||||
|
return upper == "CURRENT_TIMESTAMP" || upper == "NOW";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected cleanSql(sql: string): string {
|
||||||
|
// Clean up SQL: remove comments and normalize
|
||||||
|
const cleanedSql = sql
|
||||||
|
.replace(/--.*$/gm, '') // remove single-line comments
|
||||||
|
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
||||||
|
.replace(/\s+/g, ' ') // normalize whitespace
|
||||||
|
.replace(/;\s*/g, ';\n'); // separate statements
|
||||||
|
return cleanedSql;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected extractStatments(sql: string): ExtractedStatments {
|
||||||
|
|
||||||
|
const createTableStatements: string[] = [];
|
||||||
|
const alterTableStatements: string[] = [];
|
||||||
|
const createIndexStatements: string[] = [];
|
||||||
|
|
||||||
|
// Split into individual statements
|
||||||
|
const statements = sql
|
||||||
|
.split('\n')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
|
||||||
|
for (const stmt of statements) {
|
||||||
|
const upper = stmt.toUpperCase();
|
||||||
|
|
||||||
|
if (upper.startsWith('CREATE TABLE')) {
|
||||||
|
createTableStatements.push(stmt);
|
||||||
|
} else if (upper.startsWith('ALTER TABLE')) {
|
||||||
|
alterTableStatements.push(stmt);
|
||||||
|
} else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) {
|
||||||
|
createIndexStatements.push(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
createTableStatements,
|
||||||
|
alterTableStatements,
|
||||||
|
createIndexStatements
|
||||||
|
} as ExtractedStatments;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedDatabaase = {
|
||||||
|
tables: TableInsertType[],
|
||||||
|
relationships: RelationshipInsertType[],
|
||||||
|
indexes: IndexInsertType[],
|
||||||
|
errors: Error[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedTable = {
|
||||||
|
table: TableInsertType;
|
||||||
|
errors: Error[];
|
||||||
|
fk_constraints: any[] | undefined,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedIndex = {
|
||||||
|
index: IndexInsertType;
|
||||||
|
error?: Error;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ParsedField = {
|
||||||
|
field: FieldInsertType,
|
||||||
|
fk_constraint: any | undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ExtractedStatments = {
|
||||||
|
createTableStatements: string[],
|
||||||
|
alterTableStatements: string[],
|
||||||
|
createIndexStatements: string[],
|
||||||
|
createPostgresTypesStatements?: string[],
|
||||||
|
alterSequenceStatments?: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
START TRANSACTION;
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX `users_usernae_index` ON `users` (`username`);
|
||||||
|
|
||||||
|
COMMIT;
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE `users` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
username VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
last_login_at DATETIME NULL,
|
||||||
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
updated_at TEXT NULL DEFAULT 'Hello world' UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX `idx_users_created_at_index` ON `users` (`created_at`);
|
||||||
|
|
||||||
|
CREATE INDEX `idx_users_last_login_at` ON `users` (`last_login_at`);
|
||||||
|
|
||||||
|
CREATE TABLE `categories` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
description TEXT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `tags` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `projects` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
category_id INTEGER NULL,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER NULL,
|
||||||
|
due_date DATETIME NULL,
|
||||||
|
status ENUM (
|
||||||
|
'planning',
|
||||||
|
'in_progress',
|
||||||
|
'completed',
|
||||||
|
'on_hold',
|
||||||
|
'draft'
|
||||||
|
) NOT NULL DEFAULT 'draft',
|
||||||
|
FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`),
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `tasks` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
title VARCHAR(255) NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
status ENUM ('todo', 'in_progress', 'done') NOT NULL DEFAULT 'todo',
|
||||||
|
priority ENUM ('low', 'medium', 'high') NULL DEFAULT 'medium',
|
||||||
|
due_date DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
project_id INTEGER NULL,
|
||||||
|
assigned_to_user_id INTEGER NULL,
|
||||||
|
completed_at DATETIME NULL,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY (`project_id`) REFERENCES `projects` (`id`),
|
||||||
|
FOREIGN KEY (`assigned_to_user_id`) REFERENCES `users` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE `comments` (
|
||||||
|
id INTEGER NOT NULL AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
comment TEXT NOT NULL,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
updated_at DATETIME NULL,
|
||||||
|
FOREIGN KEY (`user_id`) REFERENCES `users` (`id`),
|
||||||
|
FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX `idx_comments_created_at` ON `comments` (`created_at`);
|
||||||
|
|
||||||
|
CREATE INDEX `idx_comments_user_id` ON `comments` (`user_id`);
|
||||||
|
|
||||||
|
CREATE INDEX `idx_comments_task_id` ON `comments` (`task_id`);
|
||||||
|
|
||||||
|
CREATE TABLE `task_tags` (
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
tag_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`),
|
||||||
|
FOREIGN KEY (`task_id`) REFERENCES `tasks` (`id`)
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE TABLE all_mysql_types (
|
||||||
|
-- Numeric types
|
||||||
|
col_tinyint TINYINT(3),
|
||||||
|
col_smallint SMALLINT(5),
|
||||||
|
col_mediumint MEDIUMINT(7),
|
||||||
|
col_int INT(11),
|
||||||
|
col_bigint BIGINT(20),
|
||||||
|
|
||||||
|
col_decimal DECIMAL(10, 2),
|
||||||
|
col_numeric NUMERIC(8, 3),
|
||||||
|
|
||||||
|
col_float FLOAT(7, 4),
|
||||||
|
col_double DOUBLE(15, 8),
|
||||||
|
col_real REAL,
|
||||||
|
|
||||||
|
col_boolean BOOLEAN,
|
||||||
|
|
||||||
|
-- Date & time types
|
||||||
|
col_date DATE,
|
||||||
|
col_datetime DATETIME(6),
|
||||||
|
col_timestamp TIMESTAMP(6),
|
||||||
|
col_time TIME(6),
|
||||||
|
col_year YEAR,
|
||||||
|
|
||||||
|
-- String types
|
||||||
|
col_char CHAR(10),
|
||||||
|
col_varchar VARCHAR(255),
|
||||||
|
|
||||||
|
col_binary BINARY(16),
|
||||||
|
col_varbinary VARBINARY(255),
|
||||||
|
|
||||||
|
col_tinytext TINYTEXT,
|
||||||
|
col_text TEXT,
|
||||||
|
col_mediumtext MEDIUMTEXT,
|
||||||
|
col_longtext LONGTEXT,
|
||||||
|
|
||||||
|
col_tinyblob TINYBLOB,
|
||||||
|
col_blob BLOB,
|
||||||
|
col_mediumblob MEDIUMBLOB,
|
||||||
|
col_longblob LONGBLOB,
|
||||||
|
|
||||||
|
col_enum ENUM('A', 'B', 'C'),
|
||||||
|
col_set SET('X', 'Y', 'Z'),
|
||||||
|
|
||||||
|
-- JSON
|
||||||
|
col_json JSON,
|
||||||
|
|
||||||
|
-- Spatial types
|
||||||
|
col_geometry GEOMETRY,
|
||||||
|
col_point POINT,
|
||||||
|
col_linestring LINESTRING,
|
||||||
|
col_polygon POLYGON
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { BaseSqlImporter } from "./base-sql-importer";
|
||||||
|
import { MysqlImporter } from "./mysql-importer";
|
||||||
|
import { MariaDbImporter } from "./mariadb-importer";
|
||||||
|
import { SqliteImporter } from "./sqlite-importer";
|
||||||
|
import { PostgreSqlImporter } from "./postgresql-importer";
|
||||||
|
import { OracleImporter } from "./oracle-importer";
|
||||||
|
import { MSSQLImporter } from "./mssql-importer";
|
||||||
|
|
||||||
|
export const getImporter = (dialect: DatabaseDialect, data_types: DataType[]) => {
|
||||||
|
let importer: BaseSqlImporter | undefined = undefined;
|
||||||
|
|
||||||
|
|
||||||
|
switch (dialect) {
|
||||||
|
case DatabaseDialect.MYSQL:
|
||||||
|
importer = (new MysqlImporter(data_types));
|
||||||
|
break;
|
||||||
|
case DatabaseDialect.MARIADB:
|
||||||
|
importer = (new MariaDbImporter(data_types));
|
||||||
|
break;
|
||||||
|
case DatabaseDialect.SQLITE:
|
||||||
|
importer = (new SqliteImporter(data_types));
|
||||||
|
break;
|
||||||
|
case DatabaseDialect.POSTGRES:
|
||||||
|
importer = (new PostgreSqlImporter(data_types));
|
||||||
|
break;
|
||||||
|
case DatabaseDialect.ORACLE:
|
||||||
|
importer = (new OracleImporter(data_types));
|
||||||
|
break;
|
||||||
|
case DatabaseDialect.MSSQL:
|
||||||
|
importer = (new MSSQLImporter(data_types));
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
importer = new BaseSqlImporter(data_types);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return importer;
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { MysqlImporter } from "./mysql-importer";
|
||||||
|
|
||||||
|
export class MariaDbImporter extends MysqlImporter {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.MYSQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||||
|
|
||||||
|
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
if (ast.Nested?.Function) {
|
||||||
|
if ( ast.Nested.Function.name?.[0]?.Identifier?.value?.toUpperCase() == "UUID" ) {
|
||||||
|
|
||||||
|
return "random" ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ast.Function) {
|
||||||
|
if ( ast.Function.name?.[0]?.Identifier?.value.toUpperCase() == "UUID" ) {
|
||||||
|
|
||||||
|
return "random" ;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,702 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||||
|
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TimeDefaultValues } from "@/lib/field";
|
||||||
|
|
||||||
|
export class MSSQLImporter extends BaseSqlImporter {
|
||||||
|
|
||||||
|
|
||||||
|
private dfConstraints: string[] = [];
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.MSSQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseSql(sql: string) {
|
||||||
|
try {
|
||||||
|
this.dfConstraints = [];
|
||||||
|
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||||
|
for (const constraint of this.dfConstraints) {
|
||||||
|
try {
|
||||||
|
this.processDefaultValueConstraint(constraint, parsedDatabaase.tables);
|
||||||
|
} catch (error) {
|
||||||
|
parsedDatabaase.errors.push(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedDatabaase;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected astToField(ast: any, sequence: number): ParsedField {
|
||||||
|
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||||
|
// get field defintion options
|
||||||
|
const options: any | undefined = [] = ast.options;
|
||||||
|
for (const option of options) {
|
||||||
|
if (option.option?.Identity) {
|
||||||
|
field.autoIncrement = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field, fk_constraint };
|
||||||
|
}
|
||||||
|
private processDefaultValueConstraint(constraint: string, tables: TableInsertType[]) {
|
||||||
|
const regex =
|
||||||
|
/ALTER\s+TABLE\s+(?:(?:\[(?<schema1>[^\]]+)\]|(?<schema2>\w+))\.)?(?:\[(?<table1>[^\]]+)\]|(?<table2>\w+))\s+ADD\s+CONSTRAINT\s+(?:\[(?<constraint1>[^\]]+)\]|(?<constraint2>\w+))\s+DEFAULT\s*\((?<defaultValue>.*?)\)\s+FOR\s+(?:\[(?<column1>[^\]]+)\]|(?<column2>\w+))/is;
|
||||||
|
|
||||||
|
const match = constraint.match(regex);
|
||||||
|
|
||||||
|
if (match?.groups) {
|
||||||
|
const schema =
|
||||||
|
match.groups.schema1 || match.groups.schema2 || null;
|
||||||
|
|
||||||
|
const tableName =
|
||||||
|
match.groups.table1 || match.groups.table2;
|
||||||
|
|
||||||
|
const constraint =
|
||||||
|
match.groups.constraint1 || match.groups.constraint2;
|
||||||
|
|
||||||
|
const column =
|
||||||
|
match.groups.column1 || match.groups.column2;
|
||||||
|
|
||||||
|
const defaultValue =
|
||||||
|
match.groups.defaultValue?.trim()
|
||||||
|
// remove only wrapping parentheses around the whole value
|
||||||
|
.replace(/^\((.*)\)$/s, "$1")
|
||||||
|
|
||||||
|
// repeat once more for cases like ((1))
|
||||||
|
.replace(/^\((.*)\)$/s, "$1")
|
||||||
|
|
||||||
|
// remove surrounding quotes
|
||||||
|
.replace(/^['"](.*)['"]$/s, "$1");
|
||||||
|
|
||||||
|
|
||||||
|
if (tableName && column && defaultValue) {
|
||||||
|
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||||
|
if (!table)
|
||||||
|
throw Error("Table in DF constraint not found : " + constraint);
|
||||||
|
|
||||||
|
const field: FieldInsertType | undefined = table.fields?.find((field: FieldInsertType) => field.name == column);
|
||||||
|
if (!field) {
|
||||||
|
throw Error("field not found in DF constraint : " + constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFunctionRegex =
|
||||||
|
/^[a-zA-Z_][a-zA-Z0-9_]*\s*\([^()]*\)$/;
|
||||||
|
|
||||||
|
if (isFunctionRegex.test(defaultValue)) {
|
||||||
|
// in case the default value is a function sysdatetimeoffset() , CONVERT([date],getdate()) ... ect
|
||||||
|
// if it's a function of time current timestamp.
|
||||||
|
const upperDefaultValue: string = defaultValue.toUpperCase();
|
||||||
|
if (upperDefaultValue.includes("GETDATE") || upperDefaultValue.includes("SYSDATETIME") || upperDefaultValue.includes("SYSDATETIMEOFFSET")) {
|
||||||
|
field.defaultValue = TimeDefaultValues.NOW;
|
||||||
|
} else if (upperDefaultValue.includes("NEWID")) {
|
||||||
|
|
||||||
|
field.defaultValue = "random";
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
field.defaultValue = defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
} else
|
||||||
|
throw Error("Failed to process a MSSQL DF constraint : " + constraint);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||||
|
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||||
|
if (dataType.name == "bit" && defaultValue) {
|
||||||
|
return defaultValue == "1" ? "true" : "false";
|
||||||
|
}
|
||||||
|
else if (!defaultValue && dataType.name == "uniqueidentifier") {
|
||||||
|
if (ast.Function) {
|
||||||
|
if (ast.Function.name?.[0]?.Identifier?.value && (ast.Function.name?.[0]?.Identifier?.value == "NEWSEQUENTIALID" || ast.Function.name?.[0]?.Identifier?.value == "NEWID")) {
|
||||||
|
return "random";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||||
|
return value == "GETDATE" || value == "SYSDATETIME" || value == "SYSDATETIMEOFFSET";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected cleanSql(sql: string): string {
|
||||||
|
const cleanedSql = sql // remove GO batches
|
||||||
|
.replace(/([^;\s])\s*\r?\nGO\b/gm, '$1;')
|
||||||
|
.replace(/^\s*GO\s*$/gm, '')
|
||||||
|
|
||||||
|
|
||||||
|
// remove WITH CHECK
|
||||||
|
.replace(/\bWITH\s+CHECK\b\s*/gi, '')
|
||||||
|
|
||||||
|
// remove CHECK CONSTRAINT enable statements (FK validation step)
|
||||||
|
.replace(
|
||||||
|
/ALTER TABLE\s+\[?.*?\]?\.?\[?.*?\]?\s+CHECK\s+CONSTRAINT\s+\[?.*?\]?;\s*/gi,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
// remove computed columns
|
||||||
|
.replace(
|
||||||
|
/^\s*(?:\[[^\]]+\]|\w+)\s+AS\s+.*?(?:PERSISTED\s*)?,?\s*$/gim,
|
||||||
|
''
|
||||||
|
)
|
||||||
|
.replace(
|
||||||
|
/(\[[^\]]+\]|\w+)\s+([^,]*?)\s+FOREIGN\s+KEY\s+REFERENCES\s+([^\s,()]+)\s*\(\s*([^\s,()]+)\s*\)/gi,
|
||||||
|
(_match, col, beforeRefs, table, refCol) => {
|
||||||
|
return `${col} ${beforeRefs.trim()} REFERENCES ${table}(${refCol})`;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
// remove CLUSTERED / NONCLUSTERED
|
||||||
|
.replace(/\bCLUSTERED\b/g, '')
|
||||||
|
.replace(/\bNONCLUSTERED\b/g, '')
|
||||||
|
|
||||||
|
// remove ASC / DESC inside constraints
|
||||||
|
.replace(/\s+\bASC\b/g, '')
|
||||||
|
.replace(/\s+\bDESC\b/g, '')
|
||||||
|
|
||||||
|
// remove WITH (...) ON [PRIMARY]
|
||||||
|
.replace(/\)\s*WITH\s*\([^)]+\)\s*ON\s*\[[^\]]+\]/g, ')')
|
||||||
|
|
||||||
|
// remove trailing table storage clauses
|
||||||
|
.replace(/\)\s*ON\s*\[[^\]]+\](\s*TEXTIMAGE_ON\s*\[[^\]]+\])?/g, ')')
|
||||||
|
|
||||||
|
// cleanup extra spaces
|
||||||
|
.replace(/[ \t]+/g, ' ')
|
||||||
|
.replace(/\n\s+\n/g, '\n\n');
|
||||||
|
|
||||||
|
|
||||||
|
return super.cleanSql(cleanedSql);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected extractStatments(sql: string): ExtractedStatments {
|
||||||
|
|
||||||
|
const statments: ExtractedStatments = super.extractStatments(sql);
|
||||||
|
|
||||||
|
statments.alterTableStatements = statments.alterTableStatements.filter((statment: string) => {
|
||||||
|
if (/ALTER\s+TABLE\s+\[[^\]]+\]\.\[[^\]]+\]\s+ADD\s+CONSTRAINT\s+\[[^\]]+\]\s+DEFAULT\s*\(.*?\)\s+FOR\s+\[[^\]]+\]/i.test(statment)) {
|
||||||
|
|
||||||
|
this.dfConstraints.push(statment);
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
return statment;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return statments
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
-- =============================================
|
||||||
|
-- Enterprise E-Commerce Platform Database
|
||||||
|
-- Version: 1.0 (No CHECK Constraints)
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Drop database if exists (for clean creation)
|
||||||
|
IF EXISTS (SELECT name FROM sys.databases WHERE name = N'ECommerceDB')
|
||||||
|
BEGIN
|
||||||
|
ALTER DATABASE ECommerceDB SET SINGLE_USER WITH ROLLBACK IMMEDIATE;
|
||||||
|
DROP DATABASE ECommerceDB;
|
||||||
|
END
|
||||||
|
GO
|
||||||
|
|
||||||
|
CREATE DATABASE ECommerceDB;
|
||||||
|
GO
|
||||||
|
|
||||||
|
USE ECommerceDB;
|
||||||
|
GO
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 1. Customer Management Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE Customers (
|
||||||
|
CustomerID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CustomerNumber AS 'CUST-' + RIGHT('000000' + CAST(CustomerID AS VARCHAR(6)), 6) PERSISTED,
|
||||||
|
Email VARCHAR(255) NOT NULL UNIQUE,
|
||||||
|
Phone VARCHAR(20) NOT NULL,
|
||||||
|
FirstName NVARCHAR(50) NOT NULL,
|
||||||
|
LastName NVARCHAR(50) NOT NULL,
|
||||||
|
DateOfBirth DATE NULL,
|
||||||
|
TaxID VARCHAR(50) NULL,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1,
|
||||||
|
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
UpdatedAt DATETIME2 NOT NULL DEFAULT GETDATE()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE CustomerAddresses (
|
||||||
|
AddressID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CustomerID INT NOT NULL,
|
||||||
|
AddressType VARCHAR(20) NOT NULL, -- Billing, Shipping, Both
|
||||||
|
AddressLine1 NVARCHAR(200) NOT NULL,
|
||||||
|
AddressLine2 NVARCHAR(200) NULL,
|
||||||
|
City NVARCHAR(100) NOT NULL,
|
||||||
|
StateProvince NVARCHAR(100) NOT NULL,
|
||||||
|
PostalCode VARCHAR(20) NOT NULL,
|
||||||
|
CountryCode CHAR(2) NOT NULL,
|
||||||
|
IsDefault BIT NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE CustomerLoyalty (
|
||||||
|
LoyaltyID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CustomerID INT NOT NULL UNIQUE,
|
||||||
|
PointsBalance INT NOT NULL DEFAULT 0,
|
||||||
|
TierLevel VARCHAR(20) NOT NULL DEFAULT 'Bronze',
|
||||||
|
TotalSpent DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
JoinDate DATE NOT NULL DEFAULT GETDATE(),
|
||||||
|
PointsExpiryDate DATE NULL,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE CustomerPreferences (
|
||||||
|
PreferenceID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CustomerID INT NOT NULL,
|
||||||
|
PreferenceKey VARCHAR(50) NOT NULL,
|
||||||
|
PreferenceValue NVARCHAR(500) NULL,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT UQ_Customer_Preference UNIQUE (CustomerID, PreferenceKey)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 2. Product Catalog Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE Categories (
|
||||||
|
CategoryID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
ParentCategoryID INT NULL,
|
||||||
|
CategoryName NVARCHAR(100) NOT NULL,
|
||||||
|
CategoryCode VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
Description NVARCHAR(500) NULL,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1,
|
||||||
|
DisplayOrder INT NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (ParentCategoryID) REFERENCES Categories(CategoryID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Brands (
|
||||||
|
BrandID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
BrandName NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
BrandCode VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
Website VARCHAR(255) NULL,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Products (
|
||||||
|
ProductID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
SKU VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
ProductName NVARCHAR(200) NOT NULL,
|
||||||
|
Description NVARCHAR(MAX) NULL,
|
||||||
|
CategoryID INT NOT NULL,
|
||||||
|
BrandID INT NULL,
|
||||||
|
UnitPrice DECIMAL(18,2) NOT NULL,
|
||||||
|
Weight DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
|
IsTaxable BIT NOT NULL DEFAULT 1,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1,
|
||||||
|
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
ModifiedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
FOREIGN KEY (CategoryID) REFERENCES Categories(CategoryID),
|
||||||
|
FOREIGN KEY (BrandID) REFERENCES Brands(BrandID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ProductAttributes (
|
||||||
|
AttributeID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
AttributeName NVARCHAR(100) NOT NULL UNIQUE,
|
||||||
|
DataType VARCHAR(20) NOT NULL -- String, Integer, Decimal, Boolean, Date
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ProductAttributeValues (
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
AttributeID INT NOT NULL,
|
||||||
|
AttributeValue NVARCHAR(500) NOT NULL,
|
||||||
|
PRIMARY KEY (ProductID, AttributeID),
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (AttributeID) REFERENCES ProductAttributes(AttributeID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Inventory (
|
||||||
|
InventoryID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
WarehouseCode VARCHAR(20) NOT NULL,
|
||||||
|
QuantityOnHand INT NOT NULL DEFAULT 0,
|
||||||
|
QuantityReserved INT NOT NULL DEFAULT 0,
|
||||||
|
ReorderLevel INT NOT NULL DEFAULT 10,
|
||||||
|
ReorderQuantity INT NOT NULL DEFAULT 50,
|
||||||
|
LastRestockedDate DATETIME2 NULL,
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
|
||||||
|
CONSTRAINT UQ_Product_Warehouse UNIQUE (ProductID, WarehouseCode)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 3. Pricing & Promotions Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE PriceTiers (
|
||||||
|
TierID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
TierName VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
MinQuantity INT NOT NULL,
|
||||||
|
DiscountPercentage DECIMAL(5,2) NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ProductPricing (
|
||||||
|
PricingID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
TierID INT NOT NULL,
|
||||||
|
Price DECIMAL(18,2) NOT NULL,
|
||||||
|
EffectiveFrom DATE NOT NULL,
|
||||||
|
EffectiveTo DATE NULL,
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID),
|
||||||
|
FOREIGN KEY (TierID) REFERENCES PriceTiers(TierID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Promotions (
|
||||||
|
PromotionID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
PromotionCode VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
PromotionName NVARCHAR(200) NOT NULL,
|
||||||
|
DiscountType VARCHAR(20) NOT NULL, -- Percentage, FixedAmount, BuyOneGetOne
|
||||||
|
DiscountValue DECIMAL(18,2) NOT NULL,
|
||||||
|
StartDate DATETIME2 NOT NULL,
|
||||||
|
EndDate DATETIME2 NOT NULL,
|
||||||
|
MinOrderAmount DECIMAL(18,2) NULL,
|
||||||
|
MaxDiscountAmount DECIMAL(18,2) NULL,
|
||||||
|
UsageLimit INT NULL,
|
||||||
|
UsedCount INT NOT NULL DEFAULT 0,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE PromotionApplicability (
|
||||||
|
ApplicabilityID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
PromotionID INT NOT NULL,
|
||||||
|
ApplicableToType VARCHAR(20) NOT NULL, -- AllProducts, SpecificProduct, SpecificCategory, SpecificBrand
|
||||||
|
ReferenceID INT NULL,
|
||||||
|
FOREIGN KEY (PromotionID) REFERENCES Promotions(PromotionID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 4. Order Management Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE OrderStatus (
|
||||||
|
StatusID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
StatusName VARCHAR(50) NOT NULL UNIQUE,
|
||||||
|
StatusDescription NVARCHAR(200) NULL,
|
||||||
|
IsFinalState BIT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Orders (
|
||||||
|
OrderID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
OrderNumber AS 'ORD-' + FORMAT(OrderID, '000000') PERSISTED,
|
||||||
|
CustomerID INT NOT NULL,
|
||||||
|
OrderDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
StatusID INT NOT NULL,
|
||||||
|
PaymentStatus VARCHAR(30) NOT NULL DEFAULT 'Pending', -- Pending, Paid, Failed, Refunded
|
||||||
|
ShippingAddressID INT NOT NULL,
|
||||||
|
BillingAddressID INT NOT NULL,
|
||||||
|
Subtotal DECIMAL(18,2) NOT NULL,
|
||||||
|
TaxAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
ShippingAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
DiscountAmount DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
TotalAmount DECIMAL(18,2) NOT NULL,
|
||||||
|
PromotionID INT NULL,
|
||||||
|
Notes NVARCHAR(MAX) NULL,
|
||||||
|
OrderWeight DECIMAL(10,3) NOT NULL DEFAULT 0,
|
||||||
|
CompletedDate DATETIME2 NULL,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID),
|
||||||
|
FOREIGN KEY (StatusID) REFERENCES OrderStatus(StatusID),
|
||||||
|
FOREIGN KEY (ShippingAddressID) REFERENCES CustomerAddresses(AddressID),
|
||||||
|
FOREIGN KEY (BillingAddressID) REFERENCES CustomerAddresses(AddressID),
|
||||||
|
FOREIGN KEY (PromotionID) REFERENCES Promotions(PromotionID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE OrderItems (
|
||||||
|
OrderItemID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
OrderID INT NOT NULL,
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
Quantity INT NOT NULL,
|
||||||
|
UnitPrice DECIMAL(18,2) NOT NULL,
|
||||||
|
DiscountApplied DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
TotalPrice DECIMAL(18,2) NOT NULL,
|
||||||
|
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE OrderTracking (
|
||||||
|
TrackingID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
OrderID INT NOT NULL,
|
||||||
|
StatusID INT NOT NULL,
|
||||||
|
StatusDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
Notes NVARCHAR(500) NULL,
|
||||||
|
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (StatusID) REFERENCES OrderStatus(StatusID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 5. Payment & Transaction Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE PaymentMethods (
|
||||||
|
PaymentMethodID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
MethodName VARCHAR(50) NOT NULL UNIQUE, -- CreditCard, PayPal, BankTransfer, Cash
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1,
|
||||||
|
ProcessingFee DECIMAL(5,2) NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Currencies (
|
||||||
|
CurrencyID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CurrencyCode CHAR(3) NOT NULL UNIQUE, -- USD, EUR, GBP, JPY
|
||||||
|
CurrencyName NVARCHAR(50) NOT NULL,
|
||||||
|
ExchangeRateToUSD DECIMAL(18,6) NOT NULL,
|
||||||
|
IsBaseCurrency BIT NOT NULL DEFAULT 0
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Payments (
|
||||||
|
PaymentID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
OrderID INT NOT NULL,
|
||||||
|
PaymentMethodID INT NOT NULL,
|
||||||
|
CurrencyID INT NOT NULL,
|
||||||
|
Amount DECIMAL(18,2) NOT NULL,
|
||||||
|
PaymentDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
TransactionID VARCHAR(100) NULL,
|
||||||
|
PaymentStatus VARCHAR(30) NOT NULL DEFAULT 'Pending',
|
||||||
|
GatewayResponse NVARCHAR(MAX) NULL,
|
||||||
|
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
|
||||||
|
FOREIGN KEY (PaymentMethodID) REFERENCES PaymentMethods(PaymentMethodID),
|
||||||
|
FOREIGN KEY (CurrencyID) REFERENCES Currencies(CurrencyID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Refunds (
|
||||||
|
RefundID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
PaymentID INT NOT NULL,
|
||||||
|
RefundAmount DECIMAL(18,2) NOT NULL,
|
||||||
|
RefundDate DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
Reason NVARCHAR(500) NULL,
|
||||||
|
ApprovalStatus VARCHAR(30) NOT NULL DEFAULT 'Pending',
|
||||||
|
ProcessedBy INT NULL,
|
||||||
|
FOREIGN KEY (PaymentID) REFERENCES Payments(PaymentID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 6. Shipping & Logistics Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE ShippingMethods (
|
||||||
|
ShippingMethodID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
MethodName VARCHAR(100) NOT NULL UNIQUE, -- Standard, Express, Overnight
|
||||||
|
EstimatedDeliveryDays INT NOT NULL,
|
||||||
|
BaseCost DECIMAL(18,2) NOT NULL,
|
||||||
|
CostPerKg DECIMAL(18,2) NOT NULL DEFAULT 0,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Shipments (
|
||||||
|
ShipmentID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
OrderID INT NOT NULL,
|
||||||
|
ShippingMethodID INT NOT NULL,
|
||||||
|
TrackingNumber VARCHAR(100) NULL,
|
||||||
|
CarrierName VARCHAR(100) NOT NULL,
|
||||||
|
ShippedDate DATETIME2 NULL,
|
||||||
|
DeliveredDate DATETIME2 NULL,
|
||||||
|
ShippingCost DECIMAL(18,2) NOT NULL,
|
||||||
|
FOREIGN KEY (OrderID) REFERENCES Orders(OrderID),
|
||||||
|
FOREIGN KEY (ShippingMethodID) REFERENCES ShippingMethods(ShippingMethodID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Warehouses (
|
||||||
|
WarehouseID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
WarehouseCode VARCHAR(20) NOT NULL UNIQUE,
|
||||||
|
WarehouseName NVARCHAR(100) NOT NULL,
|
||||||
|
LocationAddress NVARCHAR(500) NOT NULL,
|
||||||
|
City NVARCHAR(100) NOT NULL,
|
||||||
|
CountryCode CHAR(2) NOT NULL,
|
||||||
|
IsActive BIT NOT NULL DEFAULT 1
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Add WarehouseCode foreign key to Inventory
|
||||||
|
ALTER TABLE Inventory ADD WarehouseID INT NULL;
|
||||||
|
ALTER TABLE Inventory ADD FOREIGN KEY (WarehouseID) REFERENCES Warehouses(WarehouseID);
|
||||||
|
ALTER TABLE Inventory ALTER COLUMN WarehouseCode VARCHAR(20) NULL;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 7. Reviews & Ratings Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE ProductReviews (
|
||||||
|
ReviewID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
CustomerID INT NOT NULL,
|
||||||
|
Rating INT NOT NULL, -- 1 to 5 stars
|
||||||
|
Title NVARCHAR(200) NULL,
|
||||||
|
ReviewText NVARCHAR(MAX) NULL,
|
||||||
|
IsVerifiedPurchase BIT NOT NULL DEFAULT 0,
|
||||||
|
HelpfulCount INT NOT NULL DEFAULT 0,
|
||||||
|
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
IsApproved BIT NOT NULL DEFAULT 0,
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE ReviewHelpfulness (
|
||||||
|
HelpfulnessID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
ReviewID INT NOT NULL,
|
||||||
|
CustomerID INT NOT NULL,
|
||||||
|
IsHelpful BIT NOT NULL,
|
||||||
|
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
FOREIGN KEY (ReviewID) REFERENCES ProductReviews(ReviewID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID),
|
||||||
|
CONSTRAINT UQ_Review_Customer UNIQUE (ReviewID, CustomerID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 8. Shopping Cart Schema
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE ShoppingCarts (
|
||||||
|
CartID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CustomerID INT NOT NULL UNIQUE,
|
||||||
|
CreatedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
LastModified DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
FOREIGN KEY (CustomerID) REFERENCES Customers(CustomerID) ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE CartItems (
|
||||||
|
CartItemID INT IDENTITY(1,1) PRIMARY KEY,
|
||||||
|
CartID INT NOT NULL,
|
||||||
|
ProductID INT NOT NULL,
|
||||||
|
Quantity INT NOT NULL,
|
||||||
|
AddedAt DATETIME2 NOT NULL DEFAULT GETDATE(),
|
||||||
|
FOREIGN KEY (CartID) REFERENCES ShoppingCarts(CartID) ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY (ProductID) REFERENCES Products(ProductID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- INDEXES for Performance
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Customers indexes
|
||||||
|
CREATE INDEX IX_Customers_Email ON Customers(Email);
|
||||||
|
CREATE INDEX IX_Customers_LastName ON Customers(LastName);
|
||||||
|
CREATE INDEX IX_Customers_CreatedAt ON Customers(CreatedAt);
|
||||||
|
|
||||||
|
-- CustomerAddresses indexes
|
||||||
|
CREATE INDEX IX_CustomerAddresses_CustomerID ON CustomerAddresses(CustomerID);
|
||||||
|
CREATE INDEX IX_CustomerAddresses_CountryCode ON CustomerAddresses(CountryCode);
|
||||||
|
|
||||||
|
-- Products indexes
|
||||||
|
CREATE INDEX IX_Products_CategoryID ON Products(CategoryID);
|
||||||
|
CREATE INDEX IX_Products_BrandID ON Products(BrandID);
|
||||||
|
CREATE INDEX IX_Products_SKU ON Products(SKU);
|
||||||
|
CREATE INDEX IX_Products_UnitPrice ON Products(UnitPrice);
|
||||||
|
CREATE INDEX IX_Products_IsActive ON Products(IsActive);
|
||||||
|
|
||||||
|
-- Inventory indexes
|
||||||
|
CREATE INDEX IX_Inventory_ProductID ON Inventory(ProductID);
|
||||||
|
CREATE INDEX IX_Inventory_WarehouseID ON Inventory(WarehouseID);
|
||||||
|
CREATE INDEX IX_Inventory_QuantityOnHand ON Inventory(QuantityOnHand);
|
||||||
|
|
||||||
|
-- Orders indexes
|
||||||
|
CREATE INDEX IX_Orders_CustomerID ON Orders(CustomerID);
|
||||||
|
CREATE INDEX IX_Orders_OrderDate ON Orders(OrderDate);
|
||||||
|
CREATE INDEX IX_Orders_StatusID ON Orders(StatusID);
|
||||||
|
CREATE INDEX IX_Orders_PaymentStatus ON Orders(PaymentStatus);
|
||||||
|
CREATE INDEX IX_Orders_TotalAmount ON Orders(TotalAmount);
|
||||||
|
|
||||||
|
-- OrderItems indexes
|
||||||
|
CREATE INDEX IX_OrderItems_OrderID ON OrderItems(OrderID);
|
||||||
|
CREATE INDEX IX_OrderItems_ProductID ON OrderItems(ProductID);
|
||||||
|
|
||||||
|
-- Payments indexes
|
||||||
|
CREATE INDEX IX_Payments_OrderID ON Payments(OrderID);
|
||||||
|
CREATE INDEX IX_Payments_PaymentStatus ON Payments(PaymentStatus);
|
||||||
|
CREATE INDEX IX_Payments_PaymentDate ON Payments(PaymentDate);
|
||||||
|
|
||||||
|
-- Shipments indexes
|
||||||
|
CREATE INDEX IX_Shipments_OrderID ON Shipments(OrderID);
|
||||||
|
CREATE INDEX IX_Shipments_TrackingNumber ON Shipments(TrackingNumber);
|
||||||
|
CREATE INDEX IX_Shipments_ShippedDate ON Shipments(ShippedDate);
|
||||||
|
|
||||||
|
-- ProductReviews indexes
|
||||||
|
CREATE INDEX IX_ProductReviews_ProductID ON ProductReviews(ProductID);
|
||||||
|
CREATE INDEX IX_ProductReviews_CustomerID ON ProductReviews(CustomerID);
|
||||||
|
CREATE INDEX IX_ProductReviews_Rating ON ProductReviews(Rating);
|
||||||
|
CREATE INDEX IX_ProductReviews_CreatedAt ON ProductReviews(CreatedAt);
|
||||||
|
|
||||||
|
-- Promotions indexes
|
||||||
|
CREATE INDEX IX_Promotions_PromotionCode ON Promotions(PromotionCode);
|
||||||
|
CREATE INDEX IX_Promotions_StartDate_EndDate ON Promotions(StartDate, EndDate);
|
||||||
|
|
||||||
|
-- OrderTracking indexes
|
||||||
|
CREATE INDEX IX_OrderTracking_OrderID ON OrderTracking(OrderID);
|
||||||
|
CREATE INDEX IX_OrderTracking_StatusID ON OrderTracking(StatusID);
|
||||||
|
CREATE INDEX IX_OrderTracking_StatusDate ON OrderTracking(StatusDate);
|
||||||
|
|
||||||
|
-- CartItems indexes
|
||||||
|
CREATE INDEX IX_CartItems_CartID ON CartItems(CartID);
|
||||||
|
CREATE INDEX IX_CartItems_ProductID ON CartItems(ProductID);
|
||||||
|
|
||||||
|
-- Composite indexes for common query patterns
|
||||||
|
CREATE INDEX IX_Orders_Customer_Date ON Orders(CustomerID, OrderDate DESC);
|
||||||
|
CREATE INDEX IX_OrderItems_Product_Price ON OrderItems(ProductID, UnitPrice);
|
||||||
|
CREATE INDEX IX_Products_Category_Price ON Products(CategoryID, UnitPrice);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Insert initial reference data
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Order Status
|
||||||
|
INSERT INTO OrderStatus (StatusName, StatusDescription, IsFinalState) VALUES
|
||||||
|
('Pending', 'Order placed but not processed', 0),
|
||||||
|
('PaymentReceived', 'Payment has been confirmed', 0),
|
||||||
|
('Processing', 'Order is being prepared', 0),
|
||||||
|
('Shipped', 'Order has been shipped', 0),
|
||||||
|
('Delivered', 'Order delivered to customer', 1),
|
||||||
|
('Cancelled', 'Order cancelled', 1),
|
||||||
|
('Refunded', 'Order refunded', 1);
|
||||||
|
|
||||||
|
-- Payment Methods
|
||||||
|
INSERT INTO PaymentMethods (MethodName, ProcessingFee) VALUES
|
||||||
|
('CreditCard', 2.9),
|
||||||
|
('PayPal', 3.5),
|
||||||
|
('BankTransfer', 0.5),
|
||||||
|
('CashOnDelivery', 0.0);
|
||||||
|
|
||||||
|
-- Currencies
|
||||||
|
INSERT INTO Currencies (CurrencyCode, CurrencyName, ExchangeRateToUSD, IsBaseCurrency) VALUES
|
||||||
|
('USD', 'US Dollar', 1.0, 1),
|
||||||
|
('EUR', 'Euro', 1.09, 0),
|
||||||
|
('GBP', 'British Pound', 1.27, 0),
|
||||||
|
('JPY', 'Japanese Yen', 0.0067, 0);
|
||||||
|
|
||||||
|
-- Shipping Methods
|
||||||
|
INSERT INTO ShippingMethods (MethodName, EstimatedDeliveryDays, BaseCost, CostPerKg) VALUES
|
||||||
|
('Standard Shipping', 5, 5.99, 1.5),
|
||||||
|
('Express Shipping', 2, 12.99, 2.5),
|
||||||
|
('Overnight', 1, 24.99, 4.0);
|
||||||
|
|
||||||
|
-- Price Tiers
|
||||||
|
INSERT INTO PriceTiers (TierName, MinQuantity, DiscountPercentage) VALUES
|
||||||
|
('Regular', 1, 0),
|
||||||
|
('Wholesale', 10, 10),
|
||||||
|
('Bulk', 50, 15),
|
||||||
|
('Enterprise', 100, 20);
|
||||||
|
|
||||||
|
-- Sample Categories
|
||||||
|
INSERT INTO Categories (CategoryName, CategoryCode, DisplayOrder) VALUES
|
||||||
|
('Electronics', 'ELEC', 1),
|
||||||
|
('Clothing', 'CLOTH', 2),
|
||||||
|
('Books', 'BOOKS', 3),
|
||||||
|
('Home & Garden', 'HOME', 4);
|
||||||
|
|
||||||
|
-- Sample Brands
|
||||||
|
INSERT INTO Brands (BrandName, BrandCode) VALUES
|
||||||
|
('TechPro', 'TP001'),
|
||||||
|
('FashionHub', 'FH002'),
|
||||||
|
('ReadWell', 'RW003');
|
||||||
|
*/
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { BaseSqlImporter, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { Modifiers, MySQLCharset, MySQLCollation } from "@/lib/field";
|
||||||
|
|
||||||
|
export class MysqlImporter extends BaseSqlImporter {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.MYSQL;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected astToField(ast: any, sequence: number): ParsedField {
|
||||||
|
try {
|
||||||
|
const { field, fk_constraint } = super.astToField(ast, sequence)
|
||||||
|
// get the data type
|
||||||
|
const dataType: DataType = this.data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType;
|
||||||
|
// if data type is of type set then extract it's values
|
||||||
|
if (dataType.name == "set") {
|
||||||
|
if (ast.data_type["Set"] && Array.isArray(ast.data_type["Set"])) {
|
||||||
|
try {
|
||||||
|
field.values = JSON.stringify(ast.data_type["Set"])
|
||||||
|
} catch (error) {
|
||||||
|
console.error("faield to parse Mysql Set values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// get data type modifiers
|
||||||
|
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||||
|
|
||||||
|
const options: any | undefined = [] = ast.options;
|
||||||
|
|
||||||
|
for (const option of options) {
|
||||||
|
if (typeof option.option === "object") {
|
||||||
|
if (option.option?.CharacterSet?.[0]?.Identifier?.value && modifiers.includes(Modifiers.CHARSET)) {
|
||||||
|
const charset: string = option.option.CharacterSet[0].Identifier.value;
|
||||||
|
|
||||||
|
if (Object.values(MySQLCharset).includes(charset as MySQLCharset)) {
|
||||||
|
|
||||||
|
field.charset = charset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (option.option?.Collation?.[0]?.Identifier?.value && modifiers.includes(Modifiers.COLLATE)) {
|
||||||
|
|
||||||
|
const collate: string = option.option.Collation[0].Identifier.value;
|
||||||
|
|
||||||
|
if (Object.values(MySQLCollation).includes(collate as MySQLCollation)) {
|
||||||
|
|
||||||
|
field.collate = collate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { field, fk_constraint };
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected cleanSql(sql: string): string {
|
||||||
|
let cleaned: string = super.cleanSql(sql);
|
||||||
|
return cleaned.replace(/\b(?:unsigned|zerofill)\b/gi, '');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE employees (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
first_name VARCHAR(100),
|
||||||
|
last_name VARCHAR(100),
|
||||||
|
department_id INT,
|
||||||
|
|
||||||
|
CONSTRAINT uq_employee_name_dept
|
||||||
|
UNIQUE (first_name, last_name, department_id)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE products (
|
||||||
|
id INT PRIMARY KEY AUTO_INCREMENT,
|
||||||
|
sku VARCHAR(100),
|
||||||
|
name VARCHAR(255)
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE products
|
||||||
|
ADD CONSTRAINT uq_products_sku
|
||||||
|
UNIQUE (sku);
|
||||||
|
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,725 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||||
|
|
||||||
|
import { DataTypes, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||||
|
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
|
export class OracleImporter extends BaseSqlImporter {
|
||||||
|
|
||||||
|
private alterNullabilityStatments: string[] = [];
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.ORACLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public parseSql(sql: string) {
|
||||||
|
try {
|
||||||
|
this.alterNullabilityStatments = [];
|
||||||
|
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||||
|
for (const statment of this.alterNullabilityStatments) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.processAlterColumnNullabilityStatment(statment, parsedDatabaase.tables);
|
||||||
|
} catch (error) {
|
||||||
|
parsedDatabaase.errors.push(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parsedDatabaase;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected astToField(ast: any, sequence: number): ParsedField {
|
||||||
|
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||||
|
|
||||||
|
const dataType: DataType = this.data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType;
|
||||||
|
// get the modifiers to test if the data type really support auto increment or not
|
||||||
|
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
||||||
|
|
||||||
|
if (ast.data_type.Custom && Array.isArray(ast.data_type.Custom)) {
|
||||||
|
if (ast.data_type.Custom.length == 2 && ast.data_type.Custom[1]?.length == 2 && !isNaN(Number(ast.data_type.Custom[1]?.[1] && modifiers.includes(Modifiers.SCALE))))
|
||||||
|
field.scale = Number(ast.data_type.Custom[1]?.[1]);
|
||||||
|
}
|
||||||
|
// get field defintion options
|
||||||
|
const options: any | undefined = [] = ast.options;
|
||||||
|
for (const option of options) {
|
||||||
|
if (option.option?.Generated?.generated_as == "ByDefault") {
|
||||||
|
if (modifiers.includes(Modifiers.AUTO_INCREMENT)) {
|
||||||
|
field.autoIncrement = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { field, fk_constraint };
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private processAlterColumnNullabilityStatment(statement: string, tables: TableInsertType[]): void {
|
||||||
|
|
||||||
|
|
||||||
|
const regex = /ALTER\s+TABLE\s+"(?<tableName>[^"]+)"\s+MODIFY\s*\(\s*"(?<columnName>[^"]+)"\s+(?<nullability>NOT\s+NULL|NULL)(?:\s+ENABLE)?\s*\)/i;
|
||||||
|
const match = statement.match(regex);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (match?.groups) {
|
||||||
|
const { tableName, columnName, nullability } = match.groups;
|
||||||
|
|
||||||
|
|
||||||
|
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name.toUpperCase() == tableName.toUpperCase());
|
||||||
|
if (!table) {
|
||||||
|
throw Error("Altered Table not found in : " + statement);
|
||||||
|
}
|
||||||
|
const field : FieldInsertType | undefined = table.fields?.find((field: FieldInsertType) => field.name.toUpperCase() == columnName.toUpperCase())
|
||||||
|
if (!field)
|
||||||
|
throw Error("Altered field not found in : " + statement);
|
||||||
|
|
||||||
|
|
||||||
|
field.nullable = !(nullability.toUpperCase() == "NOT NULL") ;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||||
|
const defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||||
|
|
||||||
|
if (!defaultValue && ast.Identifier && ast.Identifier.value && dataType.type == DataTypes.TIME) {
|
||||||
|
|
||||||
|
if (this.isCurrentTimesTampFunction(ast.Identifier.value))
|
||||||
|
return TimeDefaultValues.NOW;
|
||||||
|
}
|
||||||
|
return defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected isCurrentTimesTampFunction(value: string): boolean {
|
||||||
|
const upper: string = value.toUpperCase();
|
||||||
|
return super.isCurrentTimesTampFunction(value) || upper == "SYSDATE" || upper == "SYSTIMESTAMP";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDataType(typeName: string): DataType | undefined {
|
||||||
|
let dataType: DataType | undefined = super.processDataType(typeName);
|
||||||
|
|
||||||
|
if (!dataType) {
|
||||||
|
if (typeName == "year") {
|
||||||
|
return this.data_types.find((dataType: DataType) => dataType.name == "interval year to month");
|
||||||
|
}
|
||||||
|
else if (typeName == "day") {
|
||||||
|
return this.data_types.find((dataType: DataType) => dataType.name == "interval day to second");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dataType;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected cleanSql(sql: string): string {
|
||||||
|
let cleaned: string = super.cleanSql(sql);
|
||||||
|
cleaned = cleaned.replace(/\bWITH\s+LOCAL\s+TIME\b/gi, 'WITH TIME');
|
||||||
|
// remove all ENABLE keywords in add foriegn key constraint
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/(ALTER\s+TABLE[\s\S]*?FOREIGN\s+KEY[\s\S]*?REFERENCES[\s\S]*?)\s+ENABLE(?=\s*;)/gi,
|
||||||
|
'$1'
|
||||||
|
);
|
||||||
|
// remove MINVALUE [X] MAXVALUE [Y] INCREMENT BY [N] START WITH [M] CACHE [C] NOORDER NOCYCLE NOKEEP NOSCALE .
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/(GENERATED\s+(?:ALWAYS|BY\s+DEFAULT(?:\s+ON\s+NULL)?)\s+AS\s+IDENTITY)([\s\S]*?)(?=,|\s+NOT\s+NULL|\s+PRIMARY\s+KEY)/gi,
|
||||||
|
'$1'
|
||||||
|
);
|
||||||
|
// remove all USING INDEX ENBALE in add Unique or Primary Key constraints .
|
||||||
|
cleaned = cleaned.replace(/\s+USING\s+INDEX\s+ENABLE(?=\s*;)/gi, '');
|
||||||
|
// remove START WITH X INCREMENT BY Y
|
||||||
|
cleaned = cleaned.replace(/\s+START\s+WITH\s+\d+(?:\s+INCREMENT\s+BY\s+\d+)?/gi, '');
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/\bINTERVAL\s+YEAR(\s*\(\s*\d+\s*\))?\s+TO\s+MONTH(?:\s*\(\s*\d+\s*\))?/gi,
|
||||||
|
'YEAR$1'
|
||||||
|
);
|
||||||
|
|
||||||
|
cleaned = cleaned.replace(
|
||||||
|
/\bINTERVAL\s+DAY(\s*\(\s*\d+\s*\))?\s+TO\s+SECOND(?:\s*\(\s*\d+\s*\))?/gi,
|
||||||
|
'DAY$1'
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
return cleaned;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected extractStatments(sql: string): ExtractedStatments {
|
||||||
|
|
||||||
|
const statments: ExtractedStatments = super.extractStatments(sql);
|
||||||
|
|
||||||
|
statments.alterTableStatements = statments.alterTableStatements.filter((statment: string) => {
|
||||||
|
if (/ALTER\s+TABLE\s+.+?\s+MODIFY\s*\(\s*".+?"\s+(?:NULL|NOT\s+NULL)(?:\s+ENABLE)?\s*\)/i.test(statment)) {
|
||||||
|
|
||||||
|
this.alterNullabilityStatments.push(statment);
|
||||||
|
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
|
||||||
|
return statment;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return statments
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Creative Enterprise Platform Database for Oracle
|
||||||
|
-- Theme: Multi-Tenant Project Management & Resource Planning
|
||||||
|
-- Version: 3.0 (Oracle Edition)
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Drop user/schema if needed (optional - run separately)
|
||||||
|
-- DROP USER creative_enterprise CASCADE;
|
||||||
|
-- CREATE USER creative_enterprise IDENTIFIED BY password;
|
||||||
|
-- GRANT CONNECT, RESOURCE, UNLIMITED TABLESPACE TO creative_enterprise;
|
||||||
|
-- CONNECT creative_enterprise/password;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 1. TENANT & ORGANIZATION SCHEMA
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Inline primary key, inline unique, default as expression
|
||||||
|
CREATE TABLE Tenants (
|
||||||
|
TenantID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 100 INCREMENT BY 1 PRIMARY KEY,
|
||||||
|
TenantCode VARCHAR2(20) NOT NULL UNIQUE,
|
||||||
|
TenantName NVARCHAR2(200) NOT NULL,
|
||||||
|
SubscriptionTier VARCHAR2(30) DEFAULT 'Trial' NOT NULL,
|
||||||
|
CreatedDate DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
IsActive CHAR(1) DEFAULT '1' NOT NULL,
|
||||||
|
MaxUsers NUMBER(5) DEFAULT 10 NOT NULL,
|
||||||
|
CONSTRAINT UQ_Tenant_Name UNIQUE (TenantName)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Constraints at end of table
|
||||||
|
CREATE TABLE Departments (
|
||||||
|
DepartmentID NUMBER(10) NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
DeptCode VARCHAR2(20) NOT NULL,
|
||||||
|
DeptName NVARCHAR2(100) NOT NULL,
|
||||||
|
Budget NUMBER(18,2) DEFAULT 0 NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT PK_Departments PRIMARY KEY (DepartmentID, TenantID),
|
||||||
|
CONSTRAINT UQ_DeptCode_Tenant UNIQUE (DeptCode, TenantID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Foreign key added via ALTER after creation
|
||||||
|
CREATE TABLE Companies (
|
||||||
|
CompanyID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 500 INCREMENT BY 5 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
CompanyName NVARCHAR2(150) NOT NULL,
|
||||||
|
LegalName NVARCHAR2(200) NOT NULL,
|
||||||
|
TaxNumber VARCHAR2(50) NOT NULL,
|
||||||
|
FoundedYear NUMBER(4) DEFAULT EXTRACT(YEAR FROM SYSDATE) NOT NULL,
|
||||||
|
IsHeadquarters CHAR(1) DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Adding constraint via ALTER
|
||||||
|
ALTER TABLE Companies ADD CONSTRAINT PK_Companies PRIMARY KEY (CompanyID);
|
||||||
|
ALTER TABLE Companies ADD CONSTRAINT UQ_Company_TaxNumber UNIQUE (TaxNumber);
|
||||||
|
ALTER TABLE Companies ADD CONSTRAINT UQ_Company_Name_Tenant UNIQUE (CompanyName, TenantID);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 2. USER & ROLE MANAGEMENT
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Mix of inline and named constraints
|
||||||
|
CREATE TABLE Roles (
|
||||||
|
RoleID RAW(16) DEFAULT SYS_GUID() PRIMARY KEY,
|
||||||
|
RoleName VARCHAR2(50) NOT NULL CONSTRAINT AK_RoleName UNIQUE,
|
||||||
|
PriorityLevel NUMBER(3) DEFAULT 5 NOT NULL,
|
||||||
|
IsSystemRole CHAR(1) DEFAULT '0' NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Primary key inline, foreign keys at end
|
||||||
|
CREATE TABLE Users (
|
||||||
|
UserID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
RoleID RAW(16) NOT NULL,
|
||||||
|
Email VARCHAR2(255) NOT NULL,
|
||||||
|
Username VARCHAR2(100) NOT NULL,
|
||||||
|
PasswordHash CHAR(64) NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
LastLogin TIMESTAMP NULL,
|
||||||
|
IsLocked CHAR(1) DEFAULT '0' NOT NULL,
|
||||||
|
LoginAttempts NUMBER(3) DEFAULT 0 NOT NULL,
|
||||||
|
CONSTRAINT PK_Users PRIMARY KEY (UserID),
|
||||||
|
CONSTRAINT UQ_User_Email UNIQUE (Email),
|
||||||
|
CONSTRAINT UQ_User_Username_Tenant UNIQUE (Username, TenantID)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Foreign keys added via ALTER
|
||||||
|
ALTER TABLE Users ADD CONSTRAINT FK_Users_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
ALTER TABLE Users ADD CONSTRAINT FK_Users_Roles FOREIGN KEY (RoleID) REFERENCES Roles(RoleID);
|
||||||
|
|
||||||
|
-- Foreign key added via ALTER
|
||||||
|
CREATE TABLE UserProfiles (
|
||||||
|
ProfileID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
UserID NUMBER(10) NOT NULL,
|
||||||
|
FirstName NVARCHAR2(50) NOT NULL,
|
||||||
|
LastName NVARCHAR2(50) NOT NULL,
|
||||||
|
PhoneNumber VARCHAR2(20) NULL,
|
||||||
|
DateOfBirth DATE NULL,
|
||||||
|
ProfileImageURL VARCHAR2(500) NULL,
|
||||||
|
LastUpdated TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE UserProfiles ADD CONSTRAINT PK_UserProfiles PRIMARY KEY (ProfileID);
|
||||||
|
ALTER TABLE UserProfiles ADD CONSTRAINT UQ_UserProfiles_UserID UNIQUE (UserID);
|
||||||
|
ALTER TABLE UserProfiles ADD CONSTRAINT FK_UserProfiles_Users FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 3. PROJECT & TASK MANAGEMENT
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Primary key as named constraint at end
|
||||||
|
CREATE TABLE Projects (
|
||||||
|
ProjectID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1000 INCREMENT BY 1 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
DepartmentID NUMBER(10) NOT NULL,
|
||||||
|
CompanyID NUMBER(10) NULL,
|
||||||
|
ProjectName NVARCHAR2(200) NOT NULL,
|
||||||
|
ProjectCode VARCHAR2(30) NOT NULL,
|
||||||
|
StartDate DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
EndDate DATE NULL,
|
||||||
|
Status VARCHAR2(20) DEFAULT 'Planning' NOT NULL,
|
||||||
|
Priority VARCHAR2(20) DEFAULT 'Medium' NOT NULL,
|
||||||
|
CompletionPercent NUMBER(5,2) DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Projects ADD CONSTRAINT PK_Projects PRIMARY KEY (ProjectID);
|
||||||
|
ALTER TABLE Projects ADD CONSTRAINT UQ_ProjectCode_Tenant UNIQUE (ProjectCode, TenantID);
|
||||||
|
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Departments FOREIGN KEY (DepartmentID, TenantID) REFERENCES Departments(DepartmentID, TenantID);
|
||||||
|
ALTER TABLE Projects ADD CONSTRAINT FK_Projects_Companies FOREIGN KEY (CompanyID) REFERENCES Companies(CompanyID);
|
||||||
|
|
||||||
|
-- All constraints inline
|
||||||
|
CREATE TABLE Tasks (
|
||||||
|
TaskID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 50000 INCREMENT BY 1 PRIMARY KEY,
|
||||||
|
ProjectID NUMBER(10) NOT NULL,
|
||||||
|
ParentTaskID NUMBER(15) NULL,
|
||||||
|
TaskTitle NVARCHAR2(200) NOT NULL,
|
||||||
|
TaskDescription NCLOB NULL,
|
||||||
|
AssignedToUserID NUMBER(10) NULL,
|
||||||
|
Status VARCHAR2(20) DEFAULT 'ToDo' NOT NULL,
|
||||||
|
StoryPoints NUMBER(3) DEFAULT 1 NOT NULL,
|
||||||
|
DueDate DATE NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
CONSTRAINT FK_Tasks_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID) ON DELETE CASCADE,
|
||||||
|
CONSTRAINT FK_Tasks_ParentTask FOREIGN KEY (ParentTaskID) REFERENCES Tasks(TaskID),
|
||||||
|
CONSTRAINT FK_Tasks_AssignedUser FOREIGN KEY (AssignedToUserID) REFERENCES Users(UserID),
|
||||||
|
CONSTRAINT UQ_Task_Project_Title UNIQUE (ProjectID, TaskTitle)
|
||||||
|
);
|
||||||
|
|
||||||
|
-- ALTER for composite foreign key
|
||||||
|
CREATE TABLE TaskDependencies (
|
||||||
|
DependencyID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
TaskID NUMBER(15) NOT NULL,
|
||||||
|
DependsOnTaskID NUMBER(15) NOT NULL,
|
||||||
|
DependencyType VARCHAR2(20) DEFAULT 'FS' NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE TaskDependencies ADD CONSTRAINT PK_TaskDependencies PRIMARY KEY (DependencyID);
|
||||||
|
ALTER TABLE TaskDependencies ADD CONSTRAINT UQ_Task_Dependency UNIQUE (TaskID, DependsOnTaskID);
|
||||||
|
ALTER TABLE TaskDependencies ADD CONSTRAINT FK_TaskDependencies_Task FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||||
|
ALTER TABLE TaskDependencies ADD CONSTRAINT FK_TaskDependencies_DependsOn FOREIGN KEY (DependsOnTaskID) REFERENCES Tasks(TaskID);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 4. RESOURCE & ALLOCATION
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE ResourceTypes (
|
||||||
|
ResourceTypeID NUMBER(3) PRIMARY KEY,
|
||||||
|
TypeName VARCHAR2(50) NOT NULL UNIQUE,
|
||||||
|
IsBillable CHAR(1) DEFAULT '1' NOT NULL,
|
||||||
|
HourlyRate NUMBER(10,2) DEFAULT 0 NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE Resources (
|
||||||
|
ResourceID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
ResourceTypeID NUMBER(3) NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
ResourceName NVARCHAR2(100) NOT NULL,
|
||||||
|
ResourceCode VARCHAR2(30) NOT NULL,
|
||||||
|
IsAvailable CHAR(1) DEFAULT '1' NOT NULL,
|
||||||
|
DailyCapacity NUMBER(8,2) DEFAULT 8 NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Resources ADD CONSTRAINT PK_Resources PRIMARY KEY (ResourceID);
|
||||||
|
ALTER TABLE Resources ADD CONSTRAINT UQ_ResourceCode_Tenant UNIQUE (ResourceCode, TenantID);
|
||||||
|
ALTER TABLE Resources ADD CONSTRAINT FK_Resources_ResourceTypes FOREIGN KEY (ResourceTypeID) REFERENCES ResourceTypes(ResourceTypeID);
|
||||||
|
ALTER TABLE Resources ADD CONSTRAINT FK_Resources_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
|
||||||
|
-- Foreign key added via ALTER (different style)
|
||||||
|
CREATE TABLE ResourceAllocations (
|
||||||
|
AllocationID RAW(16) DEFAULT SYS_GUID() NOT NULL,
|
||||||
|
ResourceID NUMBER(10) NOT NULL,
|
||||||
|
TaskID NUMBER(15) NOT NULL,
|
||||||
|
AllocatedHours NUMBER(8,2) NOT NULL,
|
||||||
|
AllocationDate DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
IsConfirmed CHAR(1) DEFAULT '0' NOT NULL,
|
||||||
|
Notes NVARCHAR2(500) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE ResourceAllocations ADD CONSTRAINT PK_ResourceAllocations PRIMARY KEY (AllocationID);
|
||||||
|
ALTER TABLE ResourceAllocations ADD CONSTRAINT UQ_Resource_Task_Date UNIQUE (ResourceID, TaskID, AllocationDate);
|
||||||
|
ALTER TABLE ResourceAllocations ADD CONSTRAINT FK_ResourceAllocations_Resources FOREIGN KEY (ResourceID) REFERENCES Resources(ResourceID);
|
||||||
|
ALTER TABLE ResourceAllocations ADD CONSTRAINT FK_ResourceAllocations_Tasks FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 5. TIME TRACKING
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE TimeEntries (
|
||||||
|
TimeEntryID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
UserID NUMBER(10) NOT NULL,
|
||||||
|
TaskID NUMBER(15) NOT NULL,
|
||||||
|
EntryDate DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
HoursWorked NUMBER(5,2) NOT NULL,
|
||||||
|
Description NVARCHAR2(500) NULL,
|
||||||
|
IsBilled CHAR(1) DEFAULT '0' NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE TimeEntries ADD CONSTRAINT PK_TimeEntries PRIMARY KEY (TimeEntryID);
|
||||||
|
ALTER TABLE TimeEntries ADD CONSTRAINT FK_TimeEntries_Users FOREIGN KEY (UserID) REFERENCES Users(UserID);
|
||||||
|
ALTER TABLE TimeEntries ADD CONSTRAINT FK_TimeEntries_Tasks FOREIGN KEY (TaskID) REFERENCES Tasks(TaskID);
|
||||||
|
ALTER TABLE TimeEntries ADD CONSTRAINT UQ_TimeEntry_User_Task_Date UNIQUE (UserID, TaskID, EntryDate);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 6. INVOICING & BILLING
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE BillingRates (
|
||||||
|
RateID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
ResourceTypeID NUMBER(3) NOT NULL,
|
||||||
|
HourlyRate NUMBER(10,2) NOT NULL,
|
||||||
|
EffectiveFrom DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
EffectiveTo DATE NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE BillingRates ADD CONSTRAINT PK_BillingRates PRIMARY KEY (RateID);
|
||||||
|
ALTER TABLE BillingRates ADD CONSTRAINT UQ_Rate_Type_Tenant_Date UNIQUE (TenantID, ResourceTypeID, EffectiveFrom);
|
||||||
|
ALTER TABLE BillingRates ADD CONSTRAINT FK_BillingRates_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
ALTER TABLE BillingRates ADD CONSTRAINT FK_BillingRates_ResourceTypes FOREIGN KEY (ResourceTypeID) REFERENCES ResourceTypes(ResourceTypeID);
|
||||||
|
|
||||||
|
CREATE TABLE Invoices (
|
||||||
|
InvoiceID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 10000 INCREMENT BY 1 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
ProjectID NUMBER(10) NOT NULL,
|
||||||
|
InvoiceNumber VARCHAR2(50) NOT NULL,
|
||||||
|
IssueDate DATE DEFAULT SYSDATE NOT NULL,
|
||||||
|
DueDate DATE NOT NULL,
|
||||||
|
TotalAmount NUMBER(18,2) NOT NULL,
|
||||||
|
TaxAmount NUMBER(18,2) DEFAULT 0 NOT NULL,
|
||||||
|
Status VARCHAR2(20) DEFAULT 'Draft' NOT NULL,
|
||||||
|
PaidDate DATE NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Invoices ADD CONSTRAINT PK_Invoices PRIMARY KEY (InvoiceID);
|
||||||
|
ALTER TABLE Invoices ADD CONSTRAINT UQ_InvoiceNumber_Tenant UNIQUE (InvoiceNumber, TenantID);
|
||||||
|
ALTER TABLE Invoices ADD CONSTRAINT FK_Invoices_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
ALTER TABLE Invoices ADD CONSTRAINT FK_Invoices_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID);
|
||||||
|
|
||||||
|
-- Foreign key with ON DELETE SET NULL
|
||||||
|
CREATE TABLE InvoiceLineItems (
|
||||||
|
LineItemID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
InvoiceID NUMBER(10) NOT NULL,
|
||||||
|
TimeEntryID NUMBER(15) NULL,
|
||||||
|
Description NVARCHAR2(255) NOT NULL,
|
||||||
|
Quantity NUMBER(10,2) DEFAULT 1 NOT NULL,
|
||||||
|
UnitPrice NUMBER(18,2) NOT NULL,
|
||||||
|
LineTotal NUMBER(18,2) GENERATED ALWAYS AS (Quantity * UnitPrice) VIRTUAL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE InvoiceLineItems ADD CONSTRAINT PK_InvoiceLineItems PRIMARY KEY (LineItemID);
|
||||||
|
ALTER TABLE InvoiceLineItems ADD CONSTRAINT FK_InvoiceLineItems_Invoices FOREIGN KEY (InvoiceID) REFERENCES Invoices(InvoiceID) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE InvoiceLineItems ADD CONSTRAINT FK_InvoiceLineItems_TimeEntries FOREIGN KEY (TimeEntryID) REFERENCES TimeEntries(TimeEntryID) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 7. NOTIFICATIONS & AUDIT
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE NotificationTypes (
|
||||||
|
NotificationTypeID NUMBER(5) NOT NULL,
|
||||||
|
TypeName VARCHAR2(50) NOT NULL,
|
||||||
|
DefaultTemplate NVARCHAR2(500) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE NotificationTypes ADD CONSTRAINT PK_NotificationTypes PRIMARY KEY (NotificationTypeID);
|
||||||
|
ALTER TABLE NotificationTypes ADD CONSTRAINT UQ_NotificationTypeName UNIQUE (TypeName);
|
||||||
|
|
||||||
|
CREATE TABLE Notifications (
|
||||||
|
NotificationID RAW(16) DEFAULT SYS_GUID() NOT NULL,
|
||||||
|
UserID NUMBER(10) NOT NULL,
|
||||||
|
NotificationTypeID NUMBER(5) NOT NULL,
|
||||||
|
Title NVARCHAR2(200) NOT NULL,
|
||||||
|
Message NCLOB NOT NULL,
|
||||||
|
IsRead CHAR(1) DEFAULT '0' NOT NULL,
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
ReadAt TIMESTAMP NULL,
|
||||||
|
MetadataJSON CLOB NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Notifications ADD CONSTRAINT PK_Notifications PRIMARY KEY (NotificationID);
|
||||||
|
ALTER TABLE Notifications ADD CONSTRAINT FK_Notifications_Users FOREIGN KEY (UserID) REFERENCES Users(UserID) ON DELETE CASCADE;
|
||||||
|
ALTER TABLE Notifications ADD CONSTRAINT FK_Notifications_NotificationTypes FOREIGN KEY (NotificationTypeID) REFERENCES NotificationTypes(NotificationTypeID);
|
||||||
|
|
||||||
|
-- Audit log with all constraints via ALTER
|
||||||
|
CREATE TABLE AuditLog (
|
||||||
|
AuditID NUMBER(15) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
TenantID NUMBER(10) NOT NULL,
|
||||||
|
UserID NUMBER(10) NULL,
|
||||||
|
TableName VARCHAR2(100) NOT NULL,
|
||||||
|
RecordID VARCHAR2(100) NOT NULL,
|
||||||
|
ActionType VARCHAR2(20) NOT NULL,
|
||||||
|
OldValueXML XMLTYPE NULL,
|
||||||
|
NewValueXML XMLTYPE NULL,
|
||||||
|
ChangedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
|
||||||
|
IPAddress VARCHAR2(45) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE AuditLog ADD CONSTRAINT PK_AuditLog PRIMARY KEY (AuditID);
|
||||||
|
ALTER TABLE AuditLog ADD CONSTRAINT FK_AuditLog_Tenants FOREIGN KEY (TenantID) REFERENCES Tenants(TenantID);
|
||||||
|
ALTER TABLE AuditLog ADD CONSTRAINT FK_AuditLog_Users FOREIGN KEY (UserID) REFERENCES Users(UserID);
|
||||||
|
ALTER TABLE AuditLog MODIFY UserID DEFAULT USER;
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- 8. MILESTONES & DELIVERABLES
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
CREATE TABLE Milestones (
|
||||||
|
MilestoneID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
ProjectID NUMBER(10) NOT NULL,
|
||||||
|
MilestoneName NVARCHAR2(200) NOT NULL,
|
||||||
|
TargetDate DATE NOT NULL,
|
||||||
|
ActualCompletionDate DATE NULL,
|
||||||
|
Status VARCHAR2(20) DEFAULT 'Pending' NOT NULL,
|
||||||
|
Budget NUMBER(18,2) NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Milestones ADD CONSTRAINT PK_Milestones PRIMARY KEY (MilestoneID);
|
||||||
|
ALTER TABLE Milestones ADD CONSTRAINT FK_Milestones_Projects FOREIGN KEY (ProjectID) REFERENCES Projects(ProjectID);
|
||||||
|
ALTER TABLE Milestones ADD CONSTRAINT UQ_Milestone_Project_Name UNIQUE (ProjectID, MilestoneName);
|
||||||
|
|
||||||
|
CREATE TABLE Deliverables (
|
||||||
|
DeliverableID NUMBER(10) GENERATED BY DEFAULT AS IDENTITY START WITH 1 INCREMENT BY 1 NOT NULL,
|
||||||
|
MilestoneID NUMBER(10) NOT NULL,
|
||||||
|
DeliverableName NVARCHAR2(200) NOT NULL,
|
||||||
|
Description NVARCHAR2(500) NULL,
|
||||||
|
ExpectedOutput NVARCHAR2(500) NOT NULL,
|
||||||
|
IsCompleted CHAR(1) DEFAULT '0' NOT NULL,
|
||||||
|
CompletedAt TIMESTAMP NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
ALTER TABLE Deliverables ADD CONSTRAINT PK_Deliverables PRIMARY KEY (DeliverableID);
|
||||||
|
ALTER TABLE Deliverables ADD CONSTRAINT FK_Deliverables_Milestones FOREIGN KEY (MilestoneID) REFERENCES Milestones(MilestoneID);
|
||||||
|
ALTER TABLE Deliverables ADD CONSTRAINT UQ_Deliverable_Milestone_Name UNIQUE (MilestoneID, DeliverableName);
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- INDEXES for Performance Optimization
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
-- Single column indexes
|
||||||
|
CREATE INDEX IX_Users_TenantID ON Users(TenantID);
|
||||||
|
CREATE INDEX IX_Users_RoleID ON Users(RoleID);
|
||||||
|
CREATE INDEX IX_Users_Email ON Users(Email);
|
||||||
|
CREATE INDEX IX_Users_Username ON Users(Username);
|
||||||
|
|
||||||
|
CREATE INDEX IX_Projects_TenantID ON Projects(TenantID);
|
||||||
|
CREATE INDEX IX_Projects_DepartmentID ON Projects(DepartmentID);
|
||||||
|
CREATE INDEX IX_Projects_Status ON Projects(Status);
|
||||||
|
CREATE INDEX IX_Projects_StartDate ON Projects(StartDate);
|
||||||
|
|
||||||
|
CREATE INDEX IX_Tasks_ProjectID ON Tasks(ProjectID);
|
||||||
|
CREATE INDEX IX_Tasks_AssignedToUserID ON Tasks(AssignedToUserID);
|
||||||
|
CREATE INDEX IX_Tasks_Status ON Tasks(Status);
|
||||||
|
CREATE INDEX IX_Tasks_DueDate ON Tasks(DueDate);
|
||||||
|
|
||||||
|
CREATE INDEX IX_TimeEntries_UserID ON TimeEntries(UserID);
|
||||||
|
CREATE INDEX IX_TimeEntries_TaskID ON TimeEntries(TaskID);
|
||||||
|
CREATE INDEX IX_TimeEntries_EntryDate ON TimeEntries(EntryDate);
|
||||||
|
CREATE INDEX IX_TimeEntries_IsBilled ON TimeEntries(IsBilled);
|
||||||
|
|
||||||
|
CREATE INDEX IX_ResourceAllocations_ResourceID ON ResourceAllocations(ResourceID);
|
||||||
|
CREATE INDEX IX_ResourceAllocations_TaskID ON ResourceAllocations(TaskID);
|
||||||
|
CREATE INDEX IX_ResourceAllocations_AllocationDate ON ResourceAllocations(AllocationDate);
|
||||||
|
|
||||||
|
CREATE INDEX IX_Invoices_TenantID ON Invoices(TenantID);
|
||||||
|
CREATE INDEX IX_Invoices_ProjectID ON Invoices(ProjectID);
|
||||||
|
CREATE INDEX IX_Invoices_Status ON Invoices(Status);
|
||||||
|
CREATE INDEX IX_Invoices_DueDate ON Invoices(DueDate);
|
||||||
|
|
||||||
|
CREATE INDEX IX_InvoiceLineItems_InvoiceID ON InvoiceLineItems(InvoiceID);
|
||||||
|
CREATE INDEX IX_InvoiceLineItems_TimeEntryID ON InvoiceLineItems(TimeEntryID);
|
||||||
|
|
||||||
|
CREATE INDEX IX_Notifications_UserID ON Notifications(UserID);
|
||||||
|
CREATE INDEX IX_Notifications_IsRead ON Notifications(IsRead);
|
||||||
|
CREATE INDEX IX_Notifications_CreatedAt ON Notifications(CreatedAt);
|
||||||
|
|
||||||
|
CREATE INDEX IX_AuditLog_TenantID ON AuditLog(TenantID);
|
||||||
|
CREATE INDEX IX_AuditLog_TableName ON AuditLog(TableName);
|
||||||
|
CREATE INDEX IX_AuditLog_ChangedAt ON AuditLog(ChangedAt);
|
||||||
|
CREATE INDEX IX_AuditLog_ActionType ON AuditLog(ActionType);
|
||||||
|
|
||||||
|
-- Composite indexes for common query patterns (Oracle specific)
|
||||||
|
CREATE INDEX IX_Tasks_Project_Status ON Tasks(ProjectID, Status);
|
||||||
|
CREATE INDEX IX_TimeEntries_User_Date ON TimeEntries(UserID, EntryDate);
|
||||||
|
CREATE INDEX IX_ResourceAllocations_Resource_Date ON ResourceAllocations(ResourceID, AllocationDate);
|
||||||
|
CREATE INDEX IX_Invoices_Tenant_Status ON Invoices(TenantID, Status);
|
||||||
|
CREATE INDEX IX_Notifications_User_Read ON Notifications(UserID, IsRead, CreatedAt DESC);
|
||||||
|
|
||||||
|
-- Bitmap indexes for low-cardinality columns (Oracle feature)
|
||||||
|
CREATE BITMAP INDEX IX_Tasks_Status_BMP ON Tasks(Status);
|
||||||
|
CREATE BITMAP INDEX IX_Projects_Status_BMP ON Projects(Status);
|
||||||
|
CREATE BITMAP INDEX IX_Invoices_Status_BMP ON Invoices(Status);
|
||||||
|
CREATE BITMAP INDEX IX_Users_IsLocked_BMP ON Users(IsLocked);
|
||||||
|
|
||||||
|
-- Function-based index (Oracle specific)
|
||||||
|
CREATE INDEX IX_Users_Email_Upper ON Users(UPPER(Email));
|
||||||
|
CREATE INDEX IX_Projects_Code_Upper ON Projects(UPPER(ProjectCode));
|
||||||
|
|
||||||
|
-- =============================================
|
||||||
|
-- Comments for documentation (Oracle style)
|
||||||
|
-- =============================================
|
||||||
|
|
||||||
|
COMMENT ON TABLE Tenants IS 'Multi-tenant organizations using the platform';
|
||||||
|
COMMENT ON TABLE Projects IS 'Projects belonging to specific tenants and departments';
|
||||||
|
COMMENT ON TABLE Tasks IS 'Work items associated with projects';
|
||||||
|
COMMENT ON TABLE TimeEntries IS 'Time logged by users against tasks';
|
||||||
|
COMMENT ON TABLE Invoices IS 'Billing documents for projects and time entries';
|
||||||
|
COMMENT ON COLUMN Users.PasswordHash IS 'SHA-256 hash of user password';
|
||||||
|
COMMENT ON COLUMN Tasks.StoryPoints IS 'Agile estimation points for task complexity';
|
||||||
|
COMMENT ON COLUMN ResourceAllocations.AllocationID IS 'Globally unique identifier using SYS_GUID';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE ORACLE_ALL_DATATYPES_TEST (
|
||||||
|
-- Identity / numeric types
|
||||||
|
ID NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
NumberCol NUMBER(18,4) NOT NULL UNIQUE DEFAULT 12345.6789,
|
||||||
|
IntegerCol INTEGER NULL DEFAULT 100,
|
||||||
|
IntCol INT NOT NULL DEFAULT 200,
|
||||||
|
SmallIntCol SMALLINT NULL UNIQUE DEFAULT 50,
|
||||||
|
FloatCol FLOAT(20) NOT NULL DEFAULT 123.45,
|
||||||
|
BinaryFloatCol BINARY_FLOAT NULL DEFAULT 10.5,
|
||||||
|
BinaryDoubleCol BINARY_DOUBLE NOT NULL DEFAULT 99.999,
|
||||||
|
|
||||||
|
-- Character types
|
||||||
|
CharCol CHAR(10) NOT NULL DEFAULT 'CHARVAL',
|
||||||
|
NCharCol NCHAR(20) NULL DEFAULT N'NCHARVAL',
|
||||||
|
VarChar2Col VARCHAR2(255) NOT NULL UNIQUE DEFAULT 'varchar2 text',
|
||||||
|
NVarChar2Col NVARCHAR2(200) NULL DEFAULT N'nvarchar2 text',
|
||||||
|
ClobCol CLOB NULL,
|
||||||
|
NClobCol NCLOB NULL,
|
||||||
|
|
||||||
|
-- RAW / binary
|
||||||
|
RawCol RAW(100) NOT NULL ,
|
||||||
|
|
||||||
|
BlobCol BLOB NULL,
|
||||||
|
|
||||||
|
-- Date & time
|
||||||
|
DateCol DATE NOT NULL DEFAULT SYSDATE,
|
||||||
|
TimestampCol TIMESTAMP(6) NULL DEFAULT SYSTIMESTAMP,
|
||||||
|
TimestampTZCol TIMESTAMP(6) WITH TIME ZONE NOT NULL DEFAULT SYSTIMESTAMP,
|
||||||
|
TimestampLTZCol TIMESTAMP(6) WITH LOCAL TIME ZONE NULL DEFAULT SYSTIMESTAMP,
|
||||||
|
IntervalYearMonthCol INTERVAL YEAR(4) TO MONTH DEFAULT INTERVAL '2-6' YEAR TO MONTH,
|
||||||
|
IntervalDaySecondCol INTERVAL DAY(3) TO SECOND(6)
|
||||||
|
DEFAULT INTERVAL '5 12:30:45.123456' DAY TO SECOND,
|
||||||
|
|
||||||
|
-- XML
|
||||||
|
XmlTypeCol XMLTYPE NULL,
|
||||||
|
|
||||||
|
-- Row identifier
|
||||||
|
RowIdCol ROWID NULL,
|
||||||
|
URowIdCol UROWID NULL,
|
||||||
|
|
||||||
|
-- JSON (stored as CLOB/VARCHAR2 in many Oracle versions)
|
||||||
|
JsonCol CLOB NULL,
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- Default expressions
|
||||||
|
CreatedAt TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TABLE all_oracle_types (
|
||||||
|
id NUMBER GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Numeric types
|
||||||
|
col_number_int NUMBER(10) NOT NULL DEFAULT 1,
|
||||||
|
col_number_decimal NUMBER(10,2) NULL DEFAULT 99.99,
|
||||||
|
col_numeric NUMERIC(8,3) NOT NULL DEFAULT 123.456,
|
||||||
|
col_decimal DECIMAL(12,4) NULL DEFAULT 456.7890,
|
||||||
|
col_float FLOAT(10) NULL DEFAULT 1.23,
|
||||||
|
col_binary_float BINARY_FLOAT NOT NULL DEFAULT 3.14,
|
||||||
|
col_binary_double BINARY_DOUBLE NULL DEFAULT 6.28318,
|
||||||
|
|
||||||
|
-- Character types
|
||||||
|
col_char CHAR(10) NOT NULL DEFAULT 'CHARVAL',
|
||||||
|
col_nchar NCHAR(10) NULL DEFAULT 'NCHARVAL',
|
||||||
|
|
||||||
|
col_varchar2 VARCHAR2(255) NULL UNIQUE DEFAULT 'varchar2 value',
|
||||||
|
col_nvarchar2 NVARCHAR2(100) NOT NULL DEFAULT 'nvarchar2 value',
|
||||||
|
|
||||||
|
col_clob CLOB NULL,
|
||||||
|
col_nclob NCLOB NULL,
|
||||||
|
|
||||||
|
-- Raw / binary
|
||||||
|
col_raw RAW(16) NULL,
|
||||||
|
col_blob BLOB NULL,
|
||||||
|
|
||||||
|
-- Date & time types
|
||||||
|
col_date DATE NOT NULL DEFAULT DATE '2024-01-01',
|
||||||
|
|
||||||
|
col_timestamp TIMESTAMP(6)
|
||||||
|
NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
col_timestamp_tz TIMESTAMP(6) WITH TIME ZONE
|
||||||
|
NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
|
||||||
|
col_timestamp_ltz TIMESTAMP(6) WITH LOCAL TIME ZONE
|
||||||
|
NULL,
|
||||||
|
|
||||||
|
col_interval_ym INTERVAL YEAR(2) TO MONTH
|
||||||
|
NULL DEFAULT INTERVAL '1-2' YEAR TO MONTH,
|
||||||
|
|
||||||
|
col_interval_ds INTERVAL DAY(2) TO SECOND(6)
|
||||||
|
NULL DEFAULT INTERVAL '3 12:30:45.123456'
|
||||||
|
DAY TO SECOND,
|
||||||
|
|
||||||
|
-- XML
|
||||||
|
col_xml XMLTYPE NULL,
|
||||||
|
|
||||||
|
-- JSON (Oracle 21c+ native JSON type)
|
||||||
|
col_json JSON NULL,
|
||||||
|
|
||||||
|
-- Row identifier
|
||||||
|
col_urowid UROWID NULL,
|
||||||
|
|
||||||
|
-- Some extra inline UNIQUE columns
|
||||||
|
col_unique_number NUMBER(5) UNIQUE DEFAULT 500,
|
||||||
|
col_unique_text VARCHAR2(50) UNIQUE NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,284 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { BaseSqlImporter, ExtractedStatments, ParsedDatabaase, ParsedField } from "./base-sql-importer";
|
||||||
|
import { init, parse, format, validate, Statement } from '@guanmingchiu/sqlparser-ts';
|
||||||
|
import { TableInsertType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldInsertType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
|
interface PostgreSQLType {
|
||||||
|
name: string;
|
||||||
|
values: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export class PostgreSqlImporter extends BaseSqlImporter {
|
||||||
|
|
||||||
|
private enums: PostgreSQLType[] = [];
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.POSTGRES;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseSql(sql: string) {
|
||||||
|
try {
|
||||||
|
|
||||||
|
this.enums = [];
|
||||||
|
|
||||||
|
const cleaned: string = this.cleanSql(sql);
|
||||||
|
|
||||||
|
const { createPostgresTypesStatements, alterSequenceStatments } = this.extractStatments(cleaned);
|
||||||
|
if (createPostgresTypesStatements)
|
||||||
|
for (const statment of createPostgresTypesStatements) {
|
||||||
|
const astStatment: any = parse(statment, this.dialect as any)?.pop();
|
||||||
|
if (astStatment?.CreateType) {
|
||||||
|
this.enums.push(this.astToEnum(astStatment.CreateType));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsedDatabaase: ParsedDatabaase = super.parseSql(sql);
|
||||||
|
|
||||||
|
if (alterSequenceStatments) {
|
||||||
|
for (const statment of alterSequenceStatments) {
|
||||||
|
try {
|
||||||
|
this.processAlterSequenceStatements(statment, parsedDatabaase.tables);
|
||||||
|
} catch (error) {
|
||||||
|
parsedDatabaase.errors.push(error as Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return parsedDatabaase;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected processAlterSequenceStatements(statment: string, tables: TableInsertType[]) {
|
||||||
|
|
||||||
|
const regex =
|
||||||
|
/ALTER\s+SEQUENCE\s+[\w"]+\.[\w"]+\s+OWNED\s+BY\s+("?[\w]+"?)\.("?[\w]+"?)\.("?[\w]+"?)/i;
|
||||||
|
|
||||||
|
const match = statment.match(regex);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const schema = match[1].replace(/"/g, '');
|
||||||
|
const tableName = match[2].replace(/"/g, '');
|
||||||
|
const columnName = match[3].replace(/"/g, '');
|
||||||
|
|
||||||
|
|
||||||
|
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == tableName);
|
||||||
|
if (!table) {
|
||||||
|
throw Error(`table ${tableName} not found in : ${statment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = table.fields?.findIndex((field: FieldInsertType) => field.name == columnName) ?? -1;
|
||||||
|
if (index < 0) {
|
||||||
|
throw Error(`field ${columnName} not found in : ${statment}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
(table.fields as FieldInsertType[])[index].autoIncrement = true ;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected astToField(ast: any, sequence: number): ParsedField {
|
||||||
|
const { field, fk_constraint } = super.astToField(ast, sequence);
|
||||||
|
|
||||||
|
if (typeof ast.data_type === "object" && ast.data_type.Custom) {
|
||||||
|
const typeName = ast.data_type.Custom[0]?.[0]?.Identifier?.value.toLowerCase();
|
||||||
|
// it's a serial type
|
||||||
|
if (typeName?.toLowerCase().includes("serial")) {
|
||||||
|
|
||||||
|
field.autoIncrement = true;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const postgreSqlType: PostgreSQLType | undefined = this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName);
|
||||||
|
if (postgreSqlType && postgreSqlType.values.length > 0) {
|
||||||
|
try {
|
||||||
|
field.values = JSON.stringify(postgreSqlType.values);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("failed to parse postgresql type values")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return { field, fk_constraint };
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(ast: any, dataType: DataType): string | undefined {
|
||||||
|
|
||||||
|
let defaultValue: string | undefined = super.processDefaultValue(ast, dataType);
|
||||||
|
|
||||||
|
if (!defaultValue) {
|
||||||
|
if (ast.Function) {
|
||||||
|
if (ast.Function.name?.[0]?.Identifier?.value == "gen_random_uuid") {
|
||||||
|
return "random";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected processDataType(typeName: string): DataType | undefined {
|
||||||
|
let dataType: DataType | undefined = super.processDataType(typeName);
|
||||||
|
|
||||||
|
if (!dataType) {
|
||||||
|
if (typeName?.toLowerCase().includes("serial")) {
|
||||||
|
// if it's a serial type , then get the base integer type (integer , smallint , bigint)
|
||||||
|
let baseType: string = typeName.toLowerCase() == "serial" ? "integer" : typeName.toLowerCase().replace("serial", "int");
|
||||||
|
return super.processDataType(baseType);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const isEnum: boolean = Boolean(this.enums.find((postgreSqlType: PostgreSQLType) => postgreSqlType.name == typeName));
|
||||||
|
if (isEnum) {
|
||||||
|
return this.data_types.find((dataType: DataType) => dataType.name == "enum")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
return dataType
|
||||||
|
}
|
||||||
|
|
||||||
|
private astToEnum(ast: any): PostgreSQLType {
|
||||||
|
|
||||||
|
const values: string[] | undefined = ast.representation?.Enum?.labels?.map((label: any) => label.value);
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: ast.name?.[0]?.Identifier?.value,
|
||||||
|
values
|
||||||
|
} as PostgreSQLType;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected extractStatments(sql: string): ExtractedStatments {
|
||||||
|
|
||||||
|
const createPostgresTypesStatements: string[] = [];
|
||||||
|
const alterSequenceStatments: string[] = [];
|
||||||
|
const { createIndexStatements, createTableStatements, alterTableStatements } = super.extractStatments(sql);
|
||||||
|
|
||||||
|
// Split into individual statements
|
||||||
|
const statements = sql
|
||||||
|
.split('\n')
|
||||||
|
.map(s => s.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
|
||||||
|
for (const stmt of statements) {
|
||||||
|
const upper = stmt.toUpperCase();
|
||||||
|
if (upper.startsWith('CREATE TYPE')) {
|
||||||
|
createPostgresTypesStatements.push(stmt);
|
||||||
|
}
|
||||||
|
if (upper.startsWith("ALTER SEQUENCE")) {
|
||||||
|
alterSequenceStatments.push(stmt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
createIndexStatements,
|
||||||
|
createTableStatements,
|
||||||
|
alterTableStatements,
|
||||||
|
createPostgresTypesStatements,
|
||||||
|
alterSequenceStatments
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
|
||||||
|
CREATE TYPE "users_user_type_enum" AS ENUM (
|
||||||
|
'individual',
|
||||||
|
'agency',
|
||||||
|
'developer',
|
||||||
|
'admin',
|
||||||
|
'employee'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TYPE "users_provider_enum" AS ENUM (
|
||||||
|
'google.com',
|
||||||
|
'facebook.com',
|
||||||
|
'password',
|
||||||
|
'anonymous'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
full_name VARCHAR NOT NULL,
|
||||||
|
email VARCHAR NOT NULL,
|
||||||
|
picture_url VARCHAR NULL,
|
||||||
|
phone_number VARCHAR NULL,
|
||||||
|
password_hash VARCHAR NOT NULL,
|
||||||
|
user_type users_user_type_enum NOT NULL DEFAULT 'individual',
|
||||||
|
provider users_provider_enum NOT NULL DEFAULT 'password',
|
||||||
|
birthday TIMESTAMP(3) NULL,
|
||||||
|
gender BOOLEAN NULL,
|
||||||
|
created_at TIMESTAMPTZ (6) NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at TIMESTAMPTZ (6) NULL,
|
||||||
|
is_email_verified BOOLEAN NOT NULL DEFAULT FALSE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE all_postgres_types (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
|
||||||
|
-- Numeric types
|
||||||
|
col_smallint SMALLINT NOT NULL DEFAULT 1,
|
||||||
|
col_integer INTEGER NOT NULL UNIQUE DEFAULT 1000,
|
||||||
|
col_bigint BIGINT NULL DEFAULT 10000,
|
||||||
|
|
||||||
|
col_decimal DECIMAL(10,2) NOT NULL DEFAULT 99.99,
|
||||||
|
col_numeric NUMERIC(8,3) NULL DEFAULT 123.456,
|
||||||
|
|
||||||
|
col_real REAL NULL DEFAULT 1.23,
|
||||||
|
col_double DOUBLE PRECISION NOT NULL DEFAULT 2.3456,
|
||||||
|
|
||||||
|
col_boolean BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
|
|
||||||
|
-- Date & time types
|
||||||
|
col_date DATE NOT NULL DEFAULT DATE '2024-01-01',
|
||||||
|
col_timestamp TIMESTAMP(6) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
col_timestamptz TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
col_time TIME(6) NULL DEFAULT TIME '12:34:56.123456',
|
||||||
|
|
||||||
|
-- String types
|
||||||
|
col_char CHAR(10) NOT NULL DEFAULT 'charval',
|
||||||
|
col_varchar VARCHAR(255) NULL UNIQUE DEFAULT 'varchar value',
|
||||||
|
col_text TEXT NULL DEFAULT 'some text',
|
||||||
|
|
||||||
|
-- Binary
|
||||||
|
col_bytea BYTEA NULL,
|
||||||
|
|
||||||
|
-- Enum (PostgreSQL requires type creation first)
|
||||||
|
col_enum TEXT NOT NULL DEFAULT 'A',
|
||||||
|
|
||||||
|
-- JSON
|
||||||
|
col_json JSON NULL,
|
||||||
|
col_jsonb JSONB NULL,
|
||||||
|
|
||||||
|
-- UUID
|
||||||
|
col_uuid UUID NOT NULL UNIQUE DEFAULT gen_random_uuid(),
|
||||||
|
|
||||||
|
-- Array
|
||||||
|
col_text_array TEXT[] NULL DEFAULT ARRAY['a','b','c'],
|
||||||
|
|
||||||
|
-- Range types
|
||||||
|
col_int_range INT4RANGE NULL,
|
||||||
|
col_ts_range TSRANGE NULL,
|
||||||
|
|
||||||
|
-- XML
|
||||||
|
col_xml XML NULL,
|
||||||
|
|
||||||
|
-- Some extra UNIQUE inline
|
||||||
|
col_unique_text TEXT UNIQUE
|
||||||
|
);
|
||||||
|
|
||||||
|
*/
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { BaseSqlImporter } from "./base-sql-importer";
|
||||||
|
|
||||||
|
export class SqliteImporter extends BaseSqlImporter {
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(data_types);
|
||||||
|
this.dialect = DatabaseDialect.SQLITE;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
CREATE TABLE "users" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
username TEXT NOT NULL UNIQUE,
|
||||||
|
email TEXT NOT NULL UNIQUE,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "projects" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
description TEXT NULL DEFAULT 'Hello world'
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "tasks" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
project_id INTEGER NOT NULL,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
description TEXT NULL,
|
||||||
|
due_date DATETIME NULL,
|
||||||
|
priority TEXT NULL,
|
||||||
|
status TEXT NULL,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
updated_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY ("project_id") REFERENCES "projects" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "comments" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
comment_text TEXT NOT NULL,
|
||||||
|
created_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE "task_assignments" (
|
||||||
|
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||||
|
task_id INTEGER NOT NULL,
|
||||||
|
user_id INTEGER NOT NULL,
|
||||||
|
assigned_at DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
|
FOREIGN KEY ("task_id") REFERENCES "tasks" ("id") ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY ("user_id") REFERENCES "users" ("id") ON DELETE CASCADE
|
||||||
|
)
|
||||||
|
*/
|
||||||
@@ -0,0 +1,153 @@
|
|||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { decomposeManyToMany, getDefaultRelationshipName } from "./relationship";
|
||||||
|
import _ from "lodash";
|
||||||
|
import { DBDiffOperation } from "./database";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
|
||||||
|
|
||||||
|
export const prepareForMigration = (db: DatabaseType) => {
|
||||||
|
|
||||||
|
let database = _.cloneDeep(db);
|
||||||
|
|
||||||
|
database.tables = database.tables.map((table: any) => {
|
||||||
|
delete table.sequence;
|
||||||
|
table.fields = table.fields.map((field: any) => {
|
||||||
|
delete field.sequence;
|
||||||
|
delete field.note;
|
||||||
|
return field;
|
||||||
|
})
|
||||||
|
return table;
|
||||||
|
});
|
||||||
|
|
||||||
|
database.relationships = database.relationships.map((relationship: RelationshipType) => {
|
||||||
|
|
||||||
|
if (relationship.name)
|
||||||
|
return relationship;
|
||||||
|
|
||||||
|
relationship.sourceTable = database.tables.find((table: TableType) => table.id == relationship.sourceTableId) as TableType;
|
||||||
|
relationship.targetTable = database.tables.find((table: TableType) => table.id == relationship.targetTableId) as TableType;
|
||||||
|
|
||||||
|
relationship.sourceField = relationship.sourceTable.fields.find((field: FieldType) => field.id == relationship.sourceFieldId) as FieldType;
|
||||||
|
relationship.targetField = relationship.targetTable.fields.find((field: FieldType) => field.id == relationship.targetFieldId) as FieldType;
|
||||||
|
|
||||||
|
relationship.name = getDefaultRelationshipName(relationship)
|
||||||
|
return relationship;
|
||||||
|
});
|
||||||
|
|
||||||
|
database = decomposeManyToMany(database);
|
||||||
|
|
||||||
|
return database;
|
||||||
|
}
|
||||||
|
|
||||||
|
// enrich a database operations is :
|
||||||
|
// - adding the type to the fields .
|
||||||
|
// - adding sourceField , targetField , sourceTable , targetTable to the relationships
|
||||||
|
export const optimizeOps = (operations: DBDiffOperation[], currentDatabase: DatabaseType, previousDatabase: DatabaseType, data_types: DataType[]): DBDiffOperation[] => {
|
||||||
|
let ops: DBDiffOperation[] = [];
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const schemaOperations = operations.filter((operation: DBDiffOperation) => (operation.type != "UPDATE_FIELD_INDICES") && !(
|
||||||
|
operation.type == "UPDATE_FIELD" && operation.changes.isPrimary !== undefined && Object.keys(operation.changes).length == 1
|
||||||
|
));
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
for (let index = 0; index < schemaOperations.length; index++) {
|
||||||
|
|
||||||
|
const operation: DBDiffOperation = _.cloneDeep(schemaOperations[index]);
|
||||||
|
|
||||||
|
if (operation.type == "UPDATE_NUM_TABLES")
|
||||||
|
continue;
|
||||||
|
if (operation.type == "RENAME_DATABASE")
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if (operation.type == "CREATE_TABLE") {
|
||||||
|
operation.table.fields = Object.values(operation.table.fields).map((field: FieldType) => ({
|
||||||
|
...field,
|
||||||
|
type: data_types.find((type: DataType) => type.id == field.typeId)
|
||||||
|
})) as FieldType[];
|
||||||
|
|
||||||
|
operation.table.indices = Object.values(operation.table.indices).map((indice: IndexType) => ({
|
||||||
|
...indice,
|
||||||
|
fieldIndices: Object.values(indice.fieldIndices),
|
||||||
|
|
||||||
|
}))
|
||||||
|
operation.table.indices = operation.table.indices.map((index: IndexType) => ({
|
||||||
|
...index,
|
||||||
|
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) => operation.table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)) as FieldType[]
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (operation.type == "CREATE_RELATIONSHIP") {
|
||||||
|
|
||||||
|
// try to get the sourceTable from the previous database
|
||||||
|
let sourceTable: TableType | undefined = previousDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId);
|
||||||
|
if (!sourceTable) {
|
||||||
|
// if the source table is not found that mean it's a new table created in this migration
|
||||||
|
// so we need to get it from the currentDatabase
|
||||||
|
sourceTable = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId) as TableType;
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceField: FieldType | undefined = sourceTable.fields.find((field: FieldType) => field.id == operation.relationship.sourceFieldId);
|
||||||
|
if (!sourceField) {
|
||||||
|
// if the source field is not found , then it could be a new field created in this new migration
|
||||||
|
// so what we need to do it is we need to get it from the current Database ,
|
||||||
|
const currentSourceTable: TableType = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.sourceTableId) as TableType;
|
||||||
|
sourceField = currentSourceTable.fields.find((field: FieldType) => field.id == operation.relationship.sourceFieldId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// now we do the same thing for the target table and field .
|
||||||
|
let targetTable: TableType | undefined = previousDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||||
|
// if not found , the target table culd be a new table created in this migration
|
||||||
|
if (!targetTable) {
|
||||||
|
targetTable = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||||
|
}
|
||||||
|
let targetField: FieldType | undefined = targetTable.fields.find((field: FieldType) => field.id == operation.relationship.targetFieldId);
|
||||||
|
if (!targetField) {
|
||||||
|
// it can be a new field , so we need to get it from the current migration .
|
||||||
|
const currentTargetTable: TableType = currentDatabase.tables.find((table: TableType) => table.id == operation.relationship.targetTableId) as TableType;
|
||||||
|
targetField = currentTargetTable.fields.find((field: FieldType) => field.id == operation.relationship.targetFieldId) as FieldType
|
||||||
|
}
|
||||||
|
operation.relationship.sourceTable = sourceTable;
|
||||||
|
operation.relationship.targetTable = targetTable;
|
||||||
|
|
||||||
|
operation.relationship.sourceField = sourceField as FieldType;
|
||||||
|
operation.relationship.targetField = targetField as FieldType;
|
||||||
|
}
|
||||||
|
|
||||||
|
ops.push(operation)
|
||||||
|
}
|
||||||
|
|
||||||
|
ops = orderOperations(ops)
|
||||||
|
|
||||||
|
return ops;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
const orderOperations = (ops: DBDiffOperation[]): DBDiffOperation[] => {
|
||||||
|
const OPERATION_ORDER = [
|
||||||
|
// Create
|
||||||
|
'CREATE_TABLE',
|
||||||
|
'CREATE_RELATIONSHIP',
|
||||||
|
];
|
||||||
|
return [...ops].sort(
|
||||||
|
(a, b) =>
|
||||||
|
OPERATION_ORDER.indexOf(a.type) - OPERATION_ORDER.indexOf(b.type)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,389 @@
|
|||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { DataTypes, ForeignKeyActions, Modifiers, MYSQL_MAX_VAR_LENGTH, TimeDefaultValues } from "@/lib/field";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { DBDiffOperation, mapDiffToDBDiffOperation, normalizeDatabase } from "@/utils/database";
|
||||||
|
import { optimizeOps, prepareForMigration } from "@/utils/migration";
|
||||||
|
import { compare } from "fast-json-patch";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import { Statement } from "pgsql-ast-parser";
|
||||||
|
import { getPostgresEnumName } from "../render-uttils";
|
||||||
|
import { emptyDb } from "@/lib/database";
|
||||||
|
|
||||||
|
export default abstract class BaseDatabaseRenderer {
|
||||||
|
|
||||||
|
protected dialect: DatabaseDialect;
|
||||||
|
protected data_types: DataType[];
|
||||||
|
protected schema?: DatabaseType | undefined;
|
||||||
|
protected parser: any;
|
||||||
|
protected readyPromise: Promise<void>;
|
||||||
|
|
||||||
|
public constructor(dialect: DatabaseDialect, data_types: DataType[]) {
|
||||||
|
this.dialect = dialect;
|
||||||
|
this.data_types = data_types;
|
||||||
|
|
||||||
|
this.readyPromise = import("node-sql-parser").then((sqlParser) => {
|
||||||
|
this.parser = new sqlParser.Parser();
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public getDialect(): DatabaseDialect {
|
||||||
|
return this.dialect;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async renderDDL(database: DatabaseType): Promise<string> {
|
||||||
|
|
||||||
|
const previousDatabase = emptyDb(database);
|
||||||
|
|
||||||
|
// we prepare both databases by enriching them and removing some UI elements that can interfear with json fast path
|
||||||
|
const preparedCurrentDatabase = prepareForMigration(database);
|
||||||
|
const preparedPreviousDatabse = prepareForMigration(previousDatabase);
|
||||||
|
|
||||||
|
// notmalize both just ignore array elements order diff
|
||||||
|
const normalizedPreviousDatabase = normalizeDatabase(preparedPreviousDatabse);
|
||||||
|
const normalizedCurrentDatabase = normalizeDatabase(preparedCurrentDatabase);
|
||||||
|
|
||||||
|
// perform a comparison to extract the diff between the previous database and the current one .
|
||||||
|
const diff_json = compare(normalizedPreviousDatabase, normalizedCurrentDatabase);
|
||||||
|
// map the diff to DB Diff operations to facilate the rendering
|
||||||
|
// optimize the operations by enriching each operation
|
||||||
|
let operations: DBDiffOperation[] = mapDiffToDBDiffOperation(diff_json)
|
||||||
|
operations = optimizeOps(operations, preparedCurrentDatabase, preparedPreviousDatabse, this.data_types);
|
||||||
|
|
||||||
|
// extract the ast based on the dialect
|
||||||
|
const ast: ASTStatment[] = this.operationsToAst(operations, database);
|
||||||
|
// render ast from sql
|
||||||
|
const sql = await this.astToSQL(ast);
|
||||||
|
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||||
|
const ast: ASTStatment[] = [];
|
||||||
|
// we basiclly loop over all operations , and turn them into ast .
|
||||||
|
// some operation can return multiple statments .
|
||||||
|
// so for that we need to check our input if its one AST or an object AST
|
||||||
|
for (const operation of operations) {
|
||||||
|
const statmentAst = this.operationToAst(operation);
|
||||||
|
if (!statmentAst)
|
||||||
|
continue;
|
||||||
|
else if (Array.isArray(statmentAst))
|
||||||
|
ast.push(...statmentAst);
|
||||||
|
else
|
||||||
|
ast.push(statmentAst);
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected operationToAst(operation: DBDiffOperation): ASTStatment | ASTStatment[] | null {
|
||||||
|
switch (operation.type) {
|
||||||
|
case "CREATE_TABLE":
|
||||||
|
return this.createTableAst(
|
||||||
|
operation.table
|
||||||
|
);
|
||||||
|
|
||||||
|
case "CREATE_RELATIONSHIP":
|
||||||
|
if (operation.relationship.cardinality == Cardinality.many_to_many)
|
||||||
|
break;
|
||||||
|
return this.createRelationshipAst(
|
||||||
|
operation.relationship,
|
||||||
|
);
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract astToSQL(ast: ASTStatment[]): Promise<string>;
|
||||||
|
|
||||||
|
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||||
|
let indices_ast: ASTStatment[] = [];
|
||||||
|
// get primary keys and check if we hame more than one
|
||||||
|
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||||
|
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
||||||
|
// check if we have multiple primary keys if so we need to create a contraint
|
||||||
|
const constraints: ASTStatment[] = multiPrimaryKeys ? [this.getPrimaryKeyContraint(primaryKeys)] : [];
|
||||||
|
// get field dification
|
||||||
|
const field_definitions = table.fields.map((field: FieldType) => this.getFieldDefinition(field, table, multiPrimaryKeys))
|
||||||
|
// the base table ast contain only the field dification and the contraints , then child renderer turn it to ast
|
||||||
|
// check if the table have indices
|
||||||
|
if (table.indices && table.indices.length > 0) {
|
||||||
|
// if so loop over it indices and ignore the index with no columns since that will cause an SQl Syntax error .
|
||||||
|
// finally get the ast of the index and push it to the indices_ast
|
||||||
|
for (const index of table.indices) {
|
||||||
|
if (index.fieldIndices.length == 0)
|
||||||
|
continue;
|
||||||
|
indices_ast.push(this.createIndexAst(
|
||||||
|
table,
|
||||||
|
index
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
field_definitions,
|
||||||
|
constraints,
|
||||||
|
indices_ast
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected updateFieldAst(table: TableType, field: FieldType, changes: FieldType): ASTStatment | ASTStatment[] {
|
||||||
|
// get the previous type
|
||||||
|
let type = field.type;
|
||||||
|
|
||||||
|
const ast: any = {
|
||||||
|
length: null,
|
||||||
|
scale: null,
|
||||||
|
dataType: null,
|
||||||
|
auto_increment: null,
|
||||||
|
}
|
||||||
|
// if there is changes in the type , max length , scale or precision or auto incrrement .
|
||||||
|
// in this case SQL Database treat this as a type changes
|
||||||
|
if (changes.typeId || changes.maxLength || changes.scale || changes.precision || changes.autoIncrement !== undefined || changes.values) {
|
||||||
|
// if the type changes , get the new type
|
||||||
|
if (changes.typeId)
|
||||||
|
type = changes.type;
|
||||||
|
|
||||||
|
// get the new or the old type name
|
||||||
|
ast.dataType = (this.dialect == DatabaseDialect.POSTGRES && type.name == "enum") ? getPostgresEnumName(table, field) : type.name?.toLocaleUpperCase();
|
||||||
|
// get the new or old type modifiers
|
||||||
|
const modifiers: string[] = type.modifiers ? JSON.parse(type.modifiers) : [];
|
||||||
|
// if the type support length as VARCHAR(length) and we either have an old max length , or new max length , then set it in the ast
|
||||||
|
if (modifiers.includes(Modifiers.LENGTH) && (changes.maxLength || field.maxLength)) {
|
||||||
|
if (changes.maxLength)
|
||||||
|
ast.length = changes.maxLength
|
||||||
|
else
|
||||||
|
ast.length = field.maxLength;
|
||||||
|
}
|
||||||
|
// if the field support precision and we either have a previous precision or the user add new one .
|
||||||
|
// then set the precision in the AST .
|
||||||
|
if (modifiers.includes(Modifiers.PRECISION) && (changes.precision || field.precision)) {
|
||||||
|
if (changes.precision) {
|
||||||
|
ast.length = changes.precision;
|
||||||
|
} else {
|
||||||
|
ast.length = field.precision;
|
||||||
|
}
|
||||||
|
if (modifiers.includes(Modifiers.SCALE) && !(changes.scale || field.scale)) {
|
||||||
|
// if the scale is not set , we have to set it as O
|
||||||
|
ast.scale = "0";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if the type support scale , and scale is set then add it to the ast
|
||||||
|
if (modifiers.includes(Modifiers.SCALE) && (changes.scale || field.scale)) {
|
||||||
|
if (changes.scale) {
|
||||||
|
ast.scale = changes.scale;
|
||||||
|
} else {
|
||||||
|
ast.scale = field.scale;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// okay if the field is auto increment in postgres we need to switch the type ro serial type .
|
||||||
|
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && (field.autoIncrement || changes.autoIncrement) && this.dialect == DatabaseDialect.POSTGRES) {
|
||||||
|
if (changes.autoIncrement !== false) {
|
||||||
|
if (ast.dataType == "INTEGER")
|
||||||
|
ast.dataType = "SERIAL";
|
||||||
|
else
|
||||||
|
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if there changes in the values
|
||||||
|
if (modifiers.includes(Modifiers.VALUES) && changes.values && this.dialect != DatabaseDialect.POSTGRES) {
|
||||||
|
try {
|
||||||
|
ast.values = JSON.parse(changes.values);
|
||||||
|
} catch (error) {
|
||||||
|
ast.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast as AST;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||||
|
|
||||||
|
let primaryKey: FieldType = relationship.sourceField;
|
||||||
|
let foreignKey: FieldType = relationship.targetField;
|
||||||
|
|
||||||
|
let sourceTable: TableType = relationship.sourceTable;
|
||||||
|
let targetTable: TableType = relationship.targetTable;
|
||||||
|
|
||||||
|
if (relationship.cardinality == Cardinality.many_to_one) {
|
||||||
|
primaryKey = relationship.targetField;
|
||||||
|
foreignKey = relationship.sourceField;
|
||||||
|
sourceTable = relationship.targetTable;
|
||||||
|
targetTable = relationship.sourceTable;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
primaryKey,
|
||||||
|
foreignKey,
|
||||||
|
sourceTable,
|
||||||
|
targetTable
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract createIndexAst(table: TableType, index: IndexType): ASTStatment;
|
||||||
|
|
||||||
|
protected abstract getPrimaryKeyContraint(field: FieldType[], table?: TableType): ASTStatment;
|
||||||
|
|
||||||
|
protected abstract startTransaction(): string;
|
||||||
|
|
||||||
|
protected abstract commit(): string;
|
||||||
|
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): ASTStatment {
|
||||||
|
// get the modifiers based on the field data type
|
||||||
|
const modifiers: string[] = field?.type?.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||||
|
|
||||||
|
const ast: any = {
|
||||||
|
length: null,
|
||||||
|
scale: null,
|
||||||
|
default_value: null,
|
||||||
|
dataType: null,
|
||||||
|
values: null,
|
||||||
|
default_value_type: null,
|
||||||
|
auto_increment: null,
|
||||||
|
primary_key: ignorePkContraint ? false : field.isPrimary,
|
||||||
|
modifiers: null
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// data types by the defaut the field data type name , except for Postgres types needs to be handled diffrenet
|
||||||
|
ast.dataType = field.type?.name?.toLocaleUpperCase();
|
||||||
|
|
||||||
|
// if the field support PRECISION & SCALE modifiers set the length to precision
|
||||||
|
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
||||||
|
ast.length = field.precision;
|
||||||
|
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
||||||
|
ast.scale = "0";
|
||||||
|
}
|
||||||
|
// if the field support sclae and sclae is set then set the scale
|
||||||
|
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
||||||
|
ast.scale = field.scale;
|
||||||
|
|
||||||
|
// the field support dynamic length , set the length ast
|
||||||
|
// but in case the max length is not set, and the dielect (Mysql , MariaDb ) require a length , then set the default length
|
||||||
|
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
||||||
|
ast.length = field.maxLength;
|
||||||
|
|
||||||
|
else if (
|
||||||
|
modifiers.includes(Modifiers.LENGTH) &&
|
||||||
|
(field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) &&
|
||||||
|
field.type.name?.startsWith('var') &&
|
||||||
|
!field.maxLength
|
||||||
|
)
|
||||||
|
ast.length = MYSQL_MAX_VAR_LENGTH;
|
||||||
|
|
||||||
|
// if the field is an enum type just parse the enum values
|
||||||
|
if (modifiers.includes(Modifiers.VALUES) && field.values) {
|
||||||
|
try {
|
||||||
|
ast.values = JSON.parse(field.values);
|
||||||
|
} catch (error) {
|
||||||
|
ast.value = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const default_value_defintition = this.processDefaultValue(field) as any
|
||||||
|
|
||||||
|
if (default_value_defintition) {
|
||||||
|
const { default_value_type, default_value } = default_value_defintition;
|
||||||
|
ast.default_value_type = default_value_type;
|
||||||
|
ast.default_value = default_value;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the field support auto increment and if the field it is , then set aut increment ast to true .
|
||||||
|
// but if the dialect is Postgres , then we have to change the type from INTEGERS to SERIALS
|
||||||
|
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) {
|
||||||
|
ast.auto_increment = true
|
||||||
|
} else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) {
|
||||||
|
ast.auto_increment = true;
|
||||||
|
if (ast.dataType == "INTEGER")
|
||||||
|
ast.dataType = "SERIAL";
|
||||||
|
else
|
||||||
|
ast.dataType = ast.dataType.replace("INT", "SERIAL");
|
||||||
|
} else {
|
||||||
|
ast.auto_increment = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.modifiers = modifiers;
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(field: FieldType): AST | null {
|
||||||
|
// if the field have a default value then we need to proccess it based on it type
|
||||||
|
if (field.defaultValue && field.defaultValue.trim().length > 0) {
|
||||||
|
|
||||||
|
const ast: any = {
|
||||||
|
default_value_type: null,
|
||||||
|
default_value: null
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((field.type.type == DataTypes.INTEGER || field.type.type == DataTypes.NUMERIC) && !isNaN(Number(field.defaultValue))) {
|
||||||
|
ast.default_value_type = "number"
|
||||||
|
if (field.type.type == DataTypes.NUMERIC)
|
||||||
|
ast.default_value = parseFloat(field.defaultValue);
|
||||||
|
else
|
||||||
|
ast.default_value = parseInt(field.defaultValue);
|
||||||
|
}
|
||||||
|
else if (field.type.type == DataTypes.TEXT || field.type.type == DataTypes.ENUM) {
|
||||||
|
if (field.type.type == DataTypes.ENUM) {
|
||||||
|
const values = field.values ? JSON.parse(field.values) : [];
|
||||||
|
|
||||||
|
if (field.type.name == "set") {
|
||||||
|
ast.default_value_type = "single_quote_string";
|
||||||
|
ast.default_value = field.defaultValue;
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (values.includes(field.defaultValue)) {
|
||||||
|
ast.default_value_type = "single_quote_string";
|
||||||
|
ast.default_value = field.defaultValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
ast.default_value_type = "single_quote_string";
|
||||||
|
ast.default_value = field.defaultValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
// if we have a field of type time and the default value is Now , then the default value is the fucntion CURRENT_TIME or NOW()
|
||||||
|
else if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW) {
|
||||||
|
ast.default_value_type = "function";
|
||||||
|
ast.default_value = "CURRENT_TIMESTAMP";
|
||||||
|
}
|
||||||
|
|
||||||
|
else if ((field.type.name == "uuid" || field.type.name == "uniqueidentifier") && field.defaultValue == "random") {
|
||||||
|
ast.default_value_type = "function";
|
||||||
|
ast.default_value = "random";
|
||||||
|
|
||||||
|
}
|
||||||
|
else if (field.type.type == DataTypes.TIME) {
|
||||||
|
ast.default_value_type = "single_quote_string";
|
||||||
|
ast.default_value = field.defaultValue;
|
||||||
|
}
|
||||||
|
// the field have a boolean value
|
||||||
|
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
||||||
|
ast.default_value_type = "bool"
|
||||||
|
ast.default_value = field.defaultValue == "true" ? true : false;
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DBRenderOutput {
|
||||||
|
schema: DatabaseType,
|
||||||
|
diff_json: any[],
|
||||||
|
sql: string;
|
||||||
|
operations: DBDiffOperation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export type ASTStatment = AST | Statement | string | null
|
||||||
@@ -0,0 +1,306 @@
|
|||||||
|
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||||
|
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import { format } from "sql-formatter";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
||||||
|
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
export abstract class BaseSQLRenderer extends BaseDatabaseRenderer {
|
||||||
|
|
||||||
|
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||||
|
// in the base SQL Render we use node-sql-parser .
|
||||||
|
// by simly passing the AST and formating the code .
|
||||||
|
// other renderers can override this method to handle the parsing based on the dielact
|
||||||
|
await this.readyPromise ;
|
||||||
|
|
||||||
|
let sql: string = this.parser.sqlify(ast as AST[], {
|
||||||
|
database: getDatabaseByDialect(this.dialect).name
|
||||||
|
});
|
||||||
|
return format(sql, { language: 'sql' });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||||
|
// get the tale defintion from parent render that contain only the field defintion and primary keys contraints
|
||||||
|
// this definition is commun amount (Mysql , MariaDB , Sqlite) , for postgres we need to override it and handle enums
|
||||||
|
const { field_definitions, constraints, indices_ast } = super.createTableAst(table) as any
|
||||||
|
// wrape those defintion with create table AST
|
||||||
|
return [{
|
||||||
|
keyword: "table",
|
||||||
|
type: "create",
|
||||||
|
table: [{
|
||||||
|
table: table.name
|
||||||
|
}],
|
||||||
|
create_definitions: [...field_definitions, ...constraints]
|
||||||
|
}, ...indices_ast] as any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||||
|
// we get the base field definition from the parent renderer
|
||||||
|
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||||
|
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||||
|
// also we need default value expression that it is compatible with node-sql-parser
|
||||||
|
let valuesExpr: any | null;
|
||||||
|
// if the definition extracted values os this field is an enum .
|
||||||
|
// and for that we generate the values expression that's compatible with out parser
|
||||||
|
if (definition.values && definition.values.length > 0) {
|
||||||
|
const values = definition.values.map((value: string) => ({
|
||||||
|
type: "single_quote_string",
|
||||||
|
value
|
||||||
|
}));
|
||||||
|
|
||||||
|
valuesExpr = {
|
||||||
|
parentheses: true,
|
||||||
|
type: "expr_list",
|
||||||
|
value: values
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const default_val = !definition.modifiers.includes(Modifiers.NO_DEFAULT) ? this.processDefaultValue(field) : undefined;
|
||||||
|
const unique: string | null = !definition.modifiers.includes(Modifiers.NO_UNIQUE) ? (field.unique ? "unique" : null) : null;
|
||||||
|
// return an AST statment for a field definition
|
||||||
|
return {
|
||||||
|
column: {
|
||||||
|
type: "column_ref",
|
||||||
|
column: {
|
||||||
|
expr: {
|
||||||
|
type: "default", value: field.name,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default_val,
|
||||||
|
unique,
|
||||||
|
auto_increment: definition.auto_increment ? "auto_increment" : undefined,
|
||||||
|
nullable: {
|
||||||
|
type: field.nullable ? "null" : "not null",
|
||||||
|
value: field.nullable ? "null" : "not null",
|
||||||
|
},
|
||||||
|
definition: {
|
||||||
|
dataType: definition.dataType,
|
||||||
|
length: definition.length,
|
||||||
|
scale: definition.scale,
|
||||||
|
expr: valuesExpr
|
||||||
|
},
|
||||||
|
primary_key: definition.primary_key ? "primary key" : null,
|
||||||
|
resource: "column"
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||||
|
// get the foriegn key , primary key source table and target table from the parent renderer
|
||||||
|
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||||
|
let on_action: any[] = [];
|
||||||
|
// get the foriegn key actions for on delete and on cascade
|
||||||
|
if (relationship.onDelete) {
|
||||||
|
const value: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
on_action.push({
|
||||||
|
type: "on delete",
|
||||||
|
value: {
|
||||||
|
type: "origin",
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if (relationship.onUpdate) {
|
||||||
|
const value: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||||
|
if (value)
|
||||||
|
on_action.push({
|
||||||
|
type: "on update",
|
||||||
|
value: {
|
||||||
|
type: "origin",
|
||||||
|
value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
// return create foriegn key contraint for Mysql Dialect
|
||||||
|
return {
|
||||||
|
type: "alter",
|
||||||
|
keyword: "table",
|
||||||
|
table: [
|
||||||
|
{
|
||||||
|
|
||||||
|
table: targetTable.name
|
||||||
|
}
|
||||||
|
],
|
||||||
|
expr: [{
|
||||||
|
action: "add",
|
||||||
|
create_definitions: {
|
||||||
|
constraint: relationship.name,
|
||||||
|
definition: [
|
||||||
|
{
|
||||||
|
type: "column_ref",
|
||||||
|
table: null,
|
||||||
|
column: {
|
||||||
|
expr: {
|
||||||
|
type: "default",
|
||||||
|
value: foreignKey.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
constraint_type: "FOREIGN KEY",
|
||||||
|
keyword: "constraint",
|
||||||
|
resource: "constraint",
|
||||||
|
reference_definition: {
|
||||||
|
definition: [
|
||||||
|
{
|
||||||
|
type: "column_ref",
|
||||||
|
table: null,
|
||||||
|
column: {
|
||||||
|
expr: {
|
||||||
|
type: "default",
|
||||||
|
value: primaryKey.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
],
|
||||||
|
table: [
|
||||||
|
{
|
||||||
|
db: null,
|
||||||
|
table: sourceTable.name
|
||||||
|
}
|
||||||
|
],
|
||||||
|
keyword: "references",
|
||||||
|
on_action
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resource: "constraint",
|
||||||
|
type: "alter"
|
||||||
|
}]
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||||
|
switch (action) {
|
||||||
|
case ForeignKeyActions.CASCADE:
|
||||||
|
return "cascade";
|
||||||
|
|
||||||
|
case ForeignKeyActions.SET_NULL:
|
||||||
|
return "set null";
|
||||||
|
|
||||||
|
case ForeignKeyActions.RESTRICT:
|
||||||
|
return "restrict";
|
||||||
|
|
||||||
|
case ForeignKeyActions.SET_DEFAULT:
|
||||||
|
return "set default";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPrimaryKeyContraint(primaryKeys: FieldType[]): AST {
|
||||||
|
// primary key contraint
|
||||||
|
return {
|
||||||
|
constraint_type: "primary key",
|
||||||
|
resource: "constraint",
|
||||||
|
definition: primaryKeys.map((field: FieldType) => ({
|
||||||
|
column: field.name,
|
||||||
|
type: "column_ref"
|
||||||
|
}))
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createIndexAst(table: TableType, index: IndexType): AST {
|
||||||
|
// create index AST for Mysql Dielect
|
||||||
|
return {
|
||||||
|
index: index.name,
|
||||||
|
type: "create",
|
||||||
|
table: { table: table.name },
|
||||||
|
keyword: "index",
|
||||||
|
on_kw: "on",
|
||||||
|
index_type: index.unique ? "unique" : null,
|
||||||
|
index_columns: index.fields.map((field: FieldType) => ({
|
||||||
|
column: field.name,
|
||||||
|
type: "column_ref"
|
||||||
|
}))
|
||||||
|
} as AST
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(field: FieldType): AST | null {
|
||||||
|
const definition = super.processDefaultValue(field) as any;
|
||||||
|
// if the field have a default value , then we generate an expression based on the type of the default value
|
||||||
|
if (definition && definition.default_value !== null) {
|
||||||
|
let default_val: any | null = null
|
||||||
|
if (definition.default_value_type == "string")
|
||||||
|
default_val = {
|
||||||
|
type: "default",
|
||||||
|
value: {
|
||||||
|
type: "single_quote_string",
|
||||||
|
value: field.defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (definition.default_value_type == "function" && field.defaultValue == TimeDefaultValues.NOW)
|
||||||
|
default_val = {
|
||||||
|
type: "default",
|
||||||
|
value: {
|
||||||
|
type: "function",
|
||||||
|
name: {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
type: "origin",
|
||||||
|
value: "CURRENT_TIMESTAMP"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
over: null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (definition.default_value_type == "function" && field.defaultValue == "random") {
|
||||||
|
default_val = {
|
||||||
|
type: "default",
|
||||||
|
value: {
|
||||||
|
type: "function",
|
||||||
|
name: {
|
||||||
|
name: [
|
||||||
|
{
|
||||||
|
type: "default",
|
||||||
|
value: this.dialect == DatabaseDialect.POSTGRES ? "gen_random_uuid" : "UUID"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
args: {
|
||||||
|
type: "expr_list",
|
||||||
|
value: []
|
||||||
|
},
|
||||||
|
parentheses: this.dialect == DatabaseDialect.MARIADB
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
default_val = {
|
||||||
|
type: "default",
|
||||||
|
value: {
|
||||||
|
type: definition.default_value_type,
|
||||||
|
value: definition.default_value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return default_val;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected startTransaction(): string {
|
||||||
|
return "BEGIN TRANSACTION;" ;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected commit(): string {
|
||||||
|
return "COMMIT;"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
protected abstract getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import MysqlRenderer from "./mysql-renderer";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
|
||||||
|
export default class MariaDbRenderer extends MysqlRenderer {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super( data_types , DatabaseDialect.MARIADB)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||||
|
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { format } from 'sql-formatter';
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
|
||||||
|
export default class MSSqlRenderer extends BaseDatabaseRenderer {
|
||||||
|
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(DatabaseDialect.MSSQL, data_types)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||||
|
let sql = ast.join("\n");
|
||||||
|
sql = format(sql, { language: 'sql' });
|
||||||
|
if (sql.length > 0) {
|
||||||
|
sql = `${this.startTransaction()}
|
||||||
|
|
||||||
|
${sql}
|
||||||
|
|
||||||
|
${this.commit()}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||||
|
const ast: any = super.createTableAst(table);
|
||||||
|
|
||||||
|
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||||
|
|
||||||
|
// get primary keys and check if we hame more than one
|
||||||
|
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
||||||
|
let constraints: ASTStatment = "";
|
||||||
|
if (primaryKeys.length > 0) {
|
||||||
|
constraints = this.getPrimaryKeyContraint(primaryKeys, table)
|
||||||
|
}
|
||||||
|
return `
|
||||||
|
CREATE TABLE ${table.name} (
|
||||||
|
${(ast as any).field_definitions.join(",\n")}
|
||||||
|
${constraints ? ',\n' + constraints : ""}
|
||||||
|
);
|
||||||
|
${indexes}
|
||||||
|
` ;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||||
|
|
||||||
|
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||||
|
|
||||||
|
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "IDENTITY(1,1)" : "") : ""
|
||||||
|
|
||||||
|
let nullable: string = ""
|
||||||
|
let unique: string = "";
|
||||||
|
|
||||||
|
if (!ast.primary_key)
|
||||||
|
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||||
|
|
||||||
|
let options: number[] | string = [];
|
||||||
|
|
||||||
|
if (ast.length)
|
||||||
|
options.push(ast.length);
|
||||||
|
|
||||||
|
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||||
|
options.push(DEFAULT_LENGTH_PARAM);
|
||||||
|
|
||||||
|
if (ast.scale && ast.scale != "0")
|
||||||
|
options.push(ast.scale);
|
||||||
|
|
||||||
|
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||||
|
|
||||||
|
let defaultValue: any = "";
|
||||||
|
|
||||||
|
if (ast.default_value !== undefined && ast.default_value !== null && !dropDefaultValue) {
|
||||||
|
defaultValue = `CONSTRAINT DF_${table.name}_${field.name} DEFAULT ${ast.default_value}`;
|
||||||
|
}
|
||||||
|
if (!ast.primary_key) {
|
||||||
|
unique = field.unique ? `CONSTRAINT UQ_${table.name}_${field.name} UNIQUE` : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||||
|
|
||||||
|
return `${field.name} ${dataType}${options} ${autoIncrement} ${nullable} ${defaultValue} ${unique} `;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected processDefaultValue(field: FieldType): AST | null {
|
||||||
|
const ast: any = super.processDefaultValue(field);
|
||||||
|
|
||||||
|
if (ast && ast.default_value_type) {
|
||||||
|
if (ast.default_value_type == "single_quote_string") {
|
||||||
|
ast.default_value = `'${ast.default_value}'`
|
||||||
|
}
|
||||||
|
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||||
|
ast.default_value = ast.default_value;
|
||||||
|
|
||||||
|
else if (ast.default_value_type == "function") {
|
||||||
|
|
||||||
|
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||||
|
|
||||||
|
switch (field.type.name?.toUpperCase()) {
|
||||||
|
case "DATE":
|
||||||
|
ast.default_value = "CAST(GETDATE() AS DATE)"
|
||||||
|
break;
|
||||||
|
case "DATETIME":
|
||||||
|
ast.default_value = "GETDATE()"
|
||||||
|
break;
|
||||||
|
case "SMALLDATETIME":
|
||||||
|
ast.default_value = "GETDATE()"
|
||||||
|
break;
|
||||||
|
case "DATETIME2":
|
||||||
|
ast.default_value = "SYSDATETIME()"
|
||||||
|
break;
|
||||||
|
case "DATETIMEOFFSET":
|
||||||
|
ast.default_value = "SYSDATETIMEOFFSET()"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (ast.default_value == "random") {
|
||||||
|
if (field.isPrimary)
|
||||||
|
ast.default_value = "NEWSEQUENTIALID()"
|
||||||
|
else
|
||||||
|
ast.default_value = "NEWID()"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
if ( ast.default_value_type == "bool") {
|
||||||
|
if (ast.default_value) {
|
||||||
|
ast.default_value = 1 ;
|
||||||
|
}else {
|
||||||
|
ast.default_value = 0 ;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||||
|
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||||
|
|
||||||
|
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||||
|
|
||||||
|
const onDeleteFKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||||
|
const onUpdateFKAction: string | null = this.foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
||||||
|
|
||||||
|
const onDeleteAction: string = onDeleteFKAction ? `ON DELETE ${onDeleteFKAction}` : ""
|
||||||
|
const onUpdateAction: string = onUpdateFKAction ? `ON UPDATE ${onUpdateFKAction}` : ""
|
||||||
|
|
||||||
|
const FKActions: string = [onDeleteAction, onUpdateAction].join(" ");
|
||||||
|
|
||||||
|
return `
|
||||||
|
ALTER TABLE ${targetTable.name}
|
||||||
|
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||||
|
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${FKActions};
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||||
|
switch (action) {
|
||||||
|
case ForeignKeyActions.CASCADE:
|
||||||
|
return "CASCADE";
|
||||||
|
|
||||||
|
case ForeignKeyActions.SET_NULL:
|
||||||
|
return "SET NULL";
|
||||||
|
case ForeignKeyActions.SET_DEFAULT:
|
||||||
|
return "SET DEFAULT";
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||||
|
const unique: string = index.unique ? " UNIQUE" : "";
|
||||||
|
|
||||||
|
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||||
|
if (columns.length == 0)
|
||||||
|
return null;
|
||||||
|
columns = `(${columns.join(",")})`
|
||||||
|
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPrimaryKeyContraint(fields: FieldType[], table?: TableType): ASTStatment {
|
||||||
|
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||||
|
return `CONSTRAINT PK_${table?.name} PRIMARY KEY (${pks.join(",")})`
|
||||||
|
}
|
||||||
|
|
||||||
|
protected startTransaction(): string {
|
||||||
|
return `BEGIN TRANSACTION;`
|
||||||
|
}
|
||||||
|
protected commit(): string {
|
||||||
|
return "COMMIT;"
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { fixCharsetPlacement } from "../render-uttils";
|
||||||
|
import { format } from 'sql-formatter';
|
||||||
|
import { Modifiers } from "@/lib/field";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
|
||||||
|
|
||||||
|
export default class MysqlRenderer extends BaseSQLRenderer {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[], dialect: DatabaseDialect = DatabaseDialect.MYSQL) {
|
||||||
|
super(dialect, data_types)
|
||||||
|
}
|
||||||
|
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||||
|
// for Mysql , MariaDB , we use node-sql-parser to turn AST to SQL ,
|
||||||
|
// the package have a bug which is CHARSET and COLLATION missplacementts
|
||||||
|
// so we patch it using fixCharsetPlacement function
|
||||||
|
// and finally we format the code
|
||||||
|
try {
|
||||||
|
let sql: string = await super.astToSQL(ast);
|
||||||
|
sql = fixCharsetPlacement(format(sql, { language: "sql" }));
|
||||||
|
sql = format(sql, { language: 'sql' });
|
||||||
|
|
||||||
|
if (sql.length > 0) {
|
||||||
|
sql = `${this.startTransaction()}
|
||||||
|
|
||||||
|
${sql};
|
||||||
|
|
||||||
|
${this.commit()}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||||
|
// get the modifiers based on the field data type
|
||||||
|
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
||||||
|
|
||||||
|
// we get the base field definition from the parent renderer
|
||||||
|
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint)
|
||||||
|
// Mysql , MariaDB ,Have some additional attributes such as charset collation , the enum values are inline unlike Postgres , Unsigned , and ZERO FILL
|
||||||
|
// also we need default value expression that it is compatible with node-sql-parser
|
||||||
|
let suffix: string[] = [];
|
||||||
|
|
||||||
|
// check if the field support ZEROFILL and if it set , and the same thing for unsigned
|
||||||
|
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
|
||||||
|
suffix.push(Modifiers.ZEROFILL.toUpperCase());
|
||||||
|
}
|
||||||
|
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
|
||||||
|
suffix.push(Modifiers.UNSIGNED.toUpperCase());
|
||||||
|
}
|
||||||
|
// if it support charset and charset is set , then generate it expression
|
||||||
|
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
|
||||||
|
|
||||||
|
definition.character_set = {
|
||||||
|
type: "CHARACTER SET",
|
||||||
|
value: {
|
||||||
|
type: "default",
|
||||||
|
value: field.charset
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it support collate and collate is set , then generate it expression
|
||||||
|
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
|
||||||
|
|
||||||
|
definition.collate = {
|
||||||
|
keyword: "collate",
|
||||||
|
type: "collate",
|
||||||
|
collate: {
|
||||||
|
name: field.collate,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (suffix.length > 0) {
|
||||||
|
definition.definition.suffix = suffix
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import BaseDatabaseRenderer, { ASTStatment } from "./base-database-renderer";
|
||||||
|
import { DEFAULT_LENGTH_PARAM, ForeignKeyActions, Modifiers } from "@/lib/field";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { format } from 'sql-formatter';
|
||||||
|
|
||||||
|
export default class OracleRenderer extends BaseDatabaseRenderer {
|
||||||
|
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(DatabaseDialect.ORACLE, data_types)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||||
|
let sql = ast.join("\n");
|
||||||
|
sql = format(sql, { language: 'sql' });
|
||||||
|
return sql;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected createTableAst(table: TableType): ASTStatment[] | ASTStatment {
|
||||||
|
const ast: any = super.createTableAst(table);
|
||||||
|
let pk_constraint: string = ast.constraints?.pop();
|
||||||
|
if (pk_constraint)
|
||||||
|
pk_constraint = "," + pk_constraint
|
||||||
|
else
|
||||||
|
pk_constraint = "";
|
||||||
|
let indexes: string = ast.indices_ast.filter((index: string) => index != null).join("\n")
|
||||||
|
return `
|
||||||
|
CREATE TABLE ${table.name} (
|
||||||
|
${(ast as any).field_definitions.join(",\n")}
|
||||||
|
${pk_constraint}
|
||||||
|
);
|
||||||
|
${indexes}
|
||||||
|
` ;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean = false, dropDefaultValue: boolean = false): ASTStatment {
|
||||||
|
|
||||||
|
const ast: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||||
|
|
||||||
|
let primaryKey: string = ast.primary_key ? "PRIMARY KEY" : "";
|
||||||
|
let autoIncrement: string = ast.modifiers.includes(Modifiers.AUTO_INCREMENT) ? (ast.auto_increment ? "GENERATED BY DEFAULT AS IDENTITY" : "") : ""
|
||||||
|
|
||||||
|
let nullable: string = ""
|
||||||
|
let unique: string = "";
|
||||||
|
|
||||||
|
if (!ast.primary_key)
|
||||||
|
nullable = (field.nullable !== undefined) ? (field.nullable ? "NULL" : "NOT NULL") : "";
|
||||||
|
|
||||||
|
if (!ast.primary_key)
|
||||||
|
unique = field.unique ? "UNIQUE" : "";
|
||||||
|
|
||||||
|
let options: number[] | string = [];
|
||||||
|
|
||||||
|
if (ast.length)
|
||||||
|
options.push(ast.length);
|
||||||
|
|
||||||
|
else if (ast.modifiers.includes(Modifiers.LENGTH))
|
||||||
|
options.push(DEFAULT_LENGTH_PARAM);
|
||||||
|
|
||||||
|
if (ast.scale && ast.scale != "0")
|
||||||
|
options.push(ast.scale);
|
||||||
|
|
||||||
|
options = options.length > 0 ? `(${options.join(",")})` : "";
|
||||||
|
|
||||||
|
let defaultValue: any = "";
|
||||||
|
|
||||||
|
if (ast.default_value !== undefined && ast.default_value !== null) {
|
||||||
|
defaultValue = "DEFAULT " + ast.default_value
|
||||||
|
}
|
||||||
|
if (dropDefaultValue) {
|
||||||
|
defaultValue = "DEFAULT NULL"
|
||||||
|
}
|
||||||
|
|
||||||
|
let dataType = ast.dataType;
|
||||||
|
if (dataType === "TIMESTAMP WITH LOCAL TIME ZONE" && options) {
|
||||||
|
dataType = `TIMESTAMP${options} WITH LOCAL TIME ZONE`
|
||||||
|
}
|
||||||
|
else if (dataType === "TIMESTAMP WITH TIME ZONE" && options) {
|
||||||
|
dataType = `TIMESTAMP${options} WITH TIME ZONE`
|
||||||
|
}
|
||||||
|
else if (dataType === "INTERVAL DAY TO SECOND" && options) {
|
||||||
|
dataType = `INTERVAL DAY${options} TO SECOND`
|
||||||
|
}
|
||||||
|
else if (dataType === "INTERVAL YEAR TO MONTH" && options) {
|
||||||
|
dataType = `INTERVAL YEAR${options} TO MONTH`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
else
|
||||||
|
dataType = dataType + options
|
||||||
|
|
||||||
|
|
||||||
|
//const dataType = field.type.name == "uuid" ? "RAW(16)" : ast.dataType;
|
||||||
|
|
||||||
|
return `${field.name} ${dataType} ${defaultValue} ${nullable} ${unique} ${autoIncrement} ${primaryKey}`;
|
||||||
|
}
|
||||||
|
protected processDefaultValue(field: FieldType): AST | null {
|
||||||
|
|
||||||
|
const ast: any = super.processDefaultValue(field);
|
||||||
|
|
||||||
|
if (ast && ast.default_value !== undefined && ast.default_value !== null) {
|
||||||
|
|
||||||
|
|
||||||
|
if (ast.default_value_type == "single_quote_string") {
|
||||||
|
ast.default_value = `'${ast.default_value}'`
|
||||||
|
}
|
||||||
|
else if (ast.default_value_type == "number" || ast.default_value_type == "bool")
|
||||||
|
ast.default_value = ast.default_value;
|
||||||
|
|
||||||
|
else if (ast.default_value_type == "function") {
|
||||||
|
|
||||||
|
if (ast.default_value == "CURRENT_TIMESTAMP") {
|
||||||
|
|
||||||
|
switch (field.type.name?.toUpperCase()) {
|
||||||
|
case "DATE":
|
||||||
|
ast.default_value = "SYSDATE"
|
||||||
|
break;
|
||||||
|
case "TIMESTAMP":
|
||||||
|
ast.default_value = "CURRENT_TIMESTAMP"
|
||||||
|
break;
|
||||||
|
case "TIMESTAMP WITH TIME ZONE":
|
||||||
|
ast.default_value = "SYSTIMESTAMP"
|
||||||
|
break;
|
||||||
|
case "TIMESTAMP WITH LOCAL TIME ZONE":
|
||||||
|
ast.default_value = "SYSTIMESTAMP"
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else if (ast.default_value == "random") {
|
||||||
|
ast.default_value = "SYS_GUID()"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createIndexAst(table: TableType, index: IndexType): ASTStatment {
|
||||||
|
const unique: string = index.unique ? " UNIQUE" : "";
|
||||||
|
|
||||||
|
let columns: string[] | string = index.fields.map((field: FieldType) => field.name);
|
||||||
|
if (columns.length == 0)
|
||||||
|
return null;
|
||||||
|
columns = `(${columns.join(",")})`
|
||||||
|
return `CREATE${unique} INDEX ${index.name} ON ${table.name} ${columns} ;`
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||||
|
const { primaryKey, foreignKey, sourceTable, targetTable } = super.createRelationshipAst(relationship) as any;
|
||||||
|
|
||||||
|
const constraintName: string = relationship.name ? " CONSTRAINT " + relationship.name : "";
|
||||||
|
|
||||||
|
const FKAction: string | null = this.foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
||||||
|
const onDeleteAction: string = FKAction ? `ON DELETE ${FKAction}` : ""
|
||||||
|
|
||||||
|
return `
|
||||||
|
ALTER TABLE ${targetTable.name}
|
||||||
|
ADD ${constraintName} FOREIGN KEY (${foreignKey.name})
|
||||||
|
REFERENCES ${sourceTable.name}(${primaryKey.name}) ${onDeleteAction};
|
||||||
|
`
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected foreignKeyActionToAst(action: ForeignKeyActions): string | null {
|
||||||
|
switch (action) {
|
||||||
|
case ForeignKeyActions.CASCADE:
|
||||||
|
return "CASCADE";
|
||||||
|
|
||||||
|
case ForeignKeyActions.SET_NULL:
|
||||||
|
return "SET NULL";
|
||||||
|
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getPrimaryKeyContraint(fields: FieldType[]): ASTStatment {
|
||||||
|
//throw new Error("Method not implemented.");
|
||||||
|
const pks: string[] = fields.map((field: FieldType) => field.name);
|
||||||
|
return `
|
||||||
|
PRIMARY KEY (${pks.join(",")})
|
||||||
|
`
|
||||||
|
}
|
||||||
|
protected startTransaction(): string {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
protected commit(): string {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,157 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import { getPostgresEnumName } from "../render-uttils";
|
||||||
|
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { IndexType } from "@/lib/schemas/index-schema";
|
||||||
|
import { Statement, toSql } from "pgsql-ast-parser";
|
||||||
|
import { ASTStatment } from "./base-database-renderer";
|
||||||
|
import { format } from "sql-formatter";
|
||||||
|
import { DBDiffOperation } from "@/utils/database";
|
||||||
|
import { DataTypes } from "@/lib/field";
|
||||||
|
|
||||||
|
|
||||||
|
export default class PostgresqlRenderer extends BaseSQLRenderer {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(DatabaseDialect.POSTGRES, data_types)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async astToSQL(ast: ASTStatment[]): Promise<string> {
|
||||||
|
// postgres renderer is different , we use a different parser pgsql-ast-parser for index , enum renaming .
|
||||||
|
// those kind of operations return Statment not AST .
|
||||||
|
// so the we pass AST to the default parser and Statment to Postgres parser
|
||||||
|
|
||||||
|
await this.readyPromise ;
|
||||||
|
let sql: string[] = [];
|
||||||
|
|
||||||
|
for (const statment of ast) {
|
||||||
|
if ((statment as Statement)?.type == "alter index" || (statment as Statement)?.type == "alter enum") {
|
||||||
|
// index renaming
|
||||||
|
sql.push(toSql.statement(statment as Statement) + ";");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const statment_sql = this.parser.sqlify(statment as AST, {
|
||||||
|
database: getDatabaseByDialect(this.dialect).name
|
||||||
|
});
|
||||||
|
sql.push(statment_sql + ";");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sql.length > 0) {
|
||||||
|
sql = [this.startTransaction( ) , ...sql , this.commit()]
|
||||||
|
}
|
||||||
|
return format(sql.join(""), { language: 'postgresql' });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected createTableAst(table: TableType): AST[] | AST {
|
||||||
|
// the only difference in create table statment in postgres is we need to get declare all enums before creating the table
|
||||||
|
const definition: AST[] = super.createTableAst(table) as any
|
||||||
|
// so we need an enum ast
|
||||||
|
const enumsAst: AST[] = [];
|
||||||
|
// get fields to type enum
|
||||||
|
let postgresEnums: FieldType[] = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
||||||
|
if (postgresEnums.length > 0) {
|
||||||
|
// loop over them and intiat them one by one
|
||||||
|
for (const postgresEnum of postgresEnums) {
|
||||||
|
(postgresEnum as any).table = table;
|
||||||
|
enumsAst.push(this.createEnumAst(postgresEnum));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
definition.splice(0, 0, ...enumsAst);
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
protected getFieldDefinition(field: FieldType, table: TableType, ignorePkContraint: boolean): AST {
|
||||||
|
// we get the base field definition from the parent renderer
|
||||||
|
const definition: any = super.getFieldDefinition(field, table, ignorePkContraint);
|
||||||
|
|
||||||
|
// since postrges uses the serial data types , then there is not need for auto incrmenet attribute
|
||||||
|
definition.auto_increment = undefined;
|
||||||
|
// postgres handle enums diffrently , we need to create the enum first then reference to it in the column line
|
||||||
|
if (definition.definition.dataType == "ENUM") {
|
||||||
|
definition.definition.dataType = getPostgresEnumName(table, field);
|
||||||
|
definition.definition.expr = undefined;
|
||||||
|
}
|
||||||
|
return definition;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private createEnumAst(field: FieldType): AST {
|
||||||
|
// get the enum values , and cast it into an array
|
||||||
|
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
||||||
|
// return the ast of creating a table
|
||||||
|
return {
|
||||||
|
as: "as",
|
||||||
|
type: "create",
|
||||||
|
resource: "enum",
|
||||||
|
name: {
|
||||||
|
schema: null,
|
||||||
|
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
||||||
|
},
|
||||||
|
keyword: "type",
|
||||||
|
create_definitions: {
|
||||||
|
parentheses: true,
|
||||||
|
type: "expr_list",
|
||||||
|
value: jsonValues.map((value: string) => ({
|
||||||
|
type: "single_quote_string",
|
||||||
|
value
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
} as any
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getUsingAst(table: TableType, field: FieldType, newDataType: DataType): AST | null {
|
||||||
|
|
||||||
|
const oldType: DataTypes = field.type.type as DataTypes;
|
||||||
|
const newType: DataTypes = newDataType.type as DataTypes;
|
||||||
|
|
||||||
|
if (oldType === newType && oldType != DataTypes.ENUM) return null;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(oldType === DataTypes.INTEGER && newType === DataTypes.NUMERIC) ||
|
||||||
|
(oldType === DataTypes.NUMERIC && newType === DataTypes.INTEGER)
|
||||||
|
) return null;
|
||||||
|
|
||||||
|
|
||||||
|
let dataType = newDataType.name?.toUpperCase();
|
||||||
|
|
||||||
|
if (newDataType.name == "enum")
|
||||||
|
dataType = "TEXT::" + getPostgresEnumName(table, field);
|
||||||
|
else if (newDataType.name != "text")
|
||||||
|
dataType = "TEXT::" + newDataType.name?.toUpperCase();
|
||||||
|
|
||||||
|
return {
|
||||||
|
as: null,
|
||||||
|
symbol: "::",
|
||||||
|
target: [
|
||||||
|
{
|
||||||
|
dataType
|
||||||
|
}
|
||||||
|
],
|
||||||
|
type: "cast",
|
||||||
|
keyword: "cast",
|
||||||
|
expr: {
|
||||||
|
type: "column_ref",
|
||||||
|
table: null,
|
||||||
|
column: {
|
||||||
|
expr: {
|
||||||
|
type: "default",
|
||||||
|
value: field.name
|
||||||
|
}
|
||||||
|
},
|
||||||
|
collate: null
|
||||||
|
}
|
||||||
|
} as any;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected startTransaction(): string {
|
||||||
|
return "BEGIN;" ;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { AST } from "node-sql-parser";
|
||||||
|
import { fixSQLiteColumnOrder } from "../render-uttils";
|
||||||
|
import { format } from 'sql-formatter';
|
||||||
|
import { RelationshipType } from "@/lib/schemas/relationship-schema";
|
||||||
|
import { ASTStatment } from "./base-database-renderer";
|
||||||
|
import { BaseSQLRenderer } from "./base-sql-renderer";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { DBDiffOperation } from "@/utils/database";
|
||||||
|
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
||||||
|
import { DatabaseType } from "@/lib/schemas/database-schema";
|
||||||
|
import { orderTables } from "@/utils/tables";
|
||||||
|
|
||||||
|
export default class SqliteRenderer extends BaseSQLRenderer {
|
||||||
|
|
||||||
|
public constructor(data_types: DataType[]) {
|
||||||
|
super(DatabaseDialect.SQLITE, data_types)
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async astToSQL(ast: AST[]): Promise<string> {
|
||||||
|
try {
|
||||||
|
let sql: string = await super.astToSQL(ast);
|
||||||
|
|
||||||
|
sql = fixSQLiteColumnOrder(format(sql, { language: "sql" }));
|
||||||
|
sql = format(sql, { language: 'sql' });
|
||||||
|
|
||||||
|
if (sql.length > 0) {
|
||||||
|
sql = `${this.startTransaction()}
|
||||||
|
|
||||||
|
${sql};
|
||||||
|
|
||||||
|
${this.commit()}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
return sql;
|
||||||
|
} catch (error) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected operationsToAst(operations: DBDiffOperation[], database?: DatabaseType): ASTStatment[] {
|
||||||
|
|
||||||
|
const ast: ASTStatment[] = [];
|
||||||
|
|
||||||
|
let sortedTables: TableType[] | RenderableTable = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_TABLE").map((operation: DBDiffOperation) => (operation as any).table)
|
||||||
|
|
||||||
|
if (database) {
|
||||||
|
let renderableTables: RenderableTable[] = sortedTables.map((table: TableType) => toRenderableTable(table, database));
|
||||||
|
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
||||||
|
|
||||||
|
const sortedTablesIds: string[] = orderTables(sortableTables);
|
||||||
|
|
||||||
|
sortedTables = sortedTablesIds.map((id: string) =>
|
||||||
|
renderableTables.find((table: TableType) => table.id == id) as TableType
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const relationshipOperations: DBDiffOperation[] = operations.filter((operation: DBDiffOperation) => operation.type == "CREATE_RELATIONSHIP");
|
||||||
|
|
||||||
|
// we basiclly loop over all operations , and turn them into ast .
|
||||||
|
// some operation can return multiple statments .
|
||||||
|
// so for that we need to check our input if its one AST or an object AST
|
||||||
|
for (const table of sortedTables) {
|
||||||
|
const statmentAst = this.createTableAst(table);
|
||||||
|
|
||||||
|
if (!statmentAst)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
|
||||||
|
if ((table as RenderableTable).foreignRelationships?.length > 0) {
|
||||||
|
const relationshipsAst: ASTStatment[] = (table as RenderableTable).foreignRelationships.map((relationship: RelationshipType) => {
|
||||||
|
const operation: DBDiffOperation = relationshipOperations.find((operation: DBDiffOperation) => (operation as any).relationship?.id == relationship.id) as DBDiffOperation;
|
||||||
|
return this.createRelationshipAst((operation as any).relationship)
|
||||||
|
})
|
||||||
|
|
||||||
|
if (Array.isArray(statmentAst)) {
|
||||||
|
|
||||||
|
const index = statmentAst.findIndex((statment: any) => statment.type == "create" && statment.keyword == "table");
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
(statmentAst[index] as any).create_definitions.push(...relationshipsAst)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (Array.isArray(statmentAst))
|
||||||
|
ast.push(...statmentAst);
|
||||||
|
else
|
||||||
|
ast.push(statmentAst);
|
||||||
|
}
|
||||||
|
return ast;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected createRelationshipAst(relationship: RelationshipType): ASTStatment {
|
||||||
|
const relationshipAst: any = super.createRelationshipAst(relationship) as any;
|
||||||
|
return relationshipAst.expr[0].create_definitions;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected getUsingAst(table: TableType, field: FieldType, dataType: DataType): AST | null {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
protected dropPrimaryKeyConstraintExpr(table: TableType): ASTStatment {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
protected createPrimaryKeyConstraintExpr(table: TableType, fields: FieldType[]): ASTStatment {
|
||||||
|
throw new Error("Method not implemented.");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,395 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
import { DatabaseDialect } from "@/lib/database";
|
|
||||||
import { DataTypes, ForeignKeyActions, Modifiers, MYSQL_MAX_VAR_LENGTH, TimeDefaultValues } from "@/lib/field";
|
|
||||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
|
||||||
import { DatabaseType } from "@/lib/schemas/database-schema";
|
|
||||||
import { FieldType } from "@/lib/schemas/field-schema";
|
|
||||||
import { FieldIndexType } from "@/lib/schemas/field_index-schema";
|
|
||||||
import { IndexType } from "@/lib/schemas/index-schema";
|
|
||||||
import { Cardinality, RelationshipType } from "@/lib/schemas/relationship-schema";
|
|
||||||
import { TableType } from "@/lib/schemas/table-schema";
|
|
||||||
import { RenderableTable, SortableTable, toRenderableTable, toSortableTable } from "@/lib/table";
|
|
||||||
import { getForeignRelationships } from "@/utils/relationship";
|
|
||||||
import { orderTables } from "@/utils/tables";
|
|
||||||
import _ from "lodash" ;
|
|
||||||
|
|
||||||
export const DatabaseToAst = (db: DatabaseType, data_types: DataType[]) => {
|
|
||||||
let dbAst: any = [];
|
|
||||||
if (!data_types || data_types.length == 0)
|
|
||||||
return dbAst;
|
|
||||||
const database = _.cloneDeep(db) ;
|
|
||||||
let renderableTables: RenderableTable[] = database.tables.map((table: TableType) => toRenderableTable(table, database));
|
|
||||||
let sortableTables: SortableTable[] = renderableTables.map((table: RenderableTable) => toSortableTable(table));
|
|
||||||
|
|
||||||
try {
|
|
||||||
const sortedTablesIds: string[] = orderTables(sortableTables);
|
|
||||||
|
|
||||||
const sortedTables: TableType[] = sortedTablesIds.map((id: string) =>
|
|
||||||
renderableTables.find((table: TableType) => table.id == id) as TableType
|
|
||||||
);
|
|
||||||
for (const table of sortedTables) {
|
|
||||||
let postgresEnums: FieldType[];
|
|
||||||
if (database.dialect == DatabaseDialect.POSTGRES) {
|
|
||||||
postgresEnums = table.fields.filter((field: FieldType) => field.type?.name == "enum");
|
|
||||||
if (postgresEnums.length > 0) {
|
|
||||||
for (const postgresEnum of postgresEnums) {
|
|
||||||
(postgresEnum as any).table = table;
|
|
||||||
dbAst.push(PotgresEnumToAst(postgresEnum));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
dbAst.push(TableToAst(table, data_types, database.dialect == DatabaseDialect.POSTGRES));
|
|
||||||
|
|
||||||
if (table.indices && table.indices.length > 0)
|
|
||||||
for (const index of table.indices)
|
|
||||||
if (index.fieldIndices?.length > 0)
|
|
||||||
dbAst.push(IndexToAst({
|
|
||||||
...index,
|
|
||||||
fields: index.fieldIndices.map((fieldIndex: FieldIndexType) =>
|
|
||||||
table.fields.find((field: FieldType) => field.id == fieldIndex.fieldId)
|
|
||||||
) as FieldType[],
|
|
||||||
}, table));
|
|
||||||
};
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
return dbAst;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const TableToAst = (table: TableType, data_types: DataType[], isPostgresDialect: boolean = false) => {
|
|
||||||
|
|
||||||
const primaryKeys: FieldType[] = table.fields.filter((field: FieldType) => field.isPrimary);
|
|
||||||
const multiPrimaryKeys: boolean = primaryKeys.length > 1;
|
|
||||||
|
|
||||||
const primaryKeysConstraints: any[] | undefined = multiPrimaryKeys ? [{
|
|
||||||
constraint_type: "primary key",
|
|
||||||
resource: "constraint",
|
|
||||||
definition: primaryKeys.map((field: FieldType) => ({
|
|
||||||
column: field.name,
|
|
||||||
type: "column_ref"
|
|
||||||
}))
|
|
||||||
}] : [];
|
|
||||||
|
|
||||||
let foreignRelationships = getForeignRelationships(table);
|
|
||||||
const foreignKeysConstraints = foreignRelationships.map((relationship: RelationshipType) => relationshipToAst(relationship));
|
|
||||||
const constraints: any[] = [
|
|
||||||
...primaryKeysConstraints,
|
|
||||||
...foreignKeysConstraints
|
|
||||||
];
|
|
||||||
|
|
||||||
const filed_definitions = table.fields.filter((field: FieldType) => field.type).map((field: FieldType) => {
|
|
||||||
const isPostgresEnum: boolean = isPostgresDialect && field.type.name == "enum";
|
|
||||||
|
|
||||||
return FieldToAst({
|
|
||||||
...field,
|
|
||||||
type: !(isPostgresEnum) ?
|
|
||||||
data_types.find((dataType: DataType) => dataType.id == field.typeId) as DataType :
|
|
||||||
{ name: `${table.name}_${field.name.toLowerCase()}_enum` } as DataType,
|
|
||||||
}, multiPrimaryKeys, !isPostgresEnum)
|
|
||||||
})
|
|
||||||
|
|
||||||
return {
|
|
||||||
keyword: "table",
|
|
||||||
type: "create",
|
|
||||||
table: [{
|
|
||||||
table: table.name
|
|
||||||
}],
|
|
||||||
create_definitions: [...filed_definitions, ...constraints]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
export const FieldToAst = (field: FieldType, ignorePrimaryKey: boolean = false, upperCaseType: boolean = true) => {
|
|
||||||
|
|
||||||
const modifiers: string[] = field?.type.modifiers ? JSON.parse(field?.type.modifiers) : [];
|
|
||||||
let suffix: string[] = [];
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.ZEROFILL) && field.zeroFill) {
|
|
||||||
suffix.push(Modifiers.ZEROFILL.toUpperCase());
|
|
||||||
}
|
|
||||||
if (modifiers.includes(Modifiers.UNSIGNED) && field.unsigned) {
|
|
||||||
suffix.push(Modifiers.UNSIGNED.toUpperCase());
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let length: number | null = null;
|
|
||||||
let scale: number | string | null = null;
|
|
||||||
|
|
||||||
let character_set: any | null = null;
|
|
||||||
let collate: any | null = null;
|
|
||||||
|
|
||||||
let values: any[] = [];
|
|
||||||
let valuesExpr: any | null;
|
|
||||||
|
|
||||||
let default_val: any | null = null;
|
|
||||||
let dataType: string | undefined = upperCaseType ? field.type?.name?.toLocaleUpperCase() : field.type?.name as string | undefined;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.PRECISION) && field.precision) {
|
|
||||||
length = field.precision;
|
|
||||||
if (modifiers.includes(Modifiers.SCALE) && !field.scale)
|
|
||||||
scale = "0";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.SCALE) && field.scale)
|
|
||||||
scale = field.scale;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.LENGTH) && field.maxLength)
|
|
||||||
length = field.maxLength;
|
|
||||||
|
|
||||||
else if (modifiers.includes(Modifiers.LENGTH) && (field.type.dialect == DatabaseDialect.MYSQL || field.type.dialect == DatabaseDialect.MARIADB) && field.type.name?.startsWith('var') && !field.maxLength)
|
|
||||||
length = MYSQL_MAX_VAR_LENGTH;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.CHARSET) && field.charset)
|
|
||||||
character_set = {
|
|
||||||
type: "CHARACTER SET",
|
|
||||||
value: {
|
|
||||||
type: "default",
|
|
||||||
value: field.charset
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.COLLATE) && field.collate)
|
|
||||||
collate = {
|
|
||||||
keyword: "collate",
|
|
||||||
type: "collate",
|
|
||||||
collate: {
|
|
||||||
name: field.collate,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.VALUES) && field.values) {
|
|
||||||
|
|
||||||
const jsonValues = JSON.parse(field.values);
|
|
||||||
if (jsonValues.length > 0) {
|
|
||||||
values = jsonValues.map((value: string) => ({
|
|
||||||
type: "single_quote_string",
|
|
||||||
value
|
|
||||||
}));
|
|
||||||
|
|
||||||
valuesExpr = {
|
|
||||||
parentheses: true,
|
|
||||||
type: "expr_list",
|
|
||||||
value: values
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (field.defaultValue && field.defaultValue.trim().length > 0) {
|
|
||||||
default_val = {
|
|
||||||
type: "default",
|
|
||||||
value: {
|
|
||||||
type: "single_quote_string",
|
|
||||||
value: field.defaultValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (field.type.type == DataTypes.TIME && field.defaultValue == TimeDefaultValues.NOW)
|
|
||||||
default_val = {
|
|
||||||
type: "default",
|
|
||||||
value: {
|
|
||||||
type: "function",
|
|
||||||
name: {
|
|
||||||
name: [
|
|
||||||
{
|
|
||||||
type: "origin",
|
|
||||||
value: "CURRENT_TIMESTAMP"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
over: null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else if (field.defaultValue == "true" || field.defaultValue == "false") {
|
|
||||||
default_val.value.type = "bool"
|
|
||||||
default_val.value.value = field.defaultValue == "true" ? true : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if it's a number (but not empty string or just whitespace)
|
|
||||||
else if (!isNaN(Number(field.defaultValue))) {
|
|
||||||
default_val.value.type = "number"
|
|
||||||
default_val.value.value = Number(field.defaultValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let auto_increment: string | undefined;
|
|
||||||
if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect != DatabaseDialect.POSTGRES) {
|
|
||||||
auto_increment = "auto_increment"
|
|
||||||
} else if (modifiers.includes(Modifiers.AUTO_INCREMENT) && field.autoIncrement && field.type.dialect == DatabaseDialect.POSTGRES) {
|
|
||||||
if (dataType == "INTEGER")
|
|
||||||
dataType = "SERIAL";
|
|
||||||
else
|
|
||||||
dataType = dataType?.replace("INT", "SERIAL");
|
|
||||||
|
|
||||||
} else {
|
|
||||||
auto_increment = undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
column: {
|
|
||||||
type: "column_ref",
|
|
||||||
column: {
|
|
||||||
expr: {
|
|
||||||
type: "default", value: field.name,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
|
||||||
collate,
|
|
||||||
character_set,
|
|
||||||
default_val,
|
|
||||||
unique: field.unique ? "unique" : null,
|
|
||||||
auto_increment,
|
|
||||||
nullable: {
|
|
||||||
type: field.nullable ? "null" : "not null",
|
|
||||||
value: field.nullable ? "null" : "not null",
|
|
||||||
},
|
|
||||||
definition: {
|
|
||||||
dataType,
|
|
||||||
length,
|
|
||||||
scale,
|
|
||||||
suffix,
|
|
||||||
expr: valuesExpr
|
|
||||||
},
|
|
||||||
primary_key: field.isPrimary && !ignorePrimaryKey ? "primary key" : null,
|
|
||||||
resource: "column"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const IndexToAst = (index: IndexType, table: TableType) => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
index: index.name,
|
|
||||||
type: "create",
|
|
||||||
table: { table: table.name },
|
|
||||||
keyword: "index",
|
|
||||||
on_kw: "on",
|
|
||||||
index_type: index.unique ? "unique" : null,
|
|
||||||
index_columns: index.fields.map((field: FieldType) => ({
|
|
||||||
column: field.name,
|
|
||||||
type: "column_ref"
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const PotgresEnumToAst = (field: FieldType) => {
|
|
||||||
|
|
||||||
const jsonValues = field.values ? JSON.parse(field.values) : [];
|
|
||||||
|
|
||||||
return {
|
|
||||||
as: "as",
|
|
||||||
type: "create",
|
|
||||||
resource: "enum",
|
|
||||||
name: {
|
|
||||||
schema: null,
|
|
||||||
name: `${(field as any).table.name}_${field.name.toLowerCase()}_enum`
|
|
||||||
},
|
|
||||||
keyword: "type",
|
|
||||||
create_definitions: {
|
|
||||||
parentheses: true,
|
|
||||||
type: "expr_list",
|
|
||||||
value: jsonValues.map((value: string) => ({
|
|
||||||
type: "single_quote_string",
|
|
||||||
value
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const foreignKeyActionToAst = (action: ForeignKeyActions): string | null => {
|
|
||||||
switch (action) {
|
|
||||||
case ForeignKeyActions.CASCADE:
|
|
||||||
return "cascade";
|
|
||||||
|
|
||||||
case ForeignKeyActions.SET_NULL:
|
|
||||||
return "set null";
|
|
||||||
|
|
||||||
case ForeignKeyActions.RESTRICT:
|
|
||||||
return "restrict";
|
|
||||||
|
|
||||||
case ForeignKeyActions.SET_DEFAULT:
|
|
||||||
return "set default";
|
|
||||||
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const relationshipToAst = (relationship: RelationshipType) => {
|
|
||||||
|
|
||||||
let primaryKey: FieldType = relationship.sourceField;
|
|
||||||
let foreignKey: FieldType = relationship.targetField;
|
|
||||||
|
|
||||||
let sourceTable: TableType = relationship.sourceTable;
|
|
||||||
let targetTable: TableType = relationship.targetTable;
|
|
||||||
|
|
||||||
if (relationship.cardinality == Cardinality.many_to_one) {
|
|
||||||
primaryKey = relationship.targetField;
|
|
||||||
foreignKey = relationship.sourceField;
|
|
||||||
sourceTable = relationship.targetTable;
|
|
||||||
targetTable = relationship.sourceTable;
|
|
||||||
}
|
|
||||||
|
|
||||||
let on_action: any[] = [];
|
|
||||||
|
|
||||||
if (relationship.onDelete) {
|
|
||||||
const value: string | null = foreignKeyActionToAst(relationship.onDelete as ForeignKeyActions);
|
|
||||||
|
|
||||||
if (value)
|
|
||||||
on_action.push({
|
|
||||||
type: "on delete",
|
|
||||||
value: {
|
|
||||||
type: "origin",
|
|
||||||
value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (relationship.onUpdate) {
|
|
||||||
const value: string | null = foreignKeyActionToAst(relationship.onUpdate as ForeignKeyActions);
|
|
||||||
if (value)
|
|
||||||
on_action.push({
|
|
||||||
type: "on update",
|
|
||||||
value: {
|
|
||||||
type: "origin",
|
|
||||||
value
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
constraint: null,
|
|
||||||
definition: [
|
|
||||||
{
|
|
||||||
type: "column_ref",
|
|
||||||
column: foreignKey.name,
|
|
||||||
|
|
||||||
}
|
|
||||||
],
|
|
||||||
constraint_type: "FOREIGN KEY",
|
|
||||||
resource: "constraint",
|
|
||||||
reference_definition: {
|
|
||||||
definition: [
|
|
||||||
{
|
|
||||||
type: "column_ref",
|
|
||||||
column: primaryKey.name,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
table: [
|
|
||||||
{
|
|
||||||
table: sourceTable.name,
|
|
||||||
}
|
|
||||||
],
|
|
||||||
keyword: "references",
|
|
||||||
on_action
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,722 +0,0 @@
|
|||||||
import { DataTypes, ForeignKeyActions, Modifiers, TimeDefaultValues } from "@/lib/field";
|
|
||||||
import { DataType } from "@/lib/schemas/data-type-schema";
|
|
||||||
import { FieldInsertType, FieldType } from "@/lib/schemas/field-schema";
|
|
||||||
import { TableInsertType } from "@/lib/schemas/table-schema";
|
|
||||||
import { Parser } from "node-sql-parser";
|
|
||||||
import { v4 } from "uuid";
|
|
||||||
import { parse } from 'pgsql-ast-parser';
|
|
||||||
import { DatabaseDialect, getDatabaseByDialect } from "@/lib/database";
|
|
||||||
import { randomColor } from "@/lib/colors";
|
|
||||||
import { Cardinality, RelationshipInsertType } from "@/lib/schemas/relationship-schema";
|
|
||||||
import { IndexInsertType } from "@/lib/schemas/index-schema";
|
|
||||||
|
|
||||||
|
|
||||||
export const SqlToDatabase = (sql: string, data_types: DataType[], dialect: DatabaseDialect) => {
|
|
||||||
const parser = new Parser();
|
|
||||||
let errors: Error[] = [];
|
|
||||||
|
|
||||||
const createTableStatements: string[] = [];
|
|
||||||
const alterTableStatements: string[] = [];
|
|
||||||
const createIndexStatements: string[] = [];
|
|
||||||
const createPostgresTypesStatements: string[] = [];
|
|
||||||
|
|
||||||
const tables: TableInsertType[] = [];
|
|
||||||
let relationships: RelationshipInsertType[] = [];
|
|
||||||
const indices: IndexInsertType[] = []
|
|
||||||
|
|
||||||
const postgresTypes: any[] = [];
|
|
||||||
// Clean up SQL: remove comments and normalize
|
|
||||||
const cleanedSql = sql
|
|
||||||
.replace(/--.*$/gm, '') // remove single-line comments
|
|
||||||
.replace(/\/\*[\s\S]*?\*\//g, '') // remove multi-line comments
|
|
||||||
.replace(/\s+/g, ' ') // normalize whitespace
|
|
||||||
.replace(/;\s*/g, ';\n'); // separate statements
|
|
||||||
|
|
||||||
// Split into individual statements
|
|
||||||
const statements = cleanedSql
|
|
||||||
.split('\n')
|
|
||||||
.map(s => s.trim())
|
|
||||||
.filter(Boolean);
|
|
||||||
|
|
||||||
for (const stmt of statements) {
|
|
||||||
const upper = stmt.toUpperCase();
|
|
||||||
|
|
||||||
if (upper.startsWith('CREATE TABLE')) {
|
|
||||||
createTableStatements.push(stmt);
|
|
||||||
} else if (upper.startsWith('ALTER TABLE')) {
|
|
||||||
alterTableStatements.push(stmt);
|
|
||||||
} else if (upper.startsWith('CREATE INDEX') || upper.startsWith('CREATE UNIQUE INDEX')) {
|
|
||||||
createIndexStatements.push(stmt);
|
|
||||||
} else if (upper.startsWith('CREATE TYPE')) {
|
|
||||||
createPostgresTypesStatements.push(stmt);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (dialect == DatabaseDialect.POSTGRES) {
|
|
||||||
for (const postgresType of createPostgresTypesStatements) {
|
|
||||||
try {
|
|
||||||
const instructionAst = parse(postgresType);
|
|
||||||
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
postgresTypes.push(instructionAst[0]);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
for (const createTable of createTableStatements) {
|
|
||||||
try {
|
|
||||||
const instructionAst = parse(createTable);
|
|
||||||
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
const table: TableInsertType = postgresAstToTable(instructionAst[0], data_types, postgresTypes);
|
|
||||||
tables.push(table);
|
|
||||||
|
|
||||||
for (const column of (instructionAst[0] as any).columns)
|
|
||||||
if (column.constraints?.length > 0) {
|
|
||||||
let referenceColumn: any | undefined = column.constraints.find((contraint: any) => contraint.type == "reference")
|
|
||||||
|
|
||||||
if (!referenceColumn)
|
|
||||||
continue
|
|
||||||
|
|
||||||
referenceColumn = {
|
|
||||||
...referenceColumn, localColumns: [
|
|
||||||
{ name: column.name.name }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst([referenceColumn], table);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
|
||||||
relationships = relationships.concat(newRelationships);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
|
||||||
relationships = relationships.concat((error as any).relationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((instructionAst[0] as any).constraints && (instructionAst[0] as any).constraints.length > 0) {
|
|
||||||
|
|
||||||
const relationshipAst: any = foreignKeyConstraintToAlterTableAst((instructionAst[0] as any).constraints, table)
|
|
||||||
try {
|
|
||||||
const newRelationships = postgresAstToRelationship(relationshipAst, tables);
|
|
||||||
relationships = relationships.concat(newRelationships);
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
|
||||||
relationships = relationships.concat((error as any).relationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
errors.push(error as Error);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const createIndex of createIndexStatements) {
|
|
||||||
try {
|
|
||||||
const instructionAst = parse(createIndex);
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
indices.push(postgresAstToIndex(instructionAst[0], tables));
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const alterTable of alterTableStatements) {
|
|
||||||
try {
|
|
||||||
const instructionAst = parse(alterTable);
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
const extractedRelationships: RelationshipInsertType[] = postgresAstToRelationship((instructionAst[0] as any), tables);
|
|
||||||
relationships = relationships.concat(extractedRelationships);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
|
|
||||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
|
||||||
relationships = relationships.concat((error as any).relationships);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
let foreignKeyConstraints: any[] = [];
|
|
||||||
let referenceDefinitions: any[] = [];
|
|
||||||
|
|
||||||
for (let createTable of createTableStatements) {
|
|
||||||
|
|
||||||
try {
|
|
||||||
|
|
||||||
if (dialect == DatabaseDialect.SQLITE)
|
|
||||||
createTable = createTable.replace(/\btext\s*\(\s*\d+\s*\)/gi, 'TEXT');
|
|
||||||
|
|
||||||
|
|
||||||
let instructionAst = parser.astify(createTable, {
|
|
||||||
database: dialect == DatabaseDialect.MARIADB ? getDatabaseByDialect(DatabaseDialect.MYSQL).name : getDatabaseByDialect(dialect).name
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
instructionAst = instructionAst[0];
|
|
||||||
}
|
|
||||||
if (instructionAst) {
|
|
||||||
const table: TableInsertType = astToTable(instructionAst, data_types);
|
|
||||||
tables.push(table);
|
|
||||||
|
|
||||||
const tableForeignKeyConstraints = (instructionAst as any).create_definitions.filter((definition: any) => definition.constraint_type == "FOREIGN KEY");
|
|
||||||
const tableReferenceDefinitions = (instructionAst as any).create_definitions.filter((definition: any) => definition.resource == "column" && definition.reference_definition);
|
|
||||||
|
|
||||||
foreignKeyConstraints = foreignKeyConstraints.concat(
|
|
||||||
tableForeignKeyConstraints.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
|
||||||
)
|
|
||||||
referenceDefinitions = referenceDefinitions.concat(
|
|
||||||
tableReferenceDefinitions.map((constraint: any) => ({ ...constraint, table: (instructionAst as any).table }))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
errors.push(error as Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
|
||||||
try {
|
|
||||||
relationships.push(astToRelationship(tables, foreignKeyConstraint) as RelationshipInsertType);
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (const referenceDefinition of referenceDefinitions) {
|
|
||||||
try {
|
|
||||||
relationships.push(astToRelationship(tables, undefined, referenceDefinition) as RelationshipInsertType);
|
|
||||||
} catch (error) {
|
|
||||||
errors.push(error as Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const alterTable of alterTableStatements) {
|
|
||||||
try {
|
|
||||||
let instructionAst: any = parser.astify(alterTable, {
|
|
||||||
database: getDatabaseByDialect(dialect).name
|
|
||||||
});
|
|
||||||
|
|
||||||
if (instructionAst) {
|
|
||||||
const extractedRelationships: RelationshipInsertType[] = astToRelationship(tables, undefined, undefined, {
|
|
||||||
...instructionAst[0],
|
|
||||||
table: instructionAst[0].table?.[0].table
|
|
||||||
}) as RelationshipInsertType[];
|
|
||||||
|
|
||||||
relationships = relationships.concat(extractedRelationships)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
|
|
||||||
if ((error as any).relationships && (error as any).relationships.length > 0)
|
|
||||||
relationships = relationships.concat((error as any).relationships);
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const createIndex of createIndexStatements) {
|
|
||||||
try {
|
|
||||||
let instructionAst = parser.astify(createIndex, {
|
|
||||||
database: getDatabaseByDialect(dialect).name
|
|
||||||
});
|
|
||||||
if (Array.isArray(instructionAst) && instructionAst.length > 0) {
|
|
||||||
instructionAst = instructionAst[0];
|
|
||||||
}
|
|
||||||
indices.push(astToIndex(instructionAst, tables));
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
|
|
||||||
errors.push(error as Error);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tables.length == 0)
|
|
||||||
throw Error("Error while Parsing")
|
|
||||||
|
|
||||||
|
|
||||||
return { tables, relationships, indices, errors };
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const astToForiegnKeyAction = (ast: string): ForeignKeyActions => {
|
|
||||||
switch (ast) {
|
|
||||||
case "cascade":
|
|
||||||
return ForeignKeyActions.CASCADE;
|
|
||||||
case "set null":
|
|
||||||
return ForeignKeyActions.SET_NULL;
|
|
||||||
case 'restrict':
|
|
||||||
return ForeignKeyActions.RESTRICT;
|
|
||||||
case "set default":
|
|
||||||
return ForeignKeyActions.SET_DEFAULT;
|
|
||||||
}
|
|
||||||
return ForeignKeyActions.NO_ACTION
|
|
||||||
}
|
|
||||||
|
|
||||||
export const astToRelationship = (tables: TableInsertType[], constraintAst?: any, columnAst?: any, alterTableAst?: any): RelationshipInsertType | RelationshipInsertType[] => {
|
|
||||||
|
|
||||||
let targetField: FieldInsertType | undefined;
|
|
||||||
let sourceTable: TableInsertType | undefined;
|
|
||||||
let sourceField: FieldInsertType | undefined;
|
|
||||||
let targetTable: TableInsertType | undefined;
|
|
||||||
|
|
||||||
let relationships: RelationshipInsertType[] = [];
|
|
||||||
|
|
||||||
|
|
||||||
let onDelete: ForeignKeyActions | undefined;
|
|
||||||
let onUpdate: ForeignKeyActions | undefined;
|
|
||||||
let on_action: any | undefined;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (constraintAst) {
|
|
||||||
|
|
||||||
targetTable = tables.find((table: TableInsertType) => table.name == constraintAst.table?.[0].table);
|
|
||||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.definition?.[0].column);
|
|
||||||
sourceTable = tables.find((table: TableInsertType) => table.name == constraintAst.reference_definition?.table?.[0].table);
|
|
||||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == constraintAst.reference_definition?.definition?.[0].column);
|
|
||||||
on_action = constraintAst.reference_definition.on_action;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if (columnAst) {
|
|
||||||
|
|
||||||
|
|
||||||
targetTable = tables.find((table: TableInsertType) => table.name == columnAst.table?.[0].table);
|
|
||||||
targetField = targetTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.column?.column);
|
|
||||||
sourceTable = tables.find((table: TableInsertType) => table.name == columnAst.reference_definition?.table?.[0].table);
|
|
||||||
sourceField = sourceTable?.fields?.find((field: FieldInsertType) => field.name == columnAst.reference_definition?.definition?.[0].column);
|
|
||||||
on_action = columnAst.reference_definition.on_action;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (on_action) {
|
|
||||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
|
||||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
|
||||||
|
|
||||||
if (onDeleteAst)
|
|
||||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
|
||||||
if (onUpdateAst)
|
|
||||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alterTableAst) {
|
|
||||||
|
|
||||||
const expressions = alterTableAst.expr;
|
|
||||||
const foreignKeyExpressions = expressions.filter((expression: any) => expression.resource == "constraint" && expression.create_definitions?.constraint_type == "FOREIGN KEY")
|
|
||||||
targetTable = tables.find((table: TableInsertType) => table.name == alterTableAst.table);
|
|
||||||
|
|
||||||
|
|
||||||
for (const expression of foreignKeyExpressions) {
|
|
||||||
|
|
||||||
const targetField: FieldInsertType | undefined = targetTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.definition?.[0].column);
|
|
||||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == expression.create_definitions?.reference_definition?.table?.[0].table)
|
|
||||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == expression.create_definitions?.reference_definition?.definition?.[0].column);
|
|
||||||
|
|
||||||
const on_action: any | undefined = expression.create_definitions?.reference_definition?.on_action;
|
|
||||||
|
|
||||||
let onDelete: ForeignKeyActions | undefined;
|
|
||||||
let onUpdate: ForeignKeyActions | undefined;
|
|
||||||
if (on_action) {
|
|
||||||
const onDeleteAst: any | undefined = on_action.find((action: any) => action.type == "on delete");
|
|
||||||
const onUpdateAst: any | undefined = on_action.find((action: any) => action.type == "on update");
|
|
||||||
|
|
||||||
if (onDeleteAst)
|
|
||||||
onDelete = astToForiegnKeyAction(onDeleteAst.value.value);
|
|
||||||
if (onUpdateAst)
|
|
||||||
onUpdate = astToForiegnKeyAction(onUpdateAst.value.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (!targetField || !sourceField || !sourceTable)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
relationships.push({
|
|
||||||
id: v4(),
|
|
||||||
targetTableId: targetTable?.id,
|
|
||||||
targetFieldId: targetField.id,
|
|
||||||
sourceTableId: sourceTable.id,
|
|
||||||
sourceFieldId: sourceField.id,
|
|
||||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
|
||||||
onDelete,
|
|
||||||
onUpdate
|
|
||||||
} as RelationshipInsertType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relationships.length == foreignKeyExpressions.length)
|
|
||||||
return relationships;
|
|
||||||
else
|
|
||||||
throw Error({
|
|
||||||
success: false,
|
|
||||||
message: "Failed to Extract all relationships",
|
|
||||||
relationships
|
|
||||||
} as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetField || !sourceField || !sourceTable)
|
|
||||||
throw Error("Failed to extract relationship");
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
targetTableId: targetTable?.id,
|
|
||||||
targetFieldId: targetField.id,
|
|
||||||
sourceTableId: sourceTable.id,
|
|
||||||
sourceFieldId: sourceField.id,
|
|
||||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
|
||||||
onDelete,
|
|
||||||
onUpdate
|
|
||||||
} as RelationshipInsertType;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const astToTable = (ast: any, data_types: DataType[]): TableInsertType => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.table[0]?.table,
|
|
||||||
fields: ast.create_definitions.filter((column: any) => column.resource == "column")
|
|
||||||
.map((fieldAst: any, index: number) => astToField(fieldAst, data_types, index)),
|
|
||||||
color: randomColor()
|
|
||||||
} as TableInsertType;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const astToField = (ast: any, data_types: DataType[], sequence: number): FieldInsertType => {
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
|
||||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
|
||||||
return dataType.name == ast.definition.dataType?.toLowerCase() || synonyms.includes(ast.definition.dataType?.toLowerCase())
|
|
||||||
});
|
|
||||||
|
|
||||||
let values: string | undefined = undefined;
|
|
||||||
|
|
||||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
|
||||||
|
|
||||||
const { length, scale } = ast.definition;
|
|
||||||
const { character_set, collate: collation } = ast;
|
|
||||||
|
|
||||||
let charset: string | undefined;
|
|
||||||
let collate: string | undefined;
|
|
||||||
|
|
||||||
let defaultValue: string | undefined = ast.default_val?.value?.value ? ast.default_val?.value?.value : undefined;
|
|
||||||
|
|
||||||
let maxLength: number | null = null;
|
|
||||||
let precision: number | null = null;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
|
||||||
maxLength = length;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
|
||||||
precision = length;
|
|
||||||
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.CHARSET) && character_set)
|
|
||||||
charset = character_set.value?.value;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.COLLATE) && collation)
|
|
||||||
collate = collation.collate?.name;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.VALUES)) {
|
|
||||||
|
|
||||||
|
|
||||||
const value = ast.definition?.expr?.value.map((value: any) => value.value);
|
|
||||||
if (value)
|
|
||||||
values = JSON.stringify(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataType?.type == DataTypes.TIME &&
|
|
||||||
ast.default_val?.value?.type == "function" &&
|
|
||||||
ast.default_val?.value?.name?.name?.length > 0 &&
|
|
||||||
ast.default_val?.value?.name?.name[0].value == "CURRENT_TIMESTAMP")
|
|
||||||
defaultValue = TimeDefaultValues.NOW;
|
|
||||||
|
|
||||||
const isPrimary: boolean = ast.primary_key == "primary key";
|
|
||||||
const nullable: boolean = ast.nullable?.value ? ast.nullable?.value != "not null" : !isPrimary;
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.column.column,
|
|
||||||
defaultValue,
|
|
||||||
typeId: dataType?.id,
|
|
||||||
nullable,
|
|
||||||
unique: ast.unique == "unique",
|
|
||||||
maxLength,
|
|
||||||
precision,
|
|
||||||
scale,
|
|
||||||
sequence,
|
|
||||||
autoIncrement: ast.auto_increment == "auto_increment",
|
|
||||||
isPrimary,
|
|
||||||
values,
|
|
||||||
charset,
|
|
||||||
collate,
|
|
||||||
} as FieldInsertType;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export const postgresAstToTable = (ast: any, data_types: DataType[], postgresTypes: any[]): TableInsertType => {
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.name.name,
|
|
||||||
fields: ast.columns.filter((column: any) => column.kind == "column")
|
|
||||||
.map((fieldAst: any, index: number) => postgresAstToField(fieldAst, data_types, index, postgresTypes)),
|
|
||||||
color: randomColor(),
|
|
||||||
} as TableInsertType;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const postgresAstToField = (ast: any, data_types: DataType[], sequence: number, postgresTypes: any[]): FieldInsertType => {
|
|
||||||
|
|
||||||
let dataType: DataType | undefined = data_types.find((dataType: DataType) => {
|
|
||||||
const synonyms: string[] = dataType.synonyms ? JSON.parse(dataType.synonyms) : [];
|
|
||||||
return dataType.name == ast.dataType.name?.toLowerCase() || synonyms.includes(ast.dataType.name?.toLowerCase())
|
|
||||||
});
|
|
||||||
|
|
||||||
let values: string | undefined;
|
|
||||||
let autoIncrement: boolean = false;
|
|
||||||
|
|
||||||
if (!dataType && postgresTypes && postgresTypes.length > 0) {
|
|
||||||
const postgresType: any = postgresTypes.find((type: any) => type.name.name == ast.dataType.name);
|
|
||||||
if (postgresType) {
|
|
||||||
dataType = data_types.find((dataType: DataType) => dataType.name == "enum") as DataType;
|
|
||||||
values = JSON.stringify(postgresType.values.map((value: any) => value.value));
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ast.dataType.name?.toLowerCase().includes("serial")) {
|
|
||||||
|
|
||||||
let baseType: string = ast.dataType.name?.toLowerCase() == "serial" ? "integer" : ast.dataType.name?.toLowerCase().replace("serial", "int");
|
|
||||||
dataType = data_types.find((dataType: DataType) => {
|
|
||||||
return dataType.name == baseType;
|
|
||||||
});
|
|
||||||
autoIncrement = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
const modifiers: string[] = dataType?.modifiers ? JSON.parse(dataType.modifiers) : [];
|
|
||||||
|
|
||||||
let length: number | undefined;
|
|
||||||
let scale: number | undefined;
|
|
||||||
let unique: boolean = false;
|
|
||||||
let primaryKey: boolean = false;
|
|
||||||
|
|
||||||
if (ast.dataType.config && ast.dataType.config.length > 0) {
|
|
||||||
if (ast.dataType.config.length >= 1)
|
|
||||||
length = ast.dataType.config[0];
|
|
||||||
|
|
||||||
if (ast.dataType.config.length == 2)
|
|
||||||
scale = ast.dataType.config[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
let maxLength: number | null = null;
|
|
||||||
let precision: number | null = null;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.LENGTH) && length)
|
|
||||||
maxLength = length;
|
|
||||||
|
|
||||||
if (modifiers.includes(Modifiers.PRECISION) && length)
|
|
||||||
precision = length;
|
|
||||||
|
|
||||||
let defaultValue: string | undefined;
|
|
||||||
let nullable: boolean = true;
|
|
||||||
|
|
||||||
const constraints: any[] | undefined = ast.constraints;
|
|
||||||
if (constraints && constraints.length > 0) {
|
|
||||||
|
|
||||||
const nullableConstraints: any | undefined = constraints.find((c: any) => c.type == "not null");
|
|
||||||
const defaultValueConstraints: any | undefined = constraints.find((c: any) => c.type == "default");
|
|
||||||
|
|
||||||
if (defaultValueConstraints) {
|
|
||||||
if (defaultValueConstraints.default.type == "keyword" && defaultValueConstraints.default.keyword == "current_timestamp")
|
|
||||||
defaultValue = TimeDefaultValues.NOW;
|
|
||||||
|
|
||||||
else if (defaultValueConstraints.default.type == "call" && defaultValueConstraints.default.function?.name == "now")
|
|
||||||
defaultValue = TimeDefaultValues.NOW;
|
|
||||||
|
|
||||||
else if (defaultValueConstraints.default.type == "cast" && defaultValueConstraints.default.operand)
|
|
||||||
defaultValue = String(defaultValueConstraints.default.operand.value)
|
|
||||||
else
|
|
||||||
defaultValue = String(defaultValueConstraints.default.value);
|
|
||||||
}
|
|
||||||
const uniqueConstraints: any | undefined = constraints.find((c: any) => c.type == "unique");
|
|
||||||
const primryKeyConstraints: any | undefined = constraints.find((c: any) => c.type == "primary key");
|
|
||||||
|
|
||||||
if (uniqueConstraints)
|
|
||||||
unique = true;
|
|
||||||
if (primryKeyConstraints)
|
|
||||||
primaryKey = true;
|
|
||||||
|
|
||||||
if (nullableConstraints || primaryKey)
|
|
||||||
nullable = false;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.name.name,
|
|
||||||
defaultValue,
|
|
||||||
typeId: dataType?.id,
|
|
||||||
nullable,
|
|
||||||
unique,
|
|
||||||
maxLength,
|
|
||||||
precision,
|
|
||||||
scale,
|
|
||||||
isPrimary: primaryKey,
|
|
||||||
sequence,
|
|
||||||
values,
|
|
||||||
autoIncrement
|
|
||||||
} as FieldInsertType;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const postgresAstToRelationship = (ast: any, tables: TableInsertType[]): RelationshipInsertType[] => {
|
|
||||||
|
|
||||||
const relationships: RelationshipInsertType[] = [];
|
|
||||||
const changes = ast.changes;
|
|
||||||
|
|
||||||
const targetTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name);
|
|
||||||
|
|
||||||
if (!targetTable)
|
|
||||||
throw Error("source table not found");
|
|
||||||
|
|
||||||
const foreignKeyConstraints = changes.filter((change: any) => change.type == 'add constraint' && change.constraint && change.constraint.type == "foreign key").map((change: any) => change.constraint);
|
|
||||||
|
|
||||||
for (const foreignKeyConstraint of foreignKeyConstraints) {
|
|
||||||
|
|
||||||
const sourceTable: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == foreignKeyConstraint.foreignTable.name);
|
|
||||||
const targetField: FieldInsertType | undefined = targetTable.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.localColumns[0]?.name)
|
|
||||||
const sourceField: FieldInsertType | undefined = sourceTable?.fields?.find((field: FieldInsertType) => field.name == foreignKeyConstraint.foreignColumns[0]?.name)
|
|
||||||
|
|
||||||
const onDelete = foreignKeyConstraint.onDelete ? astToForiegnKeyAction(foreignKeyConstraint.onDelete) : undefined;
|
|
||||||
const onUpdate = foreignKeyConstraint.onUpdate ? astToForiegnKeyAction(foreignKeyConstraint.onUpdate) : undefined;
|
|
||||||
|
|
||||||
if (!sourceField || !targetField || !sourceTable)
|
|
||||||
continue;
|
|
||||||
|
|
||||||
relationships.push({
|
|
||||||
id: v4(),
|
|
||||||
sourceTableId: sourceTable.id,
|
|
||||||
targetTableId: targetTable.id,
|
|
||||||
sourceFieldId: sourceField.id,
|
|
||||||
targetFieldId: targetField.id,
|
|
||||||
cardinality: targetField.unique ? Cardinality.one_to_one : Cardinality.one_to_many,
|
|
||||||
onDelete, onUpdate
|
|
||||||
} as RelationshipInsertType)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (relationships.length == foreignKeyConstraints.length)
|
|
||||||
return relationships;
|
|
||||||
else
|
|
||||||
throw Error({
|
|
||||||
success: false,
|
|
||||||
message: "Failed to Extract all relationships",
|
|
||||||
relationships
|
|
||||||
} as any)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const foreignKeyConstraintToAlterTableAst = (constraints: any[], table: TableInsertType) => {
|
|
||||||
|
|
||||||
const changes: any[] = constraints.filter((constraint: any) => constraint.type == "foreign key" || constraint.type == "reference").map((constraint: any) => ({
|
|
||||||
|
|
||||||
type: "add constraint",
|
|
||||||
constraint: {
|
|
||||||
type: "foreign key",
|
|
||||||
localColumns: [
|
|
||||||
{
|
|
||||||
name: constraint.localColumns?.[0].name
|
|
||||||
}
|
|
||||||
],
|
|
||||||
foreignTable: {
|
|
||||||
name: constraint.foreignTable.name,
|
|
||||||
},
|
|
||||||
foreignColumns: [
|
|
||||||
{
|
|
||||||
name: constraint.foreignColumns?.[0].name
|
|
||||||
}
|
|
||||||
],
|
|
||||||
onDelete: constraint.onDelete,
|
|
||||||
onUpdate: constraint.onUpdate,
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "alter table",
|
|
||||||
only: true,
|
|
||||||
table: {
|
|
||||||
name: table.name
|
|
||||||
},
|
|
||||||
changes
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const astToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType => {
|
|
||||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.table);
|
|
||||||
|
|
||||||
if (!table)
|
|
||||||
throw Error("table not found");
|
|
||||||
|
|
||||||
const fieldNames: string[] = ast.index_columns.map((column: any) => column.column);
|
|
||||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
|
||||||
.map((field: FieldInsertType) => field.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.index,
|
|
||||||
tableId: table.id,
|
|
||||||
unique: ast.index_type == "unique",
|
|
||||||
fieldIndices: fieldIds?.map((id: string) => ({
|
|
||||||
id: v4(),
|
|
||||||
fieldId: id
|
|
||||||
}))
|
|
||||||
} as IndexInsertType
|
|
||||||
}
|
|
||||||
|
|
||||||
export const postgresAstToIndex = (ast: any, tables: TableInsertType[]): IndexInsertType => {
|
|
||||||
const table: TableInsertType | undefined = tables.find((table: TableInsertType) => table.name == ast.table.name);
|
|
||||||
|
|
||||||
if (!table)
|
|
||||||
throw Error("table not found");
|
|
||||||
|
|
||||||
const fieldNames: string[] = ast.expressions.map((expression: any) => expression.expression.name);
|
|
||||||
|
|
||||||
|
|
||||||
const fieldIds: string[] | undefined = table.fields?.filter((field: FieldInsertType) => fieldNames.includes(field.name))
|
|
||||||
.map((field: FieldInsertType) => field.id);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: v4(),
|
|
||||||
name: ast.indexName.name,
|
|
||||||
tableId: table.id,
|
|
||||||
unique: ast.unique,
|
|
||||||
fieldIndices: fieldIds?.map((id: string) => ({
|
|
||||||
id: v4(),
|
|
||||||
fieldId: id
|
|
||||||
}))
|
|
||||||
} as IndexInsertType
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
import { DatabaseDialect } from "@/lib/database";
|
||||||
|
import { DataType } from "@/lib/schemas/data-type-schema";
|
||||||
|
import MysqlRenderer from "./database/mysql-renderer";
|
||||||
|
import PostgresqlRenderer from "./database/postgresql-renderer";
|
||||||
|
import MariaDbRenderer from "./database/mariadb-renderer";
|
||||||
|
import OracleRenderer from "./database/oracle-renderer";
|
||||||
|
import MSSqlRenderer from "./database/mssql-database-renderer";
|
||||||
|
import SqliteRenderer from "./database/sqlite-database-renderer";
|
||||||
|
import { TableType } from "@/lib/schemas/table-schema";
|
||||||
|
import { FieldType } from "@/lib/schemas/field-schema";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Fixes charset/collation placement in SQL CREATE TABLE statements
|
* Fixes charset/collation placement in SQL CREATE TABLE statements
|
||||||
* @param sql The SQL string to process
|
* @param sql The SQL string to process
|
||||||
@@ -151,4 +162,32 @@ export interface CircularDependencyError {
|
|||||||
cycle: string[];
|
cycle: string[];
|
||||||
success: boolean;
|
success: boolean;
|
||||||
message: string;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1
-1
@@ -12,7 +12,7 @@ export default defineConfig({
|
|||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
// Don't optimize these packages as they contain web workers and WASM files.
|
// Don't optimize these packages as they contain web workers and WASM files.
|
||||||
// https://github.com/vitejs/vite/issues/11672#issuecomment-1415820673
|
// 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']
|
include: ['@powersync/web > js-logger']
|
||||||
},
|
},
|
||||||
worker: {
|
worker: {
|
||||||
|
|||||||
Reference in New Issue
Block a user