Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 | 1x 29x 9x 9x 9x 1x 8x 5x 5x 11x | /** * 이 파일이 하는 일: * 학생이 작성한 SQL을 브라우저 안에서 실행하고, 기대 결과와 비교해 채점한다. * * 핵심 아이디어 — sql.js (SQLite를 WebAssembly로 컴파일한 것). * 학습 포인트: "SQL을 실행한다"고 하면 서버 DB부터 떠올리기 쉽지만, 그건 위험하다. * 학생 SQL에 DROP TABLE이 들어 있으면? 서버가 학생마다 DB를 만들어 줘야 하나? * sql.js는 진짜 SQLite 엔진을 브라우저 안에서 돌린다. 문제를 열 때마다 * 메모리 위에 1회용 DB를 새로 만들고, 닫으면 사라진다. * → 무엇을 실행하든 서버에는 아무 일도 일어나지 않는다. DROP도 DELETE도 자유. * → runJs.js(Web Worker)와 같은 철학이다: "남의 코드는 그 사람의 브라우저에서만". * * JS 채점(runJs.js)과 달리 Web Worker를 쓰지 않는 이유: * SELECT는 코드가 아니라 질의라서 무한루프를 만들기가 어렵고(재귀 CTE 정도가 예외), * 데이터가 몇 줄 안 돼 실행이 순식간이다. Worker로 감싸는 복잡함이 이득보다 크다. * — 같은 문제(격리)라도 위험의 크기에 따라 장치의 무게를 달리한다. */ import initSqlJs from 'sql.js'; // 학습 포인트: ?url 은 Vite 문법 — "이 파일을 번들에 포함하고, 그 주소(URL)를 달라". // WASM은 JS처럼 import할 수 없어서, 주소를 sql.js에 알려 주고 직접 내려받게 한다. import sqlWasmUrl from 'sql.js/dist/sql-wasm.wasm?url'; /** WASM 로딩은 느리다(수백 KB) — 한 번만 하고 계속 재사용한다(캐싱). */ let sqlJsPromise = null; function loadSqlJs() { if (!sqlJsPromise) { sqlJsPromise = initSqlJs({ locateFile: () => sqlWasmUrl }); } return sqlJsPromise; } /** * 준비 SQL(테이블+데이터)을 깐 1회용 DB에서 학생 쿼리를 실행한다. * * @param {string} setupSql CREATE TABLE + INSERT (문제가 제공) * @param {string} query 학생이 작성한 SQL * @returns {Promise<{columns?: string[], rows?: Array[], error?: string}>} */ export async function runSql(setupSql, query) { const SQL = await loadSqlJs(); const db = new SQL.Database(); // 메모리 위 1회용 DB — 새 것으로 시작 try { db.run(setupSql); // exec는 [{columns, values}] 배열을 돌려준다. 문장이 여러 개면 결과도 여러 개인데, // 채점은 "마지막 SELECT의 결과"로 한다 — 학생이 확인용 문장을 앞에 써도 괜찮도록. const results = db.exec(query); if (results.length === 0) { return { error: '결과가 없습니다. SELECT 문을 작성했는지 확인해 보세요.' }; } const last = results[results.length - 1]; return { columns: last.columns, rows: last.values }; } catch (e) { // SQLite의 에러 메시지는 영어지만 원문 그대로 보여준다 — // "near \"FORM\": syntax error" 같은 메시지를 읽어내는 것도 배워야 할 기술이다. return { error: 'SQL 오류: ' + e.message }; } finally { db.close(); // WASM 메모리는 가비지 컬렉터가 못 치운다 — 반드시 직접 닫는다. } } /** * 실행 결과를 기대 결과와 비교한다. * * 채점 규칙: * - 값만 비교한다. 컬럼 "이름"은 비교하지 않는다 — SELECT salary AS 연봉 처럼 * 별칭을 붙여도 정답이다. (컬럼 개수가 다르면 행 내용이 달라져 어차피 틀린다) * - orderMatters가 false면 행 순서는 무시한다. ORDER BY를 요구하는 문제만 true로 둔다. * * 학습 포인트: 행 순서를 무시하는 비교는 "정렬해서 비교"로 구현한다. * 두 결과를 같은 규칙으로 정렬해 놓으면, 내용이 같은지 순서와 무관하게 확인할 수 있다. * * @param {{columns: string[], rows: Array[]}} actual 학생 쿼리의 실행 결과 * @param {{expectedRows: Array[], orderMatters: boolean}} expected 문제의 기대 결과 * @returns {boolean} */ export function gradeSql(actual, expected) { // 행을 JSON 문자열로 바꿔 비교한다 — runJs.js의 배열 비교와 같은 요령이다. const normalize = (rows) => rows.map((row) => JSON.stringify(row)); const actualRows = normalize(actual.rows); const expectedRows = normalize(expected.expectedRows); if (actualRows.length !== expectedRows.length) { return false; } if (!expected.orderMatters) { actualRows.sort(); expectedRows.sort(); } return actualRows.every((row, i) => row === expectedRows[i]); } |