Skip to content

Recipes

The stuff you'll actually reach for, ready to copy. Examples use the JS Promise API (exports.vSQL.*); the Lua MySQL.* wrapper mirrors every method - append .await inside a thread, or pass a callback as the last argument.

Always parameterise

Every value goes in as a bound ? / @name parameter. Never paste user input into a query string - that's exactly how SQL injection happens, and vSQL's parameters mean you never have to.

Reading

One row, one value, or a list

js
const player  = await exports.vSQL.single('SELECT * FROM players WHERE id = ?', [id]);   // row or null
const balance = await exports.vSQL.scalar('SELECT money FROM players WHERE id = ?', [id]); // value or null
const all     = await exports.vSQL.query('SELECT * FROM players WHERE job = ?', ['police']); // rows[]
MethodReturns
singlethe first row, or null
scalarthe first column of the first row, or null
queryan array of rows

IN (...) with an array

js
// the array expands to (?, ?, ?) automatically - one binding per element
const rows = await exports.vSQL.query('SELECT * FROM vehicles WHERE plate IN ?', [[a, b, c]]);

Named parameters

js
await exports.vSQL.single(
  'SELECT * FROM players WHERE citizenid = @id AND job = :job',
  { id: citizenid, job: 'police' }
);

Both @name and :name work, and you can mix them. Pass the values as an object.

Pagination

js
const page = 2, perPage = 20;
const rows = await exports.vSQL.query(
  'SELECT * FROM players ORDER BY id LIMIT ? OFFSET ?',
  [perPage, (page - 1) * perPage]
);

Writing

Insert and get the new id

js
const id = await exports.vSQL.insert(
  'INSERT INTO players (citizenid, name) VALUES (?, ?)',
  [citizenid, name]
);

MariaDB 10.5+

You can return columns in a single round-trip - check exports.vSQL.serverInfo().supportsReturning:

js
const [row] = await exports.vSQL.query(
  'INSERT INTO players (citizenid, name) VALUES (?, ?) RETURNING id, created_at',
  [citizenid, name]
);

Update / delete (affected rows)

js
const changed = await exports.vSQL.update('UPDATE players SET money = money - ? WHERE id = ?', [50, id]);
const removed = await exports.vSQL.update('DELETE FROM inventory WHERE id = ?', [itemId]); // update() covers DELETE

update returns the affected-row count and works for both UPDATE and DELETE.

Upsert (insert or update on duplicate key)

The upsert helper builds INSERT ... ON DUPLICATE KEY UPDATE for you (values bound, identifiers escaped). It returns affected rows - 1 for an insert, 2 for an update:

js
// on a PRIMARY/UNIQUE-key conflict, overwrite every inserted column
await exports.vSQL.upsert('player_stats', { citizenid, kills: 5, deaths: 2 });

// or narrow which columns the conflict branch touches
await exports.vSQL.upsert('player_stats', { citizenid, kills: 5 }, ['kills']);

Need a non-trivial conflict update (e.g. kills = kills + VALUES(kills))? Drop to raw SQL:

js
await exports.vSQL.query(
  `INSERT INTO player_stats (citizenid, kills) VALUES (?, ?)
   ON DUPLICATE KEY UPDATE kills = kills + VALUES(kills)`,
  [citizenid, 1]
);

Soft delete

js
// "delete"
await exports.vSQL.update('UPDATE players SET deleted_at = NOW() WHERE id = ?', [id]);
// read live rows only
const live = await exports.vSQL.query('SELECT * FROM players WHERE deleted_at IS NULL');

CRUD helpers (no SQL)

For the boring cases, don't write SQL at all - these build a parameterised statement for you (values bound, identifiers escaped). The moment you need anything past equality / IN / NULL conditions, drop back to raw query.

js
// insert one row (or pass an array of objects for a bulk insert)
const id = await exports.vSQL.insertInto('players', { citizenid, name });

// insert and get the whole row back - one round-trip on MariaDB 10.5+
// (INSERT ... RETURNING), insert-then-select on MySQL
const row = await exports.vSQL.insertAndFetch('players', { citizenid, name });
// pick columns / set a non-'id' key column for the MySQL fallback:
const row2 = await exports.vSQL.insertAndFetch('players', { citizenid, name }, {
  returning: ['id', 'created_at'],
  idColumn: 'id'
});

// update / delete by a WHERE object (a WHERE is required - no accidental
// full-table writes)
await exports.vSQL.updateWhere('players', { money: 500 }, { id });
await exports.vSQL.deleteWhere('inventory', { id: itemId });

// read: find() returns rows, findOne() the first row or null
const police = await exports.vSQL.find('players', { job: 'police' }, { orderBy: 'name', limit: 20 });
const player = await exports.vSQL.findOne('players', { id });

// count rows matching a WHERE (or the whole table)
const officers = await exports.vSQL.count('players', { job: 'police' });
const total = await exports.vSQL.count('players');

// insert, or update the existing row on a PRIMARY/UNIQUE-key conflict
await exports.vSQL.upsert('player_stats', { citizenid, kills: 5 }, ['kills']);

The WHERE object ANDs its conditions; an array value becomes IN (...), and null becomes IS NULL. Need OR or a comparison? There's a raw escape hatch:

js
const rich = await exports.vSQL.find('players', ['money > ? AND job = ?', [1000, 'police']]);

Transactions & batches

Transfer money atomically (transaction)

js
await exports.vSQL.transaction(async (tx) => {
  const from = await tx.single('SELECT money FROM players WHERE id = ? FOR UPDATE', [fromId]);
  if (from.money < amount) throw new Error('insufficient funds'); // throwing rolls back
  await tx.update('UPDATE players SET money = money - ? WHERE id = ?', [amount, fromId]);
  await tx.update('UPDATE players SET money = money + ? WHERE id = ?', [amount, toId]);
});

WARNING

Transactions auto-retry on deadlock, so the callback may run more than once. Keep non-DB side effects (HTTP calls, events, in-memory mutation) out of the body - put them after the transaction resolves.

You can also pass an array of statements instead of a callback:

js
await exports.vSQL.transaction([
  ['UPDATE players SET money = money - ? WHERE id = ?', [amount, fromId]],
  ['UPDATE players SET money = money + ? WHERE id = ?', [amount, toId]],
]);

Bulk insert (one statement per row, atomic)

js
await exports.vSQL.batch('INSERT INTO logs (player, action) VALUES (?, ?)', [
  [1, 'login'],
  [2, 'logout'],
  [3, 'purchase'],
]);

batch runs the same statement once per row inside a single transaction and returns the total affected-row count.

Performance & control

Bypass the cache for a fresh read

js
// when result caching is enabled globally but this read must be current
const live = await exports.vSQL.single('SELECT money FROM players WHERE id = ?', [id], { cache: false });

Cap a heavy report query

js
// cancel server-side if it runs longer than 3s (see vsql_query_timeout)
const report = await exports.vSQL.query('SELECT ... big aggregate ...', [], { timeout: 3000 });

Targeted cache invalidation

js
await exports.vSQL.query('UPDATE players SET ...'); // any write clears the whole cache (blunt but correct)
exports.vSQL.cacheClear('players');                  // or clear only entries mentioning a table

Lifecycle

Wait for the database before using it

js
await exports.vSQL.ready();           // resolves once connected
// or react to events:
AddEventHandler('vSQL:ready', (server) => print('db up: ' + server.type));
EventFires when
vSQL:readythe pool first connects
vSQL:reconnectedthe pool reconnects after a drop
vSQL:connectionLosta fatal connection error is detected
onMySQLReadyon connect (oxmysql / mysql-async compatibility signal)

Probe the schema

Handy when a resource migrates itself or has to adapt to a database it didn't create:

js
if (!(await exports.vSQL.tableExists('players'))) { /* create it */ }
if (!(await exports.vSQL.columnExists('players', 'discord'))) { /* add it */ }

const cols = await exports.vSQL.columns('players'); // [{ name, type, nullable, key, default }]
const all  = await exports.vSQL.tables();           // ['players', 'vehicles', ...]

Inspect what's happening

js
const stats = exports.vSQL.getStats();   // counts, latency percentiles, per-resource breakdown
const top   = exports.vSQL.topQueries(); // heaviest query shapes by total time

Or from the server console: vsql, vsql top, vsql resources, vsql debug.

Released under the MIT License.