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, 기대기호=expect 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 value.
//
// 결정적이다. 같은 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.
// 셀 값. 정수냐=true면 정수 칸, false면 글(텍스트) 칸.
// A cell value. 정수냐=true means an INT cell, false means a TEXT cell.
type 값 = { 정수냐: bool, 수: int, 글: string }
// 닫힌 SQL 열 타입과 토큰 태그. 사용자 입력 문자열은 파싱 경계에서만 이 타입으로 좁힌다.
// Closed SQL column types and token tags. Raw user strings are narrowed only at parser boundaries.
type 열타입 = "INT" | "TEXT"
type 토큰종류 = "이름" | "정수" | "글" | "기호" | "끝"
type 문장종류 = "CREATE" | "INSERT" | "SELECT"
type 비교연산 = "=" | "<>" | "<" | "<=" | ">" | ">="
type 셀키 = "i" | "s"
type 열 = { 이름: string, 타입: 열타입 }
type 표 = { 이름: string, 열들: Array<열>, 행들: Array<Array<값>> }
type 토큰 = { 종류: 토큰종류, 값: string }
// 레코드 리터럴은 문자열 리터럴을 넓히므로, 토큰 생성은 헬퍼의 매개변수 타입으로 좁힌다.
// Record literals widen string literals, so this helper narrows via the parameter type.
function 토큰만들기(종류: 토큰종류, 값: string) -> 토큰 {
{ 종류: 종류, 값: 값 }
}
// HTML 이스케이프. SQL·셀 값·테이블 이름은 모두 사용자 입력이라 출력 전에 반드시 이스케이프한다.
// HTML escape. SQL, cell values, and table names are user input, so always escape before output.
function 이스케이프(원문: string) -> string {
let mut 결과 = ""
for 글 in 원문.scalars() {
if 글 == "&" { 결과 = 결과 + "&" }
else if 글 == "<" { 결과 = 결과 + "<" }
else if 글 == ">" { 결과 = 결과 + ">" }
else if 글 == "\"" { 결과 = 결과 + """ }
else { 결과 = 결과 + 글 }
}
결과
}
// 글자가 영문/숫자/밑줄인가(식별자에 쓸 수 있는 글자).
// 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 {
let mut 결과 = ""
for 글 in 낱말.scalars() {
let 코드 = 글.codePointAt(0) ?? 0
if 코드 >= 97 && 코드 <= 122 {
결과 = 결과 + (fromCodePoint(코드 - 32) ?? 글)
} else {
결과 = 결과 + 글
}
}
결과
}
// SQL 키워드인가(대문자 기준).
// Is the uppercased word a SQL keyword?
function 키워드인가(낱말: string) -> bool {
match 대문자(낱말) {
case "CREATE" => true
case "TABLE" => true
case "INSERT" => true
case "INTO" => true
case "VALUES" => true
case "SELECT" => true
case "FROM" => true
case "WHERE" => true
case "ORDER" => true
case "BY" => true
case "ASC" => true
case "DESC" => true
case "INT" => true
case "TEXT" => true
case _ => false
}
}
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 {
match 타입 {
case "INT" => 셀.정수냐
case "TEXT" => !셀.정수냐
}
}
function 기본값(타입: 열타입) -> 값 {
match 타입 {
case "INT" => { 정수냐: true, 수: 0, 글: "" }
case "TEXT" => { 정수냐: false, 수: 0, 글: "" }
}
}
function 비교연산파싱(토: 토큰) -> Result<비교연산, string> {
if 토.종류 != "기호" { 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 토 = 현재(토큰들, 자리)
if 토.종류 == "기호" && 토.값 == 기호 { 먹기(자리); true } else { false }
}
// 현재 토큰이 주어진 키워드인지(대소문자 무시) 확인하고, 맞으면 먹는다.
// If the current token is the given keyword (case-insensitive), consume it.
function 키워드먹기(토큰들: Array<토큰>, 자리: Array<int>, 키: string) -> bool {
let 토 = 현재(토큰들, 자리)
if 토.종류 == "이름" && 대문자(토.값) == 키 { 먹기(자리); true } else { false }
}
// 식별자(키워드가 아닌 이름)를 기대한다.
// Expect an identifier (a name that is not a keyword).
function 이름기대(토큰들: Array<토큰>, 자리: Array<int>, 무엇: string) -> Result<string, string> {
let 토 = 현재(토큰들, 자리)
if 토.종류 != "이름" { 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 토 = 현재(토큰들, 자리)
if 토.종류 == "정수" {
먹기(자리)
match toInt(토.값) {
case Some(엔) => Ok({ 정수냐: true, 수: 엔 * 부호, 글: "" })
case None => Err("정수가 너무 큽니다: " + 토.값)
}
} else if 토.종류 == "글" {
if 부호 == -1 { return Err("문자열 앞에 - 를 쓸 수 없습니다") }
먹기(자리)
Ok({ 정수냐: false, 수: 0, 글: 토.값 })
} else {
Err("값(정수 또는 '문자열')이 필요합니다")
}
}
// 테이블을 이름으로 찾는다. 없으면 -1.
// Find a table by name. -1 if not found.
function 표찾기(디비: Array<표>, 이름: string) -> int {
let mut 번호 = 0
let mut 찾음 = -1
while 번호 < 디비.length {
if 디비[번호].이름 == 이름 { 찾음 = 번호 }
번호 = 번호 + 1
}
찾음
}
// 열을 이름으로 찾는다. 없으면 -1.
// Find a column index by name. -1 if not found.
function 열찾기(열들: Array<열>, 이름: string) -> int {
let mut 번호 = 0
let mut 찾음 = -1
while 번호 < 열들.length {
if 열들[번호].이름 == 이름 { 찾음 = 번호 }
번호 = 번호 + 1
}
찾음
}
// 두 값을 비교한다. -1/0/1. 같은 종류만 비교한다(호출 전 타입 확인).
// Compare two cells: -1/0/1. Only same-kind cells are compared (caller checks types).
function 값비교(가: 값, 나: 값) -> int {
if 가.정수냐 {
if 가.수 < 나.수 { -1 } else if 가.수 > 나.수 { 1 } else { 0 }
} else {
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 }
}
}
// 값 하나를 표시용 문자열로(이스케이프 전).
// Render one cell as a display string (before escaping).
function 값표시(셀: 값) -> string {
if 셀.정수냐 { "{셀.수}" } else { 셀.글 }
}
// 행들을 한 열 기준으로 안정 정렬한다(삽입 정렬). 내림이면 역순.
// 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<값>> = []
let mut ㅋ = 0
while ㅋ < 결과.length {
if ㅋ == 끼움 { 새것.push(행) }
새것.push(결과[ㅋ])
ㅋ = ㅋ + 1
}
if 끼움 == 결과.length { 새것.push(행) }
결과 = 새것
}
결과
}
// 결과 표를 HTML로 그린다. 모든 값은 이스케이프한다.
// Render a result table as HTML. Every value is escaped.
function 표그리기(열이름들: Array<string>, 행들: Array<Array<값>>) -> string {
let mut 결과 = "<table class=\"grid\"><thead><tr>"
for 이름 in 열이름들 { 결과 = 결과 + "<th>" + 이스케이프(이름) + "</th>" }
결과 = 결과 + "</tr></thead><tbody>"
if 행들.length == 0 {
결과 = 결과 + "<tr><td class=\"empty\" colspan=\"{열이름들.length}\">행 없음</td></tr>"
}
for 행 in 행들 {
결과 = 결과 + "<tr>"
for 셀 in 행 { 결과 = 결과 + "<td>" + 이스케이프(값표시(셀)) + "</td>" }
결과 = 결과 + "</tr>"
}
결과 + "</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 {
let mut 결과 = "\""
for 글 in 원문.scalars() {
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 { 결과 = 결과 + 글 }
}
}
결과 + "\""
}
// 데이터베이스를 JSON으로 직렬화한다(저장). 문자열의 리터럴 중괄호는 \{ \} 로 쓴다.
// Serialize the database to JSON (save). Literal braces in strings are written \{ \}.
function 직렬화(디비: Array<표>) -> string {
let mut 결과 = "\{\"tables\":["
let mut ㅌ = 0
while ㅌ < 디비.length {
let 표 = 디비[ㅌ]
if ㅌ > 0 { 결과 = 결과 + "," }
결과 = 결과 + "\{\"name\":" + 제이슨글(표.이름) + ",\"cols\":["
let mut ㅇ = 0
while ㅇ < 표.열들.length {
if ㅇ > 0 { 결과 = 결과 + "," }
결과 = 결과 + "\{\"name\":" + 제이슨글(표.열들[ㅇ].이름) + ",\"type\":" + 제이슨글(표.열들[ㅇ].타입) + "\}"
ㅇ = ㅇ + 1
}
결과 = 결과 + "],\"rows\":["
let mut ㅎ = 0
while ㅎ < 표.행들.length {
if ㅎ > 0 { 결과 = 결과 + "," }
결과 = 결과 + "["
let 행 = 표.행들[ㅎ]
let mut ㅅ = 0
while ㅅ < 행.length {
if ㅅ > 0 { 결과 = 결과 + "," }
if 행[ㅅ].정수냐 { 결과 = 결과 + "\{\"i\":{행[ㅅ].수}\}" }
else { 결과 = 결과 + "\{\"s\":" + 제이슨글(행[ㅅ].글) + "\}" }
ㅅ = ㅅ + 1
}
결과 = 결과 + "]"
ㅎ = ㅎ + 1
}
결과 = 결과 + "]\}"
ㅌ = ㅌ + 1
}
결과 + "]\}"
}
// 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 이름결과 = 이름기대(토큰들, 자리, "테이블")
match 이름결과 {
case Err(메) => return Err(메)
case Ok(표이름) => {
if 표찾기(디, 표이름) != -1 { return Err("이미 있는 테이블입니다: " + 표이름) }
if !기호먹기(토큰들, 자리, "(") { return Err("( 가 필요합니다") }
let mut 열들: Array<열> = []
let mut 계속 = true
while 계속 {
let 열이름결과 = 이름기대(토큰들, 자리, "열")
match 열이름결과 {
case Err(메) => return Err(메)
case Ok(열이름) => {
if 열찾기(열들, 열이름) != -1 { return Err("열 이름이 겹칩니다: " + 열이름) }
let 타토 = 현재(토큰들, 자리)
if 타토.종류 != "이름" {
return Err("열 타입은 INT 또는 TEXT여야 합니다: " + 열이름)
}
let 타입 = match 열타입파싱(타토.값, 열이름) {
case Ok(ㅌ) => ㅌ
case Err(메) => return Err(메)
}
먹기(자리)
열들.push({ 이름: 열이름, 타입: 타입 })
if 기호먹기(토큰들, 자리, ",") { } else { 계속 = false }
}
}
}
if !기호먹기(토큰들, 자리, ")") { return Err(") 가 필요합니다") }
if 열들.length == 0 { return Err("열이 하나 이상 필요합니다") }
let mut 빈행들: Array<Array<값>> = []
디.push({ 이름: 표이름, 열들: 열들, 행들: 빈행들 })
Ok("테이블 '" + 이스케이프(표이름) + "' 을 만들었습니다.")
}
}
}
// INSERT INTO 실행.
// Run INSERT INTO.
function 넣기(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
let mut 디 = 디비
if !키워드먹기(토큰들, 자리, "INTO") { return Err("INSERT 다음에는 INTO가 필요합니다") }
let 이름결과 = 이름기대(토큰들, 자리, "테이블")
match 이름결과 {
case Err(메) => return Err(메)
case Ok(표이름) => {
let 표번호 = 표찾기(디, 표이름)
if 표번호 == -1 { return Err("없는 테이블입니다: " + 표이름) }
let 열들 = 디[표번호].열들
// 선택적 열 목록.
// Optional column list.
let mut 열순서: Array<int> = []
let mut 열지정 = false
if 기호먹기(토큰들, 자리, "(") {
열지정 = true
let mut 계속 = true
while 계속 {
let ㅇ결과 = 이름기대(토큰들, 자리, "열")
match ㅇ결과 {
case Err(메) => return Err(메)
case Ok(ㅇ이름) => {
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 ㄱ결과 = 값파싱(토큰들, 자리)
match ㄱ결과 {
case Err(메) => return Err(메)
case Ok(ㄱ) => {
값들.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 행: Array<값> = []
let mut ㅊ = 0
while ㅊ < 열수 { 행.push(기본값(열들[ㅊ].타입)); ㅊ = ㅊ + 1 }
if 열지정 {
if 값들.length != 열순서.length { return Err("열 수와 값 수가 다릅니다") }
let mut ㅋ = 0
while ㅋ < 값들.length {
let 대상열 = 열순서[ㅋ]
let 맞나 = 타입맞나(열들[대상열].타입, 값들[ㅋ])
if !맞나 { return Err("열 '" + 열들[대상열].이름 + "' 의 타입과 값이 맞지 않습니다") }
행[대상열] = 값들[ㅋ]
ㅋ = ㅋ + 1
}
} else {
if 값들.length != 열수 { return Err("열 수와 값 수가 다릅니다") }
let mut ㅋ = 0
while ㅋ < 열수 {
let 맞나 = 타입맞나(열들[ㅋ].타입, 값들[ㅋ])
if !맞나 { return Err("열 '" + 열들[ㅋ].이름 + "' 의 타입과 값이 맞지 않습니다") }
행[ㅋ] = 값들[ㅋ]
ㅋ = ㅋ + 1
}
}
// 미지정 열은 0 또는 빈 문자열로 둔다(기본값).
// Unspecified columns keep 0 or empty string (defaults).
let mut 디2 = 디
디2[표번호].행들.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 ㅇ결과 = 이름기대(토큰들, 자리, "열")
match ㅇ결과 {
case Err(메) => return Err(메)
case Ok(ㅇ) => {
고른열.push(ㅇ)
if 기호먹기(토큰들, 자리, ",") { } else { 계속 = false }
}
}
}
}
if !키워드먹기(토큰들, 자리, "FROM") { return Err("FROM이 필요합니다") }
let 이름결과 = 이름기대(토큰들, 자리, "테이블")
match 이름결과 {
case Err(메) => return Err(메)
case Ok(표이름) => {
let 표번호 = 표찾기(디비, 표이름)
if 표번호 == -1 { return Err("없는 테이블입니다: " + 표이름) }
let 표 = 디비[표번호]
// WHERE 절(선택).
// Optional WHERE clause.
let mut 조건있다 = false
let mut 조건열 = -1
let mut 조건연산: 비교연산 = "="
let mut 조건값: 값 = { 정수냐: true, 수: 0, 글: "" }
if 키워드먹기(토큰들, 자리, "WHERE") {
조건있다 = true
let ㅇ결과 = 이름기대(토큰들, 자리, "열")
match ㅇ결과 {
case Err(메) => return Err(메)
case Ok(ㅇ) => {
조건열 = 열찾기(표.열들, ㅇ)
if 조건열 == -1 { return Err("WHERE에 없는 열입니다: " + ㅇ) }
}
}
let 연산토 = 현재(토큰들, 자리)
let ㅇㅅ = match 비교연산파싱(연산토) {
case Ok(ㅇ) => ㅇ
case Err(메) => return Err(메)
}
먹기(자리)
조건연산 = ㅇㅅ
let ㄱ결과 = 값파싱(토큰들, 자리)
match ㄱ결과 {
case Err(메) => return Err(메)
case Ok(ㄱ) => {
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 ㅇ결과 = 이름기대(토큰들, 자리, "열")
match ㅇ결과 {
case Err(메) => return Err(메)
case Ok(ㅇ) => {
정렬열 = 열찾기(표.열들, ㅇ)
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 별표 {
let mut ㅇ = 0
while ㅇ < 표.열들.length { 뽑을열.push(ㅇ); 머리.push(표.열들[ㅇ].이름); ㅇ = ㅇ + 1 }
} else {
for 이름 in 고른열 {
let ㅇ번호 = 열찾기(표.열들, 이름)
if ㅇ번호 == -1 { return Err("없는 열입니다: " + 이름) }
뽑을열.push(ㅇ번호)
머리.push(이름)
}
}
// WHERE로 거른다.
// Filter by WHERE.
let mut 걸러진: Array<Array<값>> = []
for 행 in 표.행들 {
let mut 통과 = true
if 조건있다 {
let 비 = 값비교(행[조건열], 조건값)
통과 = 조건맞나(조건연산, 비)
}
if 통과 { 걸러진.push(행) }
}
// 정렬.
// Sort.
let 정렬됨 = if 정렬있다 { 정렬(걸러진, 정렬열, 내림) } else { 걸러진 }
// 뽑을 열만 투영.
// Project the selected columns.
let mut 결과행: Array<Array<값>> = []
for 행 in 정렬됨 {
let mut 새행: Array<값> = []
for ㅇ번호 in 뽑을열 { 새행.push(행[ㅇ번호]) }
결과행.push(새행)
}
Ok(표그리기(머리, 결과행))
}
}
}
// 문장 하나를 파싱하고 실행한다. 출력 HTML 조각을 돌려준다.
// Parse and run one statement. Returns its output HTML fragment.
function 문장하나(디비: Array<표>, 토큰들: Array<토큰>, 자리: Array<int>) -> Result<string, string> {
let 토 = 현재(토큰들, 자리)
if 토.종류 != "이름" { return Err("문장은 CREATE / INSERT / SELECT 로 시작해야 합니다") }
let 첫 = match 문장종류파싱(토.값) {
case Ok(ㅁ) => ㅁ
case Err(메) => return Err(메)
}
먹기(자리)
match 첫 {
case "CREATE" => {
match 만들기(디비, 토큰들, 자리) {
case Err(메) => Err(메)
case Ok(메) => Ok("<div class=\"ok\">" + 메 + "</div>")
}
}
case "INSERT" => {
match 넣기(디비, 토큰들, 자리) {
case Err(메) => Err(메)
case Ok(메) => Ok("<div class=\"ok\">" + 메 + "</div>")
}
}
case "SELECT" => {
match 고르기(디비, 토큰들, 자리) {
case Err(메) => Err(메)
case Ok(결과) => Ok(결과)
}
}
}
}
// 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 읽은 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
if 읽은 != 키 { return Err("'" + 키 + "' 키가 필요합니다") }
글기대(글자들, 위치, ":")
}
// 셀 하나 { "i": 정수 } 또는 { "s": "문자열" } 를 읽는다.
// Read one cell {"i":int} or {"s":"string"}.
function 셀하나읽기(글자들: Array<string>, 위치: Array<int>) -> Result<값, string> {
match 글기대(글자들, 위치, "\{") { case Ok(_) => {}; case Err(메) => return Err(메) }
let 키 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, ":") { case Ok(_) => {}; case Err(메) => return Err(메) }
let 좁힌키 = match 셀키파싱(키) {
case Ok(ㅋ) => ㅋ
case Err(메) => return Err(메)
}
match 좁힌키 {
case "i" => {
let 엔 = match 제이슨정수읽기(글자들, 위치) { case Ok(ㄴ) => ㄴ; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, "\}") { case Ok(_) => {}; case Err(메) => return Err(메) }
Ok({ 정수냐: true, 수: 엔, 글: "" })
}
case "s" => {
let ㅅ값 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, "\}") { case Ok(_) => {}; case Err(메) => return Err(메) }
Ok({ 정수냐: false, 수: 0, 글: ㅅ값 })
}
}
}
// 표 하나를 읽는다.
// Read one table object.
function 표하나읽기(글자들: Array<string>, 위치: Array<int>) -> Result<표, string> {
match 글기대(글자들, 위치, "\{") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "name") { case Ok(_) => {}; case Err(메) => return Err(메) }
let 표이름 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, ",") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "cols") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, "[") { case Ok(_) => {}; case Err(메) => return Err(메) }
let mut 열들: Array<열> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
match 글기대(글자들, 위치, "\{") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "name") { case Ok(_) => {}; case Err(메) => return Err(메) }
let ㅇ이름 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, ",") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "type") { case Ok(_) => {}; case Err(메) => return Err(메) }
let ㅇ타입문자열 = match 제이슨문자열읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
let ㅇ타입 = match 열타입파싱(ㅇ타입문자열, "") {
case Ok(ㅌ) => ㅌ
case Err(_) => return Err("열 타입은 INT 또는 TEXT여야 합니다")
}
match 글기대(글자들, 위치, "\}") { case Ok(_) => {}; case Err(메) => return Err(메) }
열들.push({ 이름: ㅇ이름, 타입: ㅇ타입 })
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속 = false }
else { return Err("열 목록에 , 또는 ] 가 필요합니다") }
}
}
match 글기대(글자들, 위치, ",") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "rows") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, "[") { case Ok(_) => {}; case Err(메) => return Err(메) }
let mut 행들: Array<Array<값>> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
match 글기대(글자들, 위치, "[") { case Ok(_) => {}; case Err(메) => return Err(메) }
let mut 행: Array<값> = []
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속2 = true
while 계속2 {
let 셀 = match 셀하나읽기(글자들, 위치) { case Ok(ㅅ) => ㅅ; case Err(메) => return Err(메) }
행.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("행 목록에 , 또는 ] 가 필요합니다") }
}
}
match 글기대(글자들, 위치, "\}") { case Ok(_) => {}; case Err(메) => 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<표> = []
match 글기대(글자들, 위치, "\{") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 키기대(글자들, 위치, "tables") { case Ok(_) => {}; case Err(메) => return Err(메) }
match 글기대(글자들, 위치, "[") { case Ok(_) => {}; case Err(메) => return Err(메) }
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치) }
else {
let mut 계속 = true
while 계속 {
let ㅍ = match 표하나읽기(글자들, 위치) { case Ok(ㅌ) => ㅌ; case Err(메) => return Err(메) }
if 표찾기(디비, ㅍ.이름) != -1 { return Err("테이블 이름이 겹칩니다: " + ㅍ.이름) }
디비.push(ㅍ)
공백넘기(글자들, 위치)
if 위치[0] < 글자들.length && 글자들[위치[0]] == "," { 먹기(위치) }
else if 위치[0] < 글자들.length && 글자들[위치[0]] == "]" { 먹기(위치); 계속 = false }
else { return Err("테이블 목록에 , 또는 ] 가 필요합니다") }
}
}
match 글기대(글자들, 위치, "\}") { case Ok(_) => {}; case Err(메) => 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>" }
let mut 결과 = ""
for 표 in 디비 {
let mut 머리: Array<string> = []
for 열 in 표.열들 { 머리.push(열.이름) }
결과 = 결과 + "<div class=\"ok\">테이블 '" + 이스케이프(표.이름) + "'</div>"
결과 = 결과 + 표그리기(머리, 표.행들)
}
결과
}
// 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 {
let 토 = 현재(토큰들, 자리)
if 토.종류 != "끝" {
출력 = 출력 + "<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()))