dbspec
dbspec은 이 저장소의 schema 언어다. dbspec 문서 하나는 database 또는 schema 하나의 table, ORM과 database에 동작을 주는 settings, table을 보여 주는 diagram을 정의한다. 같은 문서는 MySQL 8.4, PostgreSQL 17, SQLite 3.46 이상에 같은 의미로 렌더링되고, 셋 중 어느 것을 introspect해도 같은 문서가 복원된다.
- DBS-1 dbspec이 지원하는 database 기능은 schema definitions 기록이 정한다. 공통 정의가 하나 있고, 세 database에서 같은 의미로 렌더링되며, introspection이 숨은 marker 없이 같은 정의를 복원하는 기능만 지원한다. 그 밖의 것은 위치가 있는 error로 거부하며, 버리거나 비슷한 것으로 바꾸지 않는다.
문서
dbspec 1 shop
use core { users }
table orders {
id i64 identity
user_id i64
status varchar(32) default 'pending'
total decimal(13,2)
coupon_code varchar(32) null
placed_at datetime(6) default now
primary key (id)
unique uq_orders_coupon (coupon_code)
index ix_orders_user (user_id)
index ix_orders_status (status, placed_at desc)
foreign key fk_orders_user (user_id) references users (id) on delete restrict on update restrict
check ck_orders_total (total >= 0)
settings {
updated placed_at
select explicit coupon_code
}
}
diagram main {
orders at 120 340
users at 520 340
}이 문서가 쓰는 문서 core:
dbspec 1 core
table users {
id i64 identity
primary key (id)
}DBS-2 문서는 byte order mark 없는 UTF-8 text다. 줄 끝은 LF 또는 CRLF이며 한 문서에 둘이 섞일 수 있고, 마지막 줄에는 줄 끝이 없어도 된다. 단독 CR은
encodingerror다. parse는 text를 받는다. 파일을 읽는 도구는 UTF-8이 아닌 bytes를 parse 전에encodingerror로 보고한다. token 사이 구분은 공백 문자만 쓰며 tab은syntaxerror다. 단어는 ASCII 글자, 숫자,_,.과 Go의unicode.IsLetter또는unicode.IsDigit이 받는 그 밖의 문자(Unicode 17.0.0)가 이어진 것이다. 공백이 아니면서 그 밖의 문자, 예를 들어€나 결합 기호는 놓인 곳에서syntaxerror다. 첫 줄은 headerdbspec 1 <document>로, 언어 version과 문서 이름이다. 그 앞에는 빈 줄이나 comment 줄이 올 수 없다. header 뒤에는 빈 줄을 어디에나 둘 수 있다. 첫 공백 아닌 문자가#인 줄은 comment다. comment는 의미를 바꾸지 않으며 canonical 출력은 그 자리를 유지한다(Canonical form 참조).DBS-3 header 다음에는
use줄,tableblock,diagramblock이 이 순서로 온다.
파일
- DBS-4 dbspec 문서 파일의 확장자는
.dbs다. 첫 줄인 headerdbspec 1 <document>가 파일 signature다. 문서 파일을 읽는 tool은 parse 전에 파일이dbspecbytes(dbspec과 공백 하나)로 시작하는지 확인한다. 그래서 같은 확장자를 쓰는 다른 형식의 파일(예: DbSchema project 파일(XML))과 빈 파일은 parse하지 않고 dbspec 문서와 구별된다. 이 bytes로 시작하지 않는 파일은 line 1, column 1의signatureerror 하나이며, message는 그 파일을<path> is not a dbspec document로 밝히고 그 파일에는 다른 error를 보고하지 않는다. header 앞에 byte order mark가 있는 파일도signatureerror이고, byte order mark가 있는 text를 parser에 직접 주면encodingerror다. 이 bytes로 시작하는 파일은 parse하므로, 그 뒤에서dbspec 1 <document>와 달라지는 header(예:dbspec 2 shop)는headererror다. 이 bytes로 시작하지만 UTF-8이 아닌 파일은 첫 잘못된 byte의 줄과 열에서encodingerror 하나(<path> is not valid UTF-8)이며 parse하지 않는다. 각 client에는 byte check가 하나 있다. 이것은 파일의 bytes와 message가 쓸 이름을 받아 이 diagnostic이나 text를 준다: Godbspec.ReadBytes, PHPDbspec::readBytes, TypeScriptreadDbspecBytes, Pythonread_dbspec_bytes, Rustdbspec::read_bytes. 자기 규칙으로(예: symbolic link를 따라가지 않고) 파일을 읽어야 하는 tool은 bytes를 한 번 읽어 자기가 보여 주는 이름과 함께 넘긴다. 각 client에는 file reader도 하나 있다. 이것은 path의 파일을 읽어 그 bytes와 path를 byte check에 주며, 문서 파일을 읽는 나머지 모든 tool이 그것을 쓴다: Godbspec.ReadFile, PHPDbspec::readFile, TypeScriptreadDbspecFile, Pythonread_dbspec_file, Rustdbspec::read_file. 읽을 수 없는 파일은 diagnostic이 아니라 platform의 읽기 error다.
문서 집합
- DBS-5 문서 집합은
dbspec-set.json이라는 set 파일에 한 번 선언하며, 집합을 받는 모든 tool이 그 파일을 읽는다: 모든 client의 generator와 editor 도구(editor). 명령 인자나 코드 안의 목록으로 두 번째로 선언한 집합은 첫 선언과 어긋나므로, 어떤 tool도 집합의 문서를 다른 형태로 받지 않는다.
{"schema": 1, "documents": ["shop.dbs", "billing/invoices.dbs"], "external": ["../core/core.dbs"]}DBS-6 파일은 key
schema,documents,external을 가진 JSON object 하나다.schema는 숫자 1이다.documents는 집합이 소유하는 문서 파일을 나열하며 비어 있지 않다.external은 집합의 외부 문서를 나열하며 생략할 수 있고, 생략은 빈 목록이다. 경로는 set 파일의 directory, 곧 집합의 root에 대한 상대 경로이며, process의 작업 directory에 대한 상대 경로가 아니다. 경로는 모든 platform이 같게 읽도록 하나의 정규형으로 적는다:DBS-7 segment를
/로 잇고, 마지막 segment는.dbs로 끝난다;DBS-8 각 segment는
/,\,:,<,>,",|,?,*, NUL, U+0001부터 U+001F 밖의 문자로 된 비어 있지 않은 연속이며,.이 아니고,.이나 공백으로 끝나지 않으며, 첫.앞 부분에서 뒤쪽 공백을 지운 것이 ASCII 대소문자를 무시하고CON,PRN,AUX,NUL,COM0부터COM9,LPT0부터LPT9, 또는COM이나LPT뒤에¹,²,³이 오는 것, 곧 Windows의 device 이름인 segment가 아니다;DBS-9
documents경로에는..segment가 없으므로 소유 문서는 root 아래에 있다;external경로는..segment로 시작할 수 있고, 처음 나온 다른 segment 뒤에는..이 없다.DBS-10 그러므로 절대 경로, drive, backslash, 빈 segment, 끝의
/에는 정규형이 없으며, Windows가 담을 수 없는 이름에도 없다.DBS-11 각 client에는 set reader가 하나 있다: Go
dbspec.ReadSet, PHPDbspec::readSet, TypeScriptreadDbspecSet, Pythonread_dbspec_set, Rustdbspec::read_set. reader는 set 파일의 경로를 받아 이 순서로 검사한다:DBS-176
@polyspec/orm: TypeScript package는@polyspec/orm/dbspec에서parseSet(bytes, path)를 export한다.bytes는Uint8Array,path는 string이어야 하며 그렇지 않으면TypeError를 던진다. 파일을 읽지 않고 byte 내용과 path를 검사한다.readDbspecSet과 editor 도구는 이 순수 parser를 사용한다.DBS-12 set 파일. prefix는 주어진 경로에서 마지막 segment를 지운 것이며, segment는
/에서, Windows에서는/나\에서 끝난다; prefix는 비어 있거나/,a/,C:일 수 있다. root는 prefix이며, prefix가 비어 있으면.이다. 마지막 segment는dbspec-set.json이고 root가 나열하는 이름과 같다(DBS-14의 비교). root(끝의 구분자를 뺀 것,/인 root는/그대로)와 파일은 symbolic link를 따르지 않고(lstat) 조사한다. POSIX에서 구분자로 끝나는 경로의lstat은 link를 따르기 때문이다: 둘 다 symbolic link가 아니고, root는 directory이며 파일은 일반 파일이다. reader는 파일을 한 번 열어 그 handle에서 읽는다. root 위의 directory는 호출자의 것이므로 조사하지 않는다; 그 사이의 symbolic link는 platform이 해석한다.DBS-13 JSON, 이 순서로: byte는 parse 전에 검사하여 byte order mark 없는 올바른 UTF-8이다; 그 byte는 JSON object 하나(RFC 8259)를 담으며, 그 object는 그 자신을 하나로 세어 깊이 64 단계까지 중첩되고, 어떤 문자열도 짝 없는 surrogate를 escape하지 않으며, 어떤 깊이의 어떤 object에서도 key가 두 번 나오지 않는다; object에는 세 key 밖의 key가 없으며, 그 오류는 모르는 key를 모두 code point 순서로 적는다;
schema는 RFC 8259가 상호 운용을 위해 권하는 대로 IEEE 754 double로 읽은 값이 1인 JSON 숫자이며(1,1.0,1e0은 모두 1), double 범위를 넘는 숫자를 포함해 다른 숫자는 1이 아니다;documents는 비어 있지 않은 문자열 배열이다;external은 있을 때 문자열 배열이며,null은 그렇지 않다.DBS-14 각 경로를
documents먼저, 그다음external, 적은 순서로. 먼저 그 정규형. 그다음 결합 경로: DBS-12에서 정의한 prefix 뒤에 적은 경로를 그대로 붙인 것이다. 결합 경로는 정리하지 않으므로 platform이 각..을 root로부터 해석한다; root는 symbolic link가 아니므로 root로부터의..은 platform이 물리적으로(POSIX) 해석하든 어휘적으로(Windows) 해석하든 같은 directory다. 결합 경로에서 적은 경로의 segment 뒤에서 끝나는 각 prefix를lstat으로 조사한다:../core/core.dbs라면<root>/..,<root>/../core,<root>/../core/core.dbs다. 마지막을 뺀 모든 prefix는 directory이고 마지막은 일반 파일이다. 어느 것도 symbolic link가 아니되,external경로의 마지막을 뺀 prefix는 symbolic link일 수 있다: reader는 그것을 따라가(stat) 얻은 것이 directory여야 하며, platform은 적은 대로 해석한다. 마지막 prefix가 symbolic link이면, 그리고documents경로의 어느 prefix든 symbolic link이면 오류다...밖의 각 segment는 그 directory가 나열하는 이름과 같다: POSIX에서는 segment의 UTF-8 byte가 나열된 byte와 같고(Node는{encoding: 'buffer'}로 나열한다), Windows에서는 UTF-16 code unit이 나열된 것과 같다;..segment는 directory로 조사하며 이름은 비교하지 않는다. 그러므로 대소문자를 접는 file system, short name, 이름 정규화만이 해석하는 철자는 모든 platform에서 오류다. Windows에서 symbolic link는 tag가 name surrogate(IsReparseTagNameSurrogate)인 모든 reparse point, 곧 symbolic link나 junction이다. Go, Rust, Python, PHP extension은 tag를 읽어 모든 name surrogate tag에 이 규칙을 적용하고, Node의lstat(libuv)과 PHP의lstat은 reparse tag를 내주지 않고 symbolic link와 junction tag만 link로 보고하므로, TypeScript와 PHP reader는 그 둘을 감지한다. 다른 name surrogate tag(WSL symbolic link 등)는 그곳에서 대상이 아니라 그 항목 자체로 읽히며, file system filter가 처리하지 않는 tag는 Win32가 열기를 거부하므로 따라가지 않고 읽기가 실패한다.DBS-15 읽기와 파일 identity. reader는 DBS-14를 통과한 각 파일을 한 번 열어 그 handle에서 byte를 읽고, 같은 handle에서 identity를 얻는다: POSIX
fstat의 device와 inode, 또는 Windows에서 handle이 주는 volume serial number와 64-bit file index(GoFile.Stat와os.SameFile, Nodebigint를 준fstat, Pythonos.fstat().st_ino의 하위 64 bit, PHPfstat, Rustsame_file::Handle::from_file). 64-bit file index가 유일하지 않은 file system, 예컨대 ReFS는 symbolic link 없는 file system처럼 지원하지 않는다. 한 목록 안에서든 두 목록에 걸쳐서든 파일의 identity가 같은 두 경로는 뒤 경로에서set.duplicate오류다.
참고: reader는 읽는 동안 바뀌지 않는 file tree를 읽는다. 각 component를 한 번 조사하며, 조사와 열기 사이에 바뀐 파일이나 directory는 감지하지 않으므로, 그 사이에 나타난 symbolic link를 열기가 따를 수 있다; 읽는 동안 다른 process가 바꾼 tree는 reader가 판정하는 범위 밖이며, 바뀌지 않은 tree를 다시 읽으면 결과가 나온다.
DBS-17 DBS-12와 DBS-13은 그 뒤를 읽을 수 없으므로 첫 오류에서 멈추고 오류 하나를 보고한다. DBS-14와 DBS-15는 어기는 모든 경로를 각각 그 첫 오류와 함께 보고한다; DBS-15는 DBS-14를 통과한 경로만 판정한다. 오류는 단계 순, 그다음 경로 순으로
documents먼저, 그다음external, 적은 순서로 나열하며,set.duplicate의 뒤 경로는 그 순서에서 뒤의 것이다. 읽는 동안 바뀌지 않는 tree에서 symbolic link는external경로의 마지막을 뺀 prefix(DBS-14)를 제외하고 결코 따르지 않는다: set 파일, root, document 파일, 또는documents경로의 어느 prefix에서 찾은 symbolic link든 오류다.DBS-18 성공하면 reader는
documents와external을 돌려주며, 각각은 적은 순서로 파일의 결합 경로와 byte를 담은 목록이다. reader는 byte를 검사하지 않는다: tool이 각 파일의 byte를 그 경로와 함께 byte 검사에 넘기고, byte 검사가 그 파일의signature와encoding을 보고하며, 그다음 tool이 집합을 parse한다(문서와use). 실패하면 reader는 오류를 돌려주며, 각 오류는 rule, 해당 경로, 그리고 경로를, 읽기에 실패했으면 platform의 오류도 적는 message를 가진다. 경로는 DBS-12와 DBS-13, 그리고set.path에서는 set 파일의 경로이고(set.path의 message는 적은 경로를 JSON으로 인용한다), 그 밖에서는 항목의 결합 경로다.DBS-19
set.file: set 파일의 이름이dbspec-set.json이 아니거나, set 파일이나 나열된 파일이 없거나 읽을 수 없거나 일반 파일이 아니다; root나 경로의 component가 directory가 아니다; 또는 segment가 그 directory가 나열하는 이름이 아니다DBS-20
set.symlink: set 파일, root, 경로의 마지막 component, 또는documents경로의 마지막을 뺀 prefix가 symbolic link다DBS-21
set.json: set 파일이 byte order mark 없는 UTF-8이 아니거나, 깊이 64 단계까지 중첩된 JSON object 하나가 아니거나, 짝 없는 surrogate를 escape한다DBS-22
set.schema:schema가 없거나 값이 1인 숫자가 아니다DBS-23
set.key:schema,documents,external밖의 key; 두 번 나오는 key;documents없음; 또는 문자열 배열이 아닌documents나externalDBS-24
set.empty:documents가 비어 있다DBS-25
set.path: 경로에 정규형이 없거나,documents경로에..segment가 있거나,external경로에서 다른 segment 뒤에..이 있다DBS-26
set.duplicate: 경로의 파일이 앞의 경로의 파일과 같다DBS-27 오류가 있는 set 파일이나 diagnostic이 있는 나열된 파일을 받은 generator는 오류 상태로 끝나며, 모든 오류와 diagnostic을 출력하고, 아무것도 쓰지 않는다. 공유 vector
tests/dbspec/set-files.json은 각 rule을 받아들이는 case와 거부하는 case로 file tree(일반 파일, directory, symbolic link, hard link) 형태로 담으며, 모든 client의 set reader가 이를 실행한다; TypeScript의parseSet은 file tree가 필요 없는 case도 실행한다. file tree case에는 symbolic link와 hard link를 가진 file system이 필요하다: Windows에서는 NTFS와 symbolic link를 만들 권한.
Set parse
DBS-28 도구는 set의 text를 직접 key로 묶지 않고 set parse로 parse한다: Go
dbspec.ParseSet, PHPDbspec::parseSet, TypeScriptparseDbspecSet, Pythonparse_dbspec_set, Rustdbspec::parse_set. 소유한 text와 external text를 각각 set 순서로 받아, 소유한 것 먼저 그 순서로 text마다 결과 하나를 돌려준다.DBS-29 문서의 이름은 parser가 header 줄에서 읽은 이름이다. 어떤 도구도 다른 방법으로 읽지 않는다. header가 실패하거나(
header진단) header 줄을 읽기 전에 parse가 멈추면(32 MiBlimit, byte order mark, header 줄의encoding오류) text는 이름이 없다. header 줄 뒤의 결함은 이름을 남긴다.DBS-30 앞의 text가 가진 이름을 가진 text는 parse하지 않고, 그 결과는 1행 10열에 위치한 진단 하나
name.duplicatedocument <name> appears twice in the document set를 갖는다.DBS-31 그 밖의 text는 set의 다른 모든 이름 있는 문서의 text를 그
use줄이 가리키는 문서로 삼아 parse하고, 그 결과는 그 text의 한 번의 parse가 돌려주는 대로 document, 선언 위치, 진단을 갖는다. external text의 document는 external로 표시한다.
공유 vector tests/dbspec/set-parse.json은 이름, 반복, 실패한 header를 가진 set을 담으며 모든 client가 그것을 실행한다.
이름
DBS-32 문서, table, column, index, key, foreign key, check, setting 대상, diagram의 모든 이름은
[a-z][a-z0-9_]*이고 63 bytes 이하다. 더 긴 이름은 error이며 줄이지 않는다. 대문자는 error이므로 모든 database의 대소문자 접기가 같은 이름을 준다.DBS-33
dbspec,use,table,diagram,primary,unique,index,foreign,check,settings,null,identity,default,true,false,and,or,not,in,between,is는 예약어이며 어떤 이름에도 쓸 수 없다. 마지막 여섯 단어는 check predicate keyword다.DBS-34 index, unique key, foreign key, check 이름은 네 종류 전체에서, 모든 table 이름과 비교해, 그리고 이 문서와, 이 문서가 직접 쓰는 문서의 모든 table과 constraint 전체에서 하나뿐이어야 한다. MySQL, PostgreSQL, SQLite가 이 이름들의 범위를 서로 다르게 정하고, PostgreSQL은 index와 table 이름을 한 namespace에 두기 때문이다.
primary는 쓸 수 없는 이름이다. 이름은 physical 이름이다. renderer는 이름을 그대로 쓰고 prefix나 suffix를 붙이지 않는다. renderer는 자기 이름도 쓴다(rendered statements).text,bytes, identity column이 아닌 모든 column의 type CHECK에<table>$<column>,immutable에<table>$immutable_update와<table>$immutable_delete,audit에<table>$audit_insert,<table>$audit_update,<table>$audit_delete다. dbspec 이름에는$가 없으므로 선언된 이름과 겹치지 않는다. 이 생성 이름은 모두 한 규칙으로 쓰므로 항상 63 bytes에 들어간다. plain form은 한도 없이 renderer가 쓸 이름인<table>$<suffix>이며, suffix는 column 이름이나 event 이름(immutable_update,immutable_delete,audit_insert,audit_update,audit_delete)이다. plain form이 63 bytes 이하이면 그것이 이름이다. 아니면 이름은 plain form의 처음 54 bytes,$, 그리고 plain form의 UTF-8 bytes에 대한 SHA-256의 소문자 16진수 8자리다: 54 + 1 + 8 = 63 bytes. 이름은 ASCII([a-z0-9_$])만 쓰므로 bytes가 곧 문자이며 code point가 잘리지 않는다. 데이터베이스에서 읽은 이름은 document set의 모든 table과 suffix의 이름을 다시 계산해 비교하여 table과 suffix로 되돌린다.$로 나누지 않는다. hash는 plain form 전체를 덮으며, 처음 54 bytes가 같은 plain form만 hash로 구별한다. 생성 이름은 선언된 이름과 함께 schema 전체에서 유일하다. 다른 생성 이름이나 선언된 이름과 같은 생성 이름은 뒤에 나온 이름에name.duplicate오류이며, 두 출처(각 table과 column 또는 setting)를 모두 적는다. 선언된 이름에는$가 없으므로 생성 이름끼리만 겹칠 수 있다. plain form과 hash form, 또는 두 hash form이 겹치는 경우 모두 거부하며 조용히 하나로 합치지 않는다. renderer는 자기 이름만 이렇게 줄이며, 선언된 이름은 줄이지 않고 63 bytes보다 긴 선언 이름은 여전히name.lengtherror다.
문서와 use
- DBS-35
use <document> { <table>, ... }는 다른 문서의 table을 foreign key 대상으로 쓸 수 있게 한다. 다른 문서는 도구가 set 파일에서 읽는 문서 집합에서 그 이름과 같은 문서를 찾는다. 문서는 파일 경로를 적지 않는다. 쓰는 table은 이 문서가 렌더링하지 않으며 diagram에는 배치할 수 있다. 쓰는 문서 자신도 자기use줄과 함께 parse하고 검증한다. 지정한 문서가 정의하지 않은 table, 집합에 없는 문서, 문서 자신, header 이름이 쓰는 이름과 다른 문서를 쓰거나 use가 순환하면useerror다. 실패한 쓰는 문서는 그 이름에서useerror 하나를 보고하고 message에 첫 error를 담는다.use줄에서 반복한 문서나 table, 그리고 쓰는 table과 이름이 같은 table은name.duplicateerror다.use줄은 문서 이름 순으로 정렬하고, 한 줄 안의 table은 적힌 순서를 유지한다.
Table
- DBS-36
table <name> { ... }은 column 줄, 그다음 서로 순서가 자유로운 key, index, foreign key, check 줄, 그리고 최대 하나의settingsblock을 갖는다. constraint 줄 뒤의 column 줄,settings뒤의 constraint 줄, 두 번째settingsblock은ordererror다. 문서가 닫지 않은 block은 그{에서syntaxerror다. table은 column이 하나 이상이고 primary key가 정확히 하나이며 column은 1000개 이하다.
Column
<name> <type> [null] [identity] [default <value>]
- DBS-37 줄에
null이 없으면 NOT NULL이다. primary key column은null일 수 없다. - DBS-38
identity는 그 column을 자동 key로 만든다. 유일한 primary key column이어야 하고 type은i64다. insert가 값을 생략하면 database가 key를 만들고, client는 명시적 값을 거부한다. table당 identity column은 최대 하나다. - DBS-39 수식어 순서는
null,identity,default다. default가 없는nullcolumn은 insert가 값을 생략하면 SQL NULL이 된다.default null은columnerror다. - DBS-40
default <value>는 column type의 literal이거나,datetime(p)column에서 UTC statement 시각을 쓰는now다.null과default는identity와 함께 쓸 수 없다.text와bytescolumn에는 default가 없다. scale보다 소수 자릿수가 많은 decimal default는 늘어난 자리가 0이어도columnerror이며, 적은 자릿수는 canonical form에서 채운다.
Type
| dbspec | 값 | MySQL | PostgreSQL | SQLite |
|---|---|---|---|---|
i16 | −32768 ~ 32767 | SMALLINT | smallint | smallint + 범위 CHECK |
i32 | −2147483648 ~ 2147483647 | INT | integer | integer + 범위 CHECK |
i64 | signed 64-bit | BIGINT | bigint | bigint + type CHECK |
bool | true, false | tinyint(1) + (0,1) CHECK | boolean | BOOLEAN + (0,1) CHECK |
decimal(p,s) | 1 ≤ p ≤ 18, 0 ≤ s ≤ p | DECIMAL(p,s) | numeric(p,s) | DECIMALINT(p,s) + 범위 CHECK (값 × 10^s) |
f64 | 유한한 double | DOUBLE | double precision | REAL |
varchar(n) | 1 ≤ n ≤ 16383 문자, U+0000 없음 | varchar(n) | varchar(n) | varchar(n) + 길이 CHECK |
text | 길이 제한 없음, U+0000 없음 | LONGTEXT | text | TEXT |
bytes | 길이 제한 없음 | LONGBLOB | bytea | BLOB |
uuid | 소문자 canonical text | char(36) ascii_bin + pattern CHECK | uuid | TEXT + pattern CHECK |
date | 0001-01-01 ~ 9999-12-31 | DATE | date | DATE + CHECK |
time(p) | 24:00:00 미만의 하루 중 시각, 0 ≤ p ≤ 6 | TIME(p) + 하루 CHECK | time(p) + 하루 CHECK | TIME + CHECK |
datetime(p) | local date-time, 0 ≤ p ≤ 6 | DATETIME(p) | timestamp(p) | DATETIME + CHECK |
DBS-41 모든 문자 column은 binary collation 하나를 쓴다: MySQL
utf8mb4_0900_bin, PostgreSQLCOLLATE "C", SQLiteBINARY. 모든 연결은datetime을 UTC로 읽고 쓴다. SQLite table은 STRICT가 아니며, 모든 SQLite 연결은foreign_keys를 켠다. 정확한 CHECK text는 renderer version에 속하며 dialects에 있다.DBS-42 지원하지 않음: 8-bit·24-bit 정수, unsigned type,
f32,char(n),binary(n),varbinary(n), native enum(incheck가 있는varchar(n)을 쓴다), native JSON(ordered_jsoncodec이 있는text를 쓴다), time zone instant, generated column,now외의 식 default. 열 줄은name type [null] [identity] [default value]만 받으므로(packages/go/engine/dbspec/parse.go) 어떤 문서도 생성 컬럼을 만들지 않는다. introspection은 생성 식이 있는 열을 type에 따른 이유로 미지원 객체로 보고하며, 어느 client도 그 식의 text를 읽지 않는다. 따라서GENERATION_EXPRESSION에 있는 non-ASCII literal의 MySQL 이중 인코딩은 어느 client에도 닿지 않는다(dialects 참고).
Key와 index
DBS-43
primary key (<column>, ...): table마다 정확히 하나, 이름 없음.DBS-44
unique <name> (<column>, ...): NULL은 서로 다르다.DBS-45
index <name> (<column> [asc|desc], ...): 오름차순이 기본이며 canonical 형식에서는 쓰지 않는다.DBS-46 key나 index는 자기 table의 서로 다른 column을 1~16개 나열한다.
text나bytescolumn은 포함할 수 없다. primary key, unique key, index 하나의varcharcolumn 선언 길이 합은 640 이하다. prefix, partial, expression, full-text index는 지원하지 않는다.
Foreign key
foreign key <name> (<column>, ...) references <table> (<column>, ...) [on delete <action>] [on update <action>]
- DBS-47
<action>은restrict,cascade,set_null이다. 생략한 action은restrict이며 canonical 형식은 두 action을 모두 쓴다. - DBS-48 참조 column은 참조 table의 primary key나 unique key 하나의 column과 정확히 같고, 그 key의 column 순서를 따른다.
- DBS-49 둘 다 쓸 때
on delete가on update보다 먼저 온다. - DBS-50 자식 column type은 참조 column type과 같다.
- DBS-51 table은 앞쪽 column이 foreign key column인 index나 key를 선언한다.
- DBS-52
set_null은 모든 자식 column이null이어야 한다. - DBS-53 대상은 이 문서의 table이나 쓰는 table이다.
NO ACTION,SET DEFAULT, deferrable constraint,MATCH FULL은 지원하지 않는다.
Check
check <name> (<predicate>)
predicate는 다음 중 하나다.
DBS-54
=,<>,<,<=,>,>=를 쓰는<operand> <비교> <operand>. operand 중 적어도 하나는 column이다DBS-55
<column> [not] in (<literal>, ...)DBS-56
<column> is [not] nullDBS-57
<predicate> and <predicate>,<predicate> or <predicate>,(<predicate>).and가or보다 먼저 묶인다DBS-58 operand는 같은 table의 column이나 literal이다. 세 database는 양쪽 값의 종류가 같을 때에만 이 predicate를 같게 평가하므로, type이 만날 수 있는 operand를 정한다. column과 만나는 literal은 그 column type의 유효한
defaultliteral이어야 하고(textcolumn은varchar형식), canonical form은 그 default처럼 쓴다. 그래서decimal(13,2)에 대한0은0.00이다. 두 column은 다음일 때 만난다.
| Column type | 다른 column |
|---|---|
i16, i32, i64 | 다른 정수 column |
decimal(p,s) | scale s가 같은 decimal column |
f64 | f64 column |
bool | bool column |
varchar(n), text | varchar나 text column |
uuid | uuid column |
date | date column |
time(p) | p가 같은 time column |
datetime(p) | p가 같은 datetime column |
bytes | bytes column |
DBS-59 각 구현은 이 표를 한 곳에만 지니며, IR validator도 column 비교, subquery, join, relation의 두 column에 이 표를 읽으므로(protocol) 둘은 어긋나지 않는다. check는 type을 비교하기 전에
bytescolumn을 거부하므로bytes행은 요청에만 쓰인다. conformance vectorcolumn_pairs는 만나는 쌍과 만나지 않는 쌍을 세 database에서 실행한다.DBS-60
booloperand에는=,<>,in만 쓴다.DBS-61
bytescolumn, 산술, 함수,nullliteral은 predicate에 쓸 수 없다. 정수 나눗셈, overflow, scale을 곱한 SQLite decimal이 세 database에서 다른 결과를 내기 때문이다. 단항 minus는 숫자 literal에만 쓴다. MySQL이 거부하므로(3823)cascade나set_nullforeign key의 column은 check에 쓸 수 없다.DBS-62
not연산자,between,boolcolumn 하나는 없다. MySQL은not을 그것이 부정하는 비교로 바꿔 쓰고 column 하나를 거부하며, PostgreSQL은between을 비교 두 개로 쓰므로 introspection이 이를 복원할 수 없다(dialects).not (a > 3)대신a <= 3,a between 1 and 9대신a >= 1 and a <= 9,active대신active = true를 쓴다.
Settings
DBS-63
settings { ... }는 한 줄에 setting 하나를 둔다. setting은 자신이 적용되는 column의 이름을 적는다. column 이름이 동작을 고르는 일은 없다.codec은 column마다,navigation은 foreign key마다,blind_index는 AES column마다,markdown은 column마다,checkbox는 state마다 한 번씩 반복하고state_machine은 줄의 identity마다 한 번 쓴다: state마다initial줄 하나, state마다terminal줄 하나,require목록과 상관없이<from> -> <to>쌍마다 전환 줄 하나,history줄 하나, state마다limit줄 하나다. 나머지 setting은 최대 한 번 나오며, 반복은 뒤 줄에서settingerror다. 반복한 줄은 의미를 더하지 않고,require목록이 다른 전환이나 terminal 줄의 반복은 앞 줄과 모순된다. 빈settings {}block은 의미가 없으며 canonical form은 이를 쓰지 않는다. 그 안의 comment는 table 줄의 들여쓰기로 table의 닫는}앞으로 옮긴다.DBS-64 codec stage는 쓸 때 적힌 순서로 실행한다. 저장 type은 마지막 stage를 따른다.
hex,base64,ordered_json,yaml,serialize는 text를 만들므로varchar나textcolumn이 필요하고,aes,gz,ip는 bytes를 만들므로bytescolumn이 필요하다.ordered_json은 나올 때 첫 stage다.aes_version은aes를 쓰는 column이 있으면 필요하고, 그런 column 없이 쓴aes_version은settingerror다.blind_index의 index column은 n ≥ 64인varchar(n)이고(bytescolumn은 index에 넣을 수 없다), AES column과 nullability가 같고, 자신은 AES로 encode되지 않으며, 선언된 index나 unique key의 유일한 column이다.DBS-65
entity <name>: 생성 코드가 table에 쓰는 model 이름. 없으면 model 이름은 table 이름이다 (Hash: manifest)DBS-66
updated <column>: executor가 자신이 계획하는 모든UPDATE에서datetimecolumn에 UTC statement 시각을 넣는다. database에는 아무것도 렌더링하지 않는다. raw SQL은 기록되지 않는다 (Hash: manifest)DBS-67
soft_delete <column>: 읽기는 root, 각 join child(ON조건), 각 relation, 각 subquery에서 nullabledatetimecolumn이 null이 아닌 row를 제외한다. 삭제는 그 column에 UTC statement 시각을 넣고,restore는 primary key나 unique key 값이 지정한 row 하나에서 그 column을 비운다(restore) (Hash: manifest)DBS-68
select explicit <column> ...: column을 기본 select 집합에서 뺀다. 선택하지 않은 column을 읽으면COLUMN_UNSELECTED로 실패한다 (Hash: manifest)DBS-69
codec <column> <stage> ...: 쓸 때 stage 순서로 encode하고 읽을 때 역순으로 decode한다:ordered_json,aes,hex,gz,base64,serialize,yaml,ip(codecs) (Hash: manifest)DBS-70
aes_version <column>: row의 AES key version을 저장하는 non-null 정수 column.aes를 쓰는 column이 있으면 필요하다 (Hash: manifest)DBS-71
blind_index <aes column> <index column>: executor가 AES column 평문의 HMAC을 index column에 쓰고 같음 조건에 쓴다 (Hash: manifest)DBS-72
navigation <foreign key> <child name> <parent name>: 도구가 foreign key에 보여 주는 관계 이름(자식 쪽, 부모 쪽). 생성 코드는 match method로 join하며 이 이름을 읽지 않는다 (Hash: manifest)DBS-73
state_machine <column> <from> -> <to> [require (<column>, ...)],state_machine <column> terminal <state> [require (<column>, ...)]: non-null varchar 또는 text column 하나의 row 상태 기계다. 각 줄은 하나의 전환이거나 하나의 terminal 상태이고, table마다 한 column에서 순서와 섞임에 제한 없이 쓴다.require는 전환이나 terminal이 기록하는 column을 명명하고, terminal 상태를 벗어나는 전환은 거부한다. database는 기계를 강제하지 않고 row를 소유한 executor가 강제한다 (Hash: manifest)DBS-74
markdown <column>: column이 markdown 산문을 담는다. column마다 반복 가능하고 임의의 nullability varchar 또는 text column에 쓴다. database는 이를 위해 아무것도 render하지 않고 문서를 소유한 도구가 읽는다 (Hash: manifest)DBS-75
store files,store document list|table,store block <foreign key> list|table: table의 markdown corpus 저장이다.files는 행마다 file 하나를,document는 모든 행을 문서 하나에,block은 지정한 foreign key가 가리키는 부모 행 안에 행을 쓴다.list나table이 모양이다. table마다 정확히 하나를 쓴다 (Hash: manifest)DBS-76
store files shard <column> <period>,store document list|table shard <column> <period>: 저장의 시간 shard다. corpus는 column의 기간마다 행을 따로 둔다. column은 non-nulldate또는datetimecolumn이고 기간은year,month또는day다.store blocktable, self foreign key가 있는 table,order가 있는 table은 이를 거부한다. SQL backend는 이를 무시한다: 아무것도 render하지 않고 introspection도 복원하지 않는다 (Hash: manifest)DBS-77
key_prefix '<p>': executor가 varchar primary key를<p><n + 1>로 할당한다. n은 그 꼴 key 가운데 가장 큰 10진 suffix다. primary key는 varchar column 하나여야 한다 (Hash: manifest)DBS-78
title <column>,body <column>: 행의 제목과 본문이다. 각각 non-null varchar 또는 text column이다 (Hash: manifest)DBS-79
order <column>: 형제 행 사이의 위치이며 executor가 할당한다. default가 없고 어떤 key, index, check에도 없는 non-nulli32또는i64column이다 (Hash: manifest)DBS-80
checkbox <column> <state> '<glyph>': state machine column의 한 state에 대한 list glyph다. 줄들은 state 집합을 정확히 덮고, 각 glyph는 한 글자이며 서로 달라야 한다 (Hash: manifest)DBS-81
state_machine <column> initial <state>: 기계의 초기 state이며 여러 줄 가능하다. 기계의 state는 initial, 전환과 terminal 줄이 이름 붙이는 state들이다. state column의 default는 초기 state여야 하며, 초기 state는 terminal일 수 없다 (Hash: manifest)DBS-82
state_machine <column> history <table> row <foreign key column> from <column> to <column> at <column>: 기계 전환의 history table이다. foreign key column은 이 table을 가리키고,from과to는 state column의 type,at은datetime(6)이며, 필요한 column마다 같은 type의 nullable column이 있고, key와 이것들 외의 column은 없다 (Hash: manifest)DBS-83
state_machine <column> limit <state> <count>: 그 state의 행은count개를 넘을 수 없다. count는 양의 정수이고 state는 state 집합에 속한다 (Hash: manifest)DBS-84
immutable: database가 생성된 row trigger로 table row의UPDATE와DELETE를 거부한다.TRUNCATE는 포함하지 않는다.cascade나set_nullforeign key의 자식 table에서는 거부된다 (Hash: schema)DBS-85
audit into <history table> column <column> references <table> action <history column> previous <history column> [tables <table> (<record column>, <name column>)] [exclude (<column>, ...) | include (<column>, ...)]: 생성된 row trigger가 모든INSERT와UPDATE의 기록하는 column을 이력 table에 복사하고,tables가 있으면 audit 기록마다 감사 대상 table을 기록한다. Audit 참조 (Hash: schema)DBS-86
schemaHash는 table 정의와 schema로 표시한 settings를 포함하며, database를 바꿔야 할 때 정확히 바뀐다.manifestHash는 정의와 모든 settings를 포함하며 생성 코드가 확인한다. diagram과 comment는 어느 쪽에도 속하지 않는다. 두 hash는 Manifest와 hash에서 정한다.
Audit
- DBS-87 감사 대상 table은 자기 audit column에 audit 기록의 key를 갖고, 이력 table은 그 row의 모든 version을 갖는다. audit 기록은 선언한 table의 row이며, 작업 단위(transaction)마다 그 변경보다 먼저 한 번 쓴다. 작업 단위가 insert하거나 update하는 모든 감사 대상 row가 그 기록의 key를 갖는다. executor는 작업 단위가 감사 대상 table에 처음 쓰기 직전에 같은 transaction 안에서 그 기록을 insert한다. 그래서 감사 대상 row를 쓰지 않는 작업 단위는 audit 기록을 쓰지 않는다. 어떤 row와도 맞지 않는 첫 update, delete 또는 restore는 감사 대상 row를 쓰지 않으므로 그 기록도 함께 취소된다. rollback되는 savepoint나 transaction은 기록을 그것을 insert한 쓰기와 함께 취소하고, 다음 감사 대상 쓰기가 기록을 다시 insert한다. trigger는 기록하는 모든 값을 자신이 받은 row, 곧
OLD와NEW에서 읽는다. 그래서 세 database 모두 executor와 trigger 사이에 다른 전달 장치가 필요 없다.
table service {
id i64 identity
name varchar(191)
audit_seq i64
deleted_at datetime(6) null
primary key (id)
index ix_service_audit (audit_seq)
foreign key fk_service_audit (audit_seq) references audit (seq) on delete restrict on update restrict
settings {
soft_delete deleted_at
audit into service_history column audit_seq references audit action change previous previous_audit_seq tables audit_table (audit_seq, table_name)
}
}
table service_history {
history_id i64 identity
change varchar(8)
previous_audit_seq i64 null
id i64
name varchar(191)
audit_seq i64
deleted_at datetime(6) null
primary key (history_id)
index ix_service_history_row (id)
}
table audit {
seq i64 identity
account_seq i64
request_id varchar(64)
action varchar(64) null
primary key (seq)
}
table audit_table {
audit_seq i64
table_name varchar(64)
primary key (audit_seq, table_name)
foreign key fk_audit_table_audit (audit_seq) references audit (seq) on delete restrict on update restrict
}- DBS-88 Audit column.
column <column>은 감사 대상 table의 non-null column의 이름이다. executor는 그 table의 모든INSERT와UPDATE에서 transaction의 audit 기록 key를 이 column에 쓴다(사용법).NEW.<column>은 변경의 audit 기록이고OLD.<column>은 이전 version의 audit 기록이다. - DBS-89 Audit 기록 table.
references <table>은 이 문서의 table이나 쓰는 table의 이름이다. 감사 대상 table, 이력 table과 다른 table이고, 자신은 감사하지 않으며, type이 audit column의 type인 column 하나의 primary key를 갖는다. 감사 대상 table은 모든 foreign key가 요구하는 앞쪽 index와 함께 foreign key(<column>) references <table> (<key>) on delete restrict on update restrict를 선언한다. 그래서 database는 어떤 audit 기록의 key도 아닌 audit column 값과, row가 key로 쓰는 audit 기록의 삭제를 거부한다. 이 foreign key는 다른 foreign key처럼 렌더링하고 introspect한다. 없는 table, 감사 대상이나 이력 table, 감사하는 table, 다른 모양이나 type의 key, 빠진 foreign key는 table 이름이나 audit column에서settingerror다. - DBS-90 기록하는 column. 목록이 없으면 trigger는 감사 대상 table의 모든 column을 기록한다.
exclude (<column>, ...)는 적은 column 말고 모든 column을,include (<column>, ...)는 적은 column만 기록한다. audit column은 언제나 기록하므로 어느 목록에도 쓰지 않는다. setting은 목록을 많아야 하나 가지며, 목록의 column은 감사 대상 table의 column이고 한 번만 나온다. 없는 column, 두 번 적은 column, 두 목록, 목록에 적은 audit column은 적은 column이나 둘째 목록의 keyword에서settingerror다. 나중에 table에 더한 column은exclude에서는 기록하고include에서는 기록하지 않는다. - DBS-91 이력 table.
into <history table>은 이 문서의 table이나 쓰는 table의 이름이다. 이력 table은 감사 대상과 다른 table이다.i64 identityprimary key,actioncolumn(non-nullvarchar(8)),previouscolumn(nullable, audit column의 type), 그리고 감사 대상 table의 기록하는 column마다 같은 이름과 type의 column을 하나씩 갖고, 다른 column은 없다. 기록하는 column의 사본이 없거나, type이 다른 column이 있거나, 기록하지 않는 감사 대상 column을 포함해 다른 column이 있는 이력 table은 어긋난 곳마다 하나씩 이력 table 이름에서settingerror다. 이력 column은 nullable일 수 있다. 이력 table 자신은 감사하지 않는다. - DBS-92 감사한 table.
tables <table> (<record column>, <name column>)은 tables table의 이름이다. tables table은 이 문서의 table이나 쓰는 table이며, trigger는 각 audit 기록이 바꾼 감사 대상 table을 여기에 기록한다. 감사 대상, 이력, audit 기록 table과 다른 table이고, 자신은 감사하지 않는다. 적은 두 column, record column과 name column만 갖는다. name column은 table 이름(이름)을 담는 n >= 63인varchar(n)이다. primary key는(<record column>, <name column>)이므로 두 column은 non-null이고, primary key가 앞쪽 index가 되는 foreign key(<record column>) references <audit record table> (<key>) on delete restrict on update restrict를 선언하므로 record column은 audit 기록 key의 type을 갖는다. 그래서 이 table은 audit 기록과 감사 대상 table마다 row 하나를 갖고, 모든 row는 있는 audit 기록을 가리키며, row가 가리키는 audit 기록은 삭제되지 않는다. 여러 감사 대상 table이 같은 tables table을 적을 수 있다. 없는 table, 감사 대상·이력·audit 기록 table, 감사하는 table, 없는 column, 다른 type의 name column, 다른 column, 다른 primary key, 빠진 foreign key는 table 이름이나 그 column에서settingerror다.tables가 없는 setting은 table을 기록하지 않는다. tables table은 그 문서의 평범한 table이다. 생성된 model이 이를 읽으며, audit 기록 table과 이 table의 join이 한 table의 audit 기록을 거르고 page로 나눈다(사용법). - DBS-93 Trigger.
AFTER INSERTrow trigger는action = 'insert',previous = NULL과 기록하는 모든 column의NEW값을 쓴다.AFTER UPDATErow trigger는action = 'update',previous = OLD.<audit column>과 기록하는 모든 column의NEW값을 쓴다.BEFORE DELETErow trigger는 삭제를 거부한다.tables가 있으면 insert와 update trigger는 이어서(NEW.<audit column>, '<audited table>')을 tables table의 record column과 name column에, 그 row가 없을 때만 삽입한다. 그래서 한 table의 row 여럿을 바꾼 transaction은 그 table의 row 하나를 쓰고, 그 table의 row를 바꾸지 않은 transaction은 아무것도 쓰지 않는다. 값을 column 대 column으로 복사하므로 이력 row는 세 database에서 같다. row를 JSON으로 만들지 않는다. - DBS-94 Schema text와 introspection. database는 trigger가 적은 기록하는 column만 갖는다. 그래서 schema text는 setting을 기록하지 않는 column을 column 순서로 적은
exclude로 쓰고, 모든 column을 기록하면 목록 없이 쓴다.include (title)과 나머지 column의exclude목록은 같은schemaHash를 주며, manifest text는 쓴 목록을 그대로 둔다. introspection은audit_inserttrigger에서 기록하는 column과tables절을, audit column의 foreign key에서 audit 기록 table을 읽어 setting을 schema text 형식으로 되살린다. database에 다른 것은 저장하지 않는다. - DBS-95 암호화한 값. 암호화가 필요한 값은
aesstage가 있는 codec을 갖고, executor가 쓰기 전에 그 값을 encode한다(codecs). 그래서 row는 ciphertext를 갖고 trigger는 그 ciphertext를 이력 table에 복사하며, trigger가 평문을 보는 일은 없다. 그 이력 column은 같은 storage type이다. 이력에서 그 값을 빼려면 그 column을 exclude한다. - DBS-96 삭제.
BEFORE DELETEtrigger 때문에 physicalDELETE는 실패한다.soft_delete를 선언한 table은 soft delete column의UPDATE로 삭제하며, 이력이 그 audit 기록과 함께 기록한다. soft delete한 row의 restore도 이력이 같은 방법으로 기록하는UPDATE다.audit은soft_delete를 요구하지 않는다. schema setting은 database에서 다시 읽히고 manifest setting은 읽히지 않으므로, schema setting은 manifest setting에 의존하지 않는다. - DBS-97 한계.
cascade나set_nullforeign key의 자식 table에서는 이 setting을 거부한다. MySQL trigger는 foreign key action이 바꾼 row에서 실행되지 않기 때문이다.TRUNCATE는 포함하지 않는다. raw SQL은 audit column에 있는 audit 기록을 지정해야 하며 foreign key가 이를 확인한다. 이 column을 쓰지 않는UPDATE는 이전 audit 기록을 다시 기록하며, executor는 언제나 이 column을 쓴다. binary logging이 켜진 MySQL에서 trigger를 만들려면SUPER또는log_bin_trust_function_creators=ON이 필요하며, apply가 이를 먼저 확인한다.
Manifest와 hash
DBS-98 선언된 문서 집합이 유일한 schema 원천이다. generator, schema tool, schema 설치는 dbspec 문서를 읽으며, 다른 manifest 파일은 없다.
DBS-99 manifest와 rendered statements는 한 집합의 parse된 문서를 받는다. 집합은 각 문서를 한 번씩, 그리고 그 문서들이 쓰는 모든 문서를 담는다. 반복된 문서 이름은
name.duplicatediagnostic, 집합에 없는 쓰이는 문서는usediagnostic이며, 둘 다 뒤쪽 문서나 쓰는 문서의 header 이름(1줄 10열)에 위치하고 message에 문서 이름을 담는다. 문서는 이름 순으로, 한 문서가 쓰는 이름도 이름 순으로 확인한다. diagnostic이 있는 집합에는 manifest도 statement도 없다.DBS-100 문서의 manifest text는 모든 comment와 diagram을 뺀 canonical emission이다. 문서 집합의 manifest text는 소유한 문서들의 manifest text를 문서 이름 순으로 이어 붙인 것이다. 각 text는 header로 시작하고 줄 끝으로 끝나므로 이어 붙인 결과는 모호하지 않다.
DBS-101 external text는 외부 문서가 없는 집합에서 비어 있다. 그 밖에는 쓰는 table이 있는 외부 문서마다 이름 순으로, 그 문서의 manifest text를 소유한 문서가
use줄에 적은 table로만 줄인 것을 담는다. table은 문서 순서이고 canonical emission으로 쓰며, 각 table은 column, primary key, unique key, 참조하는 table도 같은 외부 문서의 쓰는 table인 foreign key, 그리고 그런 foreign key의 column을 leading column으로 갖는 index만 갖는다. 따라서 줄인 text는 전체 문서와 같은 validation을 만족한다: audittablestable이 audit record table에 선언하는 foreign key처럼 쓰는 table에 걸친 규칙이 manifest text와 external text로 만든 runtime model에서도 성립한다. 쓰는 두 table 사이의 foreign key와 그것을 이끄는 index는 external text와manifestHash에 들어가고, 어떤 소유한 문서도 쓰지 않는 table로의 foreign key는 들어가지 않으므로 그것을 바꿔도 hash는 그대로다.DBS-102 schema text는 집합이 소유한 모든 table을 table 이름 순으로, 그리고 쓰는 table이 있는 외부 문서마다 그 table을 이름 순으로 적은
use줄 하나를 담은schema라는 문서 하나의 canonical emission이며,immutable과audit을 뺀 모든 setting을 지우고,auditsetting은 기록하지 않는 column을 column 순서로 적은exclude목록으로, 모든 column을 기록하면 목록 없이 쓴다(Audit). 비게 된settingsblock은 canonical form처럼 쓰지 않는다.use줄은 소유한 table이 쓰는 외부 문서만 적고 comment와 diagram은 없으므로 table에만 달려 있다. table을 다른 문서로 옮기거나 문서 이름을 바꾸거나 나눠도 같고, 집합을 렌더링한 database를 introspect해도 같다.DBS-103
manifestHash는sha256:뒤에 manifest text와 그 뒤의 external text의 UTF-8 bytes에 대한 SHA-256을 소문자 16진수로 붙인 것이고,schemaHash는 schema text에 대해 같은 방식으로 계산한다.DBS-104 생성 코드는 자신을 만든 문서 집합의 manifest text, external text,
manifestHash를 담고, 모든 request에 그 hash를 포함한다. runtime은 process가 시작할 때 한 번 내장된 text로 model을 만든다. PHP generator는 그 model을 PHP array로 쓰므로 opcode cache가 이를 유지하고, 어떤 request도 text를 parse하지 않는다.schemaHash는 migration plan과 그 이력이 기록하는 database 상태의 식별자다.
외부 문서
DBS-105 문서 집합은 그 tool이 자기 문서로 나열한 문서를 소유한다. 집합이 table을 쓰는 다른 집합의 문서는 그 집합의 외부 문서이며 set 파일의
external목록에 나열한다. 그러면 여러 집합이 database 하나를 나눠 쓸 수 있다. 각 집합은 자기 table만 만들고 바꾸며, table을 렌더링하는 것은 그 table을 소유한 집합뿐이다. 외부 문서는 집합과 함께 parse하고 검사하므로 소유한 table의 foreign key와auditsetting은 그곳에서 대상 table을 확인하지만, 외부 문서는 렌더링, 설치, 변경, 비교, 생성하지 않는다. 어느 소유 문서도use로 닿지 않는 외부 문서는 그 header 이름에서use오류이고, 소유하지도 외부 문서도 아닌 쓰이는 문서도 그렇다.DBS-106 Hash. manifest text는 소유한 문서를, external text는 외부 문서에서 쓰는 table을 담으므로,
manifestHash는 쓰는 table이 바뀌면 바뀌고 외부 문서의 다른 table이 바뀌면 그대로다. schema text는 소유한 table을 담고 쓰는 table을use줄에 적는다.DBS-107 확인. 집합의 설치와 table과 column 추가는 database를 introspect하고(dialects) 쓰는 table마다 외부 정의의 모든 column이 같은 type과
null로 있고 primary key와 각 unique key가 있기를 요구한다. database에는 column이 더 있어도 된다. 차이는 어떤 statement보다 먼저 각 차이를 적은CONFIG다:table <t> does not exist,column <t>.<c> does not exist,column <t>.<c> is <type>, not <type>,column <t>.<c> is null, not not null(또는 반대),table <t> has the primary key (<columns>), not (<columns>),table <t> has no unique key (<columns>). 집합의 등록은 connect helper로 하는 등록도 database를 읽지 않는다(schema).DBS-108 설치. install은 소유한 table이 하나도 없으면 만들고, 모두 있으면 아무것도 바꾸지 않으며, 일부만 있으면
CONFIG로 실패하고, 그다음 소유한 table이 집합과 같기를 요구한다. addTablesAndColumns는 소유한 table만 비교하고 바꾼다(schema).DBS-109 Plan. plan은 database 전체를 바꾸므로(plans) target schema text에
use줄이 있는 plan은plan오류다. 외부 문서를 쓰는 집합은 install과 addTablesAndColumns로 설치하고 올린다.DBS-110 Runtime. 외부 문서에서 쓰는 table은 각각 쓰는 집합의 runtime model에서 그 문서의 위치에 놓이는 external entity이며, 이름은 table 이름이고 외부 정의만으로, 곧 column, primary key, unique key만으로 만들며 setting, index, foreign key는 없다. 생성 코드는 다른 entity처럼 model을 주므로 select, join, subquery와 relation에 쓰이고, generator의 relation getter로 이어지며, 조건을 받는다. 문서로 만든 model과 manifest text와 external text로 만든 model은 같다. 이름이 집합의 entity와 겹치는 external entity는
name.duplicatediagnostic이다. 쓰기는 소유자만 한다: external entity의 모든 쓰기(create, creates, update, save, delete, restore, restoreOrCreate, upsert, restartIdentity)는 어떤 statement보다 먼저CONFIG<table> is owned by another document set로 실패한다. 소유한 table의 audit 기록 table은 외부 table일 수 있다. 그 table을 소유한 집합이 같은 연결에 등록되어 있으면 audit 값을 가진 transaction이 그곳에 기록을 삽입한다(Audit).DBS-173 External models. generator는 set이
use줄로 쓰는 외부 문서의 모든 table에 대해, 스캔한 source가 그 table을 호출하는지와 관계없이 읽기 전용 model을 쓴다. 스캔은 model이 어떤 method를 가질지만 정하고 어떤 model이 있을지는 정하지 않으며, model을 있게 하려고 source에 호출을 유지하지 않는다.DBS-174
mysql,postgres,sqlite중 어느 것도 아닌 dialect 이름은 statement를 만들기 전에 거부된다:renderDbspec과planSteps는 그 이름을 적은TypeError를 던진다.
Runtime model
client는 문서 집합으로 runtime model 하나를 만들고, 생성 코드도 같은 model에서 만든다.
- DBS-111 Entity. 집합이 소유한 모든 문서의 table마다, 그리고 모든 외부 문서에서 쓰는 table마다(외부 문서) entity 하나다. 문서는 이름 순, table은 문서 순서다. entity 이름은
entitysetting이나 table 이름이다. 생성 type 이름은 snake_case 조각의 첫 글자를 대문자로 쓴 것이다(service_member→ServiceMember). - DBS-112 Field. column마다 field 하나이며 이름은 column 이름, 순서는 column 순서, 값 type은 아래 표를 따른다.
nullcolumn은 언어의 없음 값을 쓴다: Go는 pointer([]byte와 styled 값은 감싸지 않음), PHP는?T, TypeScript는T | null, Rust는Option<T>, Python은T | None이다. styled column은 언어의StyledValue를 쓴다. TypeScript는StyledValue<T>, Python은polyspec.orm.StyledValue이며, 둘 다 SQL NULL과 value 변형을 가진다. 값의 type이 다른 Python 또는 TypeScript getter는TypeError를 던진다. 생성 typed column은 public typed field 대신 typed getter와 setter로 접근한다. Go getter는(T, error), Rust getter는Result<T>를 반환하며, column을 선택하거나 대입하지 않았으면COLUMN_UNSELECTED를 반환한다. PHP, TypeScript, Python getter는 선택하지 않은 column을 읽을 때 해당 코드를 담은 오류를 던진다. - DBS-113 Key. primary key column이 row를 식별한다. identity column은 insert가 쓰지 않으며 생성된 key를 돌려준다. upsert는 호출이 지정한 primary key나 unique key에서 충돌한다.
- DBS-114 기본 select 집합.
select explicit에 없는 모든 column이다. 선택하지 않은 column을 읽으면COLUMN_UNSELECTED로 실패한다. type이나 codec이 스스로 column을 빼지는 않는다. - DBS-115 Default. default가 있는 column을 생략한 insert는 database default를 받으며 executor는 채우지 않는다. 단 clock이 millisecond 해상도인 SQLite에서는 생략한
default nowcolumn에 클라이언트 clock을 bind한다(protocol). default 없는 non-null column을 생략한 insert는 database에 닿기 전에IR_INVALID로 실패한다. - DBS-116 Codec.
codecsetting이 있는 column은 첫 stage가 encode하는 값을 담는다. stage 중ordered_json,serialize,yaml,gz,base64가 있으면 공통 값 model(codecs)의 styled 값이며 SQL NULL과 저장된 null을 구분한다. 모든 stage가aes,hex,ip이면 문자열이다(ip는 주소 text). - DBS-117 Setting.
updated는 executor가 계획하는 모든UPDATE에서 UTC statement 시각을 받는다.soft_delete는 읽기(root,ON조건의 각 join child, 각 relation, 각 subquery)를 거르고 delete를 update로 바꾸며, soft delete한 row와 맞는 호출은restore뿐이다.aes_version은 row의 key version을 저장한다.blind_index는 HMAC column을 쓰고 AES column에 대한 같음 조건을 바꾼다.audit에서는 audit 값을 가진 transaction이 연결 audit source의 값과 자기 값으로 callback 전에 audit 기록 하나를 삽입하고, executor는 자신이 insert하거나 update하는 모든 감사 row의 audit column에 그 기록의 key를 쓴다. audit 없는 감사 table의 insert나 update는CONFIG로 실패한다.immutable은 runtime 동작이 없다: database가 변경을 거부한다.navigation도 runtime 동작이 없다. - DBS-118 Relation. 생성 코드는 이전처럼 column의 match method로 두 entity를 join한다. join 결과는 호출이 준 alias나
<entity>_model,<entity>_models로 얻는다. - DBS-119 Connection. raw connection은 DSN URI와 설정을 받고 set을 등록하지 않으며, schema 경로는 받지 않는다. 생성 코드는 manifest text와
manifestHash를 담고, 그 connect helper가 connection을 열어 그 set을 등록한다.utils().schema().register(schema)는 열린 connection에 set을 등록하고,install은 자기가 설치한 set을 등록한다. 등록은 database를 읽지 않는다. schema 값으로 등록한 set은 manifest text와 external text에서 runtime model을 얻으므로 읽은 생성 코드가 필요 없다. PHP는 생성 class의 bootstrap이 실행된 뒤에는 그 array를 쓴다. request는manifestHash를 담고 그 set이 등록된 connection에서만 실행된다(protocol). - DBS-120 Generated metadata. manifest는 각 ORM client runtime model의 원천이며 생성 코드는 이를 어기지 않는다. Go, TypeScript, Rust, Python은 생성 코드가 가진 manifest와 external text에서 runtime model을 만들고, generated entity는 이름과 set만 가진다(Rust model은 build script가 다시 생성하기도 한다). PHP client는 생성 class의
meta()array를 쓰며manifest_hash를 뺀 array를 manifest와 external text가 정의한 entity와 비교한다. manifest에 없는 entity를 포함한 차이는 어떤 statement보다 먼저CONFIG로 실패하고 class, entity, 처음 다른 field 경로(columns.name.length), 재생성 명령orm-gen gen을 알린다. PHP extension은 PHP model runtime을 C로 구현한다. PHP generator는 PHP library와 extension이 함께 불러오는 typed model class를 생성하고, extension의 nativeModelclass가 그 기반 class다. - DBS-175
orm-gen gen --check는 아무것도 쓰지 않는다. generator가--out아래에 쓸 파일을 그 자리의 파일과 비교한다. 모든 파일이 같으면 0으로 끝나고, 다른 파일마다differs: <path>를 출력한 뒤 1로 끝난다.
| dbspec | Go | PHP | PHP extension | TypeScript | Rust | Python | | --- | --- | --- | --- | --- | --- | | i16 | int16 | int | PHP와 동일 (native Model) | number | i16 | int | | i32 | int32 | int | PHP와 동일 (native Model) | number | i32 | int | | i64 | int64 | int | PHP와 동일 (native Model) | number(safe integer) | i64 | int | | bool | bool | bool | PHP와 동일 (native Model) | boolean | bool | bool | | decimal(p,s) | 소수 자릿수가 정확히 s인 string | string | PHP와 동일 (native Model) | string | String | str | | f64 | float64 | float | PHP와 동일 (native Model) | number | f64 | float | | varchar(n), text, uuid | string | string | PHP와 동일 (native Model) | string | String | str | | bytes | []byte | string | PHP와 동일 (native Model) | Uint8Array | Vec<u8> | bytes | | date | time.Time | \DateTimeImmutable | PHP와 동일 (native Model) | string YYYY-MM-DD | NaiveDate | str YYYY-MM-DD | | time(p) | 소수 자릿수가 p인 string HH:MM:SS | string | PHP와 동일 (native Model) | string | String | str | | datetime(p) | UTC의 time.Time | UTC의 \DateTimeImmutable | PHP와 동일 (native Model) | 소수 자릿수가 p인 string YYYY-MM-DD HH:MM:SS | NaiveDateTime | 소수 자릿수가 p인 UTC의 str YYYY-MM-DD HH:MM:SS |
- DBS-121 생성된
date,time(p),datetime(p)컬럼의 setter는 모든 언어에서 텍스트 형태를 받는다: Go와 TypeScript는string, Python은str, PHP는string, Rust는String이다. 네이티브 날짜·시간 값은 setter가 받지 않아 문장을 실행하기 전에 타입 검사가 거부하며, 텍스트는 setter 실행 시 검사되고 바인드 계층이 다시 검사한다.
Diagram
- DBS-122
diagram <name> { <table> at <x> <y> ... }는 view를 위해 table을 배치한다. 좌표는 diagram 단위의 −2147483648 ~ 2147483647 정수다. table은 diagram에 최대 한 번 나오며, diagram은 table을 빼놓을 수 있다. diagram은 어떤 hash도, 어떤 렌더링도 바꾸지 않는다. 문서는 diagram을 여러 개 가질 수 있다.
Canonical form
DBS-123 출력은 parse한 문서를 하나의 canonical text로 쓴다. 그래서 canonical 입력에 대해
emit(parse(s)) == s이고, 모든 유효한 입력에 대해emit(parse(emit(d))) == emit(d)다.DBS-124 두 칸 들여쓰기, token 사이 공백 하나, LF 줄 끝, 마지막 줄바꿈 하나
DBS-125 header, 빈 줄, 문서 이름 순으로 정렬한
use줄, 각 block 앞의 빈 줄DBS-126 table 안: 선언 순서의 column,
primary key, 그다음 이름 순으로 정렬한unique,index,foreign key,check줄, 그다음 위 표 순서의settings줄(codec,blind_index,navigation줄은 column이나 key 이름 순)DBS-127 default와 action은 모두 쓰고(
on delete restrict on update restrict),asc는 생략DBS-128 literal은 한 형식으로 쓴다: 정수는 0의 부호나 앞자리 0 없이, decimal은 column scale만큼의 소수 자리로(
decimal(13,2)이면0.00), 문자열은 작은따옴표 안에 따옴표를''로,true와false,date는'YYYY-MM-DD',time(p)와datetime(p)는 정확히 p자리 소수로(datetime(6)이면'2026-01-01 00:00:00.000000'),uuid는 소문자로DBS-129 table과 diagram은 문서 순서, diagram 줄도 문서 순서
DBS-130
select explicitcolumn,audit의exclude나include목록의 column,use줄 안의 table은 적힌 순서로, 목록은exclude (a, b)처럼DBS-131
f64literal은 다시 읽으면 같은 값이 되는 가장 짧은 지수 없는 10진수로 쓰고, 정수 값이면 소수점 없이, 음의 0은0으로 쓴다. check literal은 만나는 column의 canonical default 형식을 따르고, predicate는and안의or가 필요로 하는 괄호만 남긴다DBS-132 comment 줄은 바로 다음 줄에 붙고 그 줄의 들여쓰기를 따른다. 닫는
}앞의 comment는 그 block 안 줄의 들여쓰기를 따른다. 마지막 block 뒤의 comment는 빈 줄 하나 뒤에 온다. comment는#부터 줄 끝까지의 text를 바꾸지 않는다
Syntax analysis
DBS-133 각 구현은 dbspec lexer 하나와 parser 하나를 가지며, 구문 분석은 그 parser의 출력이다. editor 계층, 생성 파일, 정규 표현식 어느 것도 문법 규칙을 갖지 않는다. TextMate 같은 정규 표현식 문법은 parser 문맥이 없고, 이름이 예약되지 않은 keyword 단어일 수 있으므로(
key,text,references는 올바른 column 이름이다) parser와 다르게 token을 분류한다.DBS-134
analyze(text, set)은 parse 결과를 바꾸지 않고 그대로, 그리고 text의 token을 돌려준다. token은{line, start, end, class}이다.line은 1부터 시작하고,start는 그 줄에서 token 앞에 있는 Unicode code point의 수이고,end는 token 뒤 code point 앞까지의 수다.DBS-135 줄은 단어, 문자열, 구두점(
{,},(,),,), operator(=,<>,<,<=,>,>=,+,-,*,/)로 읽는다. 숫자와 선택적 소수부로 된 단어(12,0.50)는 숫자이고, comment는 줄 전체다. parser가 각 token의 class를 정한다:
| Class | Token |
|---|---|
keyword | 문법이 그 위치에 고정한 단어, 곧 그 위치가 받는 단어 집합의 원소: header의 dbspec; use, table, diagram, settings; primary, key, unique, index, foreign, references, on, delete, update, check; action restrict, cascade, set_null; null, identity, default, asc, desc; 값 단어 true, false, null, now; setting의 option과 form 단어(explicit, files, document, block, list, table, shard, year, month, day, require, exclude, include, row, from, to, at, into, column, action, previous, initial, terminal, history, limit)와 codec stage; predicate 단어 and, or, not, in, is, 그리고 predicate가 읽는 자리의 null. 그런 위치에 있지만 원소가 아닌 단어는 word다 |
type | column의 type을 가리키는 단어 |
name | 문법이 그 위치에서 자유롭게 받는 단어: 문서, table, column, constraint, index, diagram, state의 이름, 또는 navigation의 관계 이름. 선언과 참조 모두 |
setting | settings block 줄의 첫 단어. 그 줄의 다른 단어는 위의 class를 따른다 |
number | 숫자 |
string | 문자열 |
operator | operator. ->의 -와 >도 포함한다 |
punctuation | 구두점 |
comment | #부터 줄 끝까지의 comment 줄 |
word | parser가 분류하지 않는 단어 |
parse가 끝나지 않는 경우는 다음과 같이 정한다:
DBS-136
limiterror는 token을 만들지 않는다. 검사가 할당보다 먼저이기 때문이다(한도와 error);DBS-137
encoding이나header로 중단되면 중단 줄과 그 뒤 모든 줄은 lexical token만 만든다: 단어는word이고, 숫자, 문자열, operator, 구두점, comment 줄은 그 class를 유지한다.encoding에서 단독 CR은 줄 끝이 아니며 lex는 그 뒤에서 이어진다;DBS-138 올바르지 않은 문자는 token을 만들지 않는다;
DBS-139 parser가
syntaxdiagnostic으로 포기한 줄에서 단어는word이다. 의미 diagnostic만 있는 줄은 parser가 정한 class를 유지한다.DBS-140 comment의 위치는 token에만 있고 문서 model에는 없다. token은 highlighting에 쓴다. 선언의 위치가 필요한 도구는 token이나 text가 아니라 parse 결과의 선언 위치를 읽는다.
vector는 tests/dbspec/syntax.json이다: cases.json의 canonical, normalize, invalid, sets, hashes case의 token. files case는 파일 reader와 byte reader에 남는다. vector는 TypeScript 구현에서 한 번 생성하고 검토한 뒤 고정하며, 모든 구현이 정확히 일치한다.
Formatting
- DBS-141
format(text, set)은 모든 구현에서emit(parse(text, set))이다. diagnostic이 있는 문서는 거부하며 결코 다시 쓰지 않는다. formatting은 불변식 셋을 지킨다:emit(parse(format(x))) == emit(parse(x)),format(x)의 manifest hash와 schema hash가x의 것과 같음,format(format(x)) == format(x). model 동등성은 그중 하나가 아니다. emit이 constraint와use줄을 정렬하기 때문이다(Canonical form). canonical 문서의 text는 그 자신의 format이므로, 그것을 format하는 editor는 아무것도 바꾸지 않는다. vector는cases.json의 것이다: canonical과 normalize case는 canonical text로 format되고, invalid case는 거부된다.
Declaration positions
- DBS-142 성공한 parse는 문서와 함께 그 선언의 위치를 돌려준다: source 순서의
{kind, path, line, column}목록이며,line과column은 1부터 시작하고column은 diagnostic처럼 Unicode code point 수다. 위치는 문서 model에 들지 않는다. 문서는 source text 없이도(introspection과 Mermaid import로) 만들어지며, 같은 model을 비교, diff, render, emit, hash하기 때문이다. introspection, Mermaid import, emit, 비교, diff, render, hash는 위치를 받지도 만들지도 않는다. 선언의 자리가 필요한 도구는 구문 분석의 token이나 text가 아니라 이 위치를 읽는다.
| Kind | Path | 위치 |
|---|---|---|
document | [<document>] | header의 이름 |
use | [<used document>] | use 줄의 문서 이름 |
use_table | [<used document>, <table>] | use 줄의 각 table 이름 |
table | [<table>] | table 이름 |
column | [<table>, <column>] | column 이름 |
constraint | [<table>, <name>] | unique, index, foreign key, check 줄의 이름 |
constraint | [<table>, "primary key"] | primary keyword: primary key는 이름 없는 유일한 선언이다 |
setting | [<table>, <keyword>, ...<identity>] | setting 줄의 keyword |
diagram | [<diagram>] | diagram 이름 |
diagram_entry | [<diagram>, <table>] | 항목의 table 이름 |
- DBS-143 setting의 identity는 Settings의 반복 규칙이 유일하게 만드는 것이다. 그래서 모든 path는 유일하고, 반복하는 setting은 줄마다 위치 하나를 가진다:
| Setting | Identity |
|---|---|
codec, markdown | column |
navigation | foreign key |
blind_index | AES column |
checkbox | column과 state |
state_machine | transition <from> <to>, terminal <state>, initial <state>, history, limit <state>. table마다 기계가 하나이므로 column은 빠진다 |
| 그 밖의 모든 setting | 없음: 최대 한 번 나온다 |
도구가 이 kind 너머의 참조(foreign key의 대상 table, key의 column, check의 피연산자)의 자리가 필요하면, 먼저 이 목록에 그 kind를 더하며 모든 구현과 vector에 함께 넣는다. vector는 cases.json의 positions case다.
한도와 error
DBS-144 문서는 최대 32 MiB, table 4096개, column 120000개, foreign key 20000개이며, 할당 전에 확인한다. error는
SCHEMA_INVALID이고, 문제 token의 1부터 세는 줄과 열, 아래 규칙, message를 갖는다. 열의 단위는 Unicode code point다. 멈추게 하는 error(encoding,header,limit)는 그 전에 찾은 diagnostic 뒤에 보고한다. diagnostic은 줄, 열, 아래 규칙 표의 순서로 정렬한다. 이름 규칙에 맞지 않는 header 이름은name.format이며 parse는 계속한다. table, constraint, setting 전체에 대한 규칙의 위치는 그 이름이나 keyword token이고, 다른 규칙의 위치는 규칙을 어긴 token이다.DBS-145 foreign key: index,
set_null, type, column 수, 참조 key 규칙은 constraint 이름, 모르는 column이나 대상은 그 tokenDBS-146 key: 16 column과 640자 한도는 key 이름이나
primaryDBS-147 setting: setting 전체와 함께 필요한 setting 규칙은 setting keyword, column type 규칙은 그 column, 이력 table 형태는 이력 table 이름
DBS-148 column이 없는 table은 table 이름에서
column과key를 보고한다.null identity는identity에서column을, primary key에서key를 보고한다DBS-149 실패한 쓰는 문서는 그 이름에서
usediagnostic 하나를 보고하고 message에 첫 error를 담는다. 두 쓰는 문서가 constraint 이름을 반복하면 뒤 문서 이름에서name.duplicate를 보고한다.use줄에서 문서나 table을 반복하면name.duplicate다. use 순환이나 쓰는 이름과 다른 header 이름은use다DBS-150
headererror의 위치는 줄이dbspec 1 <name>에서 처음 벗어나는 문자이고(<name>은 ASCII 문자, 숫자,_의 연속), 빠진 부분이 있으면 줄 끝 다음 열이다DBS-151 32 MiB 한도의 위치는 1줄 1열이다. column이 1000개를 넘는 table은 1001번째 column 이름에서
limiterror다DBS-152 check: predicate는 먼저 읽는다. 읽기는 산술 연산자, 함수,
nullliteral 같은 predicate 형식 밖의 token과, 형식이 column을 요구하는 자리의 literal(in,is의 대상, 두 literal 비교의 첫 literal), 그리고 비교,in,is가 뒤따르지 않는 operand를 보고한다. 그 operand 뒤에and,or,), 끝이 오면 operand에서, 그 밖에는 뒤따르는 token에서 보고한다. 읽기 diagnostic이 있는 predicate는 더 확인하지 않는다. 끝까지 읽힌 predicate는 column과 type을 확인하고, 그중 source 순서의 첫 diagnostic을 보고한다. 모르는 column,cascade나set_nullforeign key의 column,bytescolumn은 그 column, 비교에서 상대 column과 만나지 않는 column은 뒤쪽 column, 만나는 column의 default가 아닌 literal은 그 literal,booloperand(column이나true,false)에 쓴<,<=,>,>=는 그 연산자가 위치다DBS-153 한 줄의
syntaxdiagnostic은 최대 하나다. check 식은 첫 diagnostic만 보고한다. 참조 안의 잘못된 이름은name.format을 보고하고 더 해석하지 않는다. 자기 줄이 실패한 column이나 table에 대한 참조는 더 보고하지 않는다. 실패한 줄은 첫 단어가 정하는 종류와 선언하려던 이름을 유지하며, 이를 알기 위해서만 tab을 공백으로 읽는다. 그래서 실패한 key나 index 줄도 column을 모르는 key나 index를 선언한 것으로 보므로, 그 column에 기대는 규칙은 보고하지 않는다. 실패한primary key줄은 그 table의 primary key 없음과 identity 규칙을 가리고, 실패한primary key,unique,index줄은 그 table foreign key의 선두 index 규칙과 그 table을 참조하는 foreign key의 참조 key 규칙을 가린다DBS-154 첫 줄이 아닌 header(앞에 comment나 빈 줄이 있는 경우 포함), 그리고 정확히
dbspec, 공백,1, 공백, 이름이 아닌 header 줄(예: tab이 있는 줄)은headererror다 parse는 첫 error만이 아니라 문서의 모든 error를 원문 순서로 보고한다.encoding,header,limiterror는 parse를 멈춘다.DBS-155
signature: 문서 파일이dbspecbytes로 시작하지 않는다(파일). byte check와 file reader가 parse 전에 보고한다DBS-156
header: 첫 줄이dbspec 1 <document>가 아니거나 version이 1이 아니다DBS-157
syntax: token이 그 위치에 올 수 없다DBS-158
order:use,table,diagram의 순서나 table 안 줄의 순서가 틀렸다DBS-159
name.format: 이름이[a-z][a-z0-9_]*가 아니거나primary다DBS-160
name.length: 이름이 63 bytes보다 길다DBS-161
name.duplicate: table, column, constraint, index, diagram 이름이 범위 안에서 겹친다DBS-162
type: type을 모르거나 parameter가 범위를 벗어난다DBS-163
column: column의null,identity,default조합이 규칙에 어긋나거나 default가 type에 맞지 않는다DBS-164
key: primary key가 없거나 둘 이상이거나, key column이 nullable이거나, key나 index가 모르는·반복된·text·bytescolumn, 16개 넘는 column, 640자 넘는varchar를 나열한다DBS-165
foreign_key: foreign key의 대상, column type, index, column 수,set_nullnullability가 규칙에 어긋난다DBS-166
check: check predicate가 predicate 형식 밖의 것, 모르는 column, type이 만나지 않는 operand, column type 밖의 literal,cascade나set_nullforeign key의 column을 쓴다DBS-167
setting: setting을 모르거나, 반복되거나, 모르는 column을 가리키거나, column type이나 함께 필요한 setting이 틀렸다DBS-168
use: 쓰는 문서가 선언된 집합에 없거나 쓰는 table이 거기 정의되지 않았다DBS-169
diagram: diagram이 모르는 table을 가리키거나, table을 반복하거나, 좌표가 정수가 아니다DBS-170
limit: 문서가 크기나 개수 한도를 넘는다DBS-171
encoding: text가 유효한 UTF-8이 아니거나, byte order mark가 있거나, 단독 CR이 있다. UTF-8이 아닌 파일 bytes는 byte check와 file reader가 parse 전에 보고한다
Mermaid 언어에서 바뀐 이름
| Mermaid 언어 | dbspec | 이유 |
|---|---|---|
auto | identity | 생성되는 key의 SQL 용어 |
lazy | select explicit | column을 기본 select에서 뺄 뿐이며 접근할 때 load하지 않는다 |
? / =v / =now | null / default v / default now | 의미를 말하는 keyword |
onupdate | settings { updated <column> } | database 절이 아니라 executor setting |
jsontext | text + codec <column> ordered_json | ordered JSON은 codec이고 database는 text를 저장한다 |
이름 규칙(is_*, created_ts, aes_hex_*, ip, _seq) | settings | column 이름은 동작을 고르지 않는다 |
rename_table, rename_column | migration plan | rename은 상태가 아니라 변경이다 |
orm:route, orm:permission 등 API directive | 제거 | schema의 관심사가 아니다 |
schema_hash | schemaHash, manifestHash | 두 가지 다른 범위 |
ormgen, orm-gen | 모든 언어에서 CLI 이름 하나 | 도구 하나, 이름 하나 |
유지, 이동, 제거
Mermaid 언어와 manifest의 모든 기능:
| 기능 | 판정 |
|---|---|
| entity, column, PK, FK constraint로서의 relation | table, column, primary key, 이름 있는 foreign key로 유지 |
type 어휘(tinyint, int, bigint, varchar, decimal, datetime, uuid, jsontext, enum, point, unsigned) | dbspec type으로 교체. tinyint, unsigned, enum, point, native JSON은 제거 |
unique, index directive | 이름 있는 줄로 유지. 생성 이름은 제거 |
fulltext | 제거(neutral 형식 없음) |
MySQL 재작성과 prefix가 있는 check | neutral 식 집합으로 유지. 재작성과 prefix는 제거 |
timestamps와 이름 규칙 | default now와 updated setting으로 교체 |
soft_delete, aes_version, blind_index, style | settings로 유지 |
lazy, 관계 이름 | select explicit과 navigation settings로 유지 |
table_comment, column_comment | physical 객체로서는 제거. # comment는 문서에 남는다 |
rename_* | migration plan으로 이동 |
orm:table schema 한정 이름 | 제거(문서 하나는 database나 schema 하나) |
orm:foreign | use와 쓰는 table로의 foreign key로 교체 |
orm:immutable | row trigger를 쓰는 immutable setting으로 유지 |
orm:audit, orm:audit_log | audit 기록 table과 이력 table을 쓰는 audit setting으로 교체. 따로 두던 변경 table, context setting, JSON row 값은 제거 |
orm:field, route, scope, filter, operation, permission 등 전달용 directive | 제거 |
-- orm: trigger marker와 -- orm-schema-v1 SQL source | 제거. 복원은 renderer 출력과 비교한다 |
dialect SQL text가 있는 PhysicalGraph record(typeSql, predicateSql, expressionSql, collationSql, operatorClassSql)와 주석 달린 Markdown 문서 | 제거. dbspec model이 대신한다 |
PHP 확장
- DBS-172 php 구현(
polyspec/orm)과 php-extension 구현(polyspec/orm-php-ext)은 같은 PHP dbspec 표면을 가진다.polyspec/orm-php-extpackage는 PHP 확장polyspec_orm(packages/php-ext)을 제공하며, 이는 이 repository의 모든 PHP library를 C로 구현한(S10), 같은 인터페이스의 독립된 구현이다. 확장은 PHP client의 class 이름을 그대로 등록한다. dbspec 호출이 받거나 돌려주는Polyspec\Orm\Dbspec의 모든 class를 가지며, 문서 class(Document,UseLine,Table,Column,ColumnType,PrimaryKey,UniqueKey,Index,IndexColumn,ForeignKey,Check,Settings,Setting,Diagram,Placement)는 같은 public 가변 property를, 결과와 값 class는 같은 readonly property를 PHP client의 순서대로 가지며, 생성자, 메서드, 상수도 같다. 확장의Polyspec\Orm\Dbspec\Dbspec은 PHP client와 같은 인자, 결과, diagnostic message, 예외의readFile,readBytes,readSet,parse,analyze,format,emit,manifest,render,introspect,externalDifferences,parsePlan,emitPlan,chain,diff,compareSchemas,installedDifferences,addTablesAndColumnsSteps,planSteps,apply,recover,rollback,finalize,exportMermaid,importMermaid를 가지며, apply 명령의ApplyEvent,ApplyError,ApplyCleanupError도 같다.introspect와 apply 명령은 PHP client처럼 받은PDO객체의 메서드(query,prepare,exec,getAttribute, 그리고 statement의execute,fetch,fetchAll,closeCursor,errorCode,errorInfo)를 불러 쓰므로PDO하위 class는 같은 호출을 본다. apply 명령은 PHP client처럼 clock과 event closure를 부르고 clock을DateTimeImmutable로 쓴다. PHP code가 만들거나 고친 문서도 PHP client의 문서처럼 이 메서드에 준다. 확장은docs/spec/errors.yaml의 error code도Polyspec\Orm\Code로 가지며, 상수는 PHP client의 것과 같다. stub의 그 block은orm-gen errors --lang php-extension이 쓴다. user code는 확장이 있든 없든 같다. 확장이 load되면 그 class는 어떤 code보다 먼저 정의되고, 아직 정의되지 않은 class만 load하는 Composer autoloader는 그 class에 대해 PHP client의 file을 하나도 load하지 않는다. 확장이 없으면 autoloader가 PHP client를 load한다. PHP client의 어떤 code도 확장을 검사하지 않으며, 어느 구현을 고르는 다른 이름도 없다. PHP client의 test가 둘 다 검사한다: 확장을 load한 process와 load하지 않은 process에서 각각 실행한다.
확장은 PATH의 phpize와 php-config가 가리키는 PHP를 위해 phpize로 build한다: make build-php-ext가 그것을 var/php-ext/polyspec_orm.so로 build하고, packages/php-ext/composer.json은 build path가 src인 PIE package polyspec/orm-php-ext로 그것을 선언한다.
make build-php-ext
php -d extension=var/php-ext/polyspec_orm.so your-script.php선언은 packages/php-ext/src/polyspec_orm.stub.php에 있고, interface 검사(contracts/interfaces.json의 extensions)가 이를 읽는다. Native build는 설치된 gen_stub.php와 PHP-Parser를 offline으로 temporary source copy에서 실행해 polyspec_orm_arginfo.h를 생성하고, gen_stub.php가 쓰지 못하는 배열 상수는 src/polyspec_orm.c가 등록한다. make php-ext-arginfo는 ignored var/ 아래에 확인용 output을 쓴다. make dbspec-php-extension-check는 stub 선언을 확인하고 확장을 실행 directory에 build한 뒤, load한 확장이 stub과 정확히 같은 선언을 가지는지, 공유 문서, syntax, statement, plan, Mermaid vector, 변경 문서, 메모리 SQLite database introspection, 실패하는 catalog query, SQLite file의 apply, recover, rollback, finalize script에서 PHP client와 같은 결과, message, exception을 내는지, public class가 PHP client의 public dbspec class와 같은 선언을 가지는지 확인한다. make dbspec-compare-check, make dbspec-introspect-compare-check, make dbspec-apply-pairs-check는 이를 다섯 번째 runner로 실행하고, make dbspec-introspect-php-extension-check와 make dbspec-apply-php-extension-check는 PHP client의 introspection과 apply test를 MySQL, PostgreSQL, SQLite에서 실행한다.
검증
공유 vector는 tests/dbspec/에 있다: canonical 문서와 그 출력, canonical이 아닌 문서와 그 canonical 출력, 기대하는 모든 diagnostic이 있는 잘못된 문서, 그리고 tests/dbspec/files/의 문서 파일: 모든 client가 signature error로 거부하는 DbSchema project 파일(XML)과 빈 파일, encoding error로 거부하는 UTF-8이 아닌 byte가 있는 파일, 그리고 바뀌지 않게 읽는 .dbs 문서. 각 client는 모든 파일을 file reader와 byte check로 읽고, make dbspec-compare-check가 다섯 client와 PHP 확장의 두 결과를 비교한다. 모든 client와 확장이 같게 parse하고 출력한다. 각 vector file은 tests/dbspec/schema/에 JSON Schema(draft 2020-12)를 가지고, 모든 compare runner는 자기 vector file을 그 schema로 자기 생태계의 validator로 읽는다: TypeScript는 ajv, Go는 santhosh-tekuri/jsonschema, PHP와 PHP 확장은 opis/json-schema(file의 모든 거부를 보고하는 release 2.6.0-p1이 있는 fork min-0-max/json-schema에서 받는다), Python은 jsonschema-rs, Rust는 jsonschema crate다. 거부한 file은 한 형식, 문제마다 <file>: <location> <problem> 한 줄로 보고한다(tests/dbspec/compare/runners.test.mts). runner가 스스로 확인하는 것은 main 문서가 그 case의 문서 가운데 하나인지처럼 schema가 말할 수 없는 것뿐이다. 2000-table, 60000-column, 10000-foreign-key case는 성능 측정이며, 측정은 test를 실패시키지 않는다(AGENTS.md). 각 client의 stress test는 case를 다섯 번 parse해 다음을 출력한다. parse마다의 CPU 시간과 wall-clock 시간, CPU 시간의 가장 짧은 값, median, 가장 긴 값, 같은 process에서 같은 시계로 잰 기준 작업(같은 문서를 한 번 지나며 h = h·31 + b (mod 2^32)를 계산한다)의 median CPU 시간, 그것에 대한 median parse의 비율, 그리고 기계다. 비율이 client의 기준값을 넘으면 test는 경고를 출력한다. 경고는 측정값, 기준값, 기계를 담은 WARNING 줄이고, check runner는 그것을 실행 기록과 summary에 남기고 GitHub warning annotation으로 쓴다. test는 그래도 통과한다. 기준 비율은 PHP client(128 MiB memory limit 안) 11, Rust client(release mode) 34, Go client 63, Node의 TypeScript client 185다. 각 값은 CI runner에서 잰 가장 높은 비율(PHP 7.16, Rust 22.5, Go 41.7~43.0, TypeScript 122.5)의 1.5배다. 그래서 경고는 보통의 변동이 아니라 약 50% 이상의 퇴행을 알린다. 숫자가 의미를 갖는 기계는 CI runner(.github/runner)다. 그 core는 모두 같지만, Apple silicon에는 성능 core와 효율 core가 있고 process가 어느 core에서 도는지가 두 시간을 함께 바꾼다. 그래서 macOS에서 PHP 비율은 실행에 따라 6.35에서 10.1까지, Rust 비율은 17.3에서 24.0까지 나왔고, 그곳에서는 측정을 출력만 한다. 경고를 없애려고 기준값을 올리지 않는다. 바꾸려면 같은 종류의 새 CI 증거가 필요하다. 각 측정은 parse를 실행한 thread의 CPU 시간이다: Rust main thread, Go test가 고정한 OS thread, Node main thread, 한 thread로 도는 PHP process다. 공유 machine에서 wall-clock 시간은 다른 process가 processor를 쓰는 시간도 담고, Go와 Node의 process CPU 시간은 다른 core에서 도는 garbage collector thread의 시간도 담는다. make bench는 네 stress test(make dbspec-stress-bench)와 make timing-check를 실행한다. make timing-check는 네 stress test, Rust orm-schema vector test, PHP dbspec test를 그 process group이 wall-clock 시간의 10분의 1만 받는 상태로 실행하고, 모든 test가 통과하고 측정을 출력하기를 요구한다.