diff --git a/modules/system/databaseMigration.py b/modules/system/databaseMigration.py index 2637a61d..6089ad0a 100644 --- a/modules/system/databaseMigration.py +++ b/modules/system/databaseMigration.py @@ -545,6 +545,74 @@ def _prepareImport(payload: dict) -> dict: } +def _ensureDatabaseExists(dbName: str) -> bool: + """Create the PostgreSQL database if it does not yet exist. + + Connects to the ``postgres`` admin database using the same credentials + as the target DB. Returns True if the database was created, False if + it already existed. + """ + registeredDbs = getRegisteredDatabases() + configPrefix = registeredDbs.get(dbName) + if configPrefix is None: + return False + + hostKey = f"{configPrefix}_HOST" if configPrefix != "DB" else "DB_HOST" + portKey = f"{configPrefix}_PORT" if configPrefix != "DB" else "DB_PORT" + userKey = f"{configPrefix}_USER" if configPrefix != "DB" else "DB_USER" + passwordKey = f"{configPrefix}_PASSWORD_SECRET" if configPrefix != "DB" else "DB_PASSWORD_SECRET" + + adminConn = psycopg2.connect( + host=APP_CONFIG.get(hostKey, "localhost"), + port=int(APP_CONFIG.get(portKey, 5432)), + database="postgres", + user=APP_CONFIG.get(userKey), + password=APP_CONFIG.get(passwordKey), + client_encoding="utf8", + ) + try: + adminConn.autocommit = True + with adminConn.cursor() as cur: + cur.execute("SELECT 1 FROM pg_database WHERE datname = %s", (dbName,)) + if cur.fetchone(): + return False + cur.execute(f'CREATE DATABASE "{dbName}"') + logger.info("Created missing database: %s", dbName) + return True + finally: + adminConn.close() + + +def _createTableFromExport(conn, tableName: str, rows: List[dict]) -> None: + """Create a table based on the column structure found in the export data. + + Uses TEXT for all columns since we don't have the original DDL. + The ``id`` column gets a PRIMARY KEY constraint. + """ + allKeys: List[str] = [] + seen: set = set() + for row in rows: + for k in row.keys(): + if k not in seen: + allKeys.append(k) + seen.add(k) + + if not allKeys: + return + + colDefs = [] + for col in allKeys: + if col == "id": + colDefs.append(f'"{col}" TEXT PRIMARY KEY') + else: + colDefs.append(f'"{col}" TEXT') + + ddl = f'CREATE TABLE IF NOT EXISTS "{tableName}" ({", ".join(colDefs)})' + with conn.cursor() as cur: + cur.execute(ddl) + logger.info("Created table %s with %d columns", tableName, len(allKeys)) + + def _importSingleDb(payload: dict, dbName: str, mode: str, protectedIds: List[str]) -> dict: """Import a single database from the (already remapped) payload. @@ -563,11 +631,21 @@ def _importSingleDb(payload: dict, dbName: str, mode: str, protectedIds: List[st return {"database": dbName, "tables": {}, "recordCount": 0, "warnings": [f"Keine Daten fuer '{dbName}' im Payload"]} + try: + dbCreated = _ensureDatabaseExists(dbName) + except Exception as e: + logger.error("Failed to ensure database %s exists: %s", dbName, e) + return {"database": dbName, "tables": {}, "recordCount": 0, + "warnings": [f"Datenbank '{dbName}' konnte nicht erstellt werden: {e}"]} + protectedIdSet = set(protectedIds) tables = dbData.get("tables", {}) warnings: List[str] = [] dbResult: Dict[str, int] = {} + if dbCreated: + warnings.append(f"Datenbank '{dbName}' wurde neu erstellt") + conn = _getConnection(dbName) try: conn.autocommit = False @@ -576,9 +654,11 @@ def _importSingleDb(payload: dict, dbName: str, mode: str, protectedIds: List[st for tableName, rows in tables.items(): if not isinstance(rows, list): continue + if tableName not in existingTables: - warnings.append(f"Tabelle '{dbName}.{tableName}' existiert nicht, uebersprungen") - continue + _createTableFromExport(conn, tableName, rows) + conn.commit() + conn.autocommit = False physicalCols = _getPhysicalColumns(conn, tableName) if not physicalCols: