The day a sync deleted 8,802 stores: one renamed API field and a 100-row list
In our lottery retailer map app, a sync turned every store name into "Unknown" and wiped the 8,802 stores bundled with the app. Three causes — a renamed field in a public data API, a sync that deleted rows based on only the first 100 results, and a geocoder reading keys that do not exist — and how each was fixed.
The map filled up with "Unknown"
We run an app that shows lottery retailers on a map. It ships with a SQLite file (store.db) holding 8,802 stores, and compares it against Korea's public data portal API for lottery retailers, applying only what changed.
On September 18, the store tab showed every name as 「알수없음」 ("Unknown"). Pulling the app database from a real device (Galaxy Note10+) showed only 295 rows, and every single one was named "Unknown". There should have been 8,802.
There were three layers of cause.
Cause 1: the response field was renamed
The app read the store name like this:
String merchant = item.has("상호") ? item.get("상호").getAsString() : "알수없음";
상호 means "business name". Calling the API directly showed there was no such field any more. The name was now in 판매점명 ("retailer name"), and the lot-number address field 지번주소 was gone too. Calling it again today (October 7) still returns four fields:
도로명주소 (road address), 순서 (order), 판매점명 (retailer name), 판매점종류 (retailer type)
totalCount 23,503
Since has("상호") was always false, every incoming store became "Unknown". That alone only makes names wrong. The rows disappeared because of the second cause.
Cause 2: deleting based on a 100-row list
The sync worked like this:
- Ask the API for the total count (
totalCount) only. - If it equals the number of rows in the device database, use the database as is.
- If not, fetch the list and compare. Insert new stores, and delete database rows that are not in the list.
The list fetched in step 3 was page=1&perPage=100 — the first 100 rows only. As long as the total count matched the device database, step 3 never ran, so nobody noticed. Once the API total became 23,503 and no longer matched 8,802, the app fetched 100 rows, concluded that "any store not in these 100 has closed", and deleted the rest.
On top of that, because of cause 1, all 100 of those rows were named "Unknown". The comparison key was "name|road address", so none of them matched the 8,802 bundled rows, and every bundled row was deleted.
Cause 3: the geocoder read keys that do not exist either
Stores added by the sync get coordinates from Naver's geocoding API. That code read address1 and address2 from the response, but the actual keys are roadAddress and jibunAddress. So every row added by the sync was saved with an empty address.
All three are the same mistake: assuming a key exists in a response. There was no compile error and no crash. Values just went quietly empty.
What was fixed (1.0.58)
1. Read the name from several keys, and fall back to a similar field
private static String storeName(JsonObject item) {
for (String k : new String[]{"판매점명", "상호", "상호명", "판매점"}) {
String v = str(item, k);
if (!v.isEmpty()) return v;
}
for (String k : item.keySet()) {
if ((k.contains("상호") || k.contains("점명")) && item.get(k).isJsonPrimitive()) {
String v = item.get(k).getAsString().trim();
if (!v.isEmpty()) return v;
}
}
return "알수없음";
}
A store whose name still comes out as "Unknown" is not inserted at all.
2. Delete only when the fetched list is complete
boolean complete = stores.size() >= nCount;
syncStoreDataDelta(db, stores, complete);
// inside syncStoreDataDelta, after inserting new rows and updating changed ones
if (!complete) return; // partial list: never delete
With only 100 rows fetched, the sync now inserts and updates but never deletes.
3. Devices that are already broken repair themselves
Users who receive the update already have a broken database. Room's createFromAsset reads the asset file only when the database is first created, so updating the app does not bring the 8,802 deleted rows back. So the sync now does two things first:
- Delete rows named "Unknown" or with an empty address.
- If the device database has fewer than 90% of the rows in the bundled one, copy the asset
store.dbto the cache folder, open it read-only, and re-insert every row the device is missing (key = name + road address).
4. Correct the geocoder keys
Read roadAddress and jibunAddress, and if they are still empty, keep the address that was sent in the request.
Running 1.0.58 on the same Note10+ gave 8,901 rows, 0 named "Unknown", 0 with an empty address, and real store names back on the map.
Takeaways
- Do not assume field names of an external API from its documentation or from old code. Call it and look at the keys.
- A sync that deletes should delete only after confirming the fetched list is complete. Missing from a partial list means "not fetched yet", not "gone".
- Shortcuts like "skip if the counts match" can hide the code path below them for a long time. This delete path did not run until the counts diverged.
- Fixing the code stops the bug, but it does not bring deleted data back. Make the fixed version repair the bad data already sitting on users' devices.
The app in this post is Lotto Myeongdang on Google Play (Korean only).