-- Self-test for the hand-written MySQL driver. Run with: ensure rp_dbtest local passed, failed = 0, 0 local function check(name, ok, detail) if ok then passed = passed + 1 print(('^2 PASS^7 %s'):format(name)) else failed = failed + 1 print(('^1 FAIL^7 %s^1 %s^7'):format(name, detail or '')) end end CreateThread(function() print('^5[dbtest]^7 waiting for the pool...') if not DB.WaitReady(30000) then print('^1[dbtest] database never became ready^7') return end -- 1. round trip through the text protocol ------------------------------- local two = DB.Scalar('SELECT 1 + 1') check('scalar arithmetic', two == 2, ('got %s (%s)'):format(tostring(two), type(two))) local row = DB.Single('SELECT ? AS s, ? AS n, ? AS f', { 'hello', 42, 1.5 }) check('typed columns', row and row.s == 'hello' and row.n == 42 and row.f == 1.5, row and json.encode(row) or 'nil') -- 2. hostile strings must survive verbatim ------------------------------ DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'") local nasty = [[O'Brien \ "quoted" ]] .. '\n\t' .. [[ 100% ` ; -- /* */ ?]] local id = DB.Insert( 'INSERT INTO accounts (username, password_hash) VALUES (?, ?)', { 'ztest_a', nasty }) check('insert returns id', type(id) == 'number' and id > 0, tostring(id)) local back = DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id }) check('hostile string round trip', back == nasty, ('stored %q'):format(tostring(back))) -- 3. a parameter can never become SQL ----------------------------------- local inject = "'); DROP TABLE accounts; -- " DB.Insert('INSERT INTO accounts (username, password_hash) VALUES (?, ?)', { 'ztest_b', inject }) local stillThere = DB.Scalar( "SELECT COUNT(*) FROM information_schema.tables WHERE table_schema = DATABASE() AND table_name = 'accounts'") check('injection is inert', stillThere == 1, 'accounts table missing!') local storedInject = DB.Scalar("SELECT password_hash FROM accounts WHERE username = 'ztest_b'") check('injection stored literally', storedInject == inject, tostring(storedInject)) -- 4. question marks inside literals must not shift binding --------------- local q = DB.Single("SELECT 'what? really?' AS lit, ? AS bound", { 'bound-value' }) check('literal ? not treated as placeholder', q and q.lit == 'what? really?' and q.bound == 'bound-value', q and json.encode(q) or 'nil') -- 5. NULL handling ------------------------------------------------------- DB.Update('UPDATE accounts SET last_ip = ? WHERE id = ?', { DB.NULL, id }) local isNull = DB.Scalar('SELECT last_ip IS NULL FROM accounts WHERE id = ?', { id }) check('DB.NULL writes a real NULL', isNull == 1, tostring(isNull)) local nilRow = DB.Single('SELECT last_ip FROM accounts WHERE id = ?', { id }) check('NULL reads back as nil', nilRow ~= nil and nilRow.last_ip == nil, nilRow and json.encode(nilRow) or 'nil') -- 6. unicode ------------------------------------------------------------- local uni = 'Ünïcødé — 日本語 — 🚓' DB.Update('UPDATE accounts SET password_hash = ? WHERE id = ?', { uni, id }) check('utf8mb4 round trip', DB.Scalar('SELECT password_hash FROM accounts WHERE id = ?', { id }) == uni) -- 7. empty result sets --------------------------------------------------- local none = DB.Query('SELECT * FROM accounts WHERE id = ?', { -1 }) check('empty select returns empty table', type(none) == 'table' and #none == 0) check('missing single returns nil', DB.Single('SELECT * FROM accounts WHERE id = ?', { -1 }) == nil) -- 8. transactions -------------------------------------------------------- local ok = DB.Transaction({ { query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'admin', id } }, { query = 'UPDATE accounts SET banned = ? WHERE id = ?', values = { 1, id } }, }) local after = DB.Single('SELECT role, banned FROM accounts WHERE id = ?', { id }) check('transaction commits', ok and after.role == 'admin' and after.banned == 1, json.encode(after or {})) local bad, err = DB.Transaction({ { query = 'UPDATE accounts SET role = ? WHERE id = ?', values = { 'mod', id } }, { query = 'UPDATE accounts SET nonexistent_column = 1 WHERE id = ?', values = { id } }, }) local rolled = DB.Scalar('SELECT role FROM accounts WHERE id = ?', { id }) check('failed transaction rolls back', bad == false and rolled == 'admin', ('role is now %s'):format(tostring(rolled))) -- 9. errors surface instead of failing silently -------------------------- local res, qerr = DB.Query('SELECT * FROM table_that_does_not_exist') check('bad query returns an error', res == nil and type(qerr) == 'string' and #qerr > 0, tostring(qerr)) -- 10. wrong parameter count is caught, not guessed ----------------------- local _, cerr = DB.Query('SELECT ?, ?', { 1 }) check('parameter count mismatch reported', cerr ~= nil, tostring(cerr)) -- 11. a large payload exercises multi-packet framing in both directions -- local big = string.rep('x', 700000) check('700KB payload round trip', DB.Scalar('SELECT ? AS big', { big }) == big) -- 12. concurrency: many queries in flight across the pool ---------------- local done, results = 0, {} for i = 1, 40 do DB.ScalarAsync('SELECT ?', { i }, function(v) done = done + 1; results[i] = v end) end local waited = 0 while done < 40 and waited < 10000 do Wait(50); waited = waited + 50 end local allGood = done == 40 for i = 1, 40 do if results[i] ~= i then allGood = false end end check('40 concurrent queries all answered correctly', allGood, ('done=%d'):format(done)) DB.Update("DELETE FROM accounts WHERE username LIKE 'ztest%'") print(('^5[dbtest]^7 ==== %d passed, %d failed ===='):format(passed, failed)) if failed == 0 then print('^2[dbtest] driver OK^7') else print('^1[dbtest] DRIVER HAS PROBLEMS^7') end end)