sqldb.tpz
// 작은 인메모리 SQL 데이터베이스. SQL을 토큰으로 나눠 구조로 파싱하고(문자열 이어붙이기 없음)
// 인메모리 테이블에 실행한 뒤, 결과를 표로 그리고 데이터베이스를 JSON으로 저장/복원한다. 토파즈로 작성했다.
// A tiny in-memory SQL database. Tokenizes SQL and parses it into a structure (no string building),
// runs it on in-memory tables, renders results as a table, and saves/restores the database as JSON.
//
// Key identifiers: 값=cell value, 열=column, 표=table, 디비=database, 토큰=token, 토큰내기=tokenize,
// 대문자=uppercase, 키워드인가=isKeyword, 이스케이프=HTML escape, 자리=cursor, 현재=peek token,
// 먹기=advance, 기호먹기=consume symbol, 이름기대=expect identifier, 값파싱=parse a literal,
// 문장하나=parse+run one statement, 만들기=CREATE, 넣기=INSERT, 고르기=SELECT, 표찾기=find table,
// 열찾기=find column index, 값비교=compare cells, 조건맞나=row matches WHERE, 정렬=stable sort,
// 표그리기=render result table, 직렬화=serialize to JSON, 복원=restore from JSON, 제이슨글=JSON string escape.
//
// 결정적이다. 같은 SQL이면 인터프리터와 네이티브/wasm 빌드에서 같은 표와 같은 JSON이 나온다.
// Deterministic: same SQL yields the same table and JSON across interpreter and native/wasm builds.
// 안 하는 것: JOIN, 트랜잭션, 서브쿼리, 집계 함수, GROUP BY, UPDATE/DELETE, AND/OR 다중 조건, ALTER.
// Won't do: JOIN, transactions, subqueries, aggregate functions, GROUP BY, UPDATE/DELETE, AND/OR, ALTER.
// 사용자가 직접 SQL을 쓰지만 엔진은 토큰/구조로 파싱하며 값은 리터럴에서 바인딩한다(문자열 재조립 없음).
// The user authors the SQL, but the engine parses to tokens/structure and binds values from literals.
// 셀 값. 정수 칸이거나 글(텍스트) 칸이다.
// A cell value. Either an INT cell or a TEXT (string) cell.
enum 값 { 정수(int), 글자(string) }
// 닫힌 SQL 열 타입. 사용자 입력 문자열은 파싱 경계에서만 이 타입으로 좁힌다.
// Closed SQL column types. Raw user strings are narrowed only at parser boundaries.
type 열타입 = "INT" | "TEXT"
type 문장종류 = "CREATE" | "INSERT" | "SELECT"
type 비교연산 = "=" | "<>" | "<" | "<=" | ">" | ">="
type 셀키 = "i" | "s"
record 열 { 이름: string, 타입: 열타입 }
record 표 { 이름: string, 열들: Array<열>, 행들: Array<Array<값>> }
// 토큰. 이름/정수/글/기호는 원문 조각을 싣고, 끝은 페이로드 없는 입력 종료 표시다.
// A token. 이름/정수/글/기호 carry the source slice, and 끝 is the payload-less end-of-input marker.
enum 토큰 { 이름(string), 정수(string), 글(string), 기호(string), 끝 }
// HTML 이스케이프. SQL·셀 값·테이블 이름은 모두 사용자 입력이라 출력 전에 반드시 이스케이프한다.
// HTML escape. SQL, cell values, and table names are user input, so always escape before output.
function 이스케이프(원문: string) -> string {
원문.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """)
}
// 글자가 영문/숫자/밑줄인가(식별자에 쓸 수 있는 글자).
// Is the char a letter/digit/underscore (usable in an identifier)?
function 낱말글(글: string) -> bool {
let 코드 = 글.codePointAt(0) ?? 0
(코드 >= 48 && 코드 <= 57) || (코드 >= 65 && 코드 <= 90) || (코드 >= 97 && 코드 <= 122) || 글 == "_"
}
function 숫자글(글: string) -> bool {
let 코드 = 글.codePointAt(0) ?? 0
코드 >= 48 && 코드 <= 57
}
// 한 글자를 대문자로(영문 소문자만). 키워드 비교에 쓴다.
// Uppercase one char (ASCII lowercase only). Used for case-insensitive keyword compare.
function 대문자(낱말: string) -> string {
낱말.scalars().map(글 => {
let 코드 = 글.codePointAt(0) ?? 0
if 코드 >= 97 && 코드 <= 122 {
fromCodePoint(코드 - 32) ?? 글
} else {
글
}
}).join("")
}
// 예약어 집합. 식별자로 쓸 수 없는 SQL 키워드들.
// The reserved-word set: SQL keywords that cannot be used as identifiers.
let 키워드집합 = Set.of("CREATE", "TABLE", "INSERT", "INTO", "VALUES", "SELECT", "FROM", "WHERE", "ORDER", "BY", "ASC", "DESC", "INT", "TEXT")
// SQL 키워드인가(대문자 기준).
// Is the uppercased word a SQL keyword?
function 키워드인가(낱말: string) -> bool {
대문자(낱말) in 키워드집합
}
function 문장종류파싱(낱말: string) -> Result<문장종류, string> {
match 대문자(낱말) {
case "CREATE" => Ok("CREATE")
case "INSERT" => Ok("INSERT")
case "SELECT" => Ok("SELECT")
case _ => Err("알 수 없는 문장입니다: " + 낱말)
}
}
function 열타입파싱(낱말: string, 열이름: string) -> Result<열타입, string> {
match 대문자(낱말) {
case "INT" => Ok("INT")
case "TEXT" => Ok("TEXT")
case _ => Err("열 타입은 INT 또는 TEXT여야 합니다: " + 열이름)
}
}
function 타입맞나(타입: 열타입, 셀: 값) -> bool {
let 정수냐 = match 셀 { case 정수(_) => true; case 글자(_) => false }
match 타입 {
case "INT" => 정수냐
case "TEXT" => !정수냐
}
}
function 기본값(타입: 열타입) -> 값 {
match 타입 {
case "INT" => 값.정수(0)
case "TEXT" => 값.글자("")
}
}
function 비교연산파싱(토: 토큰) -> Result<비교연산, string> {
let 기 = match 토 { case 기호(기호값) => 기호값; case _ => return Err("WHERE에는 비교 연산자(= <> < <= > >=)가 필요합니다") }
match 기 {
case "=" => Ok("=")
case "<>" => Ok("<>")
case "<" => Ok("<")
case "<=" => Ok("<=")
case ">" => Ok(">")
case ">=" => Ok(">=")
case _ => Err("WHERE에는 비교 연산자(= <> < <= > >=)가 필요합니다")
}
}
function 조건맞나(연산: 비교연산, 비교값: int) -> bool {
match 연산 {
case "=" => 비교값 == 0
case "<>" => 비교값 != 0
case "<" => 비교값 < 0
case "<=" => 비교값 <= 0
case ">" => 비교값 > 0
case ">=" => 비교값 >= 0
}
}
function 셀키파싱(키: string) -> Result<셀키, string> {
match 키 {
case "i" => Ok("i")
case "s" => Ok("s")
case _ => Err("셀 키는 i 또는 s 여야 합니다")
}
}
// SQL을 토큰으로 나눈다. 이름/정수/글(문자열)/기호. 잘못된 글자는 Err.
// Tokenize SQL into name / int / string / symbol tokens. A bad char returns Err.
function 토큰내기(소스: string) -> Result<Array<토큰>, string> {
let 글자들 = 소스.scalars()
let 끝 = 글자들.length
let mut 토큰들: Array<토큰> = []
let mut 위치 = 0
while 위치 < 끝 {
let 글 = 글자들[위치]
if 글 == " " || 글 == "\t" || 글 == "\n" || 글 == "\r" {
위치 = 위치 + 1
} else if 글 == "'" {
// 문자열 리터럴. '' 는 따옴표 하나로.
// String literal. '' is an escaped single quote.
let mut 안 = ""
let mut 진행 = 위치 + 1
let mut 닫힘 = false
let mut 계속 = true
while 계속 && 진행 < 끝 {
let ㄱ = 글자들[진행]
if ㄱ == "'" {
if 진행 + 1 < 끝 && 글자들[진행 + 1] == "'" {
안 = 안 + "'"; 진행 = 진행 + 2
} else {
닫힘 = true; 계속 = false; 진행 = 진행 + 1
}
} else {
안 = 안 + ㄱ; 진행 = 진행 + 1
}
}
if !닫힘 { return Err("닫히지 않은 문자열입니다") }
토큰들.push(토큰.글(안))
위치 = 진행
} else if 숫자글(글) {
let mut 수 = ""
let mut 진행 = 위치
while 진행 < 끝 && 숫자글(글자들[진행]) { 수 = 수 + 글자들[진행]; 진행 = 진행 + 1 }
토큰들.push(토큰.정수(수))
위치 = 진행
} else if 낱말글(글) {
let mut 낱말 = ""
let mut 진행 = 위치
while 진행 < 끝 && 낱말글(글자들[진행]) { 낱말 = 낱말 + 글자들[진행]; 진행 = 진행 + 1 }
토큰들.push(토큰.이름(낱말))
위치 = 진행
} else if 글 == "<" {
if 위치 + 1 < 끝 && 글자들[위치 + 1] == "=" { 토큰들.push(토큰.기호("<=")); 위치 = 위치 + 2 }
else if 위치 + 1 < 끝 && 글자들[위치 + 1] == ">" { 토큰들.push(토큰.기호("<>")); 위치 = 위치 + 2 }
else { 토큰들.push(토큰.기호("<")); 위치 = 위치 + 1 }
} else if 글 == ">" {
if 위치 + 1 < 끝 && 글자들[위치 + 1] == "=" { 토큰들.push(토큰.기호(">=")); 위치 = 위치 + 2 }
else { 토큰들.push(토큰.기호(">")); 위치 = 위치 + 1 }
} else if 글 == "=" || 글 == "(" || 글 == ")" || 글 == "," || 글 == ";" || 글 == "*" || 글 == "-" {
토큰들.push(토큰.기호(글)); 위치 = 위치 + 1
} else {
return Err("알 수 없는 글자: '" + 글 + "'")
}
}
Ok(토큰들)
}
// 커서. 자리[0]가 현재 토큰 색인.
// Cursor: 자리[0] holds the current token index.
function 현재(토큰들: Array<토큰>, 자리: Array<int>) -> 토큰 {
if 자리[0] < 토큰들.length { 토큰들[자리[0]] } else { 토큰.끝 }
}
function 먹기(자리: Array<int>) {
let mut ㅈ = 자리
ㅈ[0] = ㅈ[0] + 1
}
// 현재 토큰이 주어진 기호인지 확인하고, 맞으면 먹는다.
// If the current token is the given symbol, consume it and return true.
function 기호먹기(토큰들: Array<토큰>, 자리: Array<int>, 기호: string) -> bool {
let 토 = 현재(토큰들, 자리)
let 맞다 = match 토 { case 기호(기호값) => 기호값 == 기호; case _ => false }
if 맞다 { 먹기(자리); true } else { false }
}
// 현재 토큰이 주어진 키워드인지(대소문자 무시) 확인하고, 맞으면 먹는다.
// If the current token is the given keyword (case-insensitive), consume it.
function 키워드먹기(토큰들: Array<토큰>, 자리: Array<int>, 키: string) -> bool {
let 토 = 현재(토큰들, 자리)
let 맞다 = match 토 { case 이름(낱말) => 대문자(낱말) == 키; case _ => false }
if 맞다 { 먹기(자리); true } else { false }
}
// 식별자(키워드가 아닌 이름)를 기대한다.
// Expect an identifier (a name that is not a keyword).
function 이름기대(토큰들: Array<토큰>, 자리: Array<int>, 무엇: string) -> Result<string, string> {
let 토 = 현재(토큰들, 자리)
let 낱말 = match 토 { case 이름(조각) => 조각; case _ => return Err(무엇 + " 이름이 필요합니다") }
if 키워드인가(낱말) { return Err("'" + 낱말 + "'는 예약어라 " + 무엇 + " 이름으로 쓸 수 없습니다") }
먹기(자리)
Ok(낱말)
}
// 리터럴 값(정수 또는 '문자열')을 파싱한다. 음수 정수도 받는다.
// Parse a literal value (integer or 'string'). Negative integers allowed.
function 값파싱(토큰들: Array<토큰>, 자리: Array<int>) -> Result<값, string> {
let mut 부호 = 1
if 기호먹기(토큰들, 자리, "-") { 부호 = -1 }
let 토 = 현재(토큰들, 자리)
match 토 {
case 정수(숫자) => {
먹기(자리)
match toInt(숫자) {
case Some(엔) => Ok(값.정수(엔 * 부호))
case None => Err("정수가 너무 큽니다: " + 숫자)
}
}
case 글(문자열) => {
if 부호 == -1 { return Err("문자열 앞에 - 를 쓸 수 없습니다") }
먹기(자리)
Ok(값.글자(문자열))
}
case _ => Err("값(정수 또는 '문자열')이 필요합니다")
}
}
// 테이블을 이름으로 찾는다. 없으면 -1.
// Find a table by name. -1 if not found.
function 표찾기(디비: Array<표>, 이름: string) -> int {
let mut 찾음 = -1
for 번호 in 0..<디비.length {
if 디비[번호].이름 == 이름 { 찾음 = 번호 }
}
찾음
}
// 열을 이름으로 찾는다. 없으면 -1.
// Find a column index by name. -1 if not found.
function 열찾기(열들: Array<열>, 이름: string) -> int {
let mut 찾음 = -1
for 번호 in 0..<열들.length {
if 열들[번호].이름 == 이름 { 찾음 = 번호 }
}
찾음
}
// 두 값을 비교한다. -1/0/1. 같은 종류만 비교한다(호출 전 타입 확인).
// Compare two cells: -1/0/1. Only same-kind cells are compared (caller checks types).
function 값비교(가: 값, 나: 값) -> int {
match 가 {
case 정수(가수) => match 나 {
case 정수(나수) => if 가수 < 나수 { -1 } else if 가수 > 나수 { 1 } else { 0 }
case 글자(_) => 0
}
case 글자(가글) => match 나 {
case 글자(나글) => {
let ㄱ = 가글.scalars()
let ㄴ = 나글.scalars()
let mut 색인 = 0
let mut 결과 = 0
let mut 계속 = true
while 계속 && 색인 < ㄱ.length && 색인 < ㄴ.length {
let ㄱ코 = ㄱ[색인].codePointAt(0) ?? 0
let ㄴ코 = ㄴ[색인].codePointAt(0) ?? 0
if ㄱ코 < ㄴ코 { 결과 = -1; 계속 = false }
else if ㄱ코 > ㄴ코 { 결과 = 1; 계속 = false }
else { 색인 = 색인 + 1 }
}
if 결과 != 0 { 결과 }
else if ㄱ.length < ㄴ.length { -1 }
else if ㄱ.length > ㄴ.length { 1 }
else { 0 }
}
case 정수(_) => 0
}
}
}
// 값 하나를 표시용 문자열로(이스케이프 전).
// Render one cell as a display string (before escaping).
function 값표시(셀: 값) -> string {
match 셀 {
case 정수(수) => "{수}"
case 글자(글) => 글
}
}
// 행들을 한 열 기준으로 안정 정렬한다(삽입 정렬). 내림이면 역순.
// Stable-sort rows by one column (insertion sort). Descending if 내림.
function 정렬(행들: Array<Array<값>>, 열번호: int, 내림: bool) -> Array<Array<값>> {
let mut 결과: Array<Array<값>> = []
for 행 in 행들 {
let mut 끼움 = 결과.length
let mut 찾는중 = true
let mut ㅈ = 0
while 찾는중 && ㅈ < 결과.length {
let 비 = 값비교(행[열번호], 결과[ㅈ][열번호])
let 앞으로 = if 내림 { 비 > 0 } else { 비 < 0 }
if 앞으로 { 끼움 = ㅈ; 찾는중 = false }
ㅈ = ㅈ + 1
}
// 끼움 위치에 삽입.
// Insert at 끼움.
let mut 새것: Array<Array<값>> = []
for ㅋ in 0..<결과.length {
if ㅋ == 끼움 { 새것.push(행) }
새것.push(결과[ㅋ])
}
if 끼움 == 결과.length { 새것.push(행) }
결과 = 새것
}
결과
}
// 결과 표를 HTML로 그린다. 모든 값은 이스케이프한다.
// Render a result table as HTML. Every value is escaped.
function 표그리기(열이름들: Array<string>, 행들: Array<Array<값>>) -> string {
let 머리 = 열이름들.map(이름 => "<th>" + 이스케이프(이름) + "</th>").join("")
let 몸 = if 행들.length == 0 {
"<tr><td class=\"empty\" colspan=\"{열이름들.length}\">행 없음</td></tr>"
} else {
행들.map(행 => "<tr>" + 행.map(셀 => "<td>" + 이스케이프(값표시(셀)) + "</td>").join("") + "</tr>").join("")
}
"<table class=\"grid\"><thead><tr>" + 머리 + "</tr></thead><tbody>" + 몸 + "</tbody></table>"
}
// 16진 한 자리(0-15)를 글자로.
// One hex digit (0-15) to a character.
function 십육(숫자: int) -> string {
let 표 = "0123456789abcdef".scalars()
if 숫자 >= 0 && 숫자 < 16 { 표[숫자] } else { "0" }
}
// JSON 문자열 이스케이프. 0x20 미만 제어문자는 \uXXXX로 쓴다.
// Escape a string for JSON. Control chars below 0x20 are written as \uXXXX.
function 제이슨글(원문: string) -> string {
"\"" + 원문.scalars().map(글 => {
if 글 == "\"" { "\\\"" }
else if 글 == "\\" { "\\\\" }
else if 글 == "\n" { "\\n" }
else if 글 == "\t" { "\\t" }
else if 글 == "\r" { "\\r" }
else {
let 코드 = 글.codePointAt(0) ?? 0
if 코드 < 32 { "\\u00" + 십육(코드 / 16) + 십육(코드 % 16) }
else { 글 }
}
}).join("") + "\""
}
// 데이터베이스를 JSON으로 직렬화한다(저장). 문자열의 리터럴 중괄호는 \{ \} 로 쓴다.
// Serialize the database to JSON (save). Literal braces in strings are written \{ \}.
function 직렬화(디비: Array<표>) -> string {
"\{\"tables\":[" + 디비.map(표 =>
"\{\"name\":" + 제이슨글(표.이름) + ",\"cols\":[" +
표.열들.map(열 => "\{\"name\":" + 제이슨글(열.이름) + ",\"type\":" + 제이슨글(열.타입) + "\}").join(",") +
"],\"rows\":[" +
표.행들.map(행 => "[" + 행.map(셀 => match 셀 {
case 정수(수) => "\{\"i\":{수}\}"
case 글자(글) => "\{\"s\":" + 제이슨글(글) + "\}"
}).join(",") + "]").join(",") +
"]\}"
).join(",") + "]\}"
}
// CREATE TABLE 실행. 디비는 참조라 호출자에 반영된다.
// Run CREATE TABLE. 디비 is shared by reference.
function 만들기(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
let mut 디 = 디비
if !키워드먹기(토큰들, 자리, "TABLE") { return Err("CREATE 다음에는 TABLE이 필요합니다") }
let 표이름 = 이름기대(토큰들, 자리, "테이블")?
if 표찾기(디, 표이름) != -1 { return Err("이미 있는 테이블입니다: " + 표이름) }
if !기호먹기(토큰들, 자리, "(") { return Err("( 가 필요합니다") }
let mut 열들: Array<열> = []
let mut 계속 = true
while 계속 {
let 열이름 = 이름기대(토큰들, 자리, "열")?
if 열찾기(열들, 열이름) != -1 { return Err("열 이름이 겹칩니다: " + 열이름) }
let 타토 = 현재(토큰들, 자리)
let 타입낱말 = match 타토 { case 이름(낱말) => 낱말; case _ => return Err("열 타입은 INT 또는 TEXT여야 합니다: " + 열이름) }
let 타입 = 열타입파싱(타입낱말, 열이름)?
먹기(자리)
열들.push(열 { 이름: 열이름, 타입: 타입 })
if 기호먹기(토큰들, 자리, ",") { } else { 계속 = false }
}
if !기호먹기(토큰들, 자리, ")") { return Err(") 가 필요합니다") }
if 열들.length == 0 { return Err("열이 하나 이상 필요합니다") }
디.push(표 { 이름: 표이름, 열들: 열들, 행들: [] })
Ok("테이블 '" + 이스케이프(표이름) + "' 을 만들었습니다.")
}
// INSERT INTO 실행.
// Run INSERT INTO.
function 넣기(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
let mut 디 = 디비
if !키워드먹기(토큰들, 자리, "INTO") { return Err("INSERT 다음에는 INTO가 필요합니다") }
let 표이름 = 이름기대(토큰들, 자리, "테이블")?
let 표번호 = 표찾기(디, 표이름)
if 표번호 == -1 { return Err("없는 테이블입니다: " + 표이름) }
let 열들 = 디[표번호].열들
// 선택적 열 목록.
// Optional column list.
let mut 열순서: Array<int> = []
let mut 열지정 = false
if 기호먹기(토큰들, 자리, "(") {
열지정 = true
let mut 계속 = true
while 계속 {
let ㅇ이름 = 이름기대(토큰들, 자리, "열")?
let ㅇ번호 = 열찾기(열들, ㅇ이름)
if ㅇ번호 == -1 { return Err("없는 열입니다: " + ㅇ이름) }
열순서.push(ㅇ번호)
if 기호먹기(토큰들, 자리, ",") { } else { 계속 = false }
}
if !기호먹기(토큰들, 자리, ")") { return Err("열 목록에 ) 가 필요합니다") }
}
if !키워드먹기(토큰들, 자리, "VALUES") { return Err("VALUES가 필요합니다") }
if !기호먹기(토큰들, 자리, "(") { return Err("VALUES 다음에 ( 가 필요합니다") }
let mut 값들: Array<값> = []
let mut 계속2 = true
while 계속2 {
let ㄱ = 값파싱(토큰들, 자리)?
값들.push(ㄱ)
if 기호먹기(토큰들, 자리, ",") { } else { 계속2 = false }
}
if !기호먹기(토큰들, 자리, ")") { return Err("값 목록에 ) 가 필요합니다") }
// 행을 만든다. 미지정 열은 채워야 하므로 전체 열 개수와 맞아야 한다.
// Build the row. Without a column list, the count must match all columns.
let 열수 = 열들.length
let mut 행 = 열들.map(ㄷ => 기본값(ㄷ.타입))
if 열지정 {
if 값들.length != 열순서.length { return Err("열 수와 값 수가 다릅니다") }
for ㅋ in 0..<값들.length {
let 대상열 = 열순서[ㅋ]
let 맞나 = 타입맞나(열들[대상열].타입, 값들[ㅋ])
if !맞나 { return Err("열 '" + 열들[대상열].이름 + "' 의 타입과 값이 맞지 않습니다") }
행[대상열] = 값들[ㅋ]
}
} else {
if 값들.length != 열수 { return Err("열 수와 값 수가 다릅니다") }
for ㅋ in 0..<열수 {
let 맞나 = 타입맞나(열들[ㅋ].타입, 값들[ㅋ])
if !맞나 { return Err("열 '" + 열들[ㅋ].이름 + "' 의 타입과 값이 맞지 않습니다") }
행[ㅋ] = 값들[ㅋ]
}
}
// 미지정 열은 0 또는 빈 문자열로 둔다(기본값).
// Unspecified columns keep 0 or empty string (defaults).
디[표번호].행들.push(행)
Ok("테이블 '" + 이스케이프(표이름) + "' 에 1행을 넣었습니다.")
}
// SELECT 실행. 결과 표 HTML을 돌려준다.
// Run SELECT. Returns the result-table HTML.
function 고르기(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
// 고를 열 이름들(또는 *).
// The selected column names (or *).
let mut 별표 = false
let mut 고른열: Array<string> = []
if 기호먹기(토큰들, 자리, "*") {
별표 = true
} else {
let mut 계속 = true
while 계속 {
let ㅇ = 이름기대(토큰들, 자리, "열")?
고른열.push(ㅇ)
if 기호먹기(토큰들, 자리, ",") { } else { 계속 = false }
}
}
if !키워드먹기(토큰들, 자리, "FROM") { return Err("FROM이 필요합니다") }
let 표이름 = 이름기대(토큰들, 자리, "테이블")?
let 표번호 = 표찾기(디비, 표이름)
if 표번호 == -1 { return Err("없는 테이블입니다: " + 표이름) }
let 표 = 디비[표번호]
// WHERE 절(선택).
// Optional WHERE clause.
let mut 조건있다 = false
let mut 조건열 = -1
let mut 조건연산: 비교연산 = "="
let mut 조건값: 값 = 값.정수(0)
if 키워드먹기(토큰들, 자리, "WHERE") {
조건있다 = true
let ㅇ = 이름기대(토큰들, 자리, "열")?
조건열 = 열찾기(표.열들, ㅇ)
if 조건열 == -1 { return Err("WHERE에 없는 열입니다: " + ㅇ) }
let 연산토 = 현재(토큰들, 자리)
let ㅇㅅ = 비교연산파싱(연산토)?
먹기(자리)
조건연산 = ㅇㅅ
let ㄱ = 값파싱(토큰들, 자리)?
if !타입맞나(표.열들[조건열].타입, ㄱ) {
return Err("WHERE 값의 타입이 열과 맞지 않습니다")
}
조건값 = ㄱ
}
// ORDER BY 절(선택).
// Optional ORDER BY clause.
let mut 정렬있다 = false
let mut 정렬열 = -1
let mut 내림 = false
if 키워드먹기(토큰들, 자리, "ORDER") {
if !키워드먹기(토큰들, 자리, "BY") { return Err("ORDER 다음에는 BY가 필요합니다") }
정렬있다 = true
let ㅇ = 이름기대(토큰들, 자리, "열")?
정렬열 = 열찾기(표.열들, ㅇ)
if 정렬열 == -1 { return Err("ORDER BY에 없는 열입니다: " + ㅇ) }
if 키워드먹기(토큰들, 자리, "DESC") { 내림 = true }
else if 키워드먹기(토큰들, 자리, "ASC") { 내림 = false }
}
// 고른 열 색인 계산.
// Resolve selected column indices.
let mut 뽑을열: Array<int> = []
let mut 머리: Array<string> = []
if 별표 {
for ㅇ in 0..<표.열들.length { 뽑을열.push(ㅇ); 머리.push(표.열들[ㅇ].이름) }
} else {
for 이름 in 고른열 {
let ㅇ번호 = 열찾기(표.열들, 이름)
if ㅇ번호 == -1 { return Err("없는 열입니다: " + 이름) }
뽑을열.push(ㅇ번호)
머리.push(이름)
}
}
// WHERE로 거른다.
// Filter by WHERE.
let 걸러진 = 표.행들.filter(행 => !조건있다 || 조건맞나(조건연산, 값비교(행[조건열], 조건값)))
// 정렬.
// Sort.
let 정렬됨 = if 정렬있다 { 정렬(걸러진, 정렬열, 내림) } else { 걸러진 }
// 뽑을 열만 투영.
// Project the selected columns.
let 결과행 = 정렬됨.map(행 => 뽑을열.map(ㅇ번호 => 행[ㅇ번호]))
Ok(표그리기(머리, 결과행))
}
// 문장 하나를 파싱하고 실행한다. 출력 HTML 조각을 돌려준다.
// Parse and run one statement. Returns its output HTML fragment.
function 문장하나(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
let 토 = 현재(토큰들, 자리)
let 첫낱말 = match 토 { case 이름(낱말) => 낱말; case _ => return Err("문장은 CREATE / INSERT / SELECT 로 시작해야 합니다") }
let 첫 = 문장종류파싱(첫낱말)?
먹기(자리)
match 첫 {
case "CREATE" => Ok("<div class=\"ok\">" + 만들기(디비, 토큰들, 자리)? + "</div>")
case "INSERT" => Ok("<div class=\"ok\">" + 넣기(디비, 토큰들, 자리)? + "</div>")
case "SELECT" => 고르기(디비, 토큰들, 자리)
}
}
// JSON으로 들어온 입력인가(공백 빼고 { 로 시작).
// Does the input look like JSON (first non-space char is {)?
function 제이슨입력인가(소스: string) -> bool {
let 글자들 = 소스.trim().scalars()
글자들.length > 0 && 글자들[0] == "\{"
}
// --- JSON 복원(파싱) --- 위치[0]는 현재 글자 색인인 커서다.
// --- JSON restore (parse) --- 위치[0] is the cursor (current char index).
function 공백넘기(글자들: Array<string>, 위치: Array<int>) {
let mut ㅈ = 위치
let mut 계속 = true
while 계속 && ㅈ[0] < 글자들.length {
let 글 = 글자들[ㅈ[0]]
if 글 == " " || 글 == "\t" || 글 == "\n" || 글 == "\r" { ㅈ[0] = ㅈ[0] + 1 } else { 계속 = false }
}
}
// 공백을 건너뛴 뒤 정확히 이 글자를 기대한다.
// Skip whitespace, then expect exactly this character.
function 글기대(글자들: Array<string>, 위치: Array<int>, 글: string) -> Result<bool, string> {
공백넘기(글자들, 위치)
let mut ㅈ = 위치
if ㅈ[0] < 글자들.length && 글자들[ㅈ[0]] == 글 { ㅈ[0] = ㅈ[0] + 1; Ok(true) }
else { Err("'" + 글 + "' 가 필요합니다") }
}
// JSON 문자열 하나를 읽는다.
// Read one JSON string.
function 제이슨문자열읽기(글자들: Array<string>, 위치: Array<int>) -> Result<string, string> {
공백넘기(글자들, 위치)
let mut ㅈ = 위치
if ㅈ[0] >= 글자들.length || 글자들[ㅈ[0]] != "\"" { return Err("문자열이 필요합니다") }
ㅈ[0] = ㅈ[0] + 1
let mut 결과 = ""
let mut 계속 = true
let mut 닫힘 = false
while 계속 && ㅈ[0] < 글자들.length {
let 글 = 글자들[ㅈ[0]]
if 글 == "\"" { 닫힘 = true; 계속 = false; ㅈ[0] = ㅈ[0] + 1 }
else if 글 == "\\" {
if ㅈ[0] + 1 >= 글자들.length { return Err("문자열 이스케이프가 끝났습니다") }
let 다 = 글자들[ㅈ[0] + 1]
if 다 == "\"" { 결과 = 결과 + "\"" }
else if 다 == "\\" { 결과 = 결과 + "\\" }
else if 다 == "n" { 결과 = 결과 + "\n" }
else if 다 == "t" { 결과 = 결과 + "\t" }
else if 다 == "r" { 결과 = 결과 + "\r" }
else if 다 == "u" {
if ㅈ[0] + 5 >= 글자들.length { return Err("유니코드 이스케이프가 끝났습니다") }
let 십육글 = 글자들[ㅈ[0] + 2] + 글자들[ㅈ[0] + 3] + 글자들[ㅈ[0] + 4] + 글자들[ㅈ[0] + 5]
match toIntRadix(십육글, 16) {
case Some(코드) => { 결과 = 결과 + (fromCodePoint(코드) ?? ""); ㅈ[0] = ㅈ[0] + 4 }
case None => { return Err("잘못된 유니코드 이스케이프입니다") }
}
}
else { return Err("알 수 없는 이스케이프입니다") }
ㅈ[0] = ㅈ[0] + 2
}
else { 결과 = 결과 + 글; ㅈ[0] = ㅈ[0] + 1 }
}
if !닫힘 { return Err("닫히지 않은 문자열입니다") }
Ok(결과)
}
// JSON 정수 하나를 읽는다.
// Read one JSON integer.
function 제이슨정수읽기(글자들: Array<string>, 위치: Array<int>) -> Result<int, string> {
공백넘기(글자들, 위치)
let mut ㅈ = 위치
let mut 수글 = ""
if ㅈ[0] < 글자들.length && 글자들[ㅈ[0]] == "-" { 수글 = "-"; ㅈ[0] = ㅈ[0] + 1 }
let mut 개수 = 0
while ㅈ[0] < 글자들.length && 숫자글(글자들[ㅈ[0]]) { 수글 = 수글 + 글자들[ㅈ[0]]; ㅈ[0] = ㅈ[0] + 1; 개수 = 개수 + 1 }
if 개수 == 0 { return Err("정수가 필요합니다") }
match toInt(수글) { case Some(엔) => Ok(엔); case None => Err("정수 범위를 벗어났습니다") }
}
// "키": 형태를 기대한다.
// Expect a "key": pair.
function 키기대(글자들: Array<string>, 위치: Array<int>, 키: string) -> Result<bool, string> {
let 읽은 = 제이슨문자열읽기(글자들, 위치)?
if 읽은 != 키 { return Err("'" + 키 + "' 키가 필요합니다") }
글기대(글자들, 위치, ":")
}
// 셀 하나 { "i": 정수 } 또는 { "s": "문자열" } 를 읽는다.
// Read one cell {"i":int} or {"s":"string"}.
function 셀하나읽기(글자들: Array<string>, 위치: Array<int>) -> Result<값, string> {
글기대(글자들, 위치, "\{")?
let 키 = 제이슨문자열읽기(글자들, 위치)?
글기대(글자들, 위치, ":")?
let 좁힌키 = 셀키파싱(키)?
match 좁힌키 {
case "i" => {
let 엔 = 제이슨정수읽기(글자들, 위치)?
글기대(글자들, 위치, "\}")?
Ok(값.정수(엔))
}
case "s" => {
let ㅅ값 = 제이슨문자열읽기(글자들, 위치)?
글기대(글자들, 위치, "\}")?
Ok(값.글자(ㅅ값))
}
}
}
// 표 하나를 읽는다.
// Read one table object.
function 표하나읽기(글자들: Array<string>, 위치: Array<int>) -> Result<표, string> {
글기대(글자들, 위치, "\{")?
키기대(글자들, 위치, "name")?
let 표이름 = 제이슨문자열읽기(글자들, 위치)?
글기대(글자들, 위치, ",")?
키기대(글자들, 위치, "cols")?
글기대(글자들, 위치, "[")?
let mut 열들: Array<열> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
글기대(글자들, 위치, "\{")?
키기대(글자들, 위치, "name")?
let ㅇ이름 = 제이슨문자열읽기(글자들, 위치)?
글기대(글자들, 위치, ",")?
키기대(글자들, 위치, "type")?
let ㅇ타입문자열 = 제이슨문자열읽기(글자들, 위치)?
let ㅇ타입 = match 열타입파싱(ㅇ타입문자열, "") {
case Ok(ㅌ) => ㅌ
case Err(_) => return Err("열 타입은 INT 또는 TEXT여야 합니다")
}
글기대(글자들, 위치, "\}")?
열들.push(열 { 이름: ㅇ이름, 타입: ㅇ타입 })
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속 = false }
else { return Err("열 목록에 , 또는 ] 가 필요합니다") }
}
}
글기대(글자들, 위치, ",")?
키기대(글자들, 위치, "rows")?
글기대(글자들, 위치, "[")?
let mut 행들: Array<Array<값>> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
글기대(글자들, 위치, "[")?
let mut 행: Array<값> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속2 = true
while 계속2 {
let 셀 = 셀하나읽기(글자들, 위치)?
행.push(셀)
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속2 = false }
else { return Err("행에 , 또는 ] 가 필요합니다") }
}
}
if 행.length != 열들.length { return Err("행의 칸 수가 열 수와 다릅니다") }
행들.push(행)
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속 = false }
else { return Err("행 목록에 , 또는 ] 가 필요합니다") }
}
}
글기대(글자들, 위치, "\}")?
Ok(표 { 이름: 표이름, 열들: 열들, 행들: 행들 })
}
// JSON 저장본을 데이터베이스로 복원한다.
// Restore the database from a JSON save.
function 복원(소스: string) -> Result<Array<표>, string> {
let 글자들 = 소스.scalars()
let mut 위치: Array<int> = []
위치.push(0)
let mut 디비: Array<표> = []
글기대(글자들, 위치, "\{")?
키기대(글자들, 위치, "tables")?
글기대(글자들, 위치, "[")?
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
let ㅍ = 표하나읽기(글자들, 위치)?
if 표찾기(디비, ㅍ.이름) != -1 { return Err("테이블 이름이 겹칩니다: " + ㅍ.이름) }
디비.push(ㅍ)
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속 = false }
else { return Err("테이블 목록에 , 또는 ] 가 필요합니다") }
}
}
글기대(글자들, 위치, "\}")?
공백넘기(글자들, 위치)
if 위치[0] != 글자들.length { return Err("JSON 뒤에 불필요한 내용이 있습니다") }
Ok(디비)
}
// 복원된 데이터베이스의 모든 표를 그린다.
// Render every table of a restored database.
function 디비그리기(디비: Array<표>) -> string {
if 디비.length == 0 { return "<div class=\"meta\">빈 데이터베이스입니다.</div>" }
디비.map(표 => "<div class=\"ok\">테이블 '" + 이스케이프(표.이름) + "'</div>" + 표그리기(표.열들.map(열 => 열.이름), 표.행들)).join("")
}
// SQL 전체를 실행한다(또는 JSON 저장본이면 복원한다).
// Run the whole SQL input (or restore a JSON save).
function 실행(소스: string) -> string {
if 제이슨입력인가(소스) {
match 복원(소스) {
case Err(메) => return "<div class=\"err\">JSON 복원 오류: " + 이스케이프(메) + "</div>"
case Ok(되디비) => return 디비그리기(되디비) + "<div class=\"meta\">저장 (JSON)</div><pre class=\"json\">" + 이스케이프(직렬화(되디비)) + "</pre>"
}
}
match 토큰내기(소스) {
case Err(메) => "<div class=\"err\">SQL 오류: " + 이스케이프(메) + "</div>"
case Ok(토큰들) => {
let mut 디비: Array<표> = []
let mut 자리: Array<int> = []
자리.push(0)
let mut 출력 = ""
let mut 문장번호 = 0
let mut 멈춤 = false
while !멈춤 && 자리[0] < 토큰들.length {
// 빈 세미콜론은 건너뛴다.
// Skip empty semicolons.
if 기호먹기(토큰들, 자리, ";") {
} else {
문장번호 = 문장번호 + 1
match 문장하나(디비, 토큰들, 자리) {
case Err(메) => {
출력 = 출력 + "<div class=\"err\">" + "{문장번호}번째 문장 오류: " + 이스케이프(메) + "</div>"
멈춤 = true
}
case Ok(조각) => {
출력 = 출력 + 조각
// 문장 끝의 세미콜론(선택).
// Optional trailing semicolon.
if 기호먹기(토큰들, 자리, ";") { } else {
match 현재(토큰들, 자리) {
case 끝 => { }
case 이름(근처글) | 정수(근처글) | 글(근처글) | 기호(근처글) => {
출력 = 출력 + "<div class=\"err\">문장 끝에 ; 또는 입력 끝이 필요합니다(근처: '" + 이스케이프(근처글) + "')</div>"
멈춤 = true
}
}
}
}
}
}
}
if 출력 == "" { 출력 = "<div class=\"meta\">실행할 문장이 없습니다.</div>" }
// 데이터베이스 상태를 JSON으로 저장 표시.
// Show the database state serialized as JSON (save).
출력 + "<div class=\"meta\">저장 (JSON)</div><pre class=\"json\">" + 이스케이프(직렬화(디비)) + "</pre>"
}
}
}
print(실행(input()))