[태그:] SQLite

  • 저장 항목을 바꿀 때, 예전 기록을 무엇으로 확인할까?

    가상의 물품 기록 앱을 생각해 보자. 이름을 담는 저장 항목 label을 display_name으로 바꾸고, 분류를 필수로 받기로 했다. 예전 기록 30건이 그대로 보이면 작업이 끝난 걸까? 같은 이름의 물품이 다른 보관함에 들어 있고, 예전에는 분류를 묻지 않았다면 건수만으로는 답할 수 없다.

    먼저 확인할 것은 같은 ID의 기록이 원래 값과 연결 관계를 유지하는가다. 이 글은 그 확인 방법을 설계하고 준비 과정에서 확인한 범위를 설명한다. 측정 실험은 실행하지 않았다(NOT RUN). 남은 전체 절차가 정한 작업 예산을 넘을 것으로 추정해 측정 전에 멈췄다. 어느 방식이 데이터를 보존했거나 중단 뒤에 안전했다는 결과는 없다.

    이 글의 목차

    코드를 요청하기 전에, 바뀌어도 같아야 할 것을 적는다

    ID는 기록을 구별하는 번호다. 화면에 보이는 이름이 같아도 ID가 다르면 다른 기록이다. 여기서 바꾸려는 것은 저장 칸의 이름이지, 그 안에 든 물품 이름이 아니다. 보관함 이름이 같아도 연결 대상의 ID까지 같다는 뜻은 아니다.

    준비한 자료는 실제 고객 정보가 없는 SQLite 데이터베이스다. 물품 기록 30건과 보관함 기록 3건을 따로 만들었다. 아래는 전체 기대값 표에서 고른 7건, 6가지 사례 묶음이다. 표는 이름과 보관함 ID를 추린 것으로, 변경 전 값과 변경 후의 기대값이지 실행 결과가 아니다. 보관함 ID도 변경 전후 같아야 한다.

    표 1 · 코드를 요청하기 전에, 바뀌어도 같아야 할 것을 적는다
    ID 기존 이름 → 기대 이름 유지할 보관함 ID 확인할 이유
    107
    공용 / Shared → 동일
    10
    같은 이름의 첫 기록
    114
    공용 / Shared → 동일
    20
    이름이 같아도 다른 기록·보관함
    128
    물건 / Item 04 → 동일
    10
    메모의 NULL도 그대로 유지
    135
    NULL → NULL
    20
    값 없음 보존
    142
    "" → ""
    30
    빈 문자열 보존
    149
    물건 / Item 07 → 동일
    NULL
    보관함 미지정 보존
    310
    물건 / Item 30 → 동일
    30
    마지막 기록도 검사

    전체 검사 설계는 30건의 ID 집합, 이름과 메모의 NULL·빈 문자열 구분, 새 분류, 보관함 연결, 보관함 자체의 값까지 비교한다. ID가 빠지거나 새로 생긴 경우도 확인한다. 일곱 예시는 전체 검사를 대신하지 않는다. 이번 자료에서 기대하는 연결은 26건이고 미지정은 4건이다. 존재하는 보관함을 가리키더라도 원래와 다른 보관함이면 의미가 달라진다. SQLite의 외래 키 검사는 관계 제약 위반을 찾는 검사다. 의도한 상대인지 판단하려면 이처럼 별도의 기대값이 필요하다.

    새 분류의 기대값은 30건 모두 unknown이다. 이유는 “이전 버전에서 수집하지 않음”이다. 이 앱의 설계가 그 상태를 허용하기 때문에 가능한 선택이다. 실제 분류가 반드시 필요한 업무라면 권위 있는 자료로 확인하거나 입력을 다시 받아야 하며, 확인할 수 없으면 전환을 멈출 수 있다. 이름이나 메모에서 추측해 채우라는 뜻이 아니다.

    NULL은 여기서 저장된 값이 없음을, ""는 길이 0인 문자열이 저장됐음을 뜻한다. 둘은 기존 이름·메모의 상태다. 새 분류의 unknown은 수집하지 않았다는 명시적 정책이고, 다른 앱에서 선택할 수 있는 ‘기타’와도 다르다. 이 예시의 분류에는 ‘기타’가 없다. SQLite의 제약 설명은 CREATE TABLE 문서에 있다. 제약을 만족해도 과거 사실의 근거가 생기지는 않는다는 것은 이 글의 판단이다.

    무엇을 비교하려 했나

    설계한 두 방식은 일부러 정한 교육용 정책이다. 실제 AI가 흔히 만드는 코드의 표본이 아니다.

    • 같은 열 이름 복사(N): 같은 이름의 칸만 옮기므로 새 display_name에는 NULL을 넣고, 필수 분류에는 모든 기록에 같은 supplies를 넣도록 정했다. 허용된 분류 값이지만 과거 분류의 근거는 없다. ID·메모·보관함 연결은 복사하고 한 건 옮길 때마다 저장을 확정하도록 했다.
    • 명시적 대응(P): label → display_name을 지정하고 ID·메모·연결을 유지하며, 새 분류에 unknown을 넣도록 정했다. 테이블을 다시 만들고 복사하는 변경을 한 단위로 묶는 트랜잭션과 재실행 정책을 함께 설계했다.

    N의 결함은 처음부터 공개된 조건이다. P도 구현과 검사에서 틀릴 수 있다. 두 방식은 대응 규칙, 확정 단위와 재실행 처리 등을 함께 바꾸므로 각 요소의 효과를 따로 측정하는 비교가 아니다. SQLite의 트랜잭션 문서가 설명하는 변경 단위와, 옮길 값이 맞는지는 별개다. 이 설계는 새 테이블에 옮긴 뒤 옛 테이블을 지우는 방식을 택했지만 열 이름 변경이 언제나 그 절차를 요구하는 것은 아니다.

    기대값 표는 변환 코드보다 먼저 고정하고, 별도 검사기가 변경 후 데이터를 읽어 비교하도록 준비했다. 변환 코드가 정답 표를 읽어 결과를 만들거나 통과 여부를 결정해서는 안 된다. 다만 아직 실행하지 않은 코드 생성 환경에는 설정·인증 파일과 일부 실행 환경 접근 등의 예외가 남아 있어 정답을 볼 수 없도록 완전히 분리된 연구라고 부를 수 없다.

    계획은 원본을 보존한 일회용 복사본 네 개에서 N/P 각각 정상 실행 한 번과 중단 실행 한 번을 하는 것이었다. 중단 지점은 새 테이블에 15번째 기록을 넣은 뒤, 옛 테이블을 지우기 전으로 고정했다. 이후 안전하게 검사 가능한 정상 완료 사본과 중단 후 복구 사본 각각에서 같은 코드를 최대 한 번 재실행하도록 했다. 이 실행과 중단·재실행은 모두 하지 않았다. 한 지점의 프로세스 종료 시험을 했더라도 정전, 하드웨어 손상, 옛 테이블 삭제 이후의 실패까지 설명할 수는 없다.

    화면과 저장된 기록은 따로 확인한다

    준비한 화면 검사 설계는 이전 이름으로 읽는 화면, 새 이름으로 읽는 화면, 읽는 코드만 새 이름에 맞게 고친 예전 화면을 구분했다. 화면 조회 오류만으로 데이터가 사라졌다고 단정할 수 없다. 반대로 읽기 코드를 고친 화면이 맞게 보이더라도 수정하지 않은 예전 앱의 호환성을 입증하지 못한다.

    중단 뒤에도 상태와 다음 행동을 나눠 적어야 한다. 온전한 이전 구조로 돌아온 것은 새 구조로의 전환 완료가 아니다. 부분적으로 바뀐 상태나 판독할 수 없는 상태는 그대로 기록한다. 재실행을 거부하고 아무것도 바꾸지 않은 것, 다시 실행해 전환을 마친 것, 이미 완료된 상태를 별도 검사로 확인하고 변경하지 않은 것은 구분해야 한다. 이번 설계는 다른 쓰기 작업을 멈춘 작업 시간을 전제로 했고 읽기 전용 화면을 대상으로 했다. 예전 앱과 새 앱의 동시 쓰기나 무중단 전환은 검증 대상이 아니다.

    준비에서 확인한 것과 측정하지 않은 이유

    2026년 10월 12일 준비 기록에는 원본과 검사 도구를 점검한 오프라인 검사 19개 통과가 남아 있다. 합성 원본 30건이 기대값 표의 입력과 맞는지, 별도 검사기가 일부러 바꾼 이름·분류·관계·메모·NULL/빈 문자열을 잡는지 확인했다. 정상 대조용 새 데이터베이스는 기대값에서 직접 만들었다. 원본을 새 구조로 변환한 결과가 아니며 N/P의 성적도 아니다. 당시 환경은 Python 3.9.6과 Python에 연결된 SQLite 3.54.0이었다.

    10월 26일 독립 검토는 후보 파일 144개의 파일 크기·내용 지문(해시) 일치와 오프라인 검사 실행 34회의 통과를 기록했다. 34회에는 반복이 있어 서로 다른 사례 34개를 뜻하지 않으며, 앞선 19개와 더할 누적 성적도 아니다. 검토 당시에는 새 작업 사본에서 원본 보호용 파일 권한이 쓰기 가능 상태로 나타나 준비 판정이 막혔다. 이후 선택된 기준 사본을 읽기 전용으로 바꾸고 고정한 입력·권한·파일 분리 검사를 다시 통과한 기록을 바탕으로 설계만 고정했다. 이후에도 사용할 때마다 권한과 입력을 다시 확인해야 한다.

    11월 5일 운영 결정은 남은 생성·실행·검증에 약 75분이 필요하지만, 60분 준비·실험 허용량 중 약 29분을 이미 썼다고 정리했다. 이는 작업 예산의 추정이며 데이터 이전 속도의 측정값이 아니다. 예정한 전체 절차가 남은 허용량을 넘을 것으로 추정해 방법론 글로 전환했다. 축소한 측정도 실행하지 않았으며, 더 작은 검사가 모두 불가능하다는 뜻은 아니다. N/P 코드 생성, 실제 변환, 중단·재실행, 변환 결과 검증과 화면 관찰은 모두 NOT RUN이다. 오프라인 준비가 끝났다는 기록으로 이 빈칸을 채울 수 없다.

    본문의 SQLite 문서 설명은 2026년 10월 11일 저장한 확인 기록에 근거하며, 발행 전 재확인이 필요하다.

    독자가 가져갈 것은 승자가 아니라 요청서다. “각 ID의 기존 값과 기대하는 새 값을 표에 나란히 적고, 새 필수 값의 근거와 유지할 연결 대상 ID를 밝혀 주세요. 중단 후 재실행·거부 기준과 화면 확인 방법도 별도로 써 주세요. 시험하지 않는 실패 상황을 밝히고, 준비 검사와 실제 변환 결과를 구분해 주세요.” AI가 돌려준 ID별 대조표부터 검토하자는 것이 이 글의 제안이다. 실제 보존 여부는 그 기준에 따른 실행과 기록이 있어야 답할 수 있다.

    이 글은 AI를 활용해 자료 조사와 초안 작성을 보조했으며, 작성자가 최종 원고를 검토하고 수정 방향을 결정했습니다.

  • When stored fields change, how should we check old records?

    Imagine an inventory-notes app. You rename the stored field label to display_name and make category a required field. If all 30 old records still appear, is the job done? A count cannot answer that when identically named items belong in different containers and the app never collected their categories.

    The first question is whether the record with each ID keeps its original values and links. This article explains how we designed that check and what preparation established. The measured experiment was NOT RUN. We stopped before measurement because the remaining full procedure was estimated to exceed the work allowance. We have no result showing that either approach preserved the data or was safe after interruption.

    In this article

    Write down what must remain true before requesting code

    An ID distinguishes one record from another. Two records can have the same displayed name and different IDs. Here we are changing the name of the storage field, not the item name inside it. Two containers can share a name and still have different IDs, so a link must point to the right one.

    We prepared a synthetic SQLite database with no customer information: 30 item records plus three container records. The table below shows seven records in six case groups, selected from the full expectations table. It samples names and container IDs, showing inputs and expected values, not observed migration results. Container IDs must also remain unchanged.

    Table 1 · Write down what must remain true before requesting code
    ID Old name → expected name Container ID to retain Reason to check
    107
    공용 / Shared → unchanged
    10
    First record in a duplicate-name pair
    114
    공용 / Shared → unchanged
    20
    Same name, different record and container
    128
    물건 / Item 04 → unchanged
    10
    Also retain the NULL note
    135
    NULL → NULL
    20
    Preserve absence of a value
    142
    "" → ""
    30
    Preserve empty text
    149
    물건 / Item 07 → unchanged
    NULL
    Preserve an unassigned container
    310
    물건 / Item 30 → unchanged
    30
    Check the final record too

    The Korean words in the stored strings mean “Shared” and “Item”; the English glosses are already part of the synthetic data. The planned conversion must preserve these stored values without translating them.

    The full check was designed to compare the exact set of 30 IDs, names, notes including NULL-versus-empty distinctions, the new category, container links, and the containers' own values. Missing or extra IDs matter too. Seven examples cannot replace that full check. In this test data, 26 records are expected to link to a container and four to have none. A link to an existing but different container would change a record's meaning. SQLite's foreign-key check detects relationship-constraint violations. Checking the intended destination requires separate expectations such as these.

    The expected new category is unknown for all 30 old records, with the reason “not collected in the previous version.” This is possible because this app's declared policy permits that state. If another workflow requires an authoritative category, it must confirm categories from an authoritative source or collect them again; if that is not possible, conversion can be stopped. This does not mean guessing categories from names or notes.

    Here NULL means no stored value, while "" is a stored string of zero length. Those are states of the existing name or note. The new category's unknown explicitly records that it was not collected. It is also distinct from an “other” option that another app might offer; this example has no such category. SQLite describes constraints in its CREATE TABLE documentation. Our inference is that satisfying a constraint does not establish a value's historical truth.

    What we planned to compare

    The two approaches are deliberately specified teaching policies, not samples of code that AI typically produces.

    • Same-column-name copy (N): copy only fields whose names match, so the renamed display_name field is left NULL, and fill every required category with supplies. This is an allowed category value with no evidence behind it for these old records. Copy IDs, notes and container links, committing each copied record separately.
    • Explicit mapping (P): specify label → display_name, retain IDs, notes and links, and assign unknown to the new category. Group the table rebuild and copying changes into one transaction and define a rerun policy.

    N's defects were declared in advance. P could still be implemented or checked incorrectly. The approaches change mapping, commit boundaries, rerun handling and other factors together, so the comparison cannot isolate each factor's effect. The unit of change described in SQLite's transaction documentation is separate from whether the chosen values are correct. Our design calls for copying into a new table and then dropping the old one; renaming a column does not always require one.

    We prepared a fixed expectations table before migration code, with a separate checker to read the resulting data. Migration code must not use the answer table to construct its output or decide whether it passes. The setup for generating migration code, which was not run, still allowed access exceptions such as configuration and authentication files and parts of the runtime environment. We cannot describe it as research in which access to the answers was fully prevented.

    The plan called for four disposable copies of the preserved baseline: one clean run and one interrupted run for each approach. The interruption point was fixed after inserting the fifteenth record into the replacement table, before dropping the old table. Each safely inspectable clean completed output and recovered interrupted copy would then receive at most one rerun with the same, unmodified code. None of those runs, interruptions or reruns took place. Even if the process-interruption test at that one point had been run, it could not have established behavior during power loss, hardware damage or failure after the old table was dropped.

    Check the screen separately from stored records

    The prepared screen-check design distinguished a screen querying the old field name, one querying the new name, and the old screen with only its read query updated. A query error alone cannot establish data loss. Conversely, a correct display after adapting the query would not prove compatibility with the unchanged old app.

    After interruption, record the state separately from the next action. Returning to a complete old structure is not completion of the conversion. A partial or unreadable state needs its own description. Refusal without changes, a retry that completes conversion, and a no-op independently verified against the expectations table must be reported separately. This design assumed a maintenance period with other writes stopped and was limited to read-only screens. Concurrent writes from old and new apps and zero-downtime deployment were outside its scope.

    What offline preparation established, and why measurement was not run

    The preparation record for October 12, 2026 reports 19 passing offline tests of the baseline and checker. They checked that the 30 synthetic baseline records matched the table's inputs and that the separate checker detected deliberately altered names, categories, relationships, notes, and NULL/empty-text distinctions. The reference target database (the positive control) was built directly from expected values. It was not produced by converting the baseline, and these checks were not N/P scores. The recorded environment was Python 3.9.6 with SQLite 3.54.0 linked to Python.

    An independent review on October 26 recorded matching file sizes and content fingerprints (hashes) for 144 candidate files and 34 passing offline test executions. The 34 included repetitions, not 34 distinct scenarios, and should not be added to the earlier 19 as a cumulative score. That review initially blocked readiness because the baseline file that should have been protected was writable in a fresh working copy. The selected baseline copy was subsequently made read-only, and checks of inputs fixed to recorded versions, permissions and file separation passed. Only the design was then frozen. Permissions and inputs must be checked again each time the setup is used.

    The November 5 operating decision estimated that generation, execution and verification would require about 75 more minutes, while about 29 minutes of the standing 60-minute preparation-and-experiment allowance had already been used. These are work-budget estimates, not measurements of migration speed. The decision was to write a methods article because the planned full procedure was estimated to exceed the remaining allowance. No reduced measurement was run either; this does not show that every smaller check would be impossible. N/P code generation, actual conversion, interruption, reruns, verification of migration outputs and screen observations all remain NOT RUN. Offline preparation cannot fill those gaps.

    The SQLite documentation descriptions above rely on the saved October 11, 2026 checks and require rechecking before publication.

    The practical takeaway is a request, not a winning approach: “Put the old and expected new values for each ID side by side in a table. State the evidence for each new required value and the destination ID of every relationship. Separately specify when to retry or refuse after interruption, and how to check the screens. Identify failure scenarios outside the test and distinguish preparation checks from actual conversion results.” Reviewing the per-ID table returned by the AI first is this article's proposal. Whether the records actually survive with their meaning intact requires execution and recorded checks against it.

    AI assisted with research and drafting. The author reviewed the final manuscript and directed the revisions.