← 제니앱

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, 기대기호=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 글 == "&" { 결과 = 결과 + "&amp;" }
    else if 글 == "<" { 결과 = 결과 + "&lt;" }
    else if 글 == ">" { 결과 = 결과 + "&gt;" }
    else if 글 == "\"" { 결과 = 결과 + "&quot;" }
    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()))
테이블 '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}]]}]}