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
Ce fișiere are site-ul, ce drum parcurge o interogare SQL, cum imită Oracle și unde se modifică fiecare lucru.
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.
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.
Tot ce se publică se află în folderul site/. Fiecare fișier are o singură responsabilitate:
| Fișier | Ce face |
|---|---|
index.html | Pagina principală, de sus în jos: titlul, lecțiile cu exercițiile, apoi editorul SQL cu rezultatele și structura bazei de date. |
cum-functioneaza.html | Pagina aceasta: explică cum funcționează site-ul (fără JavaScript). |
css/style.css | Toate stilurile. Sus, în blocul :root, sunt toate culorile, fonturile și mărimile, ca variabile comentate. |
js/main.js | Porneș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.js | Afiș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.js | Textul celor 9 lecții și exercițiile lor (doar date, fără logică). |
js/lessons-ui.js | Afișează lecțiile pe pagină (butoanele numerotate, exemplele, exercițiile) și salvează progresul în browser. |
js/checker.js | Verifică exercițiile: compară rezultatul elevului cu rezultatul soluției corecte. |
data/seed.sql | Baza de date a liceului, scrisă ca SQL citibil: CREATE TABLE și INSERT. |
favicon.svg | Iconița din tab-ul browserului: semnul „>” mov pe negru, ca logo-ul. |
vendor/sql-wasm.jsvendor/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 ă, ș, ț). |
vendor/sql-wasm.js și js/main.js
Primul fișier aduce funcția initSqlJs, al doilea pornește logica paginii.
start()
Cere crearea bazei de date, apoi o testează cu SELECT 1 FROM DUAL și aprinde punctul verde.
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).
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.
showSchema()
Cere lista tabelelor cu getSchema() și completează panoul „Structura bazei de date”.
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).
<textarea id="sql-editor">)
Scrii SQL și apeși butonul „Rulează” sau Ctrl+Enter (tastele sunt prinse de handleEditorKeys()).
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ă.
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().
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.
NVL, TO_CHAR etc., apelează funcțiile JavaScript din db.js.
[{ columns, values }]
O listă cu câte un obiect pentru fiecare SELECT, de exemplu:
[{ columns: ['nume', 'prenume', 'oras'], values: [['Popa', 'Andrei', 'Brașov'], ...] }]
renderResults()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)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ă...”)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.”
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 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)
| Cheia străină | Arată spre | Înseamnă |
|---|---|---|
elevi.id_clasa | clase.id_clasa | Fiecare elev este într-o clasă; o clasă are mulți elevi. |
note.id_elev | elevi.id_elev | Fiecare notă aparține unui elev; un elev are multe note. |
note.id_materie | materii.id_materie | Fiecare notă este la o materie. |
materii.id_profesor | profesori.id_profesor | Profesorul care predă materia. |
clase.id_diriginte | profesori.id_profesor | Dirigintele clasei. |
profesori.id_sef | profesori.id_profesor | Șeful de catedră, tot un profesor (tabelul arată spre el însuși: „self-join”). |
LEFT JOIN, nu cu JOIN simplu.IS NULL și NVL.id_sef NULL: pentru self-join.LEFT JOIN și IS NULL.
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():
addOracleDual() creează tabelul dual cu un singur rând ('X'), ca SELECT 2 + 3 FROM DUAL să funcționeze.addOracleFunctions() le adaugă în SQLite cu create_function. Funcțiile cu un argument opțional (de exemplu TO_CHAR(valoare [, format])) trec prin addFunctionWithOptionalArgument().prepareOracleSql() rescrie SQL-ul înainte de rulare: SYSDATE devine SYSDATE(), COMMIT este scos (aici fiecare modificare se salvează imediat), iar ROLLBACK (nu are ce anula) și LIMIT (nu există în Oracle) dau o eroare în română.PRAGMA case_sensitive_like = ON: LIKE 'a%' nu mai găsește „Ana”, exact ca în Oracle (SQLite ignoră implicit literele mari).PRAGMA foreign_keys = ON: cheile străine sunt verificate (SQLite nu le verifică implicit), deci nu poți pune o notă unui elev care nu există.SUBSTR('Ana', 0, 1) dă 'A'), iar o poziție negativă numără de la sfârșit (SUBSTR('Popescu', -3) dă 'scu').translateError() din render.js traduce cele mai des întâlnite erori SQLite cu o listă de tipare (ERROR_TRANSLATIONS): tabel, coloană sau funcție care nu există, sintaxă greșită, comandă incompletă, constrângere încălcată, tabel care există deja, număr greșit de argumente. Un mesaj care nu este în listă apare neschimbat.NVLNVL2SYSDATETO_DATETO_CHARINITCAPMODTRUNCLPADRPADMONTHS_BETWEENADD_MONTHSUPPERLOWERSUBSTR
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.
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).
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.
checkAnswer() trimite exercițiul și SQL-ul elevului la checkExercise(exercise, userSql).
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ă.
runOnScratchDatabase(exercise.solution, checkQuery)
La fel pentru soluția corectă, pe o altă copie curată.
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.
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.
ORDER BY (orderMatters: true).normalizeValue()), deci 7 și 7.0 sunt egale.rowKey()), apoi cele două liste de texte sunt sortate și comparate.lastResultSet()).checkQuery)
citește tabelul (de exemplu SELECT * FROM elevi ORDER BY id_elev;). La fel se face după soluție,
iar cele două rezultate sunt comparate.
INSERT OR IGNORE
(un id repetat, un NULL într-o coloană NOT NULL, o valoare care nu respectă CHECK, o valoare
repetată la UNIQUE), apoi citește tabelul. OR IGNORE sare peste un rând care încalcă o constrângere, deci
dacă tabelul are constrângerile cerute, rândurile greșite nu apar; dacă lipsește una, ele apar și rezultatul diferă de al soluției.
Cheia străină se verifică separat, cu pragma_foreign_key_list (lista cheilor străine ale tabelului), pentru că
o cheie străină încălcată dă eroare chiar și cu OR IGNORE. Numele constrângerilor nu sunt verificate.
| Situația | Mesajul afișat |
|---|---|
| Rezultat identic cu al soluției | Corect! 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ă ordine | Valorile sunt corecte, dar coloanele nu sunt în ordinea cerută. |
Lipsește ORDER BY | Râ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ângere | După 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()) |
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ția | Oracle | Aici (SQLite) |
|---|---|---|
7 / 2 | 3.5 | 3 (împărțire întreagă; scrie 7 / 2.0) |
'a' || NULL | 'a' | NULL (CONCAT('a', NULL) dă 'a', ca în Oracle) |
Textul gol '' | este NULL | nu este NULL |
ORDER BY crescător | NULL-urile la final | NULL-urile la început (scrie NULLS LAST) |
Coloană negrupată în SELECT cu GROUP BY | eroare ORA-00979 | acceptată (valoare dintr-un rând oarecare) |
| Numele coloanelor în rezultat | cu majuscule (NUME) | cum le-ai scris (nume) |
VARCHAR2(n), NUMBER(p,s) | lungimea este verificată | lungimea nu este verificată |
| Datele calendaristice | tip DATE, cu oră | text 'YYYY-MM-DD'; SYSDATE fără oră |
data + 1 | ziua următoare | nu funcționează ('2020-01-01' + 1 dă 2021) |
COMMIT / ROLLBACK | salvează / anulează modificările | COMMIT este ignorat (modificările se salvează imediat); ROLLBACK dă o eroare în română; doar butonul „Resetează” aduce înapoi datele |
LIMIT 3 | nu 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 e | eroare (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_DATE | funcț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 SYSDATE | SYSDATE | SYSDATE() |
Eroare la UNIQUE sau FOREIGN KEY | arată numele constrângerii (de exemplu FK_NOTE_ELEV) | nu arată numele (doar erorile de CHECK îl arată) |
DUAL | tabel special al sistemului | tabel obișnuit, creat de db.js |
ROWNUMFETCH FIRST n ROWS(+) join vechiMINUSDATE '...'EXTRACTDECODETO_CHAR cu format de număr> ALL (...)= ANY (...)DELETE note WHERE ... fără FROM
Fiecare linie de cod are un comentariu, iar toate culorile și mărimile sunt variabile în :root, la începutul
fișierului css/style.css.
python3 serve.py (serverul proiectului, care dezactivează cache-ul browserului) și deschizi http://localhost:8000.
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ă schimbi | Caută (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 tab | index.html (și cum-functioneaza.html) |
| Numele site-ului de lângă logo („Playground”) | site name next to the logo | index.html (și cum-functioneaza.html) |
| Textul din subsol | site footer text | index.html (și cum-functioneaza.html) |
| Textul titlului mare de pe prima pagină | big title of the home page | index.html |
| Textul de sub titlul mare | short description of the site | index.html |
| Textul butonului „Rulează” | id="run-button" | index.html |
| Interogarea scrisă la început în editor | with a sample query | index.html |
Culoarea iconiței din tab (scrisă direct în fișier, nu în style.css) | icon sign color | favicon.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 tabel | CREATE TABLE elevi | data/seed.sql |
| Textul unei lecții | Lesson 1: (până la Lesson 9:) | js/lessons.js |
| Un exercițiu | Exercise 1.1: | js/lessons.js |
| Funcțiile Oracle adăugate | function addOracleFunctions | js/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.
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.
node tests/test-db.mjs: 207 verificări pentru db.js și translateError() (numărul de rânduri, constrângerile, fiecare funcție Oracle comparată cu valoarea dată de Oracle, COMMIT/ROLLBACK/LIMIT, comentariile, rândurile modificate, resetarea, traducerea erorilor).node tests/test-lessons.mjs: 171 de verificări pentru lecții și checker.js (fiecare exemplu rulează, fiecare soluție este acceptată, cel puțin un răspuns greșit la fiecare exercițiu este respins, iar la exercițiile cu CREATE TABLE sunt respinse tabelele fără o constrângere cerută sau cu un CHECK greșit).git commit + git push
Salvez modificările și le trimit pe GitHub.
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ă.