Skip to content

Conversation

@ingoratsdorf
Copy link
Contributor

@ingoratsdorf ingoratsdorf commented Sep 10, 2025

Deleted unused imports, added more appropriate PRAGMA synchronous=NORMAL to the db open function. Tidied up some code and made added Flake8 suggestions, optimised get_sql_array.
Whitespace fixes (part of Flake8).

The change looks like it yielded some further CPU cycle savings and most of all reduces the amount of filesystem writes.
image

Comparison of CPU use before and after the changes:
image

Summary by CodeRabbit

  • Refactor

    • Database now uses write-ahead logging and tuned pragmas for faster, more reliable reads/writes; schema upgrades run inside explicit transactions to prevent partial migrations.
  • Bug Fixes

    • Safer schema migration with rollback on failure and improved handling of missing fields.
  • Chores

    • Reduced log verbosity and streamlined error reporting to reduce noise.
  • New Features

    • Added a helper for creating per-thread database connections to improve concurrency.

Added PRAGMAs for better DB performance on open. Integrated some Fake8 comments and eliminated some looping with more efficient pyton functions.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Sep 10, 2025

Walkthrough

The PR refactors server/database.py: enables WAL and related PRAGMAs on open, tightens error logging, simplifies helpers (e.g., get_sql_array), makes schema upgrades transactional (BEGIN IMMEDIATE), and adds a new get_temp_db_connection() for per-thread SQLite connections.

Changes

Cohort / File(s) Summary
Open / Connection setup
server/database.py
DB.open() now initializes SQLite with WAL (journal_mode=WAL), synchronous=NORMAL, temp_store=MEMORY, busy timeout, and Row factory; avoids re-opening and logs with adjusted verbosity.
Transactional init / schema upgrades
server/database.py
initDB() runs schema upgrades inside BEGIN IMMEDIATE; transaction, adds missing Device fields with explicit checks, creates views/indexes, commits on success and rolls back+re-raises on failure.
Helpers & refactors
server/database.py
get_sql_array() simplified to [list(row) for row in rows]; added/expanded docstrings for helpers (get_device_stats, get_all_devices, get_array_from_sql_rows); removed unused imports and minor style fixes.
Read/Query logging & error handling
server/database.py
Reduced verbosity and standardized logging in get_table_as_json, read, read_one, commitDB, and rollbackDB; commitDB() now returns boolean success.
New public API
server/database.py
Added get_temp_db_connection() to create a per-thread SQLite connection configured with WAL, busy timeout and Row factory.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor Caller
  participant DB as server/database.py
  participant SQLite

  Caller->>DB: get_temp_db_connection()
  activate DB
  DB->>SQLite: sqlite3.connect(file or :memory:)
  Note right of SQLite: PRAGMAs set:\n- journal_mode = WAL\n- synchronous = NORMAL\n- temp_store = MEMORY\n- busy_timeout\n- row_factory = Row
  SQLite-->>DB: Connection (Row factory)
  DB-->>Caller: Connection handle
  deactivate DB

  rect rgba(230,245,255,0.5)
    Note over Caller,DB: Caller uses connection for operations\n(read/read_one/get_table_as_json)
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Pre-merge checks (3 passed)

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title "DB functions tidyup and streamlining" accurately reflects the primary focus of the changeset — refactoring and optimization of database-related code in server/database.py (import cleanup, PRAGMA/WAL and synchronous changes, get_sql_array optimization, transactional schema upgrades, and a new per-thread connection helper) — and is concise enough for a quick scan by teammates.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

Poem

I hop through WAL-lit burrows bright,
Temp stores warm beneath the night.
Threads find tunnels, rows in line,
Transactions snug — integrity fine.
A rabbit nibbles, approves the plight. 🥕🐇

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
server/database.py (2)

85-124: Wrap schema upgrades in a single transaction to avoid partial upgrades

With isolation_level=None (autocommit), early returns on failure can leave the DB half-upgraded. Wrap initDB in an explicit transaction and rollback on error.

 def initDB(self):
@@
-        # Add Devices fields if missing
-        # devFQDN
-        if ensure_column(self.sql, "Devices", "devFQDN", "TEXT") is False:
-            return  # addition failed
-        # devParentRelType 
-        if ensure_column(self.sql, "Devices", "devParentRelType", "TEXT") is False:
-            return  # addition failed
-        # devRequireNicsOnline 
-        if ensure_column(self.sql, "Devices", "devReqNicsOnline", "INTEGER") is False:
-            return  # addition failed
+        try:
+            # Start transactional upgrade
+            self.sql_connection.execute('BEGIN IMMEDIATE;')
+
+            # Add Devices fields if missing
+            if ensure_column(self.sql, "Devices", "devFQDN", "TEXT") is False:
+                raise RuntimeError("ensure_column(devFQDN) failed")
+            if ensure_column(self.sql, "Devices", "devParentRelType", "TEXT") is False:
+                raise RuntimeError("ensure_column(devParentRelType) failed")
+            if ensure_column(self.sql, "Devices", "devReqNicsOnline", "INTEGER") is False:
+                raise RuntimeError("ensure_column(devReqNicsOnline) failed")
@@
-        # commit changes
-        self.commitDB()
+            # commit changes
+            self.commitDB()
+        except Exception as e:
+            self.rollbackDB()
+            mylog('minimal', ['[Database] - initDB aborted, rolled back: ', e])
+            return

184-196: Prevent TypeError when read() returns None

If read() returns None on error, len(rows) raises TypeError. Guard for falsy rows first.

-        rows = self.read(query, *args)
-        if len(rows) == 1:
-            return rows[0]
-
-        if len(rows) > 1:
+        rows = self.read(query, *args)
+        if not rows:
+            return None
+        if len(rows) == 1:
+            return rows[0]
+        if len(rows) > 1:
             mylog('verbose',[ '[Database] - Warning!: query returns multiple rows, only first row is passed on!', query, " params: ", args])
             return rows[0]
         # empty result set
         return None
🧹 Nitpick comments (7)
server/database.py (7)

25-45: WAL + PRAGMAs: good; add foreign_keys and busy_timeout, and keep PRAGMAs consistent with temp conns

To enforce referential integrity and reduce lock errors, enable foreign keys and set busy_timeout here (you already do it in get_temp_db_connection). Also consider verifying WAL activation result.

 def open(self):
@@
-            self.sql_connection = sqlite3.connect(fullDbPath, isolation_level=None)
+            self.sql_connection = sqlite3.connect(fullDbPath, isolation_level=None)
@@
             self.sql_connection.execute('pragma journal_mode=WAL;')
             # When synchronous is NORMAL (1), the SQLite database engine will still sync
             # at the most critical moments, but less often than in FULL mode.
             self.sql_connection.execute('PRAGMA synchronous=NORMAL;')
             # When temp_store is MEMORY (2) temporary tables and indices
             # are kept as if they were in pure in-memory databases.
             self.sql_connection.execute('PRAGMA temp_store=MEMORY;')
+            # Enforce FK constraints and avoid immediate "database is locked" errors
+            self.sql_connection.execute('PRAGMA foreign_keys=ON;')
+            self.sql_connection.execute('PRAGMA busy_timeout=5000;')

Note: WAL on network-mounted filesystems can be problematic; if that’s a deployment scenario, make this configurable and fall back to DELETE journal mode.


27-31: Nit: log message name

The log says "openDB" but the method is open(); align for consistency.

-            mylog('debug', 'openDB: database already open')
+            mylog('debug', 'open: database already open')

70-82: Good vectorized conversion; fix log message and consider returning empty list

Nice list-comprehension switch. Update the log string to the function name; also prefer returning [] explicitly when DB not open for a stable return type.

-            mylog('debug', 'getQueryArray: database is not open')
+            mylog('debug', 'get_sql_array: database is not open')
-            return
+            return []

116-121: Comment label mismatch

This section creates indexes, not views. Minor comment fix.

-        # Views
+        # Indexes
         ensure_Indexes(self.sql)

168-181: Support CTEs ('WITH ...') in read(), current SELECT-only assert is too strict

Queries starting with WITH will fail the assert; broaden the check. Also be aware that assert can be optimized out with -O; consider explicit checks later.

-            assert query.count('?') == len(args)
-            assert query.upper().strip().startswith('SELECT')
+            assert query.count('?') == len(args)
+            q = query.lstrip().upper()
+            assert q.startswith('SELECT') or q.startswith('WITH')

215-226: Optional: align with get_sql_array using a comprehension

Function is fine; for consistency and speed you could use a single-pass comprehension.

-    arr = []
-    for row in rows:
-        if isinstance(row, sqlite3.Row):
-            arr.append(list(row))  # Convert row to list
-        elif isinstance(row, (tuple, list)):
-            arr.append(list(row))  # Already iterable, just convert to list
-        else:
-            arr.append([row])  # Handle single values safely
-
-    return arr
+    return [list(row) if isinstance(row, (sqlite3.Row, tuple, list)) else [row] for row in rows]

230-239: Make temp connections mirror main PRAGMAs; enforce FKs; minor consistency

Align PRAGMAs with open(); add foreign_keys; set text_factory for parity. Also confirm every temp connection is closed (or expose a contextmanager).

 def get_temp_db_connection():
@@
-    conn = sqlite3.connect(fullDbPath, timeout=5, isolation_level=None)
-    conn.execute("PRAGMA journal_mode=WAL;")
-    conn.execute("PRAGMA busy_timeout=5000;")  # 5s wait before giving up
-    conn.row_factory = sqlite3.Row
+    conn = sqlite3.connect(fullDbPath, timeout=5, isolation_level=None)
+    conn.execute("PRAGMA journal_mode=WAL;")
+    conn.execute("PRAGMA synchronous=NORMAL;")
+    conn.execute("PRAGMA temp_store=MEMORY;")
+    conn.execute("PRAGMA foreign_keys=ON;")
+    conn.execute("PRAGMA busy_timeout=5000;")  # 5s wait before giving up
+    conn.text_factory = str
+    conn.row_factory = sqlite3.Row
     return conn

Would you like a small contextmanager helper to ensure connections are closed automatically?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cb63dd1 and f1e9ca2.

📒 Files selected for processing (1)
  • server/database.py (8 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/database.py (4)
server/logger.py (1)
  • mylog (86-89)
server/db/db_helper.py (2)
  • get_table_json (183-210)
  • json_obj (213-275)
server/workflows/app_events.py (1)
  • AppEvent_obj (25-179)
server/db/db_upgrade.py (6)
  • ensure_column (11-69)
  • ensure_views (72-154)
  • ensure_CurrentScan (211-237)
  • ensure_Parameters (239-258)
  • ensure_Settings (260-286)
  • ensure_Indexes (156-205)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: docker_dev
🔇 Additional comments (3)
server/database.py (3)

6-11: LGTM: import cleanup and helper consolidation

Unused imports removed; targeted imports from const and db_helper look correct.


54-57: LGTM: safe commit guard

Early return when DB is not open avoids exceptions.


150-159: LGTM: centralized table->JSON handling and tighter logging

Delegating to db_helper.get_table_json with minimal logging is cleaner.

@jokob-sk
Copy link
Collaborator

Nice one @ingoratsdorf ! Is this ready to merge or do you need to perform additional tests?

@ingoratsdorf
Copy link
Contributor Author

I have opted to review some of the CodeRabbot suggestions and will have a few test runs first with the modified code.
I'll let you know :-)

Added some of the hand picked suggestions, including some outside of the previous changes.
Some will improve documentation, some readability and some will affect performance.
Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (4)
server/database.py (4)

24-31: Consider adding type hints for DB attributes.

Helps static analysis and readability.

You can annotate after initialization:

self.sql: sqlite3.Cursor | None = None
self.sql_connection: sqlite3.Connection | None = None

59-71: Add busy timeout and enable foreign keys; also set connect timeout.

Prevents frequent “database is locked” under WAL and enforces referential integrity. Mirrors your get_temp_db_connection() behavior.

Apply:

-            self.sql_connection = sqlite3.connect(fullDbPath,
-                                                  isolation_level=None)
+            self.sql_connection = sqlite3.connect(
+                fullDbPath, timeout=5, isolation_level=None
+            )
@@
             self.sql_connection.execute('pragma journal_mode=WAL;')
@@
             self.sql_connection.execute('PRAGMA synchronous=NORMAL;')
@@
             self.sql_connection.execute('PRAGMA temp_store=MEMORY;')
+            self.sql_connection.execute('PRAGMA busy_timeout=5000;')
+            self.sql_connection.execute('PRAGMA foreign_keys=ON;')

Also consider applying the same PRAGMAs in get_temp_db_connection() for consistency.


143-149: DRY the repeated ensure_column checks; also satisfies Ruff TRY301/TRY003.

Reduces repetition and centralizes the error message.

Apply:

-            # Add Devices fields if missing
-            if not ensure_column(self.sql, "Devices", "devFQDN", "TEXT"):
-                raise RuntimeError("ensure_column(devFQDN) failed")
-            if not ensure_column(self.sql, "Devices", "devParentRelType", "TEXT"):
-                raise RuntimeError("ensure_column(devParentRelType) failed")
-            if not ensure_column(self.sql, "Devices", "devReqNicsOnline", "INTEGER"):
-                raise RuntimeError("ensure_column(devReqNicsOnline) failed")
+            # Add Devices fields if missing
+            def _ensure_devices_column(name: str, col_type: str) -> None:
+                if not ensure_column(self.sql, "Devices", name, col_type):
+                    raise RuntimeError(f"ensure_column({name}) failed")
+
+            _ensure_devices_column("devFQDN", "TEXT")
+            _ensure_devices_column("devParentRelType", "TEXT")
+            _ensure_devices_column("devReqNicsOnline", "INTEGER")

285-298: Row-to-array helper is concise; consider typing for callers.

Minor: expose an alias like RowLike = sqlite3.Row | tuple | list | Any and type rows: Iterable[RowLike] to aid tooling.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f1e9ca2 and 1874a5e.

📒 Files selected for processing (1)
  • server/database.py (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
server/database.py (4)
server/logger.py (1)
  • mylog (86-89)
server/db/db_helper.py (2)
  • get_table_json (183-210)
  • json_obj (213-275)
server/workflows/app_events.py (1)
  • AppEvent_obj (25-179)
server/db/db_upgrade.py (7)
  • ensure_column (11-69)
  • ensure_views (72-154)
  • ensure_CurrentScan (211-237)
  • ensure_plugins_tables (289-384)
  • ensure_Parameters (239-258)
  • ensure_Settings (260-286)
  • ensure_Indexes (156-205)
🪛 Ruff (0.12.2)
server/database.py

145-145: Abstract raise to an inner function

(TRY301)


145-145: Avoid specifying long messages outside the exception class

(TRY003)


147-147: Abstract raise to an inner function

(TRY301)


147-147: Avoid specifying long messages outside the exception class

(TRY003)


149-149: Abstract raise to an inner function

(TRY301)


149-149: Avoid specifying long messages outside the exception class

(TRY003)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: docker_dev
🔇 Additional comments (9)
server/database.py (9)

6-14: Imports cleanup and consolidation look good.

Reduced surface area and clearer intent. No issues spotted.


52-54: Idempotent open guard LGTM.

Avoids duplicate connections cleanly.


94-98: Rollback docstring addition LGTM.

Clearer intent; no functional change.


201-209: get_table_as_json wrapper: toned-down logging with safe fallback looks good.

Keeps call sites simple; no issues.


229-232: Reduced logging verbosity on read() errors looks appropriate.

Clear messages without noise. LGTM.


235-247: read_one behavior and diagnostics look good.

Reasonable to return first row with a warning when multiple rows are found.


253-269: get_device_stats docstring and wrapper LGTM.

Clear contract and usage.


272-281: get_all_devices docstring and wrapper LGTM.

No issues.


79-87: Return-type change — safe: no callers depend on a previous return value.

I searched the repo for commitDB() usages; all call sites invoke it for side-effects (no assignments, returns, or conditionals using its value), so changing commitDB() to return bool is safe.

@ingoratsdorf
Copy link
Contributor Author

It's been running for a few hours and looks stable.
Debug logs is all good.

I'd say ready to be merged., :-)

@jokob-sk
Copy link
Collaborator

🚀 :)

@jokob-sk jokob-sk merged commit 6c79c04 into netalertx:main Sep 11, 2025
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants