I run a small free study site for CBSE Class 11 and 12 students in India. SQL is part of their syllabus, and the syllabus is written around MySQL. The problem: most students practise on a phone or a locked school computer, where installing MySQL is not an option.

So I built a SQL lab that runs fully in the browser. It uses sql.js (SQLite compiled to WebAssembly) with a translation layer on top, so that students type MySQL and get MySQL-style answers. No server, no login.

"SQLite is close enough to MySQL" turned out to be wrong in ways that matter to a beginner. Here are the gaps that cost me the most time.

1. String comparison

In MySQL, with the default collation, this finds Amit:

SELECT * FROM student WHERE name = 'amit'; 

In SQLite it returns nothing, because text comparison is case-sensitive. A student who gets zero rows here will "learn" something that is false in their exam.

Declaring text columns as TEXT COLLATE NOCASE fixes column-to-literal comparisons, but not everything: function results and literal-to-literal comparisons lose the collation. I ended up adding COLLATE NOCASE to string literals and IN lists during translation as well.

2. SQLite accepts almost anything in any column

INSERT INTO student (roll) VALUES ('abc'); 

MySQL in strict mode rejects this with error 1366. SQLite happily stores the text in an INTEGER column. Since students are learning what data types mean, silent acceptance is the worst possible behaviour.

The layer wraps every inserted or updated value in a converter function chosen from the column's declared MySQL type. The converter either returns a clean value or raises the MySQL error.

3. Missing functions, and two sql.js surprises

MySQL functions such as MID(), DAYNAME(), NOW() and TRUNCATE() do not exist in SQLite, and a few that do exist behave differently. I registered my own versions with db.create_function() under a prefix, and the translator rewrites calls to the prefixed names.

Two things I did not expect:

  • If your JavaScript function throws an Error object, the message that reaches you from sql.js is empty. Throw a plain string and it comes through.
  • db.export() gives you the database bytes, but a database loaded from them has none of your custom functions. I wanted students' tables to survive a page reload, so I save my own JSON dump of the schema and rows to localStorage and rebuild from that.

4. Error messages are part of the lesson

no such function: MIDD is a fine message, but it is not what the student will see in the school lab. The layer maps SQLite errors onto MySQL ones:

ERROR 1305 (42000): FUNCTION school.MIDD does not exist 

Same for unknown tables (1146), unknown columns (1054) and wrong argument counts (1582).

How I tested it

I did not trust myself to remember MySQL behaviour, so I wrote about 200 statements covering the syllabus and ran each one both through the lab and through a real database server, then compared the outputs cell by cell. Every mismatch was either fixed or written down as a known limitation.

Then I fuzzed it: around 6,300 broken or mutated statements. That found an infinite loop on an unclosed USING ( and a few crashes on half-typed statements such as a bare INSERT INTO. Beginners type exactly that kind of thing.

What it still cannot do

Stored procedures, triggers, user variables and multi-table UPDATE/DELETE are not supported. They are outside the school syllabus, so I left them out rather than fake them.

If you want to try it, the lab is here: csip12.in/sql-lab. It comes with a small sample database, and I would be glad to hear about any query where it disagrees with real MySQL.