1010class Database :
1111 def __init__ (self , path : str ):
1212 self .path = path
13+ os .makedirs (os .path .dirname (path ) or "." , exist_ok = True )
1314 self .conn = duckdb .connect (path )
1415
16+ def _cursor (self ) -> duckdb .DuckDBPyConnection :
17+ """Create a thread-local cursor for safe concurrent access."""
18+ return self .conn .cursor ()
19+
1520 def init (self ):
16- self .conn .execute ("INSTALL spatial" )
17- self .conn .execute ("LOAD spatial" )
21+ cur = self ._cursor ()
22+ cur .execute ("INSTALL spatial" )
23+ cur .execute ("LOAD spatial" )
1824
19- self . conn .execute ("""
25+ cur .execute ("""
2026 CREATE TABLE IF NOT EXISTS vehicles (
2127 vehicle_id VARCHAR PRIMARY KEY,
2228 description VARCHAR,
@@ -25,10 +31,10 @@ def init(self):
2531 last_seen TIMESTAMPTZ NOT NULL
2632 )
2733 """ )
28- self . conn .execute ("""
34+ cur .execute ("""
2935 CREATE SEQUENCE IF NOT EXISTS positions_seq
3036 """ )
31- self . conn .execute ("""
37+ cur .execute ("""
3238 CREATE TABLE IF NOT EXISTS positions (
3339 id BIGINT DEFAULT nextval('positions_seq'),
3440 vehicle_id VARCHAR NOT NULL,
@@ -43,27 +49,28 @@ def init(self):
4349 PRIMARY KEY (vehicle_id, timestamp)
4450 )
4551 """ )
46- self . conn .execute ("""
52+ cur .execute ("""
4753 CREATE INDEX IF NOT EXISTS idx_positions_time_geo
4854 ON positions (timestamp, latitude, longitude)
4955 """ )
5056
5157 # Migration: add geom column to existing tables that lack it
52- cols = self . conn .execute (
58+ cols = cur .execute (
5359 "SELECT column_name FROM information_schema.columns "
5460 "WHERE table_name='positions' AND column_name='geom'"
5561 ).fetchall ()
5662 if not cols :
57- self . conn .execute ("ALTER TABLE positions ADD COLUMN geom GEOMETRY" )
63+ cur .execute ("ALTER TABLE positions ADD COLUMN geom GEOMETRY" )
5864
5965 # Backfill geom for existing rows
60- self . conn .execute (
66+ cur .execute (
6167 "UPDATE positions SET geom = ST_Point(longitude, latitude) WHERE geom IS NULL"
6268 )
6369
6470 def upsert_vehicles (self , vehicles : list [dict ], now : datetime ):
71+ cur = self ._cursor ()
6572 for v in vehicles :
66- self . conn .execute (
73+ cur .execute (
6774 """
6875 INSERT INTO vehicles (vehicle_id, description, vehicle_type, first_seen, last_seen)
6976 VALUES (?, ?, ?, ?, ?)
@@ -78,9 +85,10 @@ def upsert_vehicles(self, vehicles: list[dict], now: datetime):
7885 def insert_positions (self , positions : list [dict ], collected_at : datetime ) -> int :
7986 if not positions :
8087 return 0
81- count_before = self .conn .execute ("SELECT count(*) FROM positions" ).fetchone ()[0 ]
88+ cur = self ._cursor ()
89+ count_before = cur .execute ("SELECT count(*) FROM positions" ).fetchone ()[0 ]
8290 for p in positions :
83- self . conn .execute (
91+ cur .execute (
8492 """
8593 INSERT OR IGNORE INTO positions
8694 (vehicle_id, timestamp, collected_at, longitude, latitude, geom, bearing, speed, is_driving)
@@ -99,7 +107,7 @@ def insert_positions(self, positions: list[dict], collected_at: datetime) -> int
99107 p ["is_driving" ],
100108 ],
101109 )
102- count_after = self . conn .execute ("SELECT count(*) FROM positions" ).fetchone ()[0 ]
110+ count_after = cur .execute ("SELECT count(*) FROM positions" ).fetchone ()[0 ]
103111 return count_after - count_before
104112
105113 def get_latest_positions (
@@ -123,7 +131,7 @@ def get_latest_positions(
123131 ORDER BY timestamp ASC
124132 LIMIT $2
125133 """
126- rows = self .conn .execute (query , [after , limit ]).fetchall ()
134+ rows = self ._cursor () .execute (query , [after , limit ]).fetchall ()
127135 return [self ._row_to_dict (r ) for r in rows ]
128136
129137 def get_nearby_vehicles (
@@ -154,7 +162,11 @@ def get_nearby_vehicles(
154162 ORDER BY timestamp ASC
155163 LIMIT $5
156164 """
157- rows = self .conn .execute (query , [lng , lat , radius_deg , after , limit ]).fetchall ()
165+ rows = (
166+ self ._cursor ()
167+ .execute (query , [lng , lat , radius_deg , after , limit ])
168+ .fetchall ()
169+ )
158170 return [self ._row_to_dict (r ) for r in rows ]
159171
160172 def get_vehicle_history (
@@ -179,9 +191,11 @@ def get_vehicle_history(
179191 ORDER BY p.timestamp ASC
180192 LIMIT $5
181193 """
182- rows = self .conn .execute (
183- query , [vehicle_id , since , until , after , limit ]
184- ).fetchall ()
194+ rows = (
195+ self ._cursor ()
196+ .execute (query , [vehicle_id , since , until , after , limit ])
197+ .fetchall ()
198+ )
185199 return [self ._row_to_dict (r ) for r in rows ]
186200
187201 def get_coverage (
@@ -204,7 +218,7 @@ def get_coverage(
204218 ORDER BY p.timestamp ASC
205219 LIMIT $4
206220 """
207- rows = self .conn .execute (query , [since , until , after , limit ]).fetchall ()
221+ rows = self ._cursor () .execute (query , [since , until , after , limit ]).fetchall ()
208222 return [self ._row_to_dict (r ) for r in rows ]
209223
210224 def get_coverage_trails (
@@ -224,7 +238,7 @@ def get_coverage_trails(
224238 AND p.timestamp <= $2
225239 ORDER BY p.vehicle_id, p.timestamp ASC
226240 """
227- rows = self .conn .execute (query , [since , until ]).fetchall ()
241+ rows = self ._cursor () .execute (query , [since , until ]).fetchall ()
228242
229243 trails = []
230244 for vid , group in groupby (rows , key = itemgetter (0 )):
@@ -294,15 +308,18 @@ def _row_to_dict(self, row) -> dict:
294308 }
295309
296310 def get_stats (self ) -> dict :
297- total_positions = self .conn .execute (
298- "SELECT count(*) FROM positions"
299- ).fetchone ()[0 ]
300- total_vehicles = self .conn .execute ("SELECT count(*) FROM vehicles" ).fetchone ()[
301- 0
302- ]
303- active_vehicles = self .conn .execute (
311+ cur = self ._cursor ()
312+ row = cur .execute ("SELECT count(*) FROM positions" ).fetchone ()
313+ total_positions = row [0 ] if row else 0
314+
315+ row = cur .execute ("SELECT count(*) FROM vehicles" ).fetchone ()
316+ total_vehicles = row [0 ] if row else 0
317+
318+ row = cur .execute (
304319 "SELECT count(DISTINCT vehicle_id) FROM positions WHERE is_driving = 'maybe'"
305- ).fetchone ()[0 ]
320+ ).fetchone ()
321+ active_vehicles = row [0 ] if row else 0
322+
306323 try :
307324 db_size_bytes = os .path .getsize (self .path )
308325 except OSError :
@@ -314,11 +331,12 @@ def get_stats(self) -> dict:
314331 "db_size_bytes" : db_size_bytes ,
315332 }
316333 if total_positions > 0 :
317- row = self . conn .execute (
334+ row = cur .execute (
318335 "SELECT min(timestamp), max(timestamp) FROM positions"
319336 ).fetchone ()
320- result ["earliest" ] = row [0 ]
321- result ["latest" ] = row [1 ]
337+ if row :
338+ result ["earliest" ] = row [0 ]
339+ result ["latest" ] = row [1 ]
322340 return result
323341
324342 def close (self ):
0 commit comments