diff --git a/docs/VERSIONS.md b/docs/VERSIONS.md index 02978fbf..fa00bb09 100755 --- a/docs/VERSIONS.md +++ b/docs/VERSIONS.md @@ -22,4 +22,4 @@ For a comparison, this is how the UI looks like if you are on the latest stable ## Implementation details -During build a [/app/front/buildtimestamp.txt](https://github.com/jokob-sk/NetAlertX/blob/092797e75ccfa8359444ad149e727358ac4da05f/Dockerfile#L44) file is created. The app then periodically checks if a new release is available with a newer timestamp in GitHub's rest-based JSON endpoint (check the `def isNewVersion():` method for details). \ No newline at end of file +During build a [/app/front/buildtimestamp.txt](https://github.com/jokob-sk/NetAlertX/blob/092797e75ccfa8359444ad149e727358ac4da05f/Dockerfile#L44) file is created. The app then periodically checks if a new release is available with a newer timestamp in GitHub's rest-based JSON endpoint (check the `def isNewVersion:` method for details). \ No newline at end of file diff --git a/front/php/templates/language/ru_ru.json b/front/php/templates/language/ru_ru.json old mode 100644 new mode 100755 diff --git a/server/__main__.py b/server/__main__.py index 4e9aae78..e6a13f70 100755 --- a/server/__main__.py +++ b/server/__main__.py @@ -78,8 +78,8 @@ def main (): db.open() sql = db.sql # To-Do replace with the db class - # Upgrade DB if needed - db.upgradeDB() + # Init DB + db.initDB() # Initialize the WorkflowManager workflow_manager = WorkflowManager(db) diff --git a/server/database.py b/server/database.py index 36576bbe..5ff5954d 100755 --- a/server/database.py +++ b/server/database.py @@ -10,6 +10,7 @@ from const import fullDbPath, sql_devices_stats, sql_devices_all, sql_generateGu from logger import mylog from helper import json_obj, initOrSetParam, row_to_json, timeNowTZ from workflows.app_events import AppEvent_obj +from db.db_upgrade import ensure_column, ensure_views, ensure_CurrentScan, ensure_plugins_tables, ensure_Parameters, ensure_Settings class DB(): """ @@ -76,287 +77,35 @@ class DB(): return arr #------------------------------------------------------------------------------- - def upgradeDB(self): + def initDB(self): """ Check the current tables in the DB and upgrade them if neccessary """ - # ------------------------------------------------------------------------- - # Alter Devices table - # ------------------------------------------------------------------------- - - # VIEWS - - self.sql.execute(""" DROP VIEW IF EXISTS Events_Devices;""") - self.sql.execute(""" CREATE VIEW Events_Devices AS - SELECT * - FROM Events - LEFT JOIN Devices ON eve_MAC = devMac; - """) - - - self.sql.execute(""" DROP VIEW IF EXISTS LatestEventsPerMAC;""") - self.sql.execute("""CREATE VIEW LatestEventsPerMAC AS - WITH RankedEvents AS ( - SELECT - e.*, - ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num - FROM Events AS e - ) - SELECT - e.*, - d.*, - c.* - FROM RankedEvents AS e - LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac - INNER JOIN CurrentScan AS c ON e.eve_MAC = c.cur_MAC - WHERE e.row_num = 1;""") - - self.sql.execute(""" DROP VIEW IF EXISTS Sessions_Devices;""") - self.sql.execute("""CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac;""") - - - # add fields if missing + # Add Devices fields if missing - # devFQDN missing? - devFQDN_missing = self.sql.execute (""" - SELECT COUNT(*) AS CNTREC FROM pragma_table_info('Devices') WHERE name='devFQDN' - """).fetchone()[0] == 0 + # devFQDN + if ensure_column(self.sql, "Devices", "devFQDN", "TEXT") is False: + return # addition failed - if devFQDN_missing: - - mylog('verbose', ["[upgradeDB] Adding devFQDN to the Devices table"]) - self.sql.execute(""" - ALTER TABLE "Devices" ADD "devFQDN" TEXT - """) - - - # ------------------------------------------------------------------------- # Settings table setup - # ------------------------------------------------------------------------- + ensure_Settings(self.sql) + # Parameters tables setup + ensure_Parameters(self.sql) - # Re-creating Settings table - mylog('verbose', ["[upgradeDB] Re-creating Settings table"]) - - self.sql.execute(""" DROP TABLE IF EXISTS Settings;""") - self.sql.execute(""" - CREATE TABLE "Settings" ( - "setKey" TEXT, - "setName" TEXT, - "setDescription" TEXT, - "setType" TEXT, - "setOptions" TEXT, - "setGroup" TEXT, - "setValue" TEXT, - "setEvents" TEXT, - "setOverriddenByEnv" INTEGER - ); - """) - - - # Create Pholus_Scan table if missing - mylog('verbose', ["[upgradeDB] Removing Pholus_Scan table"]) - self.sql.execute("""DROP TABLE IF EXISTS Pholus_Scan""") - - - # ------------------------------------------------------------------------- - # Parameters table setup - # ------------------------------------------------------------------------- - - # Re-creating Parameters table - mylog('verbose', ["[upgradeDB] Re-creating Parameters table"]) - self.sql.execute("DROP TABLE Parameters;") - - self.sql.execute(""" - CREATE TABLE "Parameters" ( - "par_ID" TEXT PRIMARY KEY, - "par_Value" TEXT - ); - """) - - - # ------------------------------------------------------------------------- # Plugins tables setup - # ------------------------------------------------------------------------- - - # Plugin state - sql_Plugins_Objects = """ CREATE TABLE IF NOT EXISTS Plugins_Objects( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, - ObjectGUID TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); """ - self.sql.execute(sql_Plugins_Objects) - - # Plugin execution results - sql_Plugins_Events = """ CREATE TABLE IF NOT EXISTS Plugins_Events( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); """ - self.sql.execute(sql_Plugins_Events) - - # Plugin execution history - sql_Plugins_History = """ CREATE TABLE IF NOT EXISTS Plugins_History( - "Index" INTEGER, - Plugin TEXT NOT NULL, - Object_PrimaryID TEXT NOT NULL, - Object_SecondaryID TEXT NOT NULL, - DateTimeCreated TEXT NOT NULL, - DateTimeChanged TEXT NOT NULL, - Watched_Value1 TEXT NOT NULL, - Watched_Value2 TEXT NOT NULL, - Watched_Value3 TEXT NOT NULL, - Watched_Value4 TEXT NOT NULL, - Status TEXT NOT NULL, - Extra TEXT NOT NULL, - UserData TEXT NOT NULL, - ForeignKey TEXT NOT NULL, - SyncHubNodeName TEXT, - "HelpVal1" TEXT, - "HelpVal2" TEXT, - "HelpVal3" TEXT, - "HelpVal4" TEXT, - PRIMARY KEY("Index" AUTOINCREMENT) - ); """ - self.sql.execute(sql_Plugins_History) - - - # ------------------------------------------------------------------------- - # Plugins_Language_Strings table setup - # ------------------------------------------------------------------------- - - # Dynamically generated language strings - self.sql.execute("DROP TABLE IF EXISTS Plugins_Language_Strings;") - self.sql.execute(""" CREATE TABLE IF NOT EXISTS Plugins_Language_Strings( - "Index" INTEGER, - Language_Code TEXT NOT NULL, - String_Key TEXT NOT NULL, - String_Value TEXT NOT NULL, - Extra TEXT NOT NULL, - PRIMARY KEY("Index" AUTOINCREMENT) - ); """) - - self.commitDB() - - - - # ------------------------------------------------------------------------- + ensure_plugins_tables(self.sql) + # CurrentScan table setup - # ------------------------------------------------------------------------- - - # indicates, if CurrentScan table is available - # 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes - self.sql.execute("DROP TABLE IF EXISTS CurrentScan;") - self.sql.execute(""" CREATE TABLE IF NOT EXISTS CurrentScan ( - cur_MAC STRING(50) NOT NULL COLLATE NOCASE, - cur_IP STRING(50) NOT NULL COLLATE NOCASE, - cur_Vendor STRING(250), - cur_ScanMethod STRING(10), - cur_Name STRING(250), - cur_LastQuery STRING(250), - cur_DateTime STRING(250), - cur_SyncHubNodeName STRING(50), - cur_NetworkSite STRING(250), - cur_SSID STRING(250), - cur_NetworkNodeMAC STRING(250), - cur_PORT STRING(250), - cur_Type STRING(250), - UNIQUE(cur_MAC) - ); - """) - - self.commitDB() - - # ------------------------------------------------------------------------- - # Create the LatestEventsPerMAC view - # ------------------------------------------------------------------------- - - # Dynamically generated language strings - self.sql.execute(""" CREATE VIEW IF NOT EXISTS LatestEventsPerMAC AS - WITH RankedEvents AS ( - SELECT - e.*, - ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num - FROM Events AS e - ) - SELECT - e.*, - d.*, - c.* - FROM RankedEvents AS e - LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac - INNER JOIN CurrentScan AS c ON e.eve_MAC = c.cur_MAC - WHERE e.row_num = 1; - """) - - # handling the Convert_Events_to_Sessions / Sessions screens - self.sql.execute("""DROP VIEW IF EXISTS Convert_Events_to_Sessions;""") - self.sql.execute("""CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC, - EVE1.eve_IP, - EVE1.eve_EventType AS eve_EventTypeConnection, - EVE1.eve_DateTime AS eve_DateTimeConnection, - CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') OR - EVE2.eve_EventType IS NULL THEN EVE2.eve_EventType ELSE '' END AS eve_EventTypeDisconnection, - CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') THEN EVE2.eve_DateTime ELSE NULL END AS eve_DateTimeDisconnection, - CASE WHEN EVE2.eve_EventType IS NULL THEN 1 ELSE 0 END AS eve_StillConnected, - EVE1.eve_AdditionalInfo - FROM Events AS EVE1 - LEFT JOIN - Events AS EVE2 ON EVE1.eve_PairEventRowID = EVE2.RowID - WHERE EVE1.eve_EventType IN ('New Device', 'Connected','Down Reconnected') - UNION - SELECT eve_MAC, - eve_IP, - '' AS eve_EventTypeConnection, - NULL AS eve_DateTimeConnection, - eve_EventType AS eve_EventTypeDisconnection, - eve_DateTime AS eve_DateTimeDisconnection, - 0 AS eve_StillConnected, - eve_AdditionalInfo - FROM Events AS EVE1 - WHERE (eve_EventType = 'Device Down' OR - eve_EventType = 'Disconnected') AND - EVE1.eve_PairEventRowID IS NULL; - """) - - self.commitDB() + ensure_CurrentScan(self.sql) + # Views + ensure_views(self.sql) + + # commit changes + self.commitDB() + # Init the AppEvent database table AppEvent_obj(self) diff --git a/server/db/db_upgrade.py b/server/db/db_upgrade.py new file mode 100755 index 00000000..9df032fb --- /dev/null +++ b/server/db/db_upgrade.py @@ -0,0 +1,330 @@ +import sys + +# Register NetAlertX directories +INSTALL_PATH="/app" +sys.path.extend([f"{INSTALL_PATH}/server"]) + +from logger import mylog +from messaging.in_app import write_notification + + +def ensure_column(sql, table: str, column_name: str, column_type: str) -> bool: + """ + Ensures a column exists in the specified table. If missing, attempts to add it. + Returns True on success, False on failure. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + - table: name of the table (e.g., "Devices"). + - column_name: name of the column to ensure. + - column_type: SQL type of the column (e.g., "TEXT", "INTEGER", "BOOLEAN"). + """ + + try: + # Get actual columns from DB + sql.execute(f'PRAGMA table_info("{table}")') + actual_columns = [row[1] for row in sql.fetchall()] + + # Check if target column is already present + if column_name in actual_columns: + return True # Already exists + + # Define the expected columns (hardcoded base schema) [v25.5.24] - available in teh default app.db + expected_columns = [ + 'devMac', 'devName', 'devOwner', 'devType', 'devVendor', + 'devFavorite', 'devGroup', 'devComments', 'devFirstConnection', + 'devLastConnection', 'devLastIP', 'devStaticIP', 'devScan', + 'devLogEvents', 'devAlertEvents', 'devAlertDown', 'devSkipRepeated', + 'devLastNotification', 'devPresentLastScan', 'devIsNew', + 'devLocation', 'devIsArchived', 'devParentMAC', 'devParentPort', + 'devIcon', 'devGUID', 'devSite', 'devSSID', 'devSyncHubNode', + 'devSourcePlugin', 'devCustomProps' + ] + + # Check for mismatches in base schema + missing = set(expected_columns) - set(actual_columns) + extra = set(actual_columns) - set(expected_columns) + + if missing: + msg = (f"[db_upgrade] ⚠ ERROR: Unexpected DB structure " + f"(missing: {', '.join(missing) if missing else 'none'}, " + f"extra: {', '.join(extra) if extra else 'none'}) - " + "aborting schema change to prevent corruption. " + "Check https://github.com/jokob-sk/NetAlertX/blob/main/docs/UPDATES.md") + mylog('none', [msg]) + write_notification(msg) + return False + + if extra: + msg = f'[db_upgrade] Extra DB columns detected in {table}: {', '.join(extra)}' + mylog('none', [msg]) + + # Add missing column + mylog('verbose', [f"[db_upgrade] Adding '{column_name}' ({column_type}) to {table} table"]) + sql.execute(f'ALTER TABLE "{table}" ADD "{column_name}" {column_type}') + return True + + except Exception as e: + mylog('none', [f"[db_upgrade] ERROR while adding '{column_name}': {e}"]) + return False + + +def ensure_views(sql) -> bool: + """ + Ensures required views exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + sql.execute(""" DROP VIEW IF EXISTS Events_Devices;""") + sql.execute(""" CREATE VIEW Events_Devices AS + SELECT * + FROM Events + LEFT JOIN Devices ON eve_MAC = devMac; + """) + + + sql.execute(""" DROP VIEW IF EXISTS LatestEventsPerMAC;""") + sql.execute("""CREATE VIEW LatestEventsPerMAC AS + WITH RankedEvents AS ( + SELECT + e.*, + ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num + FROM Events AS e + ) + SELECT + e.*, + d.*, + c.* + FROM RankedEvents AS e + LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac + INNER JOIN CurrentScan AS c ON e.eve_MAC = c.cur_MAC + WHERE e.row_num = 1;""") + + sql.execute(""" DROP VIEW IF EXISTS Sessions_Devices;""") + sql.execute("""CREATE VIEW Sessions_Devices AS SELECT * FROM Sessions LEFT JOIN "Devices" ON ses_MAC = devMac;""") + + sql.execute(""" CREATE VIEW IF NOT EXISTS LatestEventsPerMAC AS + WITH RankedEvents AS ( + SELECT + e.*, + ROW_NUMBER() OVER (PARTITION BY e.eve_MAC ORDER BY e.eve_DateTime DESC) AS row_num + FROM Events AS e + ) + SELECT + e.*, + d.*, + c.* + FROM RankedEvents AS e + LEFT JOIN Devices AS d ON e.eve_MAC = d.devMac + INNER JOIN CurrentScan AS c ON e.eve_MAC = c.cur_MAC + WHERE e.row_num = 1; + """) + + # handling the Convert_Events_to_Sessions / Sessions screens + sql.execute("""DROP VIEW IF EXISTS Convert_Events_to_Sessions;""") + sql.execute("""CREATE VIEW Convert_Events_to_Sessions AS SELECT EVE1.eve_MAC, + EVE1.eve_IP, + EVE1.eve_EventType AS eve_EventTypeConnection, + EVE1.eve_DateTime AS eve_DateTimeConnection, + CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') OR + EVE2.eve_EventType IS NULL THEN EVE2.eve_EventType ELSE '' END AS eve_EventTypeDisconnection, + CASE WHEN EVE2.eve_EventType IN ('Disconnected', 'Device Down') THEN EVE2.eve_DateTime ELSE NULL END AS eve_DateTimeDisconnection, + CASE WHEN EVE2.eve_EventType IS NULL THEN 1 ELSE 0 END AS eve_StillConnected, + EVE1.eve_AdditionalInfo + FROM Events AS EVE1 + LEFT JOIN + Events AS EVE2 ON EVE1.eve_PairEventRowID = EVE2.RowID + WHERE EVE1.eve_EventType IN ('New Device', 'Connected','Down Reconnected') + UNION + SELECT eve_MAC, + eve_IP, + '' AS eve_EventTypeConnection, + NULL AS eve_DateTimeConnection, + eve_EventType AS eve_EventTypeDisconnection, + eve_DateTime AS eve_DateTimeDisconnection, + 0 AS eve_StillConnected, + eve_AdditionalInfo + FROM Events AS EVE1 + WHERE (eve_EventType = 'Device Down' OR + eve_EventType = 'Disconnected') AND + EVE1.eve_PairEventRowID IS NULL; + """) + + return True + +def ensure_CurrentScan(sql) -> bool: + """ + Ensures required CurrentScan table exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + # 🐛 CurrentScan DEBUG: comment out below when debugging to keep the CurrentScan table after restarts/scan finishes + sql.execute("DROP TABLE IF EXISTS CurrentScan;") + sql.execute(""" CREATE TABLE IF NOT EXISTS CurrentScan ( + cur_MAC STRING(50) NOT NULL COLLATE NOCASE, + cur_IP STRING(50) NOT NULL COLLATE NOCASE, + cur_Vendor STRING(250), + cur_ScanMethod STRING(10), + cur_Name STRING(250), + cur_LastQuery STRING(250), + cur_DateTime STRING(250), + cur_SyncHubNodeName STRING(50), + cur_NetworkSite STRING(250), + cur_SSID STRING(250), + cur_NetworkNodeMAC STRING(250), + cur_PORT STRING(250), + cur_Type STRING(250), + UNIQUE(cur_MAC) + ); + """) + + return True + +def ensure_Parameters(sql) -> bool: + """ + Ensures required Parameters table exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + + # Re-creating Parameters table + mylog('verbose', ["[db_upgrade] Re-creating Parameters table"]) + sql.execute("DROP TABLE Parameters;") + + sql.execute(""" + CREATE TABLE "Parameters" ( + "par_ID" TEXT PRIMARY KEY, + "par_Value" TEXT + ); + """) + + return True + +def ensure_Settings(sql) -> bool: + """ + Ensures required Settings table exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + + # Re-creating Settings table + mylog('verbose', ["[db_upgrade] Re-creating Settings table"]) + + sql.execute(""" DROP TABLE IF EXISTS Settings;""") + sql.execute(""" + CREATE TABLE "Settings" ( + "setKey" TEXT, + "setName" TEXT, + "setDescription" TEXT, + "setType" TEXT, + "setOptions" TEXT, + "setGroup" TEXT, + "setValue" TEXT, + "setEvents" TEXT, + "setOverriddenByEnv" INTEGER + ); + """) + + return True + + +def ensure_plugins_tables(sql) -> bool: + """ + Ensures required plugins tables exist. + + Parameters: + - sql: database cursor or connection wrapper (must support execute() and fetchall()). + """ + + # Plugin state + sql_Plugins_Objects = """ CREATE TABLE IF NOT EXISTS Plugins_Objects( + "Index" INTEGER, + Plugin TEXT NOT NULL, + Object_PrimaryID TEXT NOT NULL, + Object_SecondaryID TEXT NOT NULL, + DateTimeCreated TEXT NOT NULL, + DateTimeChanged TEXT NOT NULL, + Watched_Value1 TEXT NOT NULL, + Watched_Value2 TEXT NOT NULL, + Watched_Value3 TEXT NOT NULL, + Watched_Value4 TEXT NOT NULL, + Status TEXT NOT NULL, + Extra TEXT NOT NULL, + UserData TEXT NOT NULL, + ForeignKey TEXT NOT NULL, + SyncHubNodeName TEXT, + "HelpVal1" TEXT, + "HelpVal2" TEXT, + "HelpVal3" TEXT, + "HelpVal4" TEXT, + ObjectGUID TEXT, + PRIMARY KEY("Index" AUTOINCREMENT) + ); """ + sql.execute(sql_Plugins_Objects) + + # Plugin execution results + sql_Plugins_Events = """ CREATE TABLE IF NOT EXISTS Plugins_Events( + "Index" INTEGER, + Plugin TEXT NOT NULL, + Object_PrimaryID TEXT NOT NULL, + Object_SecondaryID TEXT NOT NULL, + DateTimeCreated TEXT NOT NULL, + DateTimeChanged TEXT NOT NULL, + Watched_Value1 TEXT NOT NULL, + Watched_Value2 TEXT NOT NULL, + Watched_Value3 TEXT NOT NULL, + Watched_Value4 TEXT NOT NULL, + Status TEXT NOT NULL, + Extra TEXT NOT NULL, + UserData TEXT NOT NULL, + ForeignKey TEXT NOT NULL, + SyncHubNodeName TEXT, + "HelpVal1" TEXT, + "HelpVal2" TEXT, + "HelpVal3" TEXT, + "HelpVal4" TEXT, + PRIMARY KEY("Index" AUTOINCREMENT) + ); """ + sql.execute(sql_Plugins_Events) + + # Plugin execution history + sql_Plugins_History = """ CREATE TABLE IF NOT EXISTS Plugins_History( + "Index" INTEGER, + Plugin TEXT NOT NULL, + Object_PrimaryID TEXT NOT NULL, + Object_SecondaryID TEXT NOT NULL, + DateTimeCreated TEXT NOT NULL, + DateTimeChanged TEXT NOT NULL, + Watched_Value1 TEXT NOT NULL, + Watched_Value2 TEXT NOT NULL, + Watched_Value3 TEXT NOT NULL, + Watched_Value4 TEXT NOT NULL, + Status TEXT NOT NULL, + Extra TEXT NOT NULL, + UserData TEXT NOT NULL, + ForeignKey TEXT NOT NULL, + SyncHubNodeName TEXT, + "HelpVal1" TEXT, + "HelpVal2" TEXT, + "HelpVal3" TEXT, + "HelpVal4" TEXT, + PRIMARY KEY("Index" AUTOINCREMENT) + ); """ + sql.execute(sql_Plugins_History) + + # Dynamically generated language strings + sql.execute("DROP TABLE IF EXISTS Plugins_Language_Strings;") + sql.execute(""" CREATE TABLE IF NOT EXISTS Plugins_Language_Strings( + "Index" INTEGER, + Language_Code TEXT NOT NULL, + String_Key TEXT NOT NULL, + String_Value TEXT NOT NULL, + Extra TEXT NOT NULL, + PRIMARY KEY("Index" AUTOINCREMENT) + ); """) + + return True \ No newline at end of file