← 제니앱

SQL DB

SQL을 인메모리 데이터베이스에 실행하고 결과를 표로 보여주며, 상태를 JSON으로 저장합니다.

CREATE TABLE, INSERT, SELECT을 지원하고 SELECT은 WHERE 조건과 ORDER BY 정렬을 받습니다. 셀은 INT와 TEXT만 씁니다. 저장된 JSON을 그대로 붙여 넣으면 복원합니다.

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("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;").replace("\"", "&quot;")
}

// 글자가 영문/숫자/밑줄인가(식별자에 쓸 수 있는 글자).
// 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()))
테이블 'people' 을 만들었습니다.
테이블 'people' 에 1행을 넣었습니다.
테이블 'people' 에 1행을 넣었습니다.
테이블 'people' 에 1행을 넣었습니다.
nameage
hana41
minji30
저장 (JSON)
{"tables":[{"name":"people","cols":[{"name":"id","type":"INT"},{"name":"name","type":"TEXT"},{"name":"age","type":"INT"}],"rows":[[{"i":1},{"s":"minji"},{"i":30}],[{"i":2},{"s":"jun"},{"i":24}],[{"i":3},{"s":"hana"},{"i":41}]]}]}