숫자는 SABR의 Lahman 데이터베이스에서 가져온다. 1871년부터 2025년까지의 시즌 기록이 들어 있고, 2020년에 메이저로 인정된 니그로리그도 함께 있다. 받아서 바로 쓸 수 있는 모양은 아니었다.
SABR가 배포하는 건 CSV와 MS Access와 MSSQL, 셋이다. sqlite 빌드는 없다. 엔진은 sqlite를 읽게 짰으니 CSV를 받아서 직접 지어야 했다.
테이블을 먼저 세운다
CSV를 그대로 부으면 모든 값이 TEXT가 된다. 그러면 yearID >= 1930 같은 조건이 숫자를 비교하지 않고 문자열을 비교한다. 숫자 1930 대신 글자 "1930"을 놓고 크기를 따지니 틀린 답이 조용히 나온다.
그래서 순서를 뒤집었다. 스키마 파일로 테이블을 먼저 만들고, 타입이 선 그릇에 CSV를 붓는다. 스키마는 data/lahman-schema.sql이 정본이다. 붓기 전에 CSV 헤더가 그 컬럼과 같은지 본다. 다르면 멈춘다. 배포 모양이 바뀐 건데 그걸 모르고 넘어가면 컬럼이 한 칸씩 밀린 데이터베이스가 생긴다.
/**
* CSV 더미를 sqlite로 짓는다. 순서가 규율이다:
* ① 스키마로 테이블을 만든다(타입을 먼저 세운다)
* ② CSV 헤더가 그 컬럼과 같은지 본다 — 다르면 멈춘다
* ③ 붓는다
* ④ **빈 문자열을 NULL로 되돌린다**
* ⑤ 인덱스를 만든다 — 유니크 위반이 있으면 여기서 드러난다
*/
빈 칸을 NULL로 되돌린다
네 번째가 나중에 제일 많이 걸렸다.
sqlite의 .import는 CSV의 빈 칸에 빈 문자열을 넣는다. 그대로 두면 기록이 없는 칸과 0인 칸이 같은 값이 된다. SABR의 readme가 그 둘을 갈라 적어 뒀다. 비어 있으면 0이 아니라 모른다는 뜻이다.
니그로리그 기록에서 이게 진짜 문제가 된다. 조시 깁슨의 삼진은 열여섯 시즌 중 네 해가 기록에 없다. 그 칸에 0을 적으면 지면이 그 해 삼진을 당하지 않았다고 말한다. 당시 박스스코어가 안 남았다는 뜻인데 전혀 다른 말이 된다.
붓고 나서 빈 문자열을 전부 NULL로 되돌린다. 이 원칙은 뒤에 한 번 더 나온다. 연도 블록에서도 통산 합계에서도 같은 규칙을 쓴다.
릴리스를 폴더 이름으로 못 박았다
재현성에서 한 번 막혔다.
SABR의 CSV는 릴리스 태그 없이 공유 링크로만 공개돼 있다. 같은 주소가 내년에는 다른 데이터를 준다. 그런데 이 엔진의 게이트는 산문의 숫자가 기록과 맞는지를 본다. 반년 뒤에 돌렸을 때 기록이 말없이 바뀌어 있으면, 빨갛게 뜬 게 내 문장이 틀린 건지 데이터가 바뀐 건지 알 수 없다.
고정은 폴더 이름으로 했다. 배포 폴더가 lahman_1871-2025_csv가 아니면 멈춘다.
SABR 공유 폴더가 'lahman_1871-2025_csv'가 아니다 — 새 릴리스가 올라온 것으로 보인다.
스냅샷을 갈아타는 것은 발행된 페이지의 숫자를 다시 세는 일이라, 사람이 보고 넘긴다.
새 릴리스로 갈아타면 이미 발행한 지면의 숫자를 전부 다시 검증해야 한다. 조용히 넘어갈 일이 아니고 날을 잡아서 할 일이다. 그래서 멈추고 사람을 부른다.
저장소에 넣지 않았다
지어진 파일은 70MB쯤 된다. 커밋하면 갱신할 때마다 히스토리에 통째로 쌓이고, CC BY-SA 저작물을 재배포하는 모양이 된다. 받아서 쓰기만 하면 둘 다 없어진다.
.cache/에 두고 git에서는 뺐다. 짓다 중단돼도 반쪽 파일이 안 남게 임시 파일로 짓고 다 되면 이름을 바꾼다. 정상 빌드가 70MB 안팎이라 40MB 하한을 두고, 그보다 작으면 실패로 본다.
출처 표기는 지면이 한다. 선수 지면 판권마다 Lahman의 CC BY-SA 줄이 들어가고, 니그로리그 시즌을 담은 지면에는 Seamheads가 함께 들어간다. 그 데이터의 절반이 거기서 왔다.