Skip to content

Commit cd96b99

Browse files
authored
Merge pull request #3 from abstra-app/fix/add-atomic-writing
fix: Use atomic writing when saving files to avoid corrupting them
2 parents 2f85a65 + a2b4a6a commit cd96b99

1 file changed

Lines changed: 28 additions & 9 deletions

File tree

abstra_json_sql/persistence/json.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
import json
2+
import os
3+
import tempfile
24
from pathlib import Path
35
from typing import List, Optional
46

57
from ..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+
827
class 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

Comments
 (0)