diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt index 3aaba0d..a469cdf 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/FieldDao.kt @@ -27,6 +27,7 @@ interface FieldDao { @Query("DELETE FROM fields WHERE id = :fieldId") suspend fun deleteField(fieldId: String) + // Get all fields updated since timestamp, including deleted ones for sync @Query("SELECT * FROM fields WHERE updatedAt >= :since") suspend fun getFieldsUpdatedSince(since: Long): List } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt index b045547..a9c7c9d 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ItemDao.kt @@ -32,6 +32,7 @@ interface ItemDao { @Query("DELETE FROM items WHERE id = :itemId") suspend fun deleteItem(itemId: String) + // Get all items updated since timestamp, including deleted ones for sync @Query("SELECT * FROM items WHERE updatedAt >= :since") suspend fun getItemsUpdatedSince(since: Long): List } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt index 714a0f2..463f22b 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/data/dao/ListDao.kt @@ -32,6 +32,7 @@ interface ListDao { @Query("DELETE FROM lists WHERE id = :listId") suspend fun deleteList(listId: String) + // Get all lists updated since timestamp, including deleted ones for sync @Query("SELECT * FROM lists WHERE updatedAt >= :since") suspend fun getListsUpdatedSince(since: Long): List } diff --git a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt index e7e885f..c275c7d 100644 --- a/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt +++ b/CollabTableAndroid/app/src/main/java/com/collabtable/app/ui/screens/ListDetailScreen.kt @@ -1300,12 +1300,35 @@ fun ManageColumnsDialog( var showAddField by remember { mutableStateOf(false) } var fieldToEdit by remember { mutableStateOf(null) } var fieldToDelete by remember { mutableStateOf(null) } - val reorderedFields = remember { mutableStateListOf().apply { addAll(fields) } } - // Update reordered list when fields change + // Use derivedStateOf to keep reorderedFields in sync with fields + val reorderedFields = remember(fields) { + mutableStateListOf().apply { + clear() + addAll(fields) + } + } + + // Watch for changes in fields and update reorderedFields LaunchedEffect(fields) { - reorderedFields.clear() - reorderedFields.addAll(fields) + val currentIds = reorderedFields.map { it.id }.toSet() + val newIds = fields.map { it.id }.toSet() + + // Add new fields + fields.filter { it.id !in currentIds }.forEach { newField -> + reorderedFields.add(newField) + } + + // Remove deleted fields + reorderedFields.removeAll { it.id !in newIds } + + // Update existing fields (in case of edits) + fields.forEach { updatedField -> + val index = reorderedFields.indexOfFirst { it.id == updatedField.id } + if (index >= 0 && reorderedFields[index] != updatedField) { + reorderedFields[index] = updatedField + } + } } Dialog( @@ -1358,17 +1381,20 @@ fun ManageColumnsDialog( field = field, onEdit = { fieldToEdit = field }, onDelete = { fieldToDelete = field }, - onDragStart = { _ -> - // Store the dragged item index if needed - }, - onDragEnd = { fromIndex, toIndex -> - if (fromIndex != toIndex) { - val item = reorderedFields.removeAt(fromIndex) - reorderedFields.add(toIndex, item) + onMoveUp = { + if (index > 0) { + val item = reorderedFields.removeAt(index) + reorderedFields.add(index - 1, item) } }, - currentIndex = index, - totalItems = reorderedFields.size + onMoveDown = { + if (index < reorderedFields.size - 1) { + val item = reorderedFields.removeAt(index) + reorderedFields.add(index + 1, item) + } + }, + canMoveUp = index > 0, + canMoveDown = index < reorderedFields.size - 1 ) } } @@ -1456,52 +1482,17 @@ fun ColumnItem( field: Field, onEdit: () -> Unit, onDelete: () -> Unit, - onDragStart: (Int) -> Unit, - onDragEnd: (Int, Int) -> Unit, - currentIndex: Int, - totalItems: Int + onMoveUp: () -> Unit, + onMoveDown: () -> Unit, + canMoveUp: Boolean, + canMoveDown: Boolean ) { - var isDragging by remember { mutableStateOf(false) } - var dragOffset by remember { mutableStateOf(0f) } - Card( - modifier = Modifier - .fillMaxWidth() - .offset { IntOffset(0, dragOffset.toInt()) } - .pointerInput(Unit) { - detectDragGestures( - onDragStart = { - isDragging = true - onDragStart(currentIndex) - }, - onDragEnd = { - val itemHeight = 80.dp.toPx() // Approximate item height with spacing - val movedItems = (dragOffset / itemHeight).toInt() - val targetIndex = (currentIndex + movedItems).coerceIn(0, totalItems - 1) - - onDragEnd(currentIndex, targetIndex) - isDragging = false - dragOffset = 0f - }, - onDragCancel = { - isDragging = false - dragOffset = 0f - }, - onDrag = { change, dragAmount -> - change.consume() - dragOffset += dragAmount.y - } - ) - }, + modifier = Modifier.fillMaxWidth(), colors = CardDefaults.cardColors( - containerColor = if (isDragging) - MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.7f) - else - MaterialTheme.colorScheme.surfaceVariant + containerColor = MaterialTheme.colorScheme.surfaceVariant ), - elevation = CardDefaults.cardElevation( - defaultElevation = if (isDragging) 8.dp else 1.dp - ) + elevation = CardDefaults.cardElevation(defaultElevation = 1.dp) ) { Row( modifier = Modifier @@ -1510,13 +1501,37 @@ fun ColumnItem( horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically ) { - // Drag handle on the left - Icon( - Icons.Default.Menu, - contentDescription = "Drag to reorder", - tint = MaterialTheme.colorScheme.onSurfaceVariant, - modifier = Modifier.size(24.dp) - ) + // Move up/down buttons + Column { + IconButton( + onClick = onMoveUp, + enabled = canMoveUp, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.KeyboardArrowUp, + contentDescription = "Move up", + tint = if (canMoveUp) + MaterialTheme.colorScheme.onSurfaceVariant + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + IconButton( + onClick = onMoveDown, + enabled = canMoveDown, + modifier = Modifier.size(32.dp) + ) { + Icon( + Icons.Default.KeyboardArrowDown, + contentDescription = "Move down", + tint = if (canMoveDown) + MaterialTheme.colorScheme.onSurfaceVariant + else + MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.3f) + ) + } + } Spacer(modifier = Modifier.width(12.dp)) diff --git a/CollabTableServer/package-lock.json b/CollabTableServer/package-lock.json index 372209c..55dc1a9 100644 --- a/CollabTableServer/package-lock.json +++ b/CollabTableServer/package-lock.json @@ -12,8 +12,7 @@ "better-sqlite3": "^11.0.0", "cors": "^2.8.5", "dotenv": "^16.3.1", - "express": "^4.18.2", - "sequelize": "^6.35.2" + "express": "^4.18.2" }, "devDependencies": { "@types/better-sqlite3": "^7.6.11", @@ -134,15 +133,6 @@ "@types/node": "*" } }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, "node_modules/@types/express": { "version": "4.17.24", "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.24.tgz", @@ -183,16 +173,11 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, "node_modules/@types/node": { "version": "20.19.23", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.23.tgz", "integrity": "sha512-yIdlVVVHXpmqRhtyovZAcSy0MiPcYWGkoO4CGe/+jpP0hmNuihm4XhHbADpK++MsiLHP5MVlv+bcgdF99kSiFQ==", + "dev": true, "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -259,12 +244,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/validator": { - "version": "13.15.3", - "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.3.tgz", - "integrity": "sha512-7bcUmDyS6PN3EuD9SlGGOxM77F8WLVsrwkxyWxKnxzmXoequ6c7741QBrANq6htVRGOITJ7z72mTP6Z4XyuG+Q==", - "license": "MIT" - }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -696,12 +675,6 @@ "url": "https://dotenvx.com" } }, - "node_modules/dottie": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.6.tgz", - "integrity": "sha512-iGCHkfUc5kFekGiqhe8B/mdaurD+lakO9txNnTvKtA6PISrw86LgqHvRzWYPyoE2Ph5aMIrCw9/uko6XHTKCwA==", - "license": "MIT" - }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1104,15 +1077,6 @@ ], "license": "BSD-3-Clause" }, - "node_modules/inflection": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz", - "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==", - "engines": [ - "node >= 0.4.0" - ], - "license": "MIT" - }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1208,12 +1172,6 @@ "node": ">=0.12.0" } }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -1343,27 +1301,6 @@ "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", "license": "MIT" }, - "node_modules/moment": { - "version": "2.30.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", - "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/moment-timezone": { - "version": "0.5.48", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz", - "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==", - "license": "MIT", - "dependencies": { - "moment": "^2.29.4" - }, - "engines": { - "node": "*" - } - }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -1481,12 +1418,6 @@ "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", "license": "MIT" }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", @@ -1651,12 +1582,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/retry-as-promised": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz", - "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==", - "license": "MIT" - }, "node_modules/rimraf": { "version": "2.7.1", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", @@ -1748,100 +1673,6 @@ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "license": "MIT" }, - "node_modules/sequelize": { - "version": "6.37.7", - "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.7.tgz", - "integrity": "sha512-mCnh83zuz7kQxxJirtFD7q6Huy6liPanI67BSlbzSYgVNl5eXVdE2CN1FuAeZwG1SNpGsNRCV+bJAVVnykZAFA==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/sequelize" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.1.8", - "@types/validator": "^13.7.17", - "debug": "^4.3.4", - "dottie": "^2.0.6", - "inflection": "^1.13.4", - "lodash": "^4.17.21", - "moment": "^2.29.4", - "moment-timezone": "^0.5.43", - "pg-connection-string": "^2.6.1", - "retry-as-promised": "^7.0.4", - "semver": "^7.5.4", - "sequelize-pool": "^7.1.0", - "toposort-class": "^1.0.1", - "uuid": "^8.3.2", - "validator": "^13.9.0", - "wkx": "^0.5.0" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependenciesMeta": { - "ibm_db": { - "optional": true - }, - "mariadb": { - "optional": true - }, - "mysql2": { - "optional": true - }, - "oracledb": { - "optional": true - }, - "pg": { - "optional": true - }, - "pg-hstore": { - "optional": true - }, - "snowflake-sdk": { - "optional": true - }, - "sqlite3": { - "optional": true - }, - "tedious": { - "optional": true - } - } - }, - "node_modules/sequelize-pool": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz", - "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/sequelize/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/sequelize/node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, "node_modules/serve-static": { "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", @@ -2101,12 +1932,6 @@ "node": ">=0.6" } }, - "node_modules/toposort-class": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", - "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==", - "license": "MIT" - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -2252,6 +2077,7 @@ "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -2278,15 +2104,6 @@ "node": ">= 0.4.0" } }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/v8-compile-cache-lib": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", @@ -2294,15 +2111,6 @@ "dev": true, "license": "MIT" }, - "node_modules/validator": { - "version": "13.15.15", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.15.tgz", - "integrity": "sha512-BgWVbCI72aIQy937xbawcs+hrVaN/CZ2UwutgaJ36hGqRrLNM+f5LUT/YPRbo8IV/ASeFzXszezV+y2+rq3l8A==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", @@ -2312,15 +2120,6 @@ "node": ">= 0.8" } }, - "node_modules/wkx": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz", - "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", diff --git a/CollabTableServer/src/routes/syncRoutes.ts b/CollabTableServer/src/routes/syncRoutes.ts index 1241162..7b0bcb7 100644 --- a/CollabTableServer/src/routes/syncRoutes.ts +++ b/CollabTableServer/src/routes/syncRoutes.ts @@ -179,19 +179,21 @@ router.post('/sync', async (req: Request, res: Response) => { // Get updates from server since last sync // If lastSyncTimestamp is 0, get all data (initial sync) - // Exclude deleted items from being sent + // IMPORTANT: Include deleted items so clients can sync deletions let serverLists, serverFields, serverItems, serverItemValues; if (lastSyncTimestamp === 0) { + // Initial sync: send all non-deleted items serverLists = db.prepare('SELECT * FROM lists WHERE isDeleted = 0').all(); serverFields = db.prepare('SELECT * FROM fields WHERE isDeleted = 0').all(); serverItems = db.prepare('SELECT * FROM items WHERE isDeleted = 0').all(); serverItemValues = db.prepare('SELECT * FROM item_values').all(); } else { - serverLists = db.prepare('SELECT * FROM lists WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp); - serverFields = db.prepare('SELECT * FROM fields WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp); - serverItems = db.prepare('SELECT * FROM items WHERE updatedAt > ? AND isDeleted = 0').all(lastSyncTimestamp); - serverItemValues = db.prepare('SELECT * FROM item_values WHERE updatedAt > ?').all(lastSyncTimestamp); + // Incremental sync: send all updates including deletions (use >= to include items updated exactly at lastSyncTimestamp) + serverLists = db.prepare('SELECT * FROM lists WHERE updatedAt >= ?').all(lastSyncTimestamp); + serverFields = db.prepare('SELECT * FROM fields WHERE updatedAt >= ?').all(lastSyncTimestamp); + serverItems = db.prepare('SELECT * FROM items WHERE updatedAt >= ?').all(lastSyncTimestamp); + serverItemValues = db.prepare('SELECT * FROM item_values WHERE updatedAt >= ?').all(lastSyncTimestamp); } // Convert isDeleted from INTEGER (0/1) to BOOLEAN