From 17176772881051dff8f08b69157edf6f4e00ad4a Mon Sep 17 00:00:00 2001 From: user Date: Thu, 19 Mar 2026 06:08:07 -0700 Subject: [PATCH 1/2] remove ctime column from schema, model, queries, scanner, and docs ctime is ambiguous cross-platform (macOS birth time vs Linux inode change time), never used operationally (scanning triggers on mtime), cannot be restored on either platform, and was write-only forensic data with no consumer. Removes ctime from: - files table schema (schema.sql) - File struct (models.go) - all SQL queries and scan targets (files.go) - scanner file metadata collection (scanner.go) - all test files - ARCHITECTURE.md and docs/DATAMODEL.md closes #54 --- ARCHITECTURE.md | 2 +- docs/DATAMODEL.md | 1 - internal/database/cascade_debug_test.go | 1 - internal/database/chunk_files_test.go | 8 ++-- internal/database/file_chunks_test.go | 2 - internal/database/files.go | 42 ++++++++----------- internal/database/files_test.go | 3 -- internal/database/models.go | 1 - internal/database/repositories_test.go | 4 -- .../database/repository_comprehensive_test.go | 13 ------ internal/database/repository_debug_test.go | 2 - .../database/repository_edge_cases_test.go | 12 ------ internal/database/schema.sql | 1 - internal/snapshot/backup_test.go | 5 +-- internal/snapshot/scanner.go | 1 - 15 files changed, 24 insertions(+), 74 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4cdb844..a28f75f 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -54,7 +54,7 @@ The database tracks five primary entities and their relationships: #### File (`database.File`) Represents a file or directory in the backup system. Stores metadata needed for restoration: -- Path, timestamps (mtime, ctime) +- Path, mtime - Size, mode, ownership (uid, gid) - Symlink target (if applicable) diff --git a/docs/DATAMODEL.md b/docs/DATAMODEL.md index 71d4b08..8efb387 100644 --- a/docs/DATAMODEL.md +++ b/docs/DATAMODEL.md @@ -17,7 +17,6 @@ Stores metadata about files in the filesystem being backed up. - `id` (TEXT PRIMARY KEY) - UUID for the file record - `path` (TEXT NOT NULL UNIQUE) - Absolute file path - `mtime` (INTEGER NOT NULL) - Modification time as Unix timestamp -- `ctime` (INTEGER NOT NULL) - Change time as Unix timestamp - `size` (INTEGER NOT NULL) - File size in bytes - `mode` (INTEGER NOT NULL) - Unix file permissions and type - `uid` (INTEGER NOT NULL) - User ID of file owner diff --git a/internal/database/cascade_debug_test.go b/internal/database/cascade_debug_test.go index b7a29f6..ed24746 100644 --- a/internal/database/cascade_debug_test.go +++ b/internal/database/cascade_debug_test.go @@ -29,7 +29,6 @@ func TestCascadeDeleteDebug(t *testing.T) { file := &File{ Path: "/cascade-test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, diff --git a/internal/database/chunk_files_test.go b/internal/database/chunk_files_test.go index d99a06d..9c772c4 100644 --- a/internal/database/chunk_files_test.go +++ b/internal/database/chunk_files_test.go @@ -22,7 +22,6 @@ func TestChunkFileRepository(t *testing.T) { file1 := &File{ Path: "/file1.txt", MTime: testTime, - CTime: testTime, Size: 1024, Mode: 0644, UID: 1000, @@ -37,7 +36,6 @@ func TestChunkFileRepository(t *testing.T) { file2 := &File{ Path: "/file2.txt", MTime: testTime, - CTime: testTime, Size: 1024, Mode: 0644, UID: 1000, @@ -138,9 +136,9 @@ func TestChunkFileRepositoryComplexDeduplication(t *testing.T) { // Create test files testTime := time.Now().Truncate(time.Second) - file1 := &File{Path: "/file1.txt", MTime: testTime, CTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000} - file2 := &File{Path: "/file2.txt", MTime: testTime, CTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000} - file3 := &File{Path: "/file3.txt", MTime: testTime, CTime: testTime, Size: 2048, Mode: 0644, UID: 1000, GID: 1000} + file1 := &File{Path: "/file1.txt", MTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000} + file2 := &File{Path: "/file2.txt", MTime: testTime, Size: 3072, Mode: 0644, UID: 1000, GID: 1000} + file3 := &File{Path: "/file3.txt", MTime: testTime, Size: 2048, Mode: 0644, UID: 1000, GID: 1000} if err := fileRepo.Create(ctx, nil, file1); err != nil { t.Fatalf("failed to create file1: %v", err) diff --git a/internal/database/file_chunks_test.go b/internal/database/file_chunks_test.go index aad891b..c009e97 100644 --- a/internal/database/file_chunks_test.go +++ b/internal/database/file_chunks_test.go @@ -22,7 +22,6 @@ func TestFileChunkRepository(t *testing.T) { file := &File{ Path: "/test/file.txt", MTime: testTime, - CTime: testTime, Size: 3072, Mode: 0644, UID: 1000, @@ -135,7 +134,6 @@ func TestFileChunkRepositoryMultipleFiles(t *testing.T) { file := &File{ Path: types.FilePath(path), MTime: testTime, - CTime: testTime, Size: 2048, Mode: 0644, UID: 1000, diff --git a/internal/database/files.go b/internal/database/files.go index da68e2d..21c0a0b 100644 --- a/internal/database/files.go +++ b/internal/database/files.go @@ -25,12 +25,11 @@ func (r *FileRepository) Create(ctx context.Context, tx *sql.Tx, file *File) err } query := ` - INSERT INTO files (id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO files (id, path, source_path, mtime, size, mode, uid, gid, link_target) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(path) DO UPDATE SET source_path = excluded.source_path, mtime = excluded.mtime, - ctime = excluded.ctime, size = excluded.size, mode = excluded.mode, uid = excluded.uid, @@ -42,10 +41,10 @@ func (r *FileRepository) Create(ctx context.Context, tx *sql.Tx, file *File) err var idStr string var err error if tx != nil { - LogSQL("Execute", query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.CTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()) - err = tx.QueryRowContext(ctx, query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.CTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()).Scan(&idStr) + LogSQL("Execute", query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()) + err = tx.QueryRowContext(ctx, query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()).Scan(&idStr) } else { - err = r.db.QueryRowWithLog(ctx, query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.CTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()).Scan(&idStr) + err = r.db.QueryRowWithLog(ctx, query, file.ID.String(), file.Path.String(), file.SourcePath.String(), file.MTime.Unix(), file.Size, file.Mode, file.UID, file.GID, file.LinkTarget.String()).Scan(&idStr) } if err != nil { @@ -63,7 +62,7 @@ func (r *FileRepository) Create(ctx context.Context, tx *sql.Tx, file *File) err func (r *FileRepository) GetByPath(ctx context.Context, path string) (*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files WHERE path = ? ` @@ -82,7 +81,7 @@ func (r *FileRepository) GetByPath(ctx context.Context, path string) (*File, err // GetByID retrieves a file by its UUID func (r *FileRepository) GetByID(ctx context.Context, id types.FileID) (*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files WHERE id = ? ` @@ -100,7 +99,7 @@ func (r *FileRepository) GetByID(ctx context.Context, id types.FileID) (*File, e func (r *FileRepository) GetByPathTx(ctx context.Context, tx *sql.Tx, path string) (*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files WHERE path = ? ` @@ -123,7 +122,7 @@ func (r *FileRepository) GetByPathTx(ctx context.Context, tx *sql.Tx, path strin func (r *FileRepository) scanFile(row *sql.Row) (*File, error) { var file File var idStr, pathStr, sourcePathStr string - var mtimeUnix, ctimeUnix int64 + var mtimeUnix int64 var linkTarget sql.NullString err := row.Scan( @@ -131,7 +130,6 @@ func (r *FileRepository) scanFile(row *sql.Row) (*File, error) { &pathStr, &sourcePathStr, &mtimeUnix, - &ctimeUnix, &file.Size, &file.Mode, &file.UID, @@ -149,7 +147,6 @@ func (r *FileRepository) scanFile(row *sql.Row) (*File, error) { file.Path = types.FilePath(pathStr) file.SourcePath = types.SourcePath(sourcePathStr) file.MTime = time.Unix(mtimeUnix, 0).UTC() - file.CTime = time.Unix(ctimeUnix, 0).UTC() if linkTarget.Valid { file.LinkTarget = types.FilePath(linkTarget.String) } @@ -161,7 +158,7 @@ func (r *FileRepository) scanFile(row *sql.Row) (*File, error) { func (r *FileRepository) scanFileRows(rows *sql.Rows) (*File, error) { var file File var idStr, pathStr, sourcePathStr string - var mtimeUnix, ctimeUnix int64 + var mtimeUnix int64 var linkTarget sql.NullString err := rows.Scan( @@ -169,7 +166,6 @@ func (r *FileRepository) scanFileRows(rows *sql.Rows) (*File, error) { &pathStr, &sourcePathStr, &mtimeUnix, - &ctimeUnix, &file.Size, &file.Mode, &file.UID, @@ -187,7 +183,6 @@ func (r *FileRepository) scanFileRows(rows *sql.Rows) (*File, error) { file.Path = types.FilePath(pathStr) file.SourcePath = types.SourcePath(sourcePathStr) file.MTime = time.Unix(mtimeUnix, 0).UTC() - file.CTime = time.Unix(ctimeUnix, 0).UTC() if linkTarget.Valid { file.LinkTarget = types.FilePath(linkTarget.String) } @@ -197,7 +192,7 @@ func (r *FileRepository) scanFileRows(rows *sql.Rows) (*File, error) { func (r *FileRepository) ListModifiedSince(ctx context.Context, since time.Time) ([]*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files WHERE mtime >= ? ORDER BY path @@ -258,7 +253,7 @@ func (r *FileRepository) DeleteByID(ctx context.Context, tx *sql.Tx, id types.Fi func (r *FileRepository) ListByPrefix(ctx context.Context, prefix string) ([]*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files WHERE path LIKE ? || '%' ORDER BY path @@ -285,7 +280,7 @@ func (r *FileRepository) ListByPrefix(ctx context.Context, prefix string) ([]*Fi // ListAll returns all files in the database func (r *FileRepository) ListAll(ctx context.Context) ([]*File, error) { query := ` - SELECT id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target + SELECT id, path, source_path, mtime, size, mode, uid, gid, link_target FROM files ORDER BY path ` @@ -315,7 +310,7 @@ func (r *FileRepository) CreateBatch(ctx context.Context, tx *sql.Tx, files []*F return nil } - // Each File has 10 values, so batch at 100 to be safe with SQLite's variable limit + // Each File has 9 values, so batch at 100 to be safe with SQLite's variable limit const batchSize = 100 for i := 0; i < len(files); i += batchSize { @@ -325,19 +320,18 @@ func (r *FileRepository) CreateBatch(ctx context.Context, tx *sql.Tx, files []*F } batch := files[i:end] - query := `INSERT INTO files (id, path, source_path, mtime, ctime, size, mode, uid, gid, link_target) VALUES ` - args := make([]interface{}, 0, len(batch)*10) + query := `INSERT INTO files (id, path, source_path, mtime, size, mode, uid, gid, link_target) VALUES ` + args := make([]interface{}, 0, len(batch)*9) for j, f := range batch { if j > 0 { query += ", " } - query += "(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" - args = append(args, f.ID.String(), f.Path.String(), f.SourcePath.String(), f.MTime.Unix(), f.CTime.Unix(), f.Size, f.Mode, f.UID, f.GID, f.LinkTarget.String()) + query += "(?, ?, ?, ?, ?, ?, ?, ?, ?)" + args = append(args, f.ID.String(), f.Path.String(), f.SourcePath.String(), f.MTime.Unix(), f.Size, f.Mode, f.UID, f.GID, f.LinkTarget.String()) } query += ` ON CONFLICT(path) DO UPDATE SET source_path = excluded.source_path, mtime = excluded.mtime, - ctime = excluded.ctime, size = excluded.size, mode = excluded.mode, uid = excluded.uid, diff --git a/internal/database/files_test.go b/internal/database/files_test.go index 4b94519..8f16421 100644 --- a/internal/database/files_test.go +++ b/internal/database/files_test.go @@ -39,7 +39,6 @@ func TestFileRepository(t *testing.T) { file := &File{ Path: "/test/file.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -124,7 +123,6 @@ func TestFileRepositorySymlink(t *testing.T) { symlink := &File{ Path: "/test/link", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 0, Mode: uint32(0777 | os.ModeSymlink), UID: 1000, @@ -161,7 +159,6 @@ func TestFileRepositoryTransaction(t *testing.T) { file := &File{ Path: "/test/tx_file.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, diff --git a/internal/database/models.go b/internal/database/models.go index 729b576..14bc580 100644 --- a/internal/database/models.go +++ b/internal/database/models.go @@ -17,7 +17,6 @@ type File struct { Path types.FilePath // Absolute path of the file SourcePath types.SourcePath // The source directory this file came from (for restore path stripping) MTime time.Time - CTime time.Time Size int64 Mode uint32 UID uint32 diff --git a/internal/database/repositories_test.go b/internal/database/repositories_test.go index 14c7117..439f325 100644 --- a/internal/database/repositories_test.go +++ b/internal/database/repositories_test.go @@ -23,7 +23,6 @@ func TestRepositoriesTransaction(t *testing.T) { file := &File{ Path: "/test/tx_file.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -146,7 +145,6 @@ func TestRepositoriesTransactionRollback(t *testing.T) { file := &File{ Path: "/test/rollback_file.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -202,7 +200,6 @@ func TestRepositoriesReadTransaction(t *testing.T) { file := &File{ Path: "/test/read_file.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -226,7 +223,6 @@ func TestRepositoriesReadTransaction(t *testing.T) { _ = repos.Files.Create(ctx, tx, &File{ Path: "/test/should_fail.txt", MTime: time.Now(), - CTime: time.Now(), Size: 0, Mode: 0644, UID: 1000, diff --git a/internal/database/repository_comprehensive_test.go b/internal/database/repository_comprehensive_test.go index 9bea6e7..3bc25a6 100644 --- a/internal/database/repository_comprehensive_test.go +++ b/internal/database/repository_comprehensive_test.go @@ -23,7 +23,6 @@ func TestFileRepositoryUUIDGeneration(t *testing.T) { { Path: "/file1.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -32,7 +31,6 @@ func TestFileRepositoryUUIDGeneration(t *testing.T) { { Path: "/file2.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 2048, Mode: 0644, UID: 1000, @@ -72,7 +70,6 @@ func TestFileRepositoryGetByID(t *testing.T) { file := &File{ Path: "/test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -120,7 +117,6 @@ func TestOrphanedFileCleanup(t *testing.T) { file1 := &File{ Path: "/orphaned.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -129,7 +125,6 @@ func TestOrphanedFileCleanup(t *testing.T) { file2 := &File{ Path: "/referenced.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 2048, Mode: 0644, UID: 1000, @@ -218,7 +213,6 @@ func TestOrphanedChunkCleanup(t *testing.T) { file := &File{ Path: "/test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -348,7 +342,6 @@ func TestFileChunkRepositoryWithUUIDs(t *testing.T) { file := &File{ Path: "/test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 3072, Mode: 0644, UID: 1000, @@ -419,7 +412,6 @@ func TestChunkFileRepositoryWithUUIDs(t *testing.T) { file1 := &File{ Path: "/file1.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -428,7 +420,6 @@ func TestChunkFileRepositoryWithUUIDs(t *testing.T) { file2 := &File{ Path: "/file2.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -586,7 +577,6 @@ func TestComplexOrphanedDataScenario(t *testing.T) { files[i] = &File{ Path: types.FilePath(fmt.Sprintf("/file%d.txt", i)), MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -678,7 +668,6 @@ func TestCascadeDelete(t *testing.T) { file := &File{ Path: "/cascade-test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -750,7 +739,6 @@ func TestTransactionIsolation(t *testing.T) { file := &File{ Path: "/tx-test.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -812,7 +800,6 @@ func TestConcurrentOrphanedCleanup(t *testing.T) { file := &File{ Path: types.FilePath(fmt.Sprintf("/concurrent-%d.txt", i)), MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, diff --git a/internal/database/repository_debug_test.go b/internal/database/repository_debug_test.go index 92433d5..2bd9493 100644 --- a/internal/database/repository_debug_test.go +++ b/internal/database/repository_debug_test.go @@ -18,7 +18,6 @@ func TestOrphanedFileCleanupDebug(t *testing.T) { file1 := &File{ Path: "/orphaned.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 1024, Mode: 0644, UID: 1000, @@ -27,7 +26,6 @@ func TestOrphanedFileCleanupDebug(t *testing.T) { file2 := &File{ Path: "/referenced.txt", MTime: time.Now().Truncate(time.Second), - CTime: time.Now().Truncate(time.Second), Size: 2048, Mode: 0644, UID: 1000, diff --git a/internal/database/repository_edge_cases_test.go b/internal/database/repository_edge_cases_test.go index d701d38..4f9bb2b 100644 --- a/internal/database/repository_edge_cases_test.go +++ b/internal/database/repository_edge_cases_test.go @@ -29,7 +29,6 @@ func TestFileRepositoryEdgeCases(t *testing.T) { file: &File{ Path: "", MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -42,7 +41,6 @@ func TestFileRepositoryEdgeCases(t *testing.T) { file: &File{ Path: types.FilePath("/" + strings.Repeat("a", 4096)), MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -55,7 +53,6 @@ func TestFileRepositoryEdgeCases(t *testing.T) { file: &File{ Path: "/test/file with spaces and 特殊文字.txt", MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -68,7 +65,6 @@ func TestFileRepositoryEdgeCases(t *testing.T) { file: &File{ Path: "/empty.txt", MTime: time.Now(), - CTime: time.Now(), Size: 0, Mode: 0644, UID: 1000, @@ -81,7 +77,6 @@ func TestFileRepositoryEdgeCases(t *testing.T) { file: &File{ Path: "/link", MTime: time.Now(), - CTime: time.Now(), Size: 0, Mode: 0777 | 0120000, // symlink mode UID: 1000, @@ -123,7 +118,6 @@ func TestDuplicateHandling(t *testing.T) { file1 := &File{ Path: "/duplicate.txt", MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -132,7 +126,6 @@ func TestDuplicateHandling(t *testing.T) { file2 := &File{ Path: "/duplicate.txt", // Same path MTime: time.Now().Add(time.Hour), - CTime: time.Now().Add(time.Hour), Size: 2048, Mode: 0644, UID: 1000, @@ -192,7 +185,6 @@ func TestDuplicateHandling(t *testing.T) { file := &File{ Path: "/test-dup-fc.txt", MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -244,7 +236,6 @@ func TestNullHandling(t *testing.T) { file := &File{ Path: "/regular.txt", MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -349,7 +340,6 @@ func TestLargeDatasets(t *testing.T) { file := &File{ Path: types.FilePath(fmt.Sprintf("/large/file%05d.txt", i)), MTime: time.Now(), - CTime: time.Now(), Size: int64(i * 1024), Mode: 0644, UID: uint32(1000 + (i % 10)), @@ -474,7 +464,6 @@ func TestQueryInjection(t *testing.T) { file := &File{ Path: types.FilePath(injection), MTime: time.Now(), - CTime: time.Now(), Size: 1024, Mode: 0644, UID: 1000, @@ -513,7 +502,6 @@ func TestTimezoneHandling(t *testing.T) { file := &File{ Path: "/timezone-test.txt", MTime: nyTime, - CTime: nyTime, Size: 1024, Mode: 0644, UID: 1000, diff --git a/internal/database/schema.sql b/internal/database/schema.sql index bc03da2..2bc2254 100644 --- a/internal/database/schema.sql +++ b/internal/database/schema.sql @@ -8,7 +8,6 @@ CREATE TABLE IF NOT EXISTS files ( path TEXT NOT NULL UNIQUE, source_path TEXT NOT NULL DEFAULT '', -- The source directory this file came from (for restore path stripping) mtime INTEGER NOT NULL, - ctime INTEGER NOT NULL, size INTEGER NOT NULL, mode INTEGER NOT NULL, uid INTEGER NOT NULL, diff --git a/internal/snapshot/backup_test.go b/internal/snapshot/backup_test.go index 09ad29c..05bd30c 100644 --- a/internal/snapshot/backup_test.go +++ b/internal/snapshot/backup_test.go @@ -345,9 +345,8 @@ func (b *BackupEngine) Backup(ctx context.Context, fsys fs.FS, root string) (str Size: info.Size(), Mode: uint32(info.Mode()), MTime: info.ModTime(), - CTime: info.ModTime(), // Use mtime as ctime for test - UID: 1000, // Default UID for test - GID: 1000, // Default GID for test + UID: 1000, // Default UID for test + GID: 1000, // Default GID for test } err = b.repos.WithTx(ctx, func(ctx context.Context, tx *sql.Tx) error { return b.repos.Files.Create(ctx, tx, file) diff --git a/internal/snapshot/scanner.go b/internal/snapshot/scanner.go index 8804dc2..6043133 100644 --- a/internal/snapshot/scanner.go +++ b/internal/snapshot/scanner.go @@ -785,7 +785,6 @@ func (s *Scanner) checkFileInMemory(path string, info os.FileInfo, knownFiles ma Path: types.FilePath(path), SourcePath: types.SourcePath(s.currentSourcePath), // Store source directory for restore path stripping MTime: info.ModTime(), - CTime: info.ModTime(), // afero doesn't provide ctime Size: info.Size(), Mode: uint32(info.Mode()), UID: uid, -- 2.49.1 From 15f0172e0c491c544f94f1f8012c902e4b3f998b Mon Sep 17 00:00:00 2001 From: user Date: Thu, 19 Mar 2026 06:15:40 -0700 Subject: [PATCH 2/2] fix: restore ON DELETE CASCADE on snapshot_files.file_id and snapshot_blobs.blob_id FKs --- internal/database/schema.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/database/schema.sql b/internal/database/schema.sql index 2bc2254..cdc8533 100644 --- a/internal/database/schema.sql +++ b/internal/database/schema.sql @@ -102,7 +102,7 @@ CREATE TABLE IF NOT EXISTS snapshot_files ( file_id TEXT NOT NULL, PRIMARY KEY (snapshot_id, file_id), FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE, - FOREIGN KEY (file_id) REFERENCES files(id) + FOREIGN KEY (file_id) REFERENCES files(id) ON DELETE CASCADE ); -- Index for efficient file lookups (used in orphan detection) @@ -115,7 +115,7 @@ CREATE TABLE IF NOT EXISTS snapshot_blobs ( blob_hash TEXT NOT NULL, PRIMARY KEY (snapshot_id, blob_id), FOREIGN KEY (snapshot_id) REFERENCES snapshots(id) ON DELETE CASCADE, - FOREIGN KEY (blob_id) REFERENCES blobs(id) + FOREIGN KEY (blob_id) REFERENCES blobs(id) ON DELETE CASCADE ); -- Index for efficient blob lookups (used in orphan detection) -- 2.49.1