본문으로 건너뛰기

파일 내보내기 형식 — 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 · CBORjsontcb 사이에 남는 자리가 없습니다. 런타임 적재는 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. 값 매핑 — 형식마다 갈리는 자리

스칼라 정책이 판정해야 하는 것의 전부입니다. 형식 하나를 추가하는 작업은 이 표에 열 하나를 채우는 것입니다.

tcbjsonbsoncsvsqlite
intfixed32숫자BsonInt3210진수INTEGER
bigintfixed64문자열 (2^53 우회)BsonInt64우회 불필요10진수INTEGER — 우회 불필요
floatIEEE-754 32비트왕복 가능한 최단 10진수BsonDouble64비트로 넓어집니다왕복 가능한 최단 10진수REAL — 64비트
doubleIEEE-754 64비트같음. 정수값은 .0을 뗍니다BsonDouble같음REAL
datetime100ns 틱 fixed64"2022-03-01T09:00:00"BsonDateTime밀리초 (5.2절)ISO 8601TEXT ISO 8601 (8.3절)
timespan100ns 틱 fixed64"10675199.02:48:05"BsonInt64.NET 표기 그대로INTEGER
uuid16바이트정규 문자열BsonString 정규 문자열같음TEXT 정규 문자열
boolfixed8true/falseBsonBooleantrue/falseINTEGER 0·1
enum지그재그 varint32숫자BsonInt3210진수INTEGER
참조대상 키의 타입 그대로같음같음 — RefKeyType을 씁니다같음같음
bitsetfixed64bigint와 동일bigint와 동일bigint와 동일bigint와 동일
배열 · 레코드컬럼마다 블록중첩 트리중첩 도큐먼트평탄한 열(7.1절)TEXT JSON (8.2절)
없음presence 비트nullBsonNull- (7.3절)NULL
원소 없음원소 presence 비트nullBsonNull-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가 아니고, 채워지지 않은 자리는 없음 표기(-)입니다.

7.2 확장자가 정하는 구분자와 이스케이프

설정이 아닙니다. FileExtension으로 이미 어느 형식인지 적었으므로, 한 번 더 적는 것은 나중에 서로 어긋날 자리를 만드는 것입니다 — text 타깃이 같은 방식입니다.

확장자구분자이스케이프
.csv,"로 감싸고 안의 "는 2번으로. 줄바꿈·구분자를 포함한 값에만
.tsv탭과 줄바꿈을 이스케이프 — 이 형식에서는 그것들이 구분자입니다

LineEnding을 받습니다(lf 기본). 남의 도구가 읽는 파일이고 실행마다 전부 diff가 발생하는 것을 피할 자리입니다.

7.3 없음의 표기

-입니다. 시트가 없음을 적는 표기를 그대로 씁니다(빈 칸과 없음). CSV에서 빈 필드는 빈 문자열과 구별되지 않으므로, null을 표현할 다른 방법이 없습니다. 값이 정확히 -인 문자열은 \-로 적습니다 — 시트의 규칙과 같으므로 왕복이 성립합니다.

8. sqlite

파일 1개에 테이블 전부입니다. 테이블당 파일로 나누면 조인이 성립하지 않고, 조인이 이 형식을 고르는 이유입니다.

{
"Type": "sqlite",
"Path": "./build/data",
"FileName": "data.sqlite",
"NamePrefix": ""
}

8.1 의존과 그 대가

Microsoft.Data.Sqlite가 필요하고, 그것은 네이티브 자산을 동반합니다(SQLitePCLRaw). 이 개정에서 유일하게 비용이 있는 자리이므로 함께 처리할 것을 적어 둡니다.

무엇
의존 문서 갱신DependencyDocTests.csproj와 대조합니다 — 어긋나면 스위트가 실패합니다
자체 배포 크기 실측PublishSingleFile에 네이티브 라이브러리가 함께 들어갑니다. 증가분을 문서에 적습니다
플랫폼 확인리눅스·맥 하네스에서 함께 확인합니다. 네이티브 자산은 RID마다 갈립니다

8.2 스키마

무엇어떻게
테이블 이름NamePrefix + 테이블 이름. 관계형 2종과 같은 규칙입니다
컬럼 이름snake_caseDatabaseExporterBase.ColumnName과 같습니다
기본 인덱스PRIMARY KEY
보조 인덱스CREATE INDEX
배열 · 레코드TEXT에 JSON. 관계형 2종의 선례이고, SQLite의 json_extract로 조회됩니다
참조대상의 키를 그대로. FOREIGN KEY는 걸지 않습니다 — 적재 순서에 제약이 생기고, 참조 정합성은 이미 검증 단계가 판정합니다

8.3 datetime이 텍스트인 이유

SQLite에는 날짜 타입이 없고, 3가지 관례(TEXT ISO 8601 · INTEGER 유닉스초 · REAL 율리우스일)가 있습니다. TEXT ISO 8601을 씁니다 — SQLite의 날짜 함수가 기본으로 받는 형식이고, 사람이 조회 결과에서 읽을 수 있습니다. timespan은 시각이 아니므로 틱 INTEGER이고, 이것도 관계형 2종과 같습니다.

8.4 교체 — 섀도 테이블이 아니라 파일 교체

데이터베이스 4종은 섀도 테이블에 적재한 뒤 이름을 바꿉니다. sqlite는 그러지 않습니다. 서버가 아니라 파일이므로, 파일 익스포트와 같은 장치를 씁니다 — 스테이징에 새 파일을 만들고 전부 성공했을 때 교체합니다. 그래서 DatabaseExporterBase를 쓰지 않고 TargetKind.Export의 파일 타깃입니다.

적재는 트랜잭션 1개입니다. 행마다 커밋하면 수십만 행에서 시간이 크게 늘고, 실패 시 버릴 것은 스테이징 파일 전체이므로 중간 커밋이 지킬 것이 없습니다.

manifest-sqlite.json에 항목 1개를 기록합니다 — 파일 1개이므로 Sweep이 지울 대상은 이름을 바꿨을 때의 옛 파일뿐입니다.

9. 형식이 담을 수 있는 것

ITarget의 4가지 플래그입니다. 타깃마다 선언하고, 담을 수 없는 것을 만나면 그 이름과 함께 거부합니다.

플래그bsonjsonlcsvsqlite
SupportsNestedFields예 — 평탄화합니다예 — JSON 컬럼
SupportsDeepNestedFields
SupportsOptionalFields
SupportsOptionalElements

mongodb 타깃은 현재 중첩을 지원하지 않으므로, 3절의 공유가 그쪽에 중첩을 함께 부여할 수 있습니다. 별개의 개정으로 둡니다 — 데이터베이스 타깃은 게이트가 실제 서버를 요구합니다.

10. 게이트

무엇어떻게
골든core · nested · optional · nullable-elements에 형식별 하위 폴더. JSON 골든은 무변경이어야 합니다(3절)
되읽기바이트 골든은 안정성만 나타내고 정확성은 나타내지 않습니다. bsonMongoDB.Bson으로, csv는 파싱으로, sqlite는 쿼리로 되읽어 모델의 값과 대조합니다
sqlite의 골든바이트 골든을 두지 않습니다. 페이지 배치가 라이브러리 버전에 의존하므로, 골든은 스키마 DDL과 정렬된 행 덤프의 텍스트입니다
bigint 경계2^53을 넘는 값이 bson·sqlite에서 정수로, json에서 문자열로 나가는 것을 한 픽스처에서 함께 확인합니다
없음csv-\-, sqliteNULL, 그리고 원소 없음
타깃 열거DynamicTargetTests · NestedTargetSupportTests
크기내보내기의 표에 열을 실측으로 추가합니다. sqlite는 파일 1개이므로 합계로 적습니다

11. 단계

단계무엇판정
1RowComposer와 스칼라 정책 추출골든 무변경
2bson골든 + 되읽기 + bigint 경계
3jsonl골든 + 되읽기
4csv골든 + 되읽기 + 없음 왕복
5sqlite덤프 골든 + 쿼리 되읽기 + 배포 크기 실측 + 의존 문서
6문서 — 내보내기 · recipe크기 실측 포함

각 단계 뒤에 골든 재기록 → 전 언어 비교본 재생성 → 샘플 재생성 → 기록 없이 재검증입니다. tcb 형식 버전은 올라가지 않고 생성되는 리더도 무변경입니다 — 새 형식은 외부 소비용이므로 읽는 쪽이 이 저장소 밖에 있습니다.