STUDIO HAZE · jeniapp
Topaz source
Open by house policy. Topaz is our own language.
Topaz 5.20.2 · 63 files
apps/sqldb/sqldb.tpz
Topaz · 35214 B · Original file
1
// 작은 인메모리 SQL 데이터베이스. SQL 을 토큰으로 나눠 문장마다 파싱하고 실행해 결과를 표로 그리며, 데이터베이스 상태를 JSON 으로 저장하고 복원합니다.2
// 입력은 세미콜론으로 나눈 CREATE TABLE, INSERT, SELECT 문장들이거나 이 도구가 저장한 JSON 이고, 출력은 문장별 결과와 표, 저장 JSON 을 담은 HTML 조각입니다.3
// 같은 입력이면 인터프리터와 웹 빌드에서 바이트까지 같은 출력을 냅니다. 오류가 난 문장에서 멈추고 그때까지의 상태를 저장합니다.4
// JOIN, 트랜잭션, 서브쿼리, 집계 함수, GROUP BY, UPDATE, DELETE, ALTER, 여러 조건을 잇는 AND 와 OR 는 다루지 않습니다. 값은 리터럴로만 받고 SQL 문자열을 다시 조립하지 않습니다.5
// Tiny in-memory SQL database: tokenizes and runs CREATE/INSERT/SELECT, renders result tables and saves or restores the database as JSON.7
enum 값 { 정수(int), 문자열(string) }8
type 열타입 = "INT" | "TEXT"9
type 문장종류 = "CREATE" | "INSERT" | "SELECT"10
type 비교연산 = "=" | "<>" | "<" | "<=" | ">" | ">="11
type 셀키 = "i" | "s"12
record 열정의 { 이름: string, 타입: 열타입 }13
record 테이블 { 이름: string, 열들: Array<열정의>, 행들: Array<Array<값>> }15
// 이름, 정수, 문자열, 기호는 원문 조각을 싣고 입력끝은 토큰이 다 떨어진 자리를 나타냅니다.16
enum 토큰 { 이름(string), 정수(string), 문자열(string), 기호(string), 입력끝 }18
record 행조건 { 열자리: int, 연산: 비교연산, 기준: 값 }19
record 정렬기준 { 열자리: int, 내림: bool }21
type 읽음<T> = { 값: T, 다음: int }22
record 실행결과 { 출력: string, 표들: Array<테이블>, 다음: int }24
record 목록구분 { 닫힘: bool, 다음: int }26
let 예약어들 = Set.of(27
"CREATE", "TABLE", "INSERT", "INTO", "VALUES", "SELECT", "FROM",28
"WHERE", "ORDER", "BY", "ASC", "DESC", "INT", "TEXT",29
)30
let 한글자기호 = Set.of("=", "(", ")", ",", ";", "*", "-", "<", ">")31
let 두글자기호 = Set.of("<=", "<>", ">=")32
let 십육진숫자 = "0123456789abcdef".scalars()34
// 공용 도우미 시작 (이스케이프 공백인가 숫자인가 오류알림 연속끝 글자인가 대문자로)35
// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.37
// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.38
function 이스케이프(원문: string) -> string {39
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)40
}42
function 공백인가(글자: string) -> bool {43
글자 == " " || 글자 == "\t" || 글자 == "\n" || 글자 == "\r"44
}46
function 숫자인가(글자: string) -> bool {47
let 코드 = 글자.codePointAt(0) ?? 048
코드 >= 48 && 코드 <= 5749
}51
// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.52
function 오류알림(문구: string) -> string {53
"<div class=\"err\">{문구}</div>"54
}56
// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.57
function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {58
let mut 끝 = 시작59
while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }60
끝61
}63
// 위치가 글자들 끝을 넘으면 거짓입니다.64
function 글자인가(글자들: Array<string>, 위치: int, 글자: string) -> bool {65
위치 < 글자들.length && 글자들[위치] == 글자66
}68
// ASCII 소문자만 바꿉니다. 다른 문자의 대문자 규칙을 섞지 않습니다.69
function 대문자로(글: string) -> string {70
글.scalars().map(글자 => {71
let 코드 = 글자.codePointAt(0) ?? 072
if 코드 >= 97 && 코드 <= 122 { fromCodePoint(코드 - 32) ?? 글자 } else { 글자 }73
}).join("")74
}76
// 공용 도우미 끝78
function 완료알림(문구: string) -> string {79
"<div class=\"ok\">{문구}</div>"80
}82
function 낱말글자인가(글자: string) -> bool {83
let 코드 = 글자.codePointAt(0) ?? 084
(코드 >= 48 && 코드 <= 57) || (코드 >= 65 && 코드 <= 90) || (코드 >= 97 && 코드 <= 122) || 글자 == "_"85
}87
function 예약어인가(낱말: string) -> bool {88
대문자로(낱말) in 예약어들89
}91
function 문장종류파싱(낱말: string) -> Result<문장종류, string> {92
match 대문자로(낱말) {93
case "CREATE" => Ok("CREATE")94
case "INSERT" => Ok("INSERT")95
case "SELECT" => Ok("SELECT")96
case _ => Err("알 수 없는 문장입니다: " + 낱말)97
}98
}100
function 열타입파싱(낱말: string) -> Option<열타입> {101
match 대문자로(낱말) {102
case "INT" => Some("INT")103
case "TEXT" => Some("TEXT")104
case _ => None105
}106
}108
function 비교연산파싱(기호값: string) -> Option<비교연산> {109
match 기호값 {110
case "=" => Some("=")111
case "<>" => Some("<>")112
case "<" => Some("<")113
case "<=" => Some("<=")114
case ">" => Some(">")115
case ">=" => Some(">=")116
case _ => None117
}118
}120
function 셀키파싱(키: string) -> Result<셀키, string> {121
match 키 {122
case "i" => Ok("i")123
case "s" => Ok("s")124
case _ => Err("셀 키는 i 또는 s 여야 합니다")125
}126
}128
function 타입맞는가(타입: 열타입, 셀: 값) -> bool {129
match 셀 {130
case 정수(_) => 타입 == "INT"131
case 문자열(_) => 타입 == "TEXT"132
}133
}135
function 기본값(타입: 열타입) -> 값 {136
match 타입 {137
case "INT" => 값.정수(0)138
case "TEXT" => 값.문자열("")139
}140
}142
// 문자열은 글자 코드 순서로 비교합니다. 한 열의 칸은 모두 열 타입과 같은 종류라 종류가 다른 비교는 일어나지 않습니다.143
function 값비교(왼: 값, 오른: 값) -> int {144
match 왼 {145
case 정수(왼수) => match 오른 {146
case 정수(오른수) => if 왼수 < 오른수 { -1 } else if 왼수 > 오른수 { 1 } else { 0 }147
case 문자열(_) => 0148
}149
case 문자열(왼글) => match 오른 {150
case 문자열(오른글) => if 왼글 < 오른글 { -1 } else if 왼글 > 오른글 { 1 } else { 0 }151
case 정수(_) => 0152
}153
}154
}156
function 비교맞는가(연산: 비교연산, 차이: int) -> bool {157
match 연산 {158
case "=" => 차이 == 0159
case "<>" => 차이 != 0160
case "<" => 차이 < 0161
case "<=" => 차이 <= 0162
case ">" => 차이 > 0163
case ">=" => 차이 >= 0164
}165
}167
// 위치는 여는 따옴표를 가리킵니다. 따옴표 두 개는 따옴표 하나입니다.168
function 문자열토큰읽기(글자들: Array<string>, 위치: int) -> Result<읽음<토큰>, string> {169
let mut 부분들: Array<string> = []170
let mut i = 위치 + 1171
while i < 글자들.length {172
if 글자들[i] != "'" {173
부분들.push(글자들[i])174
i += 1175
} else if 글자인가(글자들, i + 1, "'") {176
부분들.push("'")177
i += 2178
} else {179
return Ok({ 값: 토큰.문자열(부분들.join("")), 다음: i + 1 })180
}181
}182
Err("닫히지 않은 문자열입니다")183
}185
// 숫자로 시작하면 숫자만 읽으므로 1abc 는 정수 1 과 이름 abc 두 토큰이 됩니다.186
function 토큰읽기(글자들: Array<string>, 위치: int) -> Result<읽음<토큰>, string> {187
let 글자 = 글자들[위치]188
let 두글자 = if 위치 + 1 < 글자들.length { 글자 + 글자들[위치 + 1] } else { 글자 }189
if 글자 == "'" {190
문자열토큰읽기(글자들, 위치)191
} else if 숫자인가(글자) {192
let 끝 = 연속끝(글자들, 위치, 숫자인가)193
Ok({ 값: 토큰.정수(글자들.slice(위치, 끝).join("")), 다음: 끝 })194
} else if 낱말글자인가(글자) {195
let 끝 = 연속끝(글자들, 위치, 낱말글자인가)196
Ok({ 값: 토큰.이름(글자들.slice(위치, 끝).join("")), 다음: 끝 })197
} else if 두글자 in 두글자기호 {198
Ok({ 값: 토큰.기호(두글자), 다음: 위치 + 2 })199
} else if 글자 in 한글자기호 {200
Ok({ 값: 토큰.기호(글자), 다음: 위치 + 1 })201
} else {202
Err("알 수 없는 글자: '" + 글자 + "'")203
}204
}206
function 토큰내기(소스: string) -> Result<Array<토큰>, string> {207
let 글자들 = 소스.scalars()208
let mut 토큰들: Array<토큰> = []209
let mut i = 연속끝(글자들, 0, 공백인가)210
while i < 글자들.length {211
let 읽음 = 토큰읽기(글자들, i)?212
토큰들.push(읽음.값)213
i = 연속끝(글자들, 읽음.다음, 공백인가)214
}215
Ok(토큰들)216
}218
function 토큰보기(토큰들: Array<토큰>, 위치: int) -> 토큰 {219
if 위치 < 토큰들.length { 토큰들[위치] } else { 토큰.입력끝 }220
}222
function 기호인가(토큰들: Array<토큰>, 위치: int, 기호값: string) -> bool {223
match 토큰보기(토큰들, 위치) {224
case 기호(글) => 글 == 기호값225
case 이름(_) | 정수(_) | 문자열(_) | 입력끝 => false226
}227
}229
// 키워드는 대소문자를 가리지 않습니다.230
function 키워드인가(토큰들: Array<토큰>, 위치: int, 키워드: string) -> bool {231
match 토큰보기(토큰들, 위치) {232
case 이름(낱말) => 대문자로(낱말) == 키워드233
case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => false234
}235
}237
// 문장 뒤에는 ; 나 입력 끝만 옵니다. 다른 토큰이면 오류에 보여 줄 원문을 돌려줍니다.238
function 남은토큰(토큰들: Array<토큰>, 위치: int) -> Option<string> {239
match 토큰보기(토큰들, 위치) {240
case 입력끝 => None241
case 기호(글) => if 글 == ";" { None } else { Some(글) }242
case 이름(글) | 정수(글) | 문자열(글) => Some(글)243
}244
}246
function 이름읽기(토큰들: Array<토큰>, 위치: int, 무엇: string) -> Result<읽음<string>, string> {247
let 낱말 = match 토큰보기(토큰들, 위치) {248
case 이름(낱말) => 낱말249
case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => return Err(무엇 + " 이름이 필요합니다")250
}251
if 예약어인가(낱말) { return Err("'" + 낱말 + "'는 예약어라 " + 무엇 + " 이름으로 쓸 수 없습니다") }252
Ok({ 값: 낱말, 다음: 위치 + 1 })253
}255
// 정수 토큰은 숫자만 담으므로 toInt 가 실패하는 경우는 64비트를 넘는 수뿐이고, 부호를 뒤집어도 넘치지 않습니다.256
function 값읽기(토큰들: Array<토큰>, 위치: int) -> Result<읽음<값>, string> {257
let 음수인가 = 기호인가(토큰들, 위치, "-")258
let i = if 음수인가 { 위치 + 1 } else { 위치 }259
match 토큰보기(토큰들, i) {260
case 정수(숫자들) => match toInt(숫자들) {261
case Some(n) => Ok({ 값: 값.정수(if 음수인가 { -n } else { n }), 다음: i + 1 })262
case None => Err("정수가 너무 큽니다: " + 숫자들)263
}264
case 문자열(글) => if 음수인가 {265
Err("문자열 앞에 - 를 쓸 수 없습니다")266
} else {267
Ok({ 값: 값.문자열(글), 다음: i + 1 })268
}269
case 이름(_) | 기호(_) | 입력끝 => Err("값(정수 또는 '문자열')이 필요합니다")270
}271
}273
// 항목을 읽고 , 가 이어지는 동안 다음 항목을 읽습니다. 항목읽기는 앞서 읽은 항목들을 받아 겹침 같은 검사를 그 자리에서 합니다.274
function 쉼표목록읽기<T>(토큰들: Array<토큰>, 위치: int, 항목읽기: (Array<토큰>, int, Array<T>) -> Result<읽음<T>, string>) -> Result<읽음<Array<T>>, string> {275
let mut 항목들: Array<T> = []276
let mut i = 위치277
while true {278
let 읽은항목 = 항목읽기(토큰들, i, 항목들)?279
항목들.push(읽은항목.값)280
if !기호인가(토큰들, 읽은항목.다음, ",") { return Ok({ 값: 항목들, 다음: 읽은항목.다음 }) }281
i = 읽은항목.다음 + 1282
}283
}285
function 열이름항목(토큰들: Array<토큰>, 위치: int, 앞이름들: Array<string>) -> Result<읽음<string>, string> {286
이름읽기(토큰들, 위치, "열")287
}289
function 값항목(토큰들: Array<토큰>, 위치: int, 앞값들: Array<값>) -> Result<읽음<값>, string> {290
값읽기(토큰들, 위치)291
}293
// 표와 열 이름은 겹치지 않게 만들어지므로 처음 맞는 자리가 유일한 자리입니다.294
function 열번호항목(토큰들: Array<토큰>, 위치: int, 열들: Array<열정의>) -> Result<읽음<int>, string> {295
let 읽은이름 = 이름읽기(토큰들, 위치, "열")?296
Ok({ 값: 열번호(열들, 읽은이름.값, "없는 열입니다: ")?, 다음: 읽은이름.다음 })297
}299
function 표번호(표들: Array<테이블>, 이름: string) -> Result<int, string> {300
let 번호들 = [for i in 0..<표들.length if 표들[i].이름 == 이름 => i]301
if 번호들.length > 0 { Ok(번호들[0]) } else { Err("없는 테이블입니다: " + 이름) }302
}304
function 열번호(열들: Array<열정의>, 이름: string, 없을때: string) -> Result<int, string> {305
let 번호들 = [for i in 0..<열들.length if 열들[i].이름 == 이름 => i]306
if 번호들.length > 0 { Ok(번호들[0]) } else { Err(없을때 + 이름) }307
}309
function 열정의항목(토큰들: Array<토큰>, 위치: int, 앞열들: Array<열정의>) -> Result<읽음<열정의>, string> {310
let 열이름 = 이름읽기(토큰들, 위치, "열")?311
if 열이름.값 in 앞열들.map(열 => 열.이름) { return Err("열 이름이 겹칩니다: " + 열이름.값) }312
let 타입뒤 = 열이름.다음 + 1313
let 타입후보 = match 토큰보기(토큰들, 열이름.다음) {314
case 이름(낱말) => 열타입파싱(낱말)315
case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => None316
}317
let 타입 = match 타입후보 {318
case Some(타입) => 타입319
case None => return Err("열 타입은 INT 또는 TEXT여야 합니다: " + 열이름.값)320
}321
Ok({ 값: 열정의 { 이름: 열이름.값, 타입: 타입 }, 다음: 타입뒤 })322
}324
// 배열은 참조로 공유되므로 복사본에 더해 이전 상태를 그대로 둡니다.325
function 만들기(표들: Array<테이블>, 토큰들: Array<토큰>, 위치: int) -> Result<실행결과, string> {326
if !키워드인가(토큰들, 위치, "TABLE") { return Err("CREATE 다음에는 TABLE이 필요합니다") }327
let 표이름 = 이름읽기(토큰들, 위치 + 1, "테이블")?328
if 표이름.값 in 표들.map(표 => 표.이름) { return Err("이미 있는 테이블입니다: " + 표이름.값) }329
if !기호인가(토큰들, 표이름.다음, "(") { return Err("( 가 필요합니다") }330
let 정의 = 쉼표목록읽기(토큰들, 표이름.다음 + 1, 열정의항목)?331
if !기호인가(토큰들, 정의.다음, ")") { return Err(") 가 필요합니다") }332
let mut 새표들 = 표들.slice(0, 표들.length)333
새표들.push(테이블 { 이름: 표이름.값, 열들: 정의.값, 행들: [] })334
Ok(실행결과 {335
출력: 완료알림("테이블 '" + 이스케이프(표이름.값) + "' 을 만들었습니다."),336
표들: 새표들,337
다음: 정의.다음 + 1,338
})339
}341
// 열 목록이 없으면 모든 열을 정의 순서대로 채웁니다. 목록에 같은 열이 두 번 나오면 뒤의 값이 남고 빠진 열은 기본값입니다.342
function 넣기(표들: Array<테이블>, 토큰들: Array<토큰>, 위치: int) -> Result<실행결과, string> {343
if !키워드인가(토큰들, 위치, "INTO") { return Err("INSERT 다음에는 INTO가 필요합니다") }344
let 표이름 = 이름읽기(토큰들, 위치 + 1, "테이블")?345
let 번호 = 표번호(표들, 표이름.값)?346
let 대상 = 표들[번호]347
let 목록: 읽음<Array<int>> = if 기호인가(토큰들, 표이름.다음, "(") {348
// 바로 다음 열 이름이 문법에 맞지 않아도 앞 열이 없으면 그 오류를 먼저 냅니다.349
let 읽음 = 쉼표목록읽기(토큰들, 표이름.다음 + 1, (토큰묶음, 자리, 앞번호들) => 열번호항목(토큰묶음, 자리, 대상.열들))?350
if !기호인가(토큰들, 읽음.다음, ")") { return Err("열 목록에 ) 가 필요합니다") }351
읽음{ 다음: 읽음.다음 + 1 }352
} else {353
{ 값: [for i in 0..<대상.열들.length => i], 다음: 표이름.다음 }354
}355
if !키워드인가(토큰들, 목록.다음, "VALUES") { return Err("VALUES가 필요합니다") }356
if !기호인가(토큰들, 목록.다음 + 1, "(") { return Err("VALUES 다음에 ( 가 필요합니다") }357
let 값목록 = 쉼표목록읽기(토큰들, 목록.다음 + 2, 값항목)?358
if !기호인가(토큰들, 값목록.다음, ")") { return Err("값 목록에 ) 가 필요합니다") }359
let 셀들 = 값목록.값360
let 열자리들 = 목록.값361
if 셀들.length != 열자리들.length { return Err("열 수와 값 수가 다릅니다") }362
let 틀린열 = [for k in 0..<셀들.length if !타입맞는가(대상.열들[열자리들[k]].타입, 셀들[k]) => 대상.열들[열자리들[k]].이름]363
if 틀린열.length > 0 { return Err("열 '" + 틀린열[0] + "' 의 타입과 값이 맞지 않습니다") }364
let mut 행 = 대상.열들.map(열 => 기본값(열.타입))365
for k in 0..<셀들.length { 행[열자리들[k]] = 셀들[k] }366
let mut 새행들 = 대상.행들.slice(0, 대상.행들.length)367
새행들.push(행)368
let mut 새표들 = 표들.slice(0, 표들.length)369
새표들[번호] = 테이블 { ...대상, 행들: 새행들 }370
Ok(실행결과 {371
출력: 완료알림("테이블 '" + 이스케이프(표이름.값) + "' 에 1행을 넣었습니다."),372
표들: 새표들,373
다음: 값목록.다음 + 1,374
})375
}377
function 조건읽기(토큰들: Array<토큰>, 위치: int, 대상: 테이블) -> Result<읽음<Option<행조건>>, string> {378
if !키워드인가(토큰들, 위치, "WHERE") { return Ok({ 값: None, 다음: 위치 }) }379
let 열이름 = 이름읽기(토큰들, 위치 + 1, "열")?380
let 열자리 = 열번호(대상.열들, 열이름.값, "WHERE에 없는 열입니다: ")?381
let 연산후보 = match 토큰보기(토큰들, 열이름.다음) {382
case 기호(글) => 비교연산파싱(글)383
case 이름(_) | 정수(_) | 문자열(_) | 입력끝 => None384
}385
let 연산 = match 연산후보 {386
case Some(연산) => 연산387
case None => return Err("WHERE에는 비교 연산자(= <> < <= > >=)가 필요합니다")388
}389
let 기준 = 값읽기(토큰들, 열이름.다음 + 1)?390
if !타입맞는가(대상.열들[열자리].타입, 기준.값) { return Err("WHERE 값의 타입이 열과 맞지 않습니다") }391
Ok({ 값: Some(행조건 { 열자리: 열자리, 연산: 연산, 기준: 기준.값 }), 다음: 기준.다음 })392
}394
function 정렬읽기(토큰들: Array<토큰>, 위치: int, 대상: 테이블) -> Result<읽음<Option<정렬기준>>, string> {395
if !키워드인가(토큰들, 위치, "ORDER") { return Ok({ 값: None, 다음: 위치 }) }396
if !키워드인가(토큰들, 위치 + 1, "BY") { return Err("ORDER 다음에는 BY가 필요합니다") }397
let 열이름 = 이름읽기(토큰들, 위치 + 2, "열")?398
let 열자리 = 열번호(대상.열들, 열이름.값, "ORDER BY에 없는 열입니다: ")?399
let 내림 = 키워드인가(토큰들, 열이름.다음, "DESC")400
let 방향있음 = 내림 || 키워드인가(토큰들, 열이름.다음, "ASC")401
Ok({402
값: Some(정렬기준 { 열자리: 열자리, 내림: 내림 }),403
다음: if 방향있음 { 열이름.다음 + 1 } else { 열이름.다음 },404
})405
}407
function 뒤집기(행들: Array<Array<값>>) -> Array<Array<값>> {408
[for i in 0..<행들.length => 행들[행들.length - 1 - i]]409
}411
// 정렬 열의 칸은 모두 열 타입과 같은 종류라 다른 종류의 기본 열쇠는 쓰이지 않습니다.412
function 정수열쇠(셀: 값) -> int {413
match 셀 {414
case 정수(수) => 수415
case 문자열(_) => 0416
}417
}419
function 문자열열쇠(셀: 값) -> string {420
match 셀 {421
case 정수(_) => ""422
case 문자열(글) => 글423
}424
}426
// 표준 정렬은 오름차순 안정 정렬입니다. 내림차순은 뒤집어 정렬한 뒤 다시 뒤집어 같은 값끼리 입력 순서를 지킵니다.427
function 행정렬(행들: Array<Array<값>>, 정렬: 정렬기준, 타입: 열타입) -> Array<Array<값>> {428
let 입력 = if 정렬.내림 { 뒤집기(행들) } else { 행들 }429
let 오름 = match 타입 {430
case "INT" => 입력.sortedBy(행 => 정수열쇠(행[정렬.열자리]))431
case "TEXT" => 입력.sortedBy(행 => 문자열열쇠(행[정렬.열자리]))432
}433
if 정렬.내림 { 뒤집기(오름) } else { 오름 }434
}436
function 값표시(셀: 값) -> string {437
match 셀 {438
case 정수(수) => "{수}"439
case 문자열(글) => 글440
}441
}443
function 표그리기(열이름들: Array<string>, 행들: Array<Array<값>>) -> string {444
let 머리 = 열이름들.map(이름 => "<th>" + 이스케이프(이름) + "</th>").join("")445
let 몸 = if 행들.length == 0 {446
"<tr><td class=\"empty\" colspan=\"{열이름들.length}\">행 없음</td></tr>"447
} else {448
행들.map(행 => "<tr>" + 행.map(셀 => "<td>" + 이스케이프(값표시(셀)) + "</td>").join("") + "</tr>").join("")449
}450
"<table class=\"grid\"><thead><tr>" + 머리 + "</tr></thead><tbody>" + 몸 + "</tbody></table>"451
}453
// 고른 열 이름은 WHERE 와 ORDER BY 를 읽은 뒤에 확인하므로 그 절의 오류가 먼저 나옵니다.454
function 고르기(표들: Array<테이블>, 토큰들: Array<토큰>, 위치: int) -> Result<실행결과, string> {455
let 전부인가 = 기호인가(토큰들, 위치, "*")456
let 선택: 읽음<Array<string>> = if 전부인가 { { 값: [], 다음: 위치 + 1 } } else { 쉼표목록읽기(토큰들, 위치, 열이름항목)? }457
if !키워드인가(토큰들, 선택.다음, "FROM") { return Err("FROM이 필요합니다") }458
let 표이름 = 이름읽기(토큰들, 선택.다음 + 1, "테이블")?459
let 대상 = 표들[표번호(표들, 표이름.값)?]460
let 조건절 = 조건읽기(토큰들, 표이름.다음, 대상)?461
let 정렬절 = 정렬읽기(토큰들, 조건절.다음, 대상)?462
let 뽑을열 = if 전부인가 {463
[for i in 0..<대상.열들.length => i]464
} else {465
let mut 번호들: Array<int> = []466
for 이름 in 선택.값 { 번호들.push(열번호(대상.열들, 이름, "없는 열입니다: ")?) }467
번호들468
}469
let 걸러진 = match 조건절.값 {470
case None => 대상.행들471
case Some(기준) => 대상.행들.filter(행 => 비교맞는가(기준.연산, 값비교(행[기준.열자리], 기준.기준)))472
}473
let 정렬됨 = match 정렬절.값 {474
case None => 걸러진475
case Some(기준) => 행정렬(걸러진, 기준, 대상.열들[기준.열자리].타입)476
}477
let 머리 = 뽑을열.map(i => 대상.열들[i].이름)478
let 결과행 = 정렬됨.map(행 => 뽑을열.map(i => 행[i]))479
Ok(실행결과 { 출력: 표그리기(머리, 결과행), 표들: 표들, 다음: 정렬절.다음 })480
}482
function 문장실행(표들: Array<테이블>, 토큰들: Array<토큰>, 위치: int) -> Result<실행결과, string> {483
let 첫낱말 = match 토큰보기(토큰들, 위치) {484
case 이름(낱말) => 낱말485
case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => return Err("문장은 CREATE / INSERT / SELECT 로 시작해야 합니다")486
}487
match 문장종류파싱(첫낱말)? {488
case "CREATE" => 만들기(표들, 토큰들, 위치 + 1)489
case "INSERT" => 넣기(표들, 토큰들, 위치 + 1)490
case "SELECT" => 고르기(표들, 토큰들, 위치 + 1)491
}492
}494
function JSON글자(글자: string) -> string {495
match 글자 {496
case "\"" => "\\\""497
case "\\" => "\\\\"498
case "\n" => "\\n"499
case "\t" => "\\t"500
case "\r" => "\\r"501
case _ => {502
let 코드 = 글자.codePointAt(0) ?? 0503
if 코드 < 32 { "\\u00" + 십육진숫자[코드 / 16] + 십육진숫자[코드 % 16] } else { 글자 }504
}505
}506
}508
function JSON문자열(원문: string) -> string {509
"\"" + 원문.scalars().map(JSON글자).join("") + "\""510
}512
function 셀JSON(셀: 값) -> string {513
match 셀 {514
case 정수(수) => "\{\"i\":{수}\}"515
case 문자열(글) => "\{\"s\":" + JSON문자열(글) + "\}"516
}517
}519
function 테이블JSON(표: 테이블) -> string {520
let 열들 = 표.열들.map(열 => "\{\"name\":" + JSON문자열(열.이름) + ",\"type\":" + JSON문자열(열.타입) + "\}").join(",")521
let 행들 = 표.행들.map(행 => "[" + 행.map(셀JSON).join(",") + "]").join(",")522
"\{\"name\":" + JSON문자열(표.이름) + ",\"cols\":[" + 열들 + "],\"rows\":[" + 행들 + "]\}"523
}525
// 복원은 이 형식을 그대로 읽으므로 키 이름과 순서를 바꾸지 않습니다.526
function 직렬화(표들: Array<테이블>) -> string {527
"\{\"tables\":[" + 표들.map(테이블JSON).join(",") + "]\}"528
}530
function 저장블록(표들: Array<테이블>) -> string {531
"<div class=\"meta\">저장 (JSON)</div><pre class=\"json\">" + 이스케이프(직렬화(표들)) + "</pre>"532
}534
function 문장들실행(토큰들: Array<토큰>) -> string {535
let mut 표들: Array<테이블> = []536
let mut 출력들: Array<string> = []537
let mut 문장번호 = 0538
let mut i = 0539
while i < 토큰들.length {540
if 기호인가(토큰들, i, ";") {541
i += 1542
} else {543
문장번호 += 1544
match 문장실행(표들, 토큰들, i) {545
case Err(메시지) => {546
출력들.push(오류알림("{문장번호}번째 문장 오류: " + 이스케이프(메시지)))547
break548
}549
case Ok(결과) => {550
출력들.push(결과.출력)551
표들 = 결과.표들552
match 남은토큰(토큰들, 결과.다음) {553
case None => { i = 결과.다음 }554
case Some(근처) => {555
출력들.push(오류알림("문장 끝에 ; 또는 입력 끝이 필요합니다(근처: '" + 이스케이프(근처) + "')"))556
break557
}558
}559
}560
}561
}562
}563
let 결과들 = if 출력들.length == 0 { "<div class=\"meta\">실행할 문장이 없습니다.</div>" } else { 출력들.join("") }564
결과들 + 저장블록(표들)565
}567
// 공백을 건너뛴 뒤 이 글자가 있어야 하며 그 다음 자리를 돌려줍니다.568
function 글자기대(글자들: Array<string>, 위치: int, 글자: string) -> Result<int, string> {569
let i = 연속끝(글자들, 위치, 공백인가)570
if 글자인가(글자들, i, 글자) { Ok(i + 1) } else { Err("'" + 글자 + "' 가 필요합니다") }571
}573
// 위치는 백슬래시를 가리킵니다. 풀 수 없는 코드는 빈 글로 버립니다.574
function JSON이스케이프풀기(글자들: Array<string>, 위치: int) -> Result<읽음<string>, string> {575
if 위치 + 1 >= 글자들.length { return Err("문자열 이스케이프가 끝났습니다") }576
let 풀린글 = match 글자들[위치 + 1] {577
case "\"" => "\""578
case "\\" => "\\"579
case "n" => "\n"580
case "t" => "\t"581
case "r" => "\r"582
case "u" => {583
if 위치 + 5 >= 글자들.length { return Err("유니코드 이스케이프가 끝났습니다") }584
match toIntRadix(글자들.slice(위치 + 2, 위치 + 6).join(""), 16) {585
case Some(코드) => return Ok({ 값: fromCodePoint(코드) ?? "", 다음: 위치 + 6 })586
case None => return Err("잘못된 유니코드 이스케이프입니다")587
}588
}589
case _ => return Err("알 수 없는 이스케이프입니다")590
}591
Ok({ 값: 풀린글, 다음: 위치 + 2 })592
}594
function JSON문자열읽기(글자들: Array<string>, 위치: int) -> Result<읽음<string>, string> {595
let 시작 = 연속끝(글자들, 위치, 공백인가)596
if !글자인가(글자들, 시작, "\"") { return Err("문자열이 필요합니다") }597
let mut 부분들: Array<string> = []598
let mut i = 시작 + 1599
while i < 글자들.length {600
let 글자 = 글자들[i]601
if 글자 == "\"" { return Ok({ 값: 부분들.join(""), 다음: i + 1 }) }602
if 글자 == "\\" {603
let 풀림 = JSON이스케이프풀기(글자들, i)?604
부분들.push(풀림.값)605
i = 풀림.다음606
} else {607
부분들.push(글자)608
i += 1609
}610
}611
Err("닫히지 않은 문자열입니다")612
}614
// 64비트를 넘는 수는 toInt 가 거부하므로 저장본의 정수도 SQL 로 넣은 정수와 같은 범위에 갇힙니다.615
function JSON정수읽기(글자들: Array<string>, 위치: int) -> Result<읽음<int>, string> {616
let 시작 = 연속끝(글자들, 위치, 공백인가)617
let 숫자시작 = if 글자인가(글자들, 시작, "-") { 시작 + 1 } else { 시작 }618
let 끝 = 연속끝(글자들, 숫자시작, 숫자인가)619
if 끝 == 숫자시작 { return Err("정수가 필요합니다") }620
match toInt(글자들.slice(시작, 끝).join("")) {621
case Some(수) => Ok({ 값: 수, 다음: 끝 })622
case None => Err("정수 범위를 벗어났습니다")623
}624
}626
// 앞 글자와 "키": 를 읽고 값이 시작하는 자리를 돌려줍니다.627
function 필드시작(글자들: Array<string>, 위치: int, 앞글자: string, 키: string) -> Result<int, string> {628
let 읽음 = JSON문자열읽기(글자들, 글자기대(글자들, 위치, 앞글자)?)?629
if 읽음.값 != 키 { return Err("'" + 키 + "' 키가 필요합니다") }630
글자기대(글자들, 읽음.다음, ":")631
}633
// 비어 있지 않은 목록에서 원소 뒤의 , 나 ] 를 읽습니다.634
function 목록구분읽기(글자들: Array<string>, 위치: int, 목록: string) -> Result<목록구분, string> {635
let i = 연속끝(글자들, 위치, 공백인가)636
if 글자인가(글자들, i, ",") {637
Ok(목록구분 { 닫힘: false, 다음: i + 1 })638
} else if 글자인가(글자들, i, "]") {639
Ok(목록구분 { 닫힘: true, 다음: i + 1 })640
} else {641
Err(목록 + "에 , 또는 ] 가 필요합니다")642
}643
}645
// 키는 : 까지 읽은 뒤에 확인합니다. 칸 종류는 열 타입과 맞춰 보지 않습니다.646
function JSON셀읽기(글자들: Array<string>, 위치: int) -> Result<읽음<값>, string> {647
let 키 = JSON문자열읽기(글자들, 글자기대(글자들, 위치, "\{")?)?648
let 값자리 = 글자기대(글자들, 키.다음, ":")?649
match 셀키파싱(키.값)? {650
case "i" => {651
let 읽음 = JSON정수읽기(글자들, 값자리)?652
Ok({ 값: 값.정수(읽음.값), 다음: 글자기대(글자들, 읽음.다음, "\}")? })653
}654
case "s" => {655
let 읽음 = JSON문자열읽기(글자들, 값자리)?656
Ok({ 값: 값.문자열(읽음.값), 다음: 글자기대(글자들, 읽음.다음, "\}")? })657
}658
}659
}661
// [ 와 ] 사이의 항목을 , 로 나눠 읽습니다. 목록은 오류 문구에 쓰는 이름이고, 항목읽기는 앞서 읽은 항목들을 받아 겹침 같은 검사를 그 자리에서 합니다.662
function JSON목록읽기<T>(글자들: Array<string>, 위치: int, 목록: string, 항목읽기: (Array<string>, int, Array<T>) -> Result<읽음<T>, string>) -> Result<읽음<Array<T>>, string> {663
let 첫자리 = 연속끝(글자들, 글자기대(글자들, 위치, "[")?, 공백인가)664
let mut 항목들: Array<T> = []665
if 글자인가(글자들, 첫자리, "]") { return Ok({ 값: 항목들, 다음: 첫자리 + 1 }) }666
let mut i = 첫자리667
while true {668
let 읽은항목 = 항목읽기(글자들, i, 항목들)?669
항목들.push(읽은항목.값)670
let 구분 = 목록구분읽기(글자들, 읽은항목.다음, 목록)?671
if 구분.닫힘 { return Ok({ 값: 항목들, 다음: 구분.다음 }) }672
i = 구분.다음673
}674
}676
function JSON셀항목(글자들: Array<string>, 위치: int, 앞셀들: Array<값>) -> Result<읽음<값>, string> {677
JSON셀읽기(글자들, 위치)678
}680
function JSON행읽기(글자들: Array<string>, 위치: int) -> Result<읽음<Array<값>>, string> {681
JSON목록읽기(글자들, 위치, "행", JSON셀항목)682
}684
function JSON행항목(글자들: Array<string>, 위치: int, 열수: int) -> Result<읽음<Array<값>>, string> {685
let 읽은행 = JSON행읽기(글자들, 위치)?686
if 읽은행.값.length != 열수 { return Err("행의 칸 수가 열 수와 다릅니다") }687
Ok(읽은행)688
}690
function JSON열읽기(글자들: Array<string>, 위치: int) -> Result<읽음<열정의>, string> {691
let 이름 = JSON문자열읽기(글자들, 필드시작(글자들, 위치, "\{", "name")?)?692
let 타입글 = JSON문자열읽기(글자들, 필드시작(글자들, 이름.다음, ",", "type")?)?693
let 타입 = match 열타입파싱(타입글.값) {694
case Some(타입) => 타입695
case None => return Err("열 타입은 INT 또는 TEXT여야 합니다")696
}697
Ok({ 값: 열정의 { 이름: 이름.값, 타입: 타입 }, 다음: 글자기대(글자들, 타입글.다음, "\}")? })698
}700
function JSON열항목(글자들: Array<string>, 위치: int, 앞열들: Array<열정의>) -> Result<읽음<열정의>, string> {701
JSON열읽기(글자들, 위치)702
}704
function JSON테이블읽기(글자들: Array<string>, 위치: int) -> Result<읽음<테이블>, string> {705
let 이름 = JSON문자열읽기(글자들, 필드시작(글자들, 위치, "\{", "name")?)?706
let 열목록 = JSON목록읽기(글자들, 필드시작(글자들, 이름.다음, ",", "cols")?, "열 목록", JSON열항목)?707
let 행목록 = JSON목록읽기(글자들, 필드시작(글자들, 열목록.다음, ",", "rows")?, "행 목록", (글자묶음, 자리, 앞행들) => JSON행항목(글자묶음, 자리, 열목록.값.length))?708
Ok({709
값: 테이블 { 이름: 이름.값, 열들: 열목록.값, 행들: 행목록.값 },710
다음: 글자기대(글자들, 행목록.다음, "\}")?,711
})712
}714
function JSON테이블항목(글자들: Array<string>, 위치: int, 앞표들: Array<테이블>) -> Result<읽음<테이블>, string> {715
let 읽은표 = JSON테이블읽기(글자들, 위치)?716
if 읽은표.값.이름 in 앞표들.map(표 => 표.이름) { return Err("테이블 이름이 겹칩니다: " + 읽은표.값.이름) }717
Ok(읽은표)718
}720
// 저장본은 이 도구가 쓴 형식만 받습니다. 키 순서가 고정이고 테이블 이름이 겹치거나 뒤에 다른 내용이 있으면 거부합니다.721
function 복원(소스: string) -> Result<Array<테이블>, string> {722
let 글자들 = 소스.scalars()723
let 목록 = JSON목록읽기(글자들, 필드시작(글자들, 0, "\{", "tables")?, "테이블 목록", JSON테이블항목)?724
let 끝 = 연속끝(글자들, 글자기대(글자들, 목록.다음, "\}")?, 공백인가)725
if 끝 != 글자들.length { return Err("JSON 뒤에 불필요한 내용이 있습니다") }726
Ok(목록.값)727
}729
function 표들그리기(표들: Array<테이블>) -> string {730
if 표들.length == 0 { return "<div class=\"meta\">빈 데이터베이스입니다.</div>" }731
표들.map(표 => 완료알림("테이블 '" + 이스케이프(표.이름) + "'") + 표그리기(표.열들.map(열 => 열.이름), 표.행들)).join("")732
}734
function JSON입력인가(소스: string) -> bool {735
소스.trim().startsWith("\{")736
}738
function 실행(소스: string) -> string {739
if JSON입력인가(소스) {740
match 복원(소스) {741
case Err(메시지) => 오류알림("JSON 복원 오류: " + 이스케이프(메시지))742
case Ok(표들) => 표들그리기(표들) + 저장블록(표들)743
}744
} else {745
match 토큰내기(소스) {746
case Err(메시지) => 오류알림("SQL 오류: " + 이스케이프(메시지))747
case Ok(토큰들) => 문장들실행(토큰들)748
}749
}750
}752
input() |> 실행 |> printSHA256 e575a9b4b7c624ea222067fa924567a94c7c8059910f670265368da8546b4c45