11import json
2+ import os
3+ import tempfile
24from pathlib import Path
35from typing import List , Optional
46
57from ..tables import Column , ColumnType , ITablesSnapshot , Table
68
79
10+ def _atomic_write_text (path : Path , content : str ) -> None :
11+ """Write content to a file atomically using write-to-temp + os.replace."""
12+ fd , tmp_path = tempfile .mkstemp (dir = path .parent , suffix = ".tmp" )
13+ try :
14+ with os .fdopen (fd , "w" , encoding = "utf-8" ) as f :
15+ f .write (content )
16+ f .flush ()
17+ os .fsync (f .fileno ())
18+ os .replace (tmp_path , path )
19+ except BaseException :
20+ try :
21+ os .unlink (tmp_path )
22+ except OSError :
23+ pass
24+ raise
25+
26+
827class FileSystemJsonTables (ITablesSnapshot ):
928 workdir : Path
1029
@@ -16,7 +35,7 @@ def _ensure_metadata_table(self):
1635 """Ensure the metadata table exists"""
1736 metadata_path = self .workdir / "__schema__.json"
1837 if not metadata_path .exists ():
19- metadata_path . write_text ( json .dumps ({}))
38+ _atomic_write_text ( metadata_path , json .dumps ({}))
2039
2140 def _get_table_metadata_by_name (
2241 self , table_name : str
@@ -62,15 +81,15 @@ def _save_table_metadata(
6281 column_dicts .append (col_dict )
6382
6483 metadata [table_id ] = {"table_name" : table_name , "columns" : column_dicts }
65- metadata_path . write_text ( json .dumps (metadata , indent = 2 ))
84+ _atomic_write_text ( metadata_path , json .dumps (metadata , indent = 2 ))
6685
6786 def _remove_table_metadata (self , table_id : str ):
6887 """Remove table metadata from the __schema__.json file"""
6988 metadata_path = self .workdir / "__schema__.json"
7089 metadata = json .loads (metadata_path .read_text ())
7190 if table_id in metadata :
7291 del metadata [table_id ]
73- metadata_path . write_text ( json .dumps (metadata , indent = 2 ))
92+ _atomic_write_text ( metadata_path , json .dumps (metadata , indent = 2 ))
7493
7594 def get_table (self , name : str ) -> Optional [Table ]:
7695 table_id , columns = self ._get_table_metadata_by_name (name )
@@ -123,7 +142,7 @@ def add_table(self, table: Table):
123142 row_with_ids = table .convert_row_to_column_ids (row )
124143 data_with_ids .append (row_with_ids )
125144
126- table_path . write_text ( json .dumps (data_with_ids , indent = 2 ))
145+ _atomic_write_text ( table_path , json .dumps (data_with_ids , indent = 2 ))
127146 # Save columns metadata
128147 self ._save_table_metadata (table .table_id , table .name , table .columns )
129148
@@ -172,7 +191,7 @@ def _insert(self, table_name: str, row: dict):
172191 # Convert row to column ID format
173192 row_with_ids = temp_table .convert_row_to_column_ids (row )
174193 rows .append (row_with_ids )
175- table_path . write_text ( json .dumps (rows , indent = 2 ))
194+ _atomic_write_text ( table_path , json .dumps (rows , indent = 2 ))
176195
177196 def add_column (self , table_name : str , column : Column ):
178197 table_id , existing_columns = self ._get_table_metadata_by_name (table_name )
@@ -197,7 +216,7 @@ def add_column(self, table_name: str, column: Column):
197216 # Add column to data using column ID
198217 for row in rows :
199218 row [column .column_id ] = column .default
200- table_path . write_text ( json .dumps (rows , indent = 2 ))
219+ _atomic_write_text ( table_path , json .dumps (rows , indent = 2 ))
201220
202221 # Update metadata
203222 existing_columns .append (column )
@@ -228,7 +247,7 @@ def remove_column(self, table_name: str, column_name: str):
228247 for row in rows :
229248 if column_to_remove .column_id in row :
230249 del row [column_to_remove .column_id ]
231- table_path . write_text ( json .dumps (rows , indent = 2 ))
250+ _atomic_write_text ( table_path , json .dumps (rows , indent = 2 ))
232251
233252 # Update metadata
234253 columns = [col for col in columns if col .name != column_name ]
@@ -293,7 +312,7 @@ def _update(self, table_name: str, idx: int, changes: dict):
293312 # Convert changes to column ID format
294313 changes_with_ids = temp_table .convert_row_to_column_ids (changes )
295314 rows [idx ].update (changes_with_ids )
296- table_path . write_text ( json .dumps (rows , indent = 2 ))
315+ _atomic_write_text ( table_path , json .dumps (rows , indent = 2 ))
297316
298317 def _delete (self , table_name : str , idxs : List [int ]):
299318 table_id , _ = self ._get_table_metadata_by_name (table_name )
@@ -314,4 +333,4 @@ def _delete(self, table_name: str, idxs: List[int]):
314333 if idx < 0 or idx >= len (rows ):
315334 raise IndexError (f"Index { idx } out of range for table { table_name } " )
316335 del rows [idx ]
317- table_path . write_text ( json .dumps (rows , indent = 2 ))
336+ _atomic_write_text ( table_path , json .dumps (rows , indent = 2 ))
0 commit comments