slow.monster

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.
7enum 값 { 정수(int), 문자열(string) }
8type 열타입 = "INT" | "TEXT"
9type 문장종류 = "CREATE" | "INSERT" | "SELECT"
10type 비교연산 = "=" | "<>" | "<" | "<=" | ">" | ">="
11type 셀키 = "i" | "s"
12record 열정의 { 이름: string, 타입: 열타입 }
13record 테이블 { 이름: string, 열들: Array<열정의>, 행들: Array<Array<값>> }
15// 이름, 정수, 문자열, 기호는 원문 조각을 싣고 입력끝은 토큰이 다 떨어진 자리를 나타냅니다.
16enum 토큰 { 이름(string), 정수(string), 문자열(string), 기호(string), 입력끝 }
18record 행조건 { 열자리: int, 연산: 비교연산, 기준: 값 }
19record 정렬기준 { 열자리: int, 내림: bool }
21type 읽음<T> = { 값: T, 다음: int }
22record 실행결과 { 출력: string, 표들: Array<테이블>, 다음: int }
24record 목록구분 { 닫힘: bool, 다음: int }
26let 예약어들 = Set.of(
27 "CREATE", "TABLE", "INSERT", "INTO", "VALUES", "SELECT", "FROM",
28 "WHERE", "ORDER", "BY", "ASC", "DESC", "INT", "TEXT",
30let 한글자기호 = Set.of("=", "(", ")", ",", ";", "*", "-", "<", ">")
31let 두글자기호 = Set.of("<=", "<>", ">=")
32let 십육진숫자 = "0123456789abcdef".scalars()
34// 공용 도우미 시작 (이스케이프 공백인가 숫자인가 오류알림 연속끝 글자인가 대문자로)
35// 여러 도구가 같은 원본을 씁니다. 여기서 고치지 않고 원본에서 고친 뒤 다시 생성합니다.
37// 사용자 입력을 HTML 에 넣기 전에 반드시 거칩니다.
38function 이스케이프(원문: string) -> string {
39 원문.replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
42function 공백인가(글자: string) -> bool {
43 글자 == " " || 글자 == "\t" || 글자 == "\n" || 글자 == "\r"
46function 숫자인가(글자: string) -> bool {
47 let 코드 = 글자.codePointAt(0) ?? 0
48 코드 >= 48 && 코드 <= 57
51// 문구는 고정 문장이거나 이미 이스케이프한 값이어야 합니다.
52function 오류알림(문구: string) -> string {
53 "<div class=\"err\">{문구}</div>"
56// 시작부터 맞는가가 참인 글자가 이어지는 끝 위치를 돌려줍니다. 맞는 글자가 없으면 시작 그대로입니다.
57function 연속끝(글자들: Array<string>, 시작: int, 맞는가: (string) -> bool) -> int {
58 let mut 끝 = 시작
59 while 끝 < 글자들.length && 맞는가(글자들[끝]) { 끝 += 1 }
60 끝
63// 위치가 글자들 끝을 넘으면 거짓입니다.
64function 글자인가(글자들: Array<string>, 위치: int, 글자: string) -> bool {
65 위치 < 글자들.length && 글자들[위치] == 글자
68// ASCII 소문자만 바꿉니다. 다른 문자의 대문자 규칙을 섞지 않습니다.
69function 대문자로(글: string) -> string {
70 글.scalars().map(글자 => {
71 let 코드 = 글자.codePointAt(0) ?? 0
72 if 코드 >= 97 && 코드 <= 122 { fromCodePoint(코드 - 32) ?? 글자 } else { 글자 }
73 }).join("")
76// 공용 도우미 끝
78function 완료알림(문구: string) -> string {
79 "<div class=\"ok\">{문구}</div>"
82function 낱말글자인가(글자: string) -> bool {
83 let 코드 = 글자.codePointAt(0) ?? 0
84 (코드 >= 48 && 코드 <= 57) || (코드 >= 65 && 코드 <= 90) || (코드 >= 97 && 코드 <= 122) || 글자 == "_"
87function 예약어인가(낱말: string) -> bool {
88 대문자로(낱말) in 예약어들
91function 문장종류파싱(낱말: string) -> Result<문장종류, string> {
92 match 대문자로(낱말) {
93 case "CREATE" => Ok("CREATE")
94 case "INSERT" => Ok("INSERT")
95 case "SELECT" => Ok("SELECT")
96 case _ => Err("알 수 없는 문장입니다: " + 낱말)
97 }
100function 열타입파싱(낱말: string) -> Option<열타입> {
101 match 대문자로(낱말) {
102 case "INT" => Some("INT")
103 case "TEXT" => Some("TEXT")
104 case _ => None
105 }
108function 비교연산파싱(기호값: string) -> Option<비교연산> {
109 match 기호값 {
110 case "=" => Some("=")
111 case "<>" => Some("<>")
112 case "<" => Some("<")
113 case "<=" => Some("<=")
114 case ">" => Some(">")
115 case ">=" => Some(">=")
116 case _ => None
117 }
120function 셀키파싱(키: string) -> Result<셀키, string> {
121 match 키 {
122 case "i" => Ok("i")
123 case "s" => Ok("s")
124 case _ => Err("셀 키는 i 또는 s 여야 합니다")
125 }
128function 타입맞는가(타입: 열타입, 셀: 값) -> bool {
129 match 셀 {
130 case 정수(_) => 타입 == "INT"
131 case 문자열(_) => 타입 == "TEXT"
132 }
135function 기본값(타입: 열타입) -> 값 {
136 match 타입 {
137 case "INT" => 값.정수(0)
138 case "TEXT" => 값.문자열("")
139 }
142// 문자열은 글자 코드 순서로 비교합니다. 한 열의 칸은 모두 열 타입과 같은 종류라 종류가 다른 비교는 일어나지 않습니다.
143function 값비교(왼: 값, 오른: 값) -> int {
144 match 왼 {
145 case 정수(왼수) => match 오른 {
146 case 정수(오른수) => if 왼수 < 오른수 { -1 } else if 왼수 > 오른수 { 1 } else { 0 }
147 case 문자열(_) => 0
148 }
149 case 문자열(왼글) => match 오른 {
150 case 문자열(오른글) => if 왼글 < 오른글 { -1 } else if 왼글 > 오른글 { 1 } else { 0 }
151 case 정수(_) => 0
152 }
153 }
156function 비교맞는가(연산: 비교연산, 차이: int) -> bool {
157 match 연산 {
158 case "=" => 차이 == 0
159 case "<>" => 차이 != 0
160 case "<" => 차이 < 0
161 case "<=" => 차이 <= 0
162 case ">" => 차이 > 0
163 case ">=" => 차이 >= 0
164 }
167// 위치는 여는 따옴표를 가리킵니다. 따옴표 두 개는 따옴표 하나입니다.
168function 문자열토큰읽기(글자들: Array<string>, 위치: int) -> Result<읽음<토큰>, string> {
169 let mut 부분들: Array<string> = []
170 let mut i = 위치 + 1
171 while i < 글자들.length {
172 if 글자들[i] != "'" {
173 부분들.push(글자들[i])
174 i += 1
175 } else if 글자인가(글자들, i + 1, "'") {
176 부분들.push("'")
177 i += 2
178 } else {
179 return Ok({ 값: 토큰.문자열(부분들.join("")), 다음: i + 1 })
180 }
181 }
182 Err("닫히지 않은 문자열입니다")
185// 숫자로 시작하면 숫자만 읽으므로 1abc 는 정수 1 과 이름 abc 두 토큰이 됩니다.
186function 토큰읽기(글자들: 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 }
206function 토큰내기(소스: 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(토큰들)
218function 토큰보기(토큰들: Array<토큰>, 위치: int) -> 토큰 {
219 if 위치 < 토큰들.length { 토큰들[위치] } else { 토큰.입력끝 }
222function 기호인가(토큰들: Array<토큰>, 위치: int, 기호값: string) -> bool {
223 match 토큰보기(토큰들, 위치) {
224 case 기호(글) => 글 == 기호값
225 case 이름(_) | 정수(_) | 문자열(_) | 입력끝 => false
226 }
229// 키워드는 대소문자를 가리지 않습니다.
230function 키워드인가(토큰들: Array<토큰>, 위치: int, 키워드: string) -> bool {
231 match 토큰보기(토큰들, 위치) {
232 case 이름(낱말) => 대문자로(낱말) == 키워드
233 case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => false
234 }
237// 문장 뒤에는 ; 나 입력 끝만 옵니다. 다른 토큰이면 오류에 보여 줄 원문을 돌려줍니다.
238function 남은토큰(토큰들: Array<토큰>, 위치: int) -> Option<string> {
239 match 토큰보기(토큰들, 위치) {
240 case 입력끝 => None
241 case 기호(글) => if 글 == ";" { None } else { Some(글) }
242 case 이름(글) | 정수(글) | 문자열(글) => Some(글)
243 }
246function 이름읽기(토큰들: Array<토큰>, 위치: int, 무엇: string) -> Result<읽음<string>, string> {
247 let 낱말 = match 토큰보기(토큰들, 위치) {
248 case 이름(낱말) => 낱말
249 case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => return Err(무엇 + " 이름이 필요합니다")
250 }
251 if 예약어인가(낱말) { return Err("'" + 낱말 + "'는 예약어라 " + 무엇 + " 이름으로 쓸 수 없습니다") }
252 Ok({ 값: 낱말, 다음: 위치 + 1 })
255// 정수 토큰은 숫자만 담으므로 toInt 가 실패하는 경우는 64비트를 넘는 수뿐이고, 부호를 뒤집어도 넘치지 않습니다.
256function 값읽기(토큰들: 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 }
273// 항목을 읽고 , 가 이어지는 동안 다음 항목을 읽습니다. 항목읽기는 앞서 읽은 항목들을 받아 겹침 같은 검사를 그 자리에서 합니다.
274function 쉼표목록읽기<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 = 읽은항목.다음 + 1
282 }
285function 열이름항목(토큰들: Array<토큰>, 위치: int, 앞이름들: Array<string>) -> Result<읽음<string>, string> {
286 이름읽기(토큰들, 위치, "열")
289function 값항목(토큰들: Array<토큰>, 위치: int, 앞값들: Array<값>) -> Result<읽음<값>, string> {
290 값읽기(토큰들, 위치)
293// 표와 열 이름은 겹치지 않게 만들어지므로 처음 맞는 자리가 유일한 자리입니다.
294function 열번호항목(토큰들: Array<토큰>, 위치: int, 열들: Array<열정의>) -> Result<읽음<int>, string> {
295 let 읽은이름 = 이름읽기(토큰들, 위치, "열")?
296 Ok({ 값: 열번호(열들, 읽은이름.값, "없는 열입니다: ")?, 다음: 읽은이름.다음 })
299function 표번호(표들: Array<테이블>, 이름: string) -> Result<int, string> {
300 let 번호들 = [for i in 0..<표들.length if 표들[i].이름 == 이름 => i]
301 if 번호들.length > 0 { Ok(번호들[0]) } else { Err("없는 테이블입니다: " + 이름) }
304function 열번호(열들: Array<열정의>, 이름: string, 없을때: string) -> Result<int, string> {
305 let 번호들 = [for i in 0..<열들.length if 열들[i].이름 == 이름 => i]
306 if 번호들.length > 0 { Ok(번호들[0]) } else { Err(없을때 + 이름) }
309function 열정의항목(토큰들: Array<토큰>, 위치: int, 앞열들: Array<열정의>) -> Result<읽음<열정의>, string> {
310 let 열이름 = 이름읽기(토큰들, 위치, "열")?
311 if 열이름.값 in 앞열들.map(열 => 열.이름) { return Err("열 이름이 겹칩니다: " + 열이름.값) }
312 let 타입뒤 = 열이름.다음 + 1
313 let 타입후보 = match 토큰보기(토큰들, 열이름.다음) {
314 case 이름(낱말) => 열타입파싱(낱말)
315 case 정수(_) | 문자열(_) | 기호(_) | 입력끝 => None
316 }
317 let 타입 = match 타입후보 {
318 case Some(타입) => 타입
319 case None => return Err("열 타입은 INT 또는 TEXT여야 합니다: " + 열이름.값)
320 }
321 Ok({ 값: 열정의 { 이름: 열이름.값, 타입: 타입 }, 다음: 타입뒤 })
324// 배열은 참조로 공유되므로 복사본에 더해 이전 상태를 그대로 둡니다.
325function 만들기(표들: 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 })
341// 열 목록이 없으면 모든 열을 정의 순서대로 채웁니다. 목록에 같은 열이 두 번 나오면 뒤의 값이 남고 빠진 열은 기본값입니다.
342function 넣기(표들: 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 })
377function 조건읽기(토큰들: Array<토큰>, 위치: int, 대상: 테이블) -> Result<읽음<Option<행조건>>, string> {
378 if !키워드인가(토큰들, 위치, "WHERE") { return Ok({ 값: None, 다음: 위치 }) }
379 let 열이름 = 이름읽기(토큰들, 위치 + 1, "열")?
380 let 열자리 = 열번호(대상.열들, 열이름.값, "WHERE에 없는 열입니다: ")?
381 let 연산후보 = match 토큰보기(토큰들, 열이름.다음) {
382 case 기호(글) => 비교연산파싱(글)
383 case 이름(_) | 정수(_) | 문자열(_) | 입력끝 => None
384 }
385 let 연산 = match 연산후보 {
386 case Some(연산) => 연산
387 case None => return Err("WHERE에는 비교 연산자(= <> < <= > >=)가 필요합니다")
388 }
389 let 기준 = 값읽기(토큰들, 열이름.다음 + 1)?
390 if !타입맞는가(대상.열들[열자리].타입, 기준.값) { return Err("WHERE 값의 타입이 열과 맞지 않습니다") }
391 Ok({ 값: Some(행조건 { 열자리: 열자리, 연산: 연산, 기준: 기준.값 }), 다음: 기준.다음 })
394function 정렬읽기(토큰들: 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 })
407function 뒤집기(행들: Array<Array<값>>) -> Array<Array<값>> {
408 [for i in 0..<행들.length => 행들[행들.length - 1 - i]]
411// 정렬 열의 칸은 모두 열 타입과 같은 종류라 다른 종류의 기본 열쇠는 쓰이지 않습니다.
412function 정수열쇠(셀: 값) -> int {
413 match 셀 {
414 case 정수(수) => 수
415 case 문자열(_) => 0
416 }
419function 문자열열쇠(셀: 값) -> string {
420 match 셀 {
421 case 정수(_) => ""
422 case 문자열(글) => 글
423 }
426// 표준 정렬은 오름차순 안정 정렬입니다. 내림차순은 뒤집어 정렬한 뒤 다시 뒤집어 같은 값끼리 입력 순서를 지킵니다.
427function 행정렬(행들: Array<Array<값>>, 정렬: 정렬기준, 타입: 열타입) -> Array<Array<값>> {
428 let 입력 = if 정렬.내림 { 뒤집기(행들) } else { 행들 }
429 let 오름 = match 타입 {
430 case "INT" => 입력.sortedBy(행 => 정수열쇠(행[정렬.열자리]))
431 case "TEXT" => 입력.sortedBy(행 => 문자열열쇠(행[정렬.열자리]))
432 }
433 if 정렬.내림 { 뒤집기(오름) } else { 오름 }
436function 값표시(셀: 값) -> string {
437 match 셀 {
438 case 정수(수) => "{수}"
439 case 문자열(글) => 글
440 }
443function 표그리기(열이름들: 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>"
453// 고른 열 이름은 WHERE 와 ORDER BY 를 읽은 뒤에 확인하므로 그 절의 오류가 먼저 나옵니다.
454function 고르기(표들: 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(실행결과 { 출력: 표그리기(머리, 결과행), 표들: 표들, 다음: 정렬절.다음 })
482function 문장실행(표들: 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 }
494function 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) ?? 0
503 if 코드 < 32 { "\\u00" + 십육진숫자[코드 / 16] + 십육진숫자[코드 % 16] } else { 글자 }
504 }
505 }
508function JSON문자열(원문: string) -> string {
509 "\"" + 원문.scalars().map(JSON글자).join("") + "\""
512function 셀JSON(셀: 값) -> string {
513 match 셀 {
514 case 정수(수) => "\{\"i\":{수}\}"
515 case 문자열(글) => "\{\"s\":" + JSON문자열(글) + "\}"
516 }
519function 테이블JSON(표: 테이블) -> string {
520 let 열들 = 표.열들.map(열 => "\{\"name\":" + JSON문자열(열.이름) + ",\"type\":" + JSON문자열(열.타입) + "\}").join(",")
521 let 행들 = 표.행들.map(행 => "[" + 행.map(셀JSON).join(",") + "]").join(",")
522 "\{\"name\":" + JSON문자열(표.이름) + ",\"cols\":[" + 열들 + "],\"rows\":[" + 행들 + "]\}"
525// 복원은 이 형식을 그대로 읽으므로 키 이름과 순서를 바꾸지 않습니다.
526function 직렬화(표들: Array<테이블>) -> string {
527 "\{\"tables\":[" + 표들.map(테이블JSON).join(",") + "]\}"
530function 저장블록(표들: Array<테이블>) -> string {
531 "<div class=\"meta\">저장 (JSON)</div><pre class=\"json\">" + 이스케이프(직렬화(표들)) + "</pre>"
534function 문장들실행(토큰들: Array<토큰>) -> string {
535 let mut 표들: Array<테이블> = []
536 let mut 출력들: Array<string> = []
537 let mut 문장번호 = 0
538 let mut i = 0
539 while i < 토큰들.length {
540 if 기호인가(토큰들, i, ";") {
541 i += 1
542 } else {
543 문장번호 += 1
544 match 문장실행(표들, 토큰들, i) {
545 case Err(메시지) => {
546 출력들.push(오류알림("{문장번호}번째 문장 오류: " + 이스케이프(메시지)))
547 break
548 }
549 case Ok(결과) => {
550 출력들.push(결과.출력)
551 표들 = 결과.표들
552 match 남은토큰(토큰들, 결과.다음) {
553 case None => { i = 결과.다음 }
554 case Some(근처) => {
555 출력들.push(오류알림("문장 끝에 ; 또는 입력 끝이 필요합니다(근처: '" + 이스케이프(근처) + "')"))
556 break
557 }
558 }
559 }
560 }
561 }
562 }
563 let 결과들 = if 출력들.length == 0 { "<div class=\"meta\">실행할 문장이 없습니다.</div>" } else { 출력들.join("") }
564 결과들 + 저장블록(표들)
567// 공백을 건너뛴 뒤 이 글자가 있어야 하며 그 다음 자리를 돌려줍니다.
568function 글자기대(글자들: Array<string>, 위치: int, 글자: string) -> Result<int, string> {
569 let i = 연속끝(글자들, 위치, 공백인가)
570 if 글자인가(글자들, i, 글자) { Ok(i + 1) } else { Err("'" + 글자 + "' 가 필요합니다") }
573// 위치는 백슬래시를 가리킵니다. 풀 수 없는 코드는 빈 글로 버립니다.
574function 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 })
594function JSON문자열읽기(글자들: Array<string>, 위치: int) -> Result<읽음<string>, string> {
595 let 시작 = 연속끝(글자들, 위치, 공백인가)
596 if !글자인가(글자들, 시작, "\"") { return Err("문자열이 필요합니다") }
597 let mut 부분들: Array<string> = []
598 let mut i = 시작 + 1
599 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 += 1
609 }
610 }
611 Err("닫히지 않은 문자열입니다")
614// 64비트를 넘는 수는 toInt 가 거부하므로 저장본의 정수도 SQL 로 넣은 정수와 같은 범위에 갇힙니다.
615function 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 }
626// 앞 글자와 "키": 를 읽고 값이 시작하는 자리를 돌려줍니다.
627function 필드시작(글자들: Array<string>, 위치: int, 앞글자: string, 키: string) -> Result<int, string> {
628 let 읽음 = JSON문자열읽기(글자들, 글자기대(글자들, 위치, 앞글자)?)?
629 if 읽음.값 != 키 { return Err("'" + 키 + "' 키가 필요합니다") }
630 글자기대(글자들, 읽음.다음, ":")
633// 비어 있지 않은 목록에서 원소 뒤의 , 나 ] 를 읽습니다.
634function 목록구분읽기(글자들: 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 }
645// 키는 : 까지 읽은 뒤에 확인합니다. 칸 종류는 열 타입과 맞춰 보지 않습니다.
646function 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 }
661// [ 와 ] 사이의 항목을 , 로 나눠 읽습니다. 목록은 오류 문구에 쓰는 이름이고, 항목읽기는 앞서 읽은 항목들을 받아 겹침 같은 검사를 그 자리에서 합니다.
662function 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 }
676function JSON셀항목(글자들: Array<string>, 위치: int, 앞셀들: Array<값>) -> Result<읽음<값>, string> {
677 JSON셀읽기(글자들, 위치)
680function JSON행읽기(글자들: Array<string>, 위치: int) -> Result<읽음<Array<값>>, string> {
681 JSON목록읽기(글자들, 위치, "행", JSON셀항목)
684function JSON행항목(글자들: Array<string>, 위치: int, 열수: int) -> Result<읽음<Array<값>>, string> {
685 let 읽은행 = JSON행읽기(글자들, 위치)?
686 if 읽은행.값.length != 열수 { return Err("행의 칸 수가 열 수와 다릅니다") }
687 Ok(읽은행)
690function 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({ 값: 열정의 { 이름: 이름.값, 타입: 타입 }, 다음: 글자기대(글자들, 타입글.다음, "\}")? })
700function JSON열항목(글자들: Array<string>, 위치: int, 앞열들: Array<열정의>) -> Result<읽음<열정의>, string> {
701 JSON열읽기(글자들, 위치)
704function 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 })
714function JSON테이블항목(글자들: Array<string>, 위치: int, 앞표들: Array<테이블>) -> Result<읽음<테이블>, string> {
715 let 읽은표 = JSON테이블읽기(글자들, 위치)?
716 if 읽은표.값.이름 in 앞표들.map(표 => 표.이름) { return Err("테이블 이름이 겹칩니다: " + 읽은표.값.이름) }
717 Ok(읽은표)
720// 저장본은 이 도구가 쓴 형식만 받습니다. 키 순서가 고정이고 테이블 이름이 겹치거나 뒤에 다른 내용이 있으면 거부합니다.
721function 복원(소스: string) -> Result<Array<테이블>, string> {
722 let 글자들 = 소스.scalars()
723 let 목록 = JSON목록읽기(글자들, 필드시작(글자들, 0, "\{", "tables")?, "테이블 목록", JSON테이블항목)?
724 let 끝 = 연속끝(글자들, 글자기대(글자들, 목록.다음, "\}")?, 공백인가)
725 if 끝 != 글자들.length { return Err("JSON 뒤에 불필요한 내용이 있습니다") }
726 Ok(목록.값)
729function 표들그리기(표들: Array<테이블>) -> string {
730 if 표들.length == 0 { return "<div class=\"meta\">빈 데이터베이스입니다.</div>" }
731 표들.map(표 => 완료알림("테이블 '" + 이스케이프(표.이름) + "'") + 표그리기(표.열들.map(열 => 열.이름), 표.행들)).join("")
734function JSON입력인가(소스: string) -> bool {
735 소스.trim().startsWith("\{")
738function 실행(소스: 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 }
752input() |> 실행 |> print

SHA256 e575a9b4b7c624ea222067fa924567a94c7c8059910f670265368da8546b4c45