66import contextlib
77import sqlite3
88import time
9- from datetime import UTC , datetime
109from pathlib import Path
11- from typing import Any
1210
1311from pydantic import BaseModel
1412
@@ -62,7 +60,7 @@ def __init__(self, db_path: str, preserve_existing_db: bool = False):
6260 self .conn : sqlite3 .Connection | None = None
6361
6462 # Batch processing settings
65- self .batch_buffer : list [SensorReadingSchema ] = []
63+ self .batch_buffer : list [tuple ] = [] # List of tuples for SQL insertion
6664 self .batch_size = 50
6765 self .batch_timeout = 10.0
6866 self .last_batch_time = time .time ()
@@ -154,7 +152,31 @@ def store_reading(self, reading: SensorReadingSchema):
154152 Args:
155153 reading: SensorReadingSchema object with validated data
156154 """
157- self .batch_buffer .append (reading )
155+ # Convert Pydantic model to tuple for SQL insertion
156+ reading_tuple = (
157+ reading .timestamp ,
158+ reading .sensor_id ,
159+ reading .temperature ,
160+ reading .humidity ,
161+ reading .pressure ,
162+ reading .voltage ,
163+ reading .vibration ,
164+ reading .status_code ,
165+ reading .anomaly_flag ,
166+ reading .anomaly_type ,
167+ reading .firmware_version ,
168+ reading .model ,
169+ reading .manufacturer ,
170+ reading .serial_number ,
171+ reading .location ,
172+ reading .latitude ,
173+ reading .longitude ,
174+ reading .original_timezone , # Fixed: was timezone
175+ reading .deployment_type ,
176+ reading .installation_date ,
177+ reading .height_meters ,
178+ )
179+ self .batch_buffer .append (reading_tuple )
158180
159181 # Check if we should commit
160182 current_time = time .time ()
@@ -164,191 +186,88 @@ def store_reading(self, reading: SensorReadingSchema):
164186 self .commit_batch ()
165187
166188 def commit_batch (self ):
167- """Commit the current batch to the database."""
189+ """Commit the current batch of readings to the database."""
168190 if not self .batch_buffer :
169- return 0
170-
171- assert self .conn is not None
172- cursor = self .conn .cursor ()
191+ return
173192
174- # Prepare data for insertion
175- batch_data = []
176- for reading in self .batch_buffer :
177- batch_data .append (
178- (
179- reading .timestamp ,
180- reading .sensor_id ,
181- reading .temperature ,
182- reading .humidity ,
183- reading .pressure ,
184- reading .vibration ,
185- reading .voltage ,
186- reading .status_code ,
187- 1 if reading .anomaly_flag else 0 ,
188- reading .anomaly_type ,
189- reading .firmware_version ,
190- reading .model ,
191- reading .manufacturer ,
192- reading .location ,
193- reading .latitude ,
194- reading .longitude ,
195- reading .original_timezone ,
196- reading .serial_number ,
197- reading .manufacture_date ,
198- reading .deployment_type ,
199- reading .installation_date ,
200- reading .height_meters ,
201- reading .orientation_degrees ,
202- reading .instance_id ,
203- reading .sensor_type ,
204- )
193+ try :
194+ # Use executemany for efficient bulk insert
195+ self .conn .executemany (
196+ """
197+ INSERT INTO sensor_readings (
198+ timestamp, sensor_id, temperature, humidity, pressure,
199+ voltage, vibration, status_code, anomaly_flag, anomaly_type,
200+ firmware_version, model, manufacturer, serial_number,
201+ location, latitude, longitude, timezone,
202+ deployment_type, installation_date, height_meters
203+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
204+ """ ,
205+ self .batch_buffer ,
205206 )
207+ self .conn .commit ()
208+
209+ # Checkpoint WAL periodically for Docker volume sync
210+ # Do this every 10 commits (roughly every 100 seconds)
211+ if not hasattr (self , "_commit_count" ):
212+ self ._commit_count = 0
213+ self ._commit_count += 1
214+
215+ if self ._commit_count % 10 == 0 :
216+ try :
217+ self .conn .execute ("PRAGMA wal_checkpoint(PASSIVE)" )
218+ self .logger .debug (f"WAL checkpoint at commit { self ._commit_count } " )
219+ except Exception as e :
220+ self .logger .warning (f"Periodic WAL checkpoint failed: { e } " )
221+
222+ # Clear the buffer
223+ self .batch_buffer = []
224+ self .last_commit_time = time .time ()
225+
226+ except sqlite3 .Error as e :
227+ self .logger .error (f"Failed to commit batch: { e } " )
228+ raise
229+ # Ignore read-only errors on close
230+
231+ # Checkpoint WAL to ensure data is written to main database file
232+ # This is critical for Docker volumes on macOS
233+ try :
234+ if hasattr (self , "conn" ) and self .conn :
235+ self .conn .execute ("PRAGMA wal_checkpoint(TRUNCATE)" )
236+ self .logger .info ("WAL checkpoint completed" )
237+ except Exception as e :
238+ self .logger .warning (f"WAL checkpoint failed: { e } " )
206239
207- # Insert batch
208- cursor .executemany (
209- """
210- INSERT INTO sensor_readings (
211- timestamp, sensor_id, temperature, humidity, pressure,
212- vibration, voltage, status_code, anomaly_flag, anomaly_type,
213- firmware_version, model, manufacturer, location,
214- latitude, longitude, original_timezone,
215- serial_number, manufacture_date, deployment_type,
216- installation_date, height_meters, orientation_degrees,
217- instance_id, sensor_type
218- ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
219- """ ,
220- batch_data ,
221- )
222-
223- assert self .conn is not None
224- self .conn .commit ()
225-
226- count = len (self .batch_buffer )
227- self .insert_count += count
228- self .batch_insert_count += 1
229-
230- # Clear buffer and reset timer
231- self .batch_buffer .clear ()
232- self .last_batch_time = time .time ()
233-
234- cursor .close ()
235- return count
236-
237- def insert_reading (
238- self ,
239- sensor_id : str ,
240- temperature : float ,
241- vibration : float ,
242- voltage : float ,
243- status_code : int ,
244- anomaly_flag : bool = False ,
245- anomaly_type : str | None = None ,
246- ** kwargs ,
247- ):
248- """
249- Legacy method for backward compatibility with tests.
250- """
251- reading = SensorReadingSchema (
252- timestamp = datetime .now (UTC ).isoformat (),
253- sensor_id = sensor_id ,
254- temperature = temperature ,
255- vibration = vibration ,
256- voltage = voltage ,
257- status_code = status_code ,
258- anomaly_flag = anomaly_flag ,
259- anomaly_type = anomaly_type ,
260- ** kwargs ,
261- )
262- self .store_reading (reading )
263-
264- def get_readings (self , limit : int = 100 , offset : int = 0 ) -> list [dict ]:
265- """Get readings from the database."""
266- assert self .conn is not None
267- cursor = self .conn .cursor ()
268- cursor .execute (
269- """
270- SELECT * FROM sensor_readings
271- ORDER BY id DESC
272- LIMIT ? OFFSET ?
273- """ ,
274- (limit , offset ),
275- )
276-
277- readings = []
278- for row in cursor .fetchall ():
279- readings .append (dict (row ))
280-
281- cursor .close ()
282- return readings
283-
284- def get_reading_stats (self ) -> dict :
285- """Get basic statistics."""
286- assert self .conn is not None
287- cursor = self .conn .cursor ()
288- cursor .execute ("SELECT COUNT(*) FROM sensor_readings" )
289- total = cursor .fetchone ()[0 ]
290-
291- cursor .close ()
240+ # Close connection
241+ if self .conn :
242+ self .conn .close ()
243+ self .conn = None
292244
293- return {
294- "total_readings" : total ,
295- }
296-
297- def get_database_stats (self ) -> dict [str , Any ]:
298- """Get comprehensive database statistics."""
299- stats = self .get_reading_stats ()
300-
301- # Add file size if not in-memory
302- if self .db_path != ":memory:" and Path (self .db_path ).exists ():
303- stats ["database_size_bytes" ] = Path (self .db_path ).stat ().st_size
304- stats ["database_size_mb" ] = stats ["database_size_bytes" ] / (1024 * 1024 )
305- else :
306- stats ["database_size_bytes" ] = 0
307- stats ["database_size_mb" ] = 0
308-
309- # Add placeholders for expected fields
310- stats ["sensor_stats" ] = {}
311- stats ["anomaly_stats" ] = {}
312-
313- # Add performance metrics for compatibility
314- stats ["performance_metrics" ] = {
315- "total_batches" : self .batch_insert_count ,
316- "total_inserts" : self .insert_count ,
317- "avg_batch_size" : self .insert_count / max (1 , self .batch_insert_count ),
318- "avg_insert_time_ms" : 0 , # Not tracked in simple version
319- }
320-
321- return stats
322-
323- def is_healthy (self ) -> bool :
324- """Check if database is healthy."""
325- try :
326- assert self .conn is not None
327- cursor = self .conn .cursor ()
328- cursor .execute ("SELECT 1" )
329- result = cursor .fetchone ()
330- cursor .close ()
331- return result is not None and result [0 ] == 1
332- except Exception :
333- return False
245+ self .logger .info ("Database closed" )
334246
335247 def close (self ):
336- """Close the database connection."""
337- # Commit any pending data (skip if read-only database)
248+ """Close the database connection properly ."""
249+ # Commit any pending data
338250 if self .batch_buffer :
339251 try :
340252 self .commit_batch ()
341253 except sqlite3 .OperationalError as e :
342254 if "readonly database" not in str (e ):
343255 raise
344- # Ignore read-only errors on close
345256
346- # Close connection
347- if self .conn :
257+ # Checkpoint WAL to ensure data is written to main database file
258+ # This is critical for Docker volumes on macOS/Windows
259+ try :
260+ if hasattr (self , "conn" ) and self .conn :
261+ self .conn .execute ("PRAGMA wal_checkpoint(TRUNCATE)" )
262+ self .logger .info ("WAL checkpoint completed on close" )
263+ except Exception as e :
264+ self .logger .warning (f"WAL checkpoint failed: { e } " )
265+
266+ # Close the connection
267+ if hasattr (self , "conn" ) and self .conn :
348268 self .conn .close ()
349269 self .conn = None
350-
351- self .logger .info ("Database closed" )
270+ self .logger .info ("Database connection closed" )
352271
353272 def __del__ (self ):
354273 """Cleanup on deletion."""
0 commit comments