파일 내보내기 형식 — bson · jsonl · csv · sqlite
상태: 설계
파일로 나가는 형식을 binary·json 2개에서 6개로 늘리는 설계입니다. 형식을 고르는 근거와,
형식마다 답이 갈리는 값 표현을 한자리에 모으는 것이 이 문서의 내용입니다.
함께 정하는 것이 하나 더 있습니다 — 행을 객체 트리로 접는 처리의 단일화이고, 그것이 없으면 형식마다 중첩 규칙이 어긋납니다(3절).
1. 문제 — 두 형식이 도달하지 못하는 소비처
현재 파일 형식은 2개이고 각각의 소비처가 정해져 있습니다.
| 형식 | 읽는 쪽 |
|---|---|
binary (.tcb) | 생성된 테이블 리더. 런타임 적재 전용입니다 |
json | 생성된 TypeScript, 그리고 「다른 도구로 가져다 쓰는」 경로 전부 |
두 번째 줄에 소비처가 몰려 있습니다. json이 텍스트 상호운용의 유일한 출구이므로, 아래 4가지는
전부 JSON을 한 번 더 변환해서 쓰고 있거나 쓸 수 없습니다.
| 하려는 것 | json으로 하면 |
|---|---|
| MongoDB 적재본을 파일로 보관·이관 | mongoimport가 JSON을 받으나, bigint가 문자열로 실려 있어 적재 후 타입이 달라집니다 |
| 행 단위 스트리밍 적재 (BigQuery · ClickHouse · DuckDB) | 배열 하나가 파일 하나이므로 전체를 메모리에 펼쳐야 합니다 |
스프레드시트·pandas에서 열기, git diff로 값 변경 확인 | 배열 하나가 한 줄이거나 들여쓴 트리이므로 행 단위 대조가 되지 않습니다 |
| 테이블 간 조인으로 데이터 점검 (「이 아이템을 참조하는 행 전부」) | 형식에 조회가 없습니다. 파일 수십 개를 읽는 스크립트를 프로젝트마다 작성하게 됩니다 |
bigint가 문자열인 것이 우회이지 표현이 아니라는 점이 특히 중요합니다. JSON에는 숫자 타입이
하나뿐이므로 그 우회가 정당하지만, 정수 타입이 있는
형식에서는 정당하지 않습니다. 지금은 그런 형식이 없어서 우회가 곧 규칙처럼 되어 있습니다.
2. 결정 — 형식 4개
Type | 산출물 | 역할 | 새 의존 |
|---|---|---|---|
bson | 테이블당 .bson | 타입을 보존하는 바이너리 문서. MongoDB 도구가 변환 없이 읽습니다 | 없음 — MongoDB.Driver가 이미 있습니다 |
jsonl | 테이블당 .jsonl | 행 하나가 줄 하나. 스트리밍 적재와 행 단위 diff | 없음 |
csv | 테이블당 .csv · .tsv | 평탄한 표. 스프레드시트·분석 도구·git diff | 없음 |
sqlite | 파일 1개 | 조회 가능한 데이터셋. 테이블 간 조인이 성립합니다 | Microsoft.Data.Sqlite (8절) |
앞 3개는 .csproj를 고치지 않으므로 의존 문서와 자체 배포 크기가
무변경입니다. sqlite만 의존이 늘고, 그것이 이 형식의 유일한 비용입니다(8.1절).
행 벌은 형식이 갈리지 않습니다. 테이블의 행 벌은 json과 같이
산출물이 갈립니다 — 파일 형식은 벌마다 파일 하나, sqlite는 벌마다 테이블 하나입니다.
보류한 것과 근거입니다. 없는 형식의 자리를 적어 두지 않으면 같은 검토가 반복됩니다.
| 형식 | 보류 근거 |
|---|---|
| MessagePack · CBOR | json과 tcb 사이에 남는 자리가 없습니다. 런타임 적재는 tcb가 더 작고 파싱이 없으며, 상호운용은 JSON 쪽 지원 범위가 넓습니다 |
| Parquet | 컬럼 지향이라 tcb의 모델과 정합하나, 중첩 타입 매핑 비용이 큽니다. sqlite가 분석 수요의 대부분을 덮으므로 그 뒤에 다시 판정합니다 |
| YAML | 데이터 테이블용 형식이 아닙니다. 설정 파일의 형식입니다 |
| XML | 비용은 낮으나(System.Xml) 요청이 확인되지 않았습니다 |
| xlsx 쓰기 | Sylvan은 읽기 전용이고 NPOI는 저장소 안에서만 쓰는 것이므로 writer 의존이 새로 필요합니다 |
3. 선행 조건 — 행 구성의 단일화
형식을 추가하는 비용은 타깃 등록이 아니라 행 구성에 있습니다. 타깃은
레지스트리가 어트리뷰트로 찾으므로 파일 하나면 되지만,
행을 객체 트리로 접는 처리는 전부 JsonExporter에 있습니다.
JsonExporter의 것 | 무엇 |
|---|---|
Compose · MemberValue · RecordElement | 레코드·배열·다중 중첩을 레벨마다 접습니다. 설계의 「이름이 있는가·반복하는가」 2개 비트가 여기 있습니다 |
ForJson(2종) | 없음·원소 없음·가변 길이 배열의 판정, 그리고 JSON에서만 필요한 값 보정 |
| compact 투영 | 와이어 컬럼 순서로 평탄화합니다 |
앞 2줄은 형식과 무관하고 마지막 줄은 csv·sqlite가 다시 필요합니다. 형식마다 복제하면 중첩
규칙이 형식별로 갈라집니다 — 이 저장소에는 그 결과가 이미 기록되어 있습니다. 타깃 이름이 3곳에
적혀 있어서 DB 4종의 타깃 사이드 검증이 빠져 있었고,
그것이 레지스트리를 만든 이유입니다.
결정. 행 구성을 RowComposer로 옮기고, 형식별로 갈리는 것만 스칼라 정책으로 분리합니다.
RowComposer 레벨마다 이름·반복 2개 비트로 트리를 만듭니다. 형식과 무관합니다
IScalarPolicy 값 하나를 그 형식의 표현으로. 형식마다 1개
RowComposer.Flat 와이어 컬럼 순서의 평탄한 열. json compact · csv · sqlite가 씁니다
판정 기준은 JSON 골든이 한 바이트도 움직이지 않는 것입니다. 동작 보존 리팩터링이므로 골든에 diff가 나오면 그것이 결함입니다.
4. 값 매핑 — 형식마다 갈리는 자리
스칼라 정책이 판정해야 하는 것의 전부입니다. 형식 하나를 추가하는 작업은 이 표에 열 하나를 채우는 것입니다.
| 값 | tcb | json | bson | csv | sqlite |
|---|---|---|---|---|---|
int | fixed32 | 숫자 | BsonInt32 | 10진수 | INTEGER |
bigint | fixed64 | 문자열 (2^53 우회) | BsonInt64 — 우회 불필요 | 10진수 | INTEGER — 우회 불필요 |
float | IEEE-754 32비트 | 왕복 가능한 최단 10진수 | BsonDouble — 64비트로 넓어집니다 | 왕복 가능한 최단 10진수 | REAL — 64비트 |
double | IEEE-754 64비트 | 같음. 정수값은 .0을 뗍니다 | BsonDouble | 같음 | REAL |
datetime | 100ns 틱 fixed64 | "2022-03-01T09:00:00" | BsonDateTime — 밀리초 (5.2절) | ISO 8601 | TEXT ISO 8601 (8.3절) |
timespan | 100ns 틱 fixed64 | "10675199.02:48:05" | BsonInt64 틱 | .NET 표기 그대로 | INTEGER 틱 |
uuid | 16바이트 | 정규 문자열 | BsonString 정규 문자열 | 같음 | TEXT 정규 문자열 |
bool | fixed8 | true/false | BsonBoolean | true/false | INTEGER 0·1 |
enum | 지그재그 varint32 | 숫자 | BsonInt32 | 10진수 | INTEGER |
| 참조 | 대상 키의 타입 그대로 | 같음 | 같음 — RefKeyType을 씁니다 | 같음 | 같음 |
bitset | fixed64 | bigint와 동일 | bigint와 동일 | bigint와 동일 | bigint와 동일 |
| 배열 · 레코드 | 컬럼마다 블록 | 중첩 트리 | 중첩 도큐먼트 | 평탄한 열(7.1절) | TEXT JSON (8.2절) |
| 없음 | presence 비트 | null | BsonNull | - (7.3절) | NULL |
| 원소 없음 | 원소 presence 비트 | null | BsonNull | - | JSON의 null |
bson 열은 새로 정하는 것이 아니라 mongodb 타깃의 것입니다. 같은 데이터가 파일로 나가는
경로와 데이터베이스로 나가는 경로에서 다르게 표현되면, 둘 중 무엇이 기준인지 말할 수 없게
됩니다. MongoDbExporter.ToBsonScalar를 공유 정책으로 옮기고 양쪽이 그것을 씁니다.
sqlite 열도 같은 근거로 관계형 2종(mysql·postgresql)의 매핑을 따릅니다 — 배열이
JSON 텍스트이고 timespan이 틱인 것이 그것입니다.
참조 키는 int32가 아닙니다. 참조 키 타입 개정에서
DatabaseExporterBase.ElementTypeOf가 이미 대상 키의 타입으로 해석하므로, 그 처리가 정책과
함께 이동합니다.
5. bson
5.1 프레이밍 — 도큐먼트 연속 기록
BSON은 루트가 도큐먼트여야 하므로 JSON처럼 배열 하나를 파일로 둘 수 없습니다. 2가지 중 하나를 정해야 합니다.
| 안 | 형태 | 판정 |
|---|---|---|
| 도큐먼트 연속 기록 | 행마다 도큐먼트 하나를 이어 붙입니다. 길이 접두어가 있으므로 구분자가 필요 없습니다 | 채택. bsondump·mongorestore가 변환 없이 읽습니다 — mongodump가 내보내는 .bson과 같은 형식입니다 |
{"rows":[…]} 포장 | 도큐먼트 1개 안에 배열 1개 | json과 형식이 같아지나, 표준 도구가 읽을 수 있는 파일이 아니게 됩니다. 그리고 배열 원소마다 인덱스 키가 실립니다(아래) |
5.2 이 형식에 없는 것 2개
compact 형식을 받지 않습니다. BSON 배열은 도큐먼트이고 원소마다 "0", "1" 같은 키를
함께 싣습니다. 행을 배열로 바꾸면 필드 이름이 인덱스 문자열로 대체되는 것이므로, 작아지지
않고 대개 커집니다. UseCompactRowFormat를 받지 않고, 적혀 있으면 거부합니다 — 무시하면
「설정이 적용되지 않는다」로만 나타납니다.
datetime이 밀리초입니다. BsonDateTime은 UTC 밀리초 int64이므로 100나노초 틱에서
정밀도가 낮아집니다. 그럼에도 mongodb 타깃과 같은 매핑을 유지하는 것이 결정이고, 근거는
4절과 같습니다 — 두 경로의 표현이 갈리 는 것이 더 큰 비용입니다. 무손실이 필요한 경로는
tcb입니다(근거).
5.3 매니페스트와 교체
manifest-bson.json을 함께 쓰고, 스테이징에 먼저 기록한 뒤 전부 성공했을 때 교체합니다.
Sweep 기본값은 true입니다 — 파일 익스포트 전부와 같습니다.
6. jsonl
json과 다른 것은 프레이밍 하나입니다. 행 하나가 도큐먼트 하나이고 줄바꿈으로 구분되므로,
Indented를 받지 않습니다(줄바꿈이 구분자입니다). 값 표현은 json과 완전히 동일하고, 같은
스칼라 정책을 씁니다.
FileExtension으로 .jsonl과 .ndjson을 고릅니다. 같은 형식의 두 이름이고 소비하는 도구에
따라 갈립니다.
json에 옵션으로 붙이지 않는 이유입니다. Framing 키 하나면 되지만, 그러면 확장자·매니페스트
이름·Indented의 유효성이 그 키에 딸린 조건이 됩니다. 타깃 1개를 지우는 일이 파일 1개를 지우는
일이어야 한다는 것이 이 저장소의 방침입니다.
7. csv · tsv
7.1 평탄화 — 와이어 컬럼
CSV에는 중첩이 없으므로 와이어 컬럼 순서로 평탄화합니다. json compact가 이미 그
순서이므로 같은 처리(RowComposer.Flat)를 씁니다.
헤더 이름은 WireColumn.Name과 원소 번호로 만듭니다.
index,name,slots[0].itemId,slots[0].count,slots[1].itemId,slots[1].count
가변 길이 배열은 선언된 최대 길이만큼 열을 냅니다. 열의 수가 행마다 달라지는 CSV는 CSV가
아니고, 채워지지 않은 자리는 없음 표기(-)입니다.