EEltaCrew
Home › Blog

The day a sync deleted 8,802 stores: one renamed API field and a 100-row list

Published 2026-10-07Measured 2026-10-07About 4 min readelta · EltaCrew

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:

  1. Ask the API for the total count (totalCount) only.
  2. If it equals the number of rows in the device database, use the database as is.
  3. 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:

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

The app in this post is Lotto Myeongdang on Google Play (Korean only).

AndroidRoomPublic data APISyncSQLite

Related

Why users quit at the TV pairing code screen: a 6-character code and a Korean keyboardAfter fixing a pairing bug in our TV remote app, the share of users who cancelled on the code entry screen went from 9% to 19%. How the GA4 failure reasons were read, how one input field turned out to open the Korean keyboard, and what was changed.dispatchDetachedFromWindow NullPointerException: tracking an Android crash with none of my code in the stackMdViewer hit a NullPointerException whose stack trace contained only framework frames. The cause was one line that removed a sibling view inside onDetachedFromWindow, combined with how a parent ViewGroup walks its child array. How it was reproduced, why it happens and how it was fixed.How to open .md files on Android when AI-generated markdown shows up as raw symbolsOpen a .md file from ChatGPT, Claude or Codex, an Obsidian note or a GitHub README on your phone and you often see raw #, * and |. Why that happens, three ways to read markdown properly on Android, and what breaks most often, from building a markdown viewer.