Cum funcționează site-ul?

Ce fișiere are site-ul, ce drum parcurge o interogare SQL, cum imită Oracle și unde se modifică fiecare lucru.

Pe scurt

SQL Playground este un site pe care înveți Oracle SQL direct în browser: citești o lecție scurtă, rulezi interogări reale pe baza de date a unui liceu și rezolvi exerciții verificate automat.

Ideea principală: nu există server de baze de date. Motorul SQL (SQLite, compilat în WebAssembly) rulează chiar în browserul vizitatorului, iar baza de date este creată în memoria calculatorului la fiecare deschidere a paginii. Fiecare vizitator are propria copie, așa că poate șterge sau modifica orice fără să strice nimic altcuiva.

Tehnologii folosite

Nu am folosit framework-uri, npm sau un pas de „build”: fișierele scrise de mine sunt exact fișierele pe care le primește browserul. Singura bibliotecă externă este sql.js, copiată în folderul vendor/, deci site-ul merge și fără internet după ce a fost încărcat.

Structura fișierelor

Tot ce se publică se află în folderul site/. Fiecare fișier are o singură responsabilitate:

FișierCe face
index.htmlPagina principală, de sus în jos: titlul, lecțiile cu exercițiile, apoi editorul SQL cu rezultatele și structura bazei de date.
cum-functioneaza.htmlPagina aceasta: explică cum funcționează site-ul (fără JavaScript).
css/style.cssToate stilurile. Sus, în blocul :root, sunt toate culorile, fonturile și mărimile, ca variabile comentate.
js/main.jsPornește pagina principală: creează baza de date, leagă butoanele (Rulează, Resetează, Ctrl+Enter) și pornește lecțiile.
js/db.jsÎncarcă sql.js, creează baza de date din seed.sql, adaugă funcțiile Oracle și rulează interogările.
js/render.jsAfișează rezultatele ca tabele (cel mult 500 de rânduri) și mesajele (eroare, succes, informație); traduce erorile SQLite în română (translateError()); scrie corect „1 rând”, „5 rânduri”, „40 de rânduri” (formatCount()).
js/lessons.jsTextul celor 9 lecții și exercițiile lor (doar date, fără logică).
js/lessons-ui.jsAfișează lecțiile pe pagină (butoanele numerotate, exemplele, exercițiile) și salvează progresul în browser.
js/checker.jsVerifică exercițiile: compară rezultatul elevului cu rezultatul soluției corecte.
data/seed.sqlBaza de date a liceului, scrisă ca SQL citibil: CREATE TABLE și INSERT.
favicon.svgIconița din tab-ul browserului: semnul „>” mov pe negru, ca logo-ul.
vendor/sql-wasm.js
vendor/sql-wasm.wasm
Biblioteca sql.js: motorul SQLite compilat în WebAssembly (nu e scris de mine).
fonts/Fontul JetBrains Mono, păstrat local (două fișiere: litere de bază și ă, ș, ț).

Drumul unei interogări

1. Când se deschide pagina

  1. index.html Încarcă vendor/sql-wasm.js și js/main.js Primul fișier aduce funcția initSqlJs, al doilea pornește logica paginii.
  2. js/main.js start() Cere crearea bazei de date, apoi o testează cu SELECT 1 FROM DUAL și aprinde punctul verde.
  3. js/db.js createDatabase() Încarcă motorul (sql-wasm.wasm) și descarcă textul din data/seed.sql cu fetch (cu cache: 'no-cache', ca o modificare a datelor să apară după F5).
  4. js/db.js buildDatabase() Creează o bază de date goală în memorie, pornește regulile Oracle (PRAGMA), adaugă tabelul DUAL (addOracleDual()), funcțiile Oracle (addOracleFunctions()) și rulează tot seed.sql.
  5. js/main.js showSchema() Cere lista tabelelor cu getSchema() și completează panoul „Structura bazei de date”.
  6. js/main.js + js/lessons-ui.js startLessons(tryInEditor) Înapoi în start(): butoanele „Rulează” și „Resetează” sunt pornite (până acum erau dezactivate), apoi startLessons() afișează lecțiile din lessons.js și redeschide ultima lecție deschisă (ținută minte în localStorage).

2. Când apeși „Rulează”

  1. index.html Editorul (<textarea id="sql-editor">) Scrii SQL și apeși butonul „Rulează” sau Ctrl+Enter (tastele sunt prinse de handleEditorKeys()).
  2. js/main.js runEditorQuery() Citește textul din editor și apelează runQuery(sql) într-un bloc try/catch, ca pagina să nu se strice la o greșeală.
  3. js/db.js runQuery(sql) Trece SQL-ul prin prepareOracleSql(sql), ține minte câte rânduri fuseseră modificate până atunci (pentru countChangedRows()) și îl rulează cu db.exec().
  4. js/db.js prepareOracleSql(sql) Adaptează SQL-ul Oracle: SYSDATE devine SYSDATE() și COMMIT este scos, iar ROLLBACK și LIMIT opresc comanda cu o eroare în română. Textele dintre apostrofuri sau ghilimele și comentariile nu sunt atinse.
  5. vendor/sql-wasm.wasm sql.js / SQLite (WebAssembly) Motorul execută SQL-ul pe baza de date din memorie. Când întâlnește NVL, TO_CHAR etc., apelează funcțiile JavaScript din db.js.
  6. rezultatul [{ columns, values }] O listă cu câte un obiect pentru fiecare SELECT, de exemplu: [{ columns: ['nume', 'prenume', 'oras'], values: [['Popa', 'Andrei', 'Brașov'], ...] }]
  7. js/main.js Alege ce se afișează
    • Are rânduri: renderResults()
    • Fără rânduri: showResultWithoutRows() scrie „Interogarea nu a returnat niciun rând” sau, pentru INSERT/UPDATE/DELETE, „N rânduri afectate” (countChangedRows() adună rândurile tuturor comenzilor din text)
    • Eroare: renderMessage(..., 'error') scrie „Eroare SQL: ...” în roșu; translateError() traduce întâi mesajul SQLite în română (de exemplu no such table: elev devine „Tabelul elev nu există...”)
  8. js/render.js renderResults(container, results) Construiește tabelul cu createResultTable(), createHeaderRow(), createBodyRows() și createCell(). Valorile NULL apar gri. Se desenează cel mult 500 de rânduri (MAX_ROWS), ca pagina să nu se blocheze; pentru mai multe apare „Se afișează primele 500 din N rânduri.”
  9. pagina Tabelul cu rezultate + „N rânduri” Apoi showSchema() reîmprospătează panoul cu tabele (poate ai creat sau șters un tabel).

Butoanele „Încearcă în editor” din lecții folosesc același drum: tryInEditor(sql) din main.js scrie exemplul în editor, apelează runEditorQuery() și derulează pagina până la editor.

Securitate: rezultatele sunt puse în pagină doar cu document.createElement și textContent, niciodată ca text HTML. Astfel, chiar dacă cineva scrie cod HTML într-o interogare, el apare ca text și nu se execută. La fel și lecțiile: appendTextWithCode() din lessons-ui.js transformă textul dintre semnele ` în elemente <code>, tot cu textContent.

Butonul „Resetează baza de date”: resetAll() din main.js apelează resetDatabase() din db.js, care închide baza veche și construiește una nouă tot cu buildDatabase().

Baza de date

Baza de date descrie un liceu. Este scrisă în data/seed.sql în stil Oracle: tipuri VARCHAR2, NUMBER, DATE și constrângeri cu nume (de exemplu pk_elevi, fk_note_elev).

PK cheie primară FK cheie străină (arată spre alt tabel)

profesori 15 rânduri

  • PK id_profesorNUMBER(4)
  • numeVARCHAR2(30) NOT NULL
  • prenumeVARCHAR2(30) NOT NULL
  • emailVARCHAR2(50) UNIQUE
  • data_angajariiDATE
  • salariuNUMBER(7,2)
  • FK id_sefprofesori

clase 8 rânduri

  • PK id_clasaNUMBER(4)
  • numeVARCHAR2(10) NOT NULL UNIQUE
  • an_studiuNUMBER(2) CHECK 9-12
  • profilVARCHAR2(30)
  • FK id_diriginteprofesori

materii 12 rânduri

  • PK id_materieNUMBER(4)
  • denumireVARCHAR2(40) NOT NULL
  • ore_pe_saptamanaNUMBER(2)
  • FK id_profesorprofesori

elevi 40 rânduri

  • PK id_elevNUMBER(5)
  • numeVARCHAR2(30) NOT NULL
  • prenumeVARCHAR2(30) NOT NULL
  • data_nasteriiDATE
  • orasVARCHAR2(30)
  • emailVARCHAR2(50)
  • FK id_clasaclase

note 153 rânduri

  • PK id_notaNUMBER(6)
  • FK id_elevelevi
  • FK id_materiematerii
  • notaNUMBER(2) CHECK 1-10
  • data_noteiDATE

Legăturile dintre tabele

Cheia străinăArată spreÎnseamnă
elevi.id_clasaclase.id_clasaFiecare elev este într-o clasă; o clasă are mulți elevi.
note.id_elevelevi.id_elevFiecare notă aparține unui elev; un elev are multe note.
note.id_materiematerii.id_materieFiecare notă este la o materie.
materii.id_profesorprofesori.id_profesorProfesorul care predă materia.
clase.id_diriginteprofesori.id_profesorDirigintele clasei.
profesori.id_sefprofesori.id_profesorȘeful de catedră, tot un profesor (tabelul arată spre el însuși: „self-join”).

Rânduri „speciale”, puse intenționat

Cum face site-ul să semene cu Oracle

Oracle adevărat nu poate rula într-un browser, așa că motorul este SQLite. Diferențele cele mai importante sunt acoperite în db.js, în funcția buildDatabase():

Funcțiile adăugate

Funcțiile pe care SQLite le are deja și care se comportă ca în Oracle nu au fost adăugate, dar pot fi folosite: LENGTH, ROUND, INSTR, REPLACE, TRIM, CONCAT, ABS și funcțiile de grup COUNT, SUM, AVG, MIN, MAX.

Exemplu: funcția NVL din db.js

/**
 * NVL(value, replacement): returns replacement when value is NULL, otherwise value.
 */
function nvl(value, replacement) {
  return value === null ? replacement : value; // NULL arrives in JavaScript as null
}

Când SQLite întâlnește NVL(email, 'fără email'), apelează această funcție JavaScript pentru fiecare rând. Valoarea NULL din SQL ajunge în JavaScript ca null: dacă value este null, funcția întoarce înlocuitorul, altfel întoarce valoarea neschimbată. Legătura dintre numele SQL și funcția JavaScript se face într-o singură linie: database.create_function('NVL', nvl).

Cum sunt verificate exercițiile

Fiecare exercițiu din js/lessons.js are un enunț (statement), soluția corectă (solution), un indiciu (hint), orderMatters (contează ordinea rândurilor?) și, la exercițiile cu INSERT/UPDATE/DELETE/CREATE, o interogare de control (checkQuery). Site-ul nu compară textul SQL, ci rezultatele: o problemă poate fi rezolvată corect în mai multe feluri.

  1. js/lessons-ui.js Butonul „Verifică” (sau Ctrl+Enter) checkAnswer() trimite exercițiul și SQL-ul elevului la checkExercise(exercise, userSql).
  2. js/checker.js runOnScratchDatabase(userSql, checkQuery) Creează o copie nouă a bazei de date cu createScratchDatabase(), rulează pe ea SQL-ul elevului (trecut prin prepareOracleSql()), apoi închide copia. Baza de date din editor nu este atinsă.
  3. js/checker.js runOnScratchDatabase(exercise.solution, checkQuery) La fel pentru soluția corectă, pe o altă copie curată.
  4. js/checker.js findDifference(userResult, expectedResult, exercise) Compară cele două rezultate, pe rând: numărul de coloane, numărul de rânduri, valorile (fără să conteze ordinea rândurilor) și, doar dacă orderMatters este true, ordinea rândurilor.
  5. js/lessons-ui.js Mesajul: verde sau roșu La exercițiile cu SELECT, sub mesaj apare și tabelul cu rândurile elevului. La cele cu INSERT/UPDATE/DELETE/CREATE, tabelul citit de interogarea ascunsă nu este afișat, iar mesajul explică în cuvinte ce diferă. Un exercițiu rezolvat primește „rezolvat” și este salvat în browser (localStorage, sub numele sqlPlayground.solved), deci progresul rămâne și după ce închizi pagina.

Regulile comparației

Exemple de mesaje

SituațiaMesajul afișat
Rezultat identic cu al soluțieiCorect! Exercițiul este rezolvat.
Lipsește o coloanăRezultatul tău are 2 coloane, dar ar trebui să aibă 3 coloane.
Lipsește WHERE oras = 'Brașov'Rezultatul tău are 40 de rânduri, dar ar trebui să aibă 22 de rânduri.
Coloanele în altă ordineValorile sunt corecte, dar coloanele nu sunt în ordinea cerută.
Lipsește ORDER BYRândurile sunt corecte, dar nu sunt în ordinea cerută. Verifică ORDER BY.
INSERT care nu a adăugat rândul cerut (tabelul elevi)După comanda ta, tabelul are 40 de rânduri, dar ar trebui să aibă 41 de rânduri.
Tabelul are alte valori sau (la CREATE TABLE) îi lipsește o constrângereDupă comanda ta, tabelul nu arată cum trebuie: unele valori sau constrângeri sunt diferite. ...
SQL greșit (de exemplu un tabel care nu există)Eroare SQL: Tabelul elev nu există. Verifică numele tabelului. (mesajul SQLite, tradus de translateExerciseError(), care apelează translateError())

Diferențe față de Oracle

SQLite seamănă mult cu Oracle, dar nu este identic. Lecțiile și exercițiile evită aceste cazuri; lista de mai jos este aici ca să fie clar ce nu se comportă la fel.

SituațiaOracleAici (SQLite)
7 / 23.53 (împărțire întreagă; scrie 7 / 2.0)
'a' || NULL'a'NULL (CONCAT('a', NULL) dă 'a', ca în Oracle)
Textul gol ''este NULLnu este NULL
ORDER BY crescătorNULL-urile la finalNULL-urile la început (scrie NULLS LAST)
Coloană negrupată în SELECT cu GROUP BYeroare ORA-00979acceptată (valoare dintr-un rând oarecare)
Numele coloanelor în rezultatcu majuscule (NUME)cum le-ai scris (nume)
VARCHAR2(n), NUMBER(p,s)lungimea este verificatălungimea nu este verificată
Datele calendaristicetip DATE, cu orătext 'YYYY-MM-DD'; SYSDATE fără oră
data + 1ziua următoarenu funcționează ('2020-01-01' + 1 dă 2021)
COMMIT / ROLLBACKsalvează / anulează modificărileCOMMIT este ignorat (modificările se salvează imediat); ROLLBACK dă o eroare în română; doar butonul „Resetează” aduce înapoi datele
LIMIT 3nu există (eroare)dă o eroare în română (în SQLite ar funcționa, dar nu se învață)
Text între ghilimele "abc"nume de coloană (eroare dacă nu există)acceptat ca text 'abc'
FROM elevi AS eeroare (aliasul unui tabel se scrie fără AS)acceptat
Dată scrisă ca text: data_nasterii >= '2009-03-15'poate da ORA-01861 (depinde de setările NLS); corect cu TO_DATEfuncționează și fără TO_DATE
TO_DATE(text, format)orice format (MON, ore, minute...)doar formate cu YYYY, MM și DD (de exemplu 'DD.MM.YYYY'); fără MON și fără ore
Numele coloanei pentru SELECT SYSDATESYSDATESYSDATE()
Eroare la UNIQUE sau FOREIGN KEYarată numele constrângerii (de exemplu FK_NOTE_ELEV)nu arată numele (doar erorile de CHECK îl arată)
DUALtabel special al sistemuluitabel obișnuit, creat de db.js

Nu sunt suportate (dau eroare)

Unde modific?

Fiecare linie de cod are un comentariu, iar toate culorile și mărimile sunt variabile în :root, la începutul fișierului css/style.css.

  1. Pornești site-ul cu python3 serve.py (serverul proiectului, care dezactivează cache-ul browserului) și deschizi http://localhost:8000.
  2. Deschizi fișierul din coloana „Fișier”, apeși Ctrl+F și cauți textul din coloana „Caută”.
  3. Schimbi valoarea, salvezi fișierul (Ctrl+S) și apeși F5 în browser: modificarea apare imediat.

Dublu-clic pe index.html (adresă file://) nu merge: browserul nu lasă sql.js să își încarce fișierul .wasm și baza de date nu pornește. Site-ul trebuie deschis prin server.

Ce vrei să schimbiCaută (Ctrl+F)Fișier
Culoarea mov a întregului site (semnul „>”, linkuri, margini, strălucire, fundalurile mov deschis, titlul mare, butoanele „Rulează” și „Verifică”)--color-accent:css/style.css
Doar strălucirea din jurul elementelor evidențiate--color-accent-glow:css/style.css
Doar marginea cardurilor--color-border:css/style.css
Culoarea de fundal a paginii--color-background:css/style.css
Fundalul cardurilor--color-surface:css/style.css
Culoarea textului--color-text:css/style.css
Culoarea titlurilor secțiunilor (h2, h3)--color-heading:css/style.css
Culorile titlului mare (gradient)--color-title-gradient:css/style.css
Culoarea butonului „Rulează” și a butoanelor „Verifică” din exerciții (împreună cu strălucirea lor)--color-button-primary:css/style.css
Culoarea mesajelor de eroare / de succes--color-error: / --color-success:css/style.css
Fontul--font-main:css/style.css
Mărimea textului normal--font-size-base:css/style.css
Mărimea titlului mare--font-size-title:css/style.css
Mărimea titlurilor secțiunilor--font-size-heading:css/style.css
Rotunjirea colțurilor--radius:css/style.css
Lățimea maximă a paginii--max-width:css/style.css
Textul din tab-ul browserului (titlul paginii)text shown in the browser tabindex.html (și cum-functioneaza.html)
Numele site-ului de lângă logo („Playground”)site name next to the logoindex.html (și cum-functioneaza.html)
Textul din subsolsite footer textindex.html (și cum-functioneaza.html)
Textul titlului mare de pe prima paginăbig title of the home pageindex.html
Textul de sub titlul mareshort description of the siteindex.html
Textul butonului „Rulează”id="run-button"index.html
Interogarea scrisă la început în editorwith a sample queryindex.html
Culoarea iconiței din tab (scrisă direct în fișier, nu în style.css)icon sign colorfavicon.svg
Mesajul „Baza de date este pregătită”Baza de date este pregătităjs/main.js
Datele din tabele (elevi, note...)DATA: elevi (sau DATA: note etc.)data/seed.sql
Coloanele unui tabelCREATE TABLE elevidata/seed.sql
Textul unei lecțiiLesson 1: (până la Lesson 9:)js/lessons.js
Un exercițiuExercise 1.1:js/lessons.js
Funcțiile Oracle adăugatefunction addOracleFunctionsjs/db.js

--color-accent schimbă tot mov-ul: celelalte nuanțe (strălucirea --color-accent-glow, fundalul mov deschis --color-accent-soft, marginile --color-border, cuvintele de cod, titlul mare și butonul „Rulează”) sunt calculate din el (cu color-mix() sau direct cu var(--color-accent)), deci se schimbă singure. Dacă vrei ca una dintre ele să aibă altă culoare, scrii o culoare în locul formulei de pe linia ei. Singura excepție este iconița din tab (favicon.svg), care nu poate citi variabilele din CSS. --font-size-base schimbă doar textul normal; titlurile, butoanele și editorul au mărimile lor.

Cum am testat

Pe lângă verificarea în browser (pe calculator și pe telefon), proiectul are teste automate în folderul tests/. Ele nu sunt publicate (se publică doar site/) și rulează în Node.js, pe aceleași fișiere JavaScript ca site-ul.

Publicare

  1. calculatorul meu git commit + git push Salvez modificările și le trimit pe GitHub.
  2. GitHub Depozitul (repository) proiectului Păstrează codul și istoricul tuturor modificărilor.
  3. Cloudflare Pages Publicare automată La fiecare push, publică folderul site/ așa cum este (fără „build”), în câteva secunde.

Pentru că toate fișierele sunt statice (HTML, CSS, JavaScript, SQL) și baza de date rulează în browser, site-ul nu are nevoie de un server propriu, de PHP sau de o bază de date găzduită.