From f949f5e2af3cbea192e0691d3fd7a0dc1667b61a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 2 Sep 2026 15:17:14 +0000 Subject: [PATCH 1/2] Agrega soporte de lenguaje SQL (sql-sqlite3-wandbox) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extiende el registro de lenguajes de la plataforma (src/lib/code-languages) con `LanguageId = "sql"` y `ExecutionProfileId = "sql-sqlite3-wandbox"`: perfil Monaco `sql`, fence `sql`, extensión `.sql`, `main.sql`. Migración Prisma aditiva (`ALTER TYPE ... ADD VALUE`) agrega `sql` al enum `ProgrammingLanguage`, sin tocar IDs ni progreso existente. Wandbox (src/lib/executor/wandbox.ts) gana el perfil `sql-sqlite3-wandbox`: - `runTests` implementa la semántica de fixtures SQL (sólo para este perfil): `effectiveSource = TestCase.stdin + "\n" + sourceCode`, stdin real enviado a sqlite = "". C++ y C# no cambian su semántica de stdin. - `execute()` (run libre) no reinterpreta stdin: manda el SQL tal cual. - SIN `DEFAULT_SQL_COMPILER` congelado: el preflight que pide el contrato técnico (consultar /api/list.json, elegir versión estable no-*-head, smoke real) no pudo completarse — la política de red de este entorno bloqueó la salida a wandbox.org. En vez de inventar un id, el perfil exige `WANDBOX_SQL_COMPILER` por env (mismo patrón que JUDGE0_CSHARP_LANGUAGE_ID/PISTON_CSHARP_VERSION) y falla cerrado con `ExecutorProfileUnavailableError` hasta que se configure. judge0.ts y piston.ts ya fallaban cerrado para perfiles desconocidos — sin cambios. UI: NEEDS_STDIN_RE agrega `sql` (nunca matchea: un script no lee stdin interactivo); el campo de entrada libre y el panel de tests visibles relabelan a "Preparación SQL" en vez de "Entrada"/"stdin" para cursos SQL (TECHNICAL_CONTRACT §4). Editor: completions de SQL (keywords/snippets del subconjunto DDL/DML/consultas del curso), tokenizer para fill_blank, clasificación de errores de compilación (SQLite) y diagnostics (sin marcadores de línea: SQLite no da archivo/línea/columna, se muestra el stderr legible). scripts/verify-content.ts gana un runner de SQL con `node:sqlite` (DatabaseSync) — no hay `sqlite3` CLI en este entorno de verificación; `node:sqlite` es el mecanismo "ya disponible más barato/estable" que pide el contrato, sin introducir una base compartida (una `:memory:` nueva por caso). Usa `setReturnArrays(true)` para evitar que dos columnas con el mismo nombre (`a.nombre, b.nombre`) colisionen en un objeto JS — sqlite3 CLI imprime por posición, no por nombre. Tipos ambientales mínimos en scripts/node-sqlite.d.ts (@types/node en este repo no los trae todavía). Tests nuevos: sql es LanguageId válido, sql-sqlite3-wandbox pertenece a sql, fence sql resuelve, Wandbox soporta el perfil (y falla cerrado sin compiler configurado), fixture se concatena SÓLO para SQL, dos fixtures ocultos con la misma query producen peticiones distintas, C++/C# no cambian payload, Judge0/Piston fallan cerrado para SQL. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011GYhqUig6NqJyJV6VrFDjX --- .env.example | 7 + .../migration.sql | 6 + prisma/schema.prisma | 1 + scripts/node-sqlite.d.ts | 24 ++++ scripts/verify-content.ts | 105 ++++++++++++++ src/components/editor/code-playground.tsx | 6 +- src/components/editor/completions/index.ts | 3 + src/components/editor/completions/sql.ts | 79 +++++++++++ src/components/editor/diagnostics.ts | 12 ++ src/components/exercise/example-tests.tsx | 18 ++- src/env.ts | 10 ++ .../components/step-code-challenge.tsx | 2 +- .../lessons/components/step-code-example.tsx | 5 + src/features/lessons/lib/code-syntax.tsx | 15 ++ src/features/lessons/lib/code-validation.ts | 1 + .../practice/components/practice-viewer.tsx | 2 +- src/lib/analytics/error-category.ts | 20 +++ src/lib/code-languages/index.ts | 24 +++- src/lib/executor/index.ts | 4 + src/lib/executor/wandbox.ts | 82 ++++++++++- tests/lib/code-languages.test.ts | 16 +++ tests/lib/executor/profiles.test.ts | 134 ++++++++++++++++++ 22 files changed, 562 insertions(+), 14 deletions(-) create mode 100644 prisma/migrations/20260902130000_add_sql_language/migration.sql create mode 100644 scripts/node-sqlite.d.ts create mode 100644 src/components/editor/completions/sql.ts diff --git a/.env.example b/.env.example index 9564e96..e741403 100644 --- a/.env.example +++ b/.env.example @@ -42,6 +42,13 @@ WANDBOX_COMPILER_OPTIONS="" WANDBOX_CSHARP_COMPILER="" # default mono-6.12.0.199 WANDBOX_CSHARP_OPTIONS="" # vacío: el subconjunto del curso no necesita flags +# Perfil sql-sqlite3-wandbox — SIN default. Verifica el compiler SQLite/SQLite3 +# estable (no *-head) contra https://wandbox.org/api/list.json y ponlo aquí. +# Sin esta variable, el perfil SQL queda registrado pero NO disponible para +# ejecutar (mismo patrón que JUDGE0_CSHARP_LANGUAGE_ID / PISTON_CSHARP_VERSION). +WANDBOX_SQL_COMPILER="" +WANDBOX_SQL_OPTIONS="" # vacío: el subconjunto SQL del curso no necesita flags + # piston / piston-selfhosted PISTON_URL="" # obligatorio con piston-selfhosted (ej http://127.0.0.1:2000) PISTON_CPP_VERSION="" diff --git a/prisma/migrations/20260902130000_add_sql_language/migration.sql b/prisma/migrations/20260902130000_add_sql_language/migration.sql new file mode 100644 index 0000000..065e80c --- /dev/null +++ b/prisma/migrations/20260902130000_add_sql_language/migration.sql @@ -0,0 +1,6 @@ +-- Base de Datos I: agrega `sql` al enum de lenguajes soportados. +-- ALTER TYPE ... ADD VALUE es seguro (idempotente con IF NOT EXISTS) y el +-- valor nuevo no se usa en esta misma migración — el seed crea el Course +-- `bases-de-datos` en una corrida posterior. + +ALTER TYPE "ProgrammingLanguage" ADD VALUE IF NOT EXISTS 'sql'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 67eefcc..6a25abc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -164,6 +164,7 @@ model Verification { enum ProgrammingLanguage { cpp csharp + sql } model Course { diff --git a/scripts/node-sqlite.d.ts b/scripts/node-sqlite.d.ts new file mode 100644 index 0000000..7165256 --- /dev/null +++ b/scripts/node-sqlite.d.ts @@ -0,0 +1,24 @@ +// `node:sqlite` es experimental desde Node 22.5 (sin dependencias +// externas). `@types/node` en este repo está fijado en la línea 20 y no +// trae sus tipos todavía. Declaración ambiental mínima con SÓLO lo que +// `scripts/verify-content.ts` usa — la incompatibilidad real es de versión +// de TIPOS, no de runtime: `node --version` en este entorno ya es 22.x y el +// módulo existe. +declare module "node:sqlite" { + export class StatementSync { + /** + * Filas como ARRAY posicional en vez de objeto por nombre de columna. + * Necesario: dos columnas con el mismo nombre (`a.nombre, b.nombre`) + * colisionarían en un solo objeto JS y perderían una — sqlite3 CLI no + * tiene ese problema porque imprime por posición, no por nombre. + */ + setReturnArrays(value: boolean): void; + all(): unknown[][]; + } + export class DatabaseSync { + constructor(location: string); + exec(sql: string): void; + prepare(sql: string): StatementSync; + close(): void; + } +} diff --git a/scripts/verify-content.ts b/scripts/verify-content.ts index 779f488..96a7334 100644 --- a/scripts/verify-content.ts +++ b/scripts/verify-content.ts @@ -17,6 +17,13 @@ * Toolchains (locales, NO el proveedor de producción): * cpp → g++ -std=c++17 -O0 -Wall * csharp → mcs + mono (mismo compilador que el perfil csharp-mono-6.12) + * sql → node:sqlite (DatabaseSync), una base efímera en memoria POR CASO + * + * SQL no usa el `sqlite3` CLI: no está instalado en este entorno de + * verificación (ni necesariamente en CI). `node:sqlite` (disponible desde + * Node 22, sin dependencias externas) es el mecanismo "ya disponible más + * barato/estable" que pide TECHNICAL_CONTRACT §5 — sin introducir una base + * compartida: cada caso abre y descarta su propia `:memory:`. * * Uso: * npx tsx scripts/verify-content.ts # todos los cursos @@ -26,6 +33,7 @@ import { execFile } from "node:child_process"; import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { DatabaseSync } from "node:sqlite"; import { promisify } from "node:util"; import { allCourses } from "../prisma/content"; @@ -33,6 +41,10 @@ import { allPracticeSets } from "../prisma/content/exercises"; import type { LanguageId } from "../src/lib/code-languages"; import { normalizeOutput } from "../src/lib/executor/normalize"; +// Los tipos de `node:sqlite` viven en `scripts/node-sqlite.d.ts` — ver ese +// archivo para el porqué (incompatibilidad de versión de `@types/node`, +// no de runtime). + const run = promisify(execFile); const RUN_TIMEOUT_MS = 15_000; @@ -92,6 +104,10 @@ interface Failure { async function compileAndRun(unit: Unit, dir: string): Promise { const failures: Failure[] = []; + if (unit.language === "sql") { + return runSqlCases(unit); + } + if (unit.language === "cpp") { const src = join(dir, "main.cpp"); const bin = join(dir, "program"); @@ -127,13 +143,102 @@ async function compileAndRun(unit: Unit, dir: string): Promise { * ¿El programa lee de la entrada estándar? Un ejemplo interactivo no se * puede verificar por salida sin darle una entrada que el contenido no * declara. + * + * SQL no tiene equivalente: un script no lee entrada interactiva mientras + * corre (su "stdin" es el fixture, ver TECHNICAL_CONTRACT §4) — siempre + * `false`. */ function readsStdin(code: string, language: LanguageId): boolean { + if (language === "sql") return false; return language === "cpp" ? /\bcin\s*>>|\bscanf\s*\(|\bgetline\s*\(|\bfgets\s*\(/.test(code) : /\bConsole\s*\.\s*Read(Line|Key)?\s*\(/.test(code); } +/** + * Ejecuta los casos de una pieza SQL. A diferencia de C++/C#, aquí NO hay + * un binario compartido: cada caso construye su propio `effectiveSource` + * (fixture + solución, exactamente como `WandboxExecutor.runTests` — ver + * TECHNICAL_CONTRACT §4) y corre contra una `:memory:` NUEVA. Dos casos del + * mismo ejercicio nunca comparten estado. + */ +function runSqlCases(unit: Unit): Failure[] { + const failures: Failure[] = []; + for (const testCase of unit.cases) { + const label = testCase.description + ? `${unit.id} [${testCase.description}]` + : `${unit.id} [fixture=${JSON.stringify(testCase.stdin)}]`; + const effectiveSource = `${testCase.stdin}\n${unit.code}`; + + const db = new DatabaseSync(":memory:"); + try { + const actual = runSqlScript(db, effectiveSource); + if (!testCase.compareOutput) continue; + const expected = normalizeOutput(testCase.expectedStdout); + const normalizedActual = normalizeOutput(actual); + if (normalizedActual !== expected) { + failures.push({ + id: label, + kind: "output", + detail: `esperado:\n${expected}\nobtenido:\n${normalizedActual}`, + }); + } + } catch (err) { + failures.push({ + id: label, + kind: "runtime", + detail: err instanceof Error ? err.message : String(err), + }); + } finally { + db.close(); + } + } + return failures; +} + +/** + * Corre un script con múltiples sentencias contra una DB abierta y devuelve + * su salida en el formato "list mode" del CLI de SQLite (el que asumen los + * `expectedStdout` del paquete): sin encabezados, columnas separadas por + * `|`, NULL como cadena vacía, una fila por línea. Sólo las sentencias + * `SELECT` producen salida — el resto (DDL/DML/PRAGMA de configuración) se + * ejecuta por su efecto. + */ +function runSqlScript(db: DatabaseSync, script: string): string { + const lines: string[] = []; + for (const statement of splitSqlStatements(script)) { + if (/^select\b/i.test(statement)) { + const stmt = db.prepare(statement); + stmt.setReturnArrays(true); + for (const row of stmt.all()) { + lines.push(row.map(formatSqlValue).join("|")); + } + } else { + db.exec(statement); + } + } + return lines.join("\n"); +} + +/** `NULL` imprime como cadena vacía en list mode; todo lo demás, su texto. */ +function formatSqlValue(value: unknown): string { + if (value === null || value === undefined) return ""; + return String(value); +} + +/** + * Separa un script en sentencias individuales por `;`. Naïve a propósito + * (sin parser SQL, ver TECHNICAL_CONTRACT §7): el contenido del curso no + * usa `;` dentro de literales ni comentarios con `;`, así que partir por el + * delimitador es suficiente y exactamente lo que hace `sqlite3 < script.sql`. + */ +function splitSqlStatements(script: string): string[] { + return script + .split(";") + .map((s) => s.trim()) + .filter((s) => s.length > 0); +} + async function runCase( unit: Unit, command: string, diff --git a/src/components/editor/code-playground.tsx b/src/components/editor/code-playground.tsx index 93f547e..44e8c3e 100644 --- a/src/components/editor/code-playground.tsx +++ b/src/components/editor/code-playground.tsx @@ -70,7 +70,7 @@ export function CodePlayground({ {showStdin ? (