rqdb
v0.3.0
Published
Unofficial client library for rqlite
Downloads
18
Readme
rqdb
This is an unofficial TypeScript fetch-based client library for rqlite.
This project is guided by the following principles, in order:
- Minimal Dependencies: This only requires the standard TypeScript/ESLint setup, without any runtime dependencies beyond node.
- Pragmatism: Avoid excessive boilerplate, provide sane defaults
- Thin: Avoid encapsulation, prefer data objects that mirror the API
- Simple: Avoid state, prefer dependency injection to configuration
- Fast: Avoid copies, prefer performance over defensiveness
This project should be used with
rqdb-eslint-plugin
to check SQL, enforce consistent SQL styling, and enforce consistent use of
execute
for a single query, executeMany2
iff multiple queries without
parameters, and executeMany3
iff multiple queries with parameters.
Getting started
npm install --save rqdb
Usage
import { RqliteConnection } from 'rqdb';
import crypto from 'crypto';
import { inspect } from 'util';
async function main() {
const conn = new RqliteConnection(['http://127.0.0.1:4001']);
const cursor = conn.cursor();
await cursor.executeMany2([
'CREATE TABLE persons (id INTEGER PRIMARY KEY, uid TEXT UNIQUE NOT NULL, name TEXT NOT NULL)',
'CREATE TABLE pets (id INTEGER PRIMARY KEY, name TEXT NOT NULL, owner_id INTEGER NOT NULL REFERENCES persons(id) ON DELETE CASCADE)',
]);
const personName = 'John Doe';
const personUid = crypto.randomBytes(16).toString('base64url');
const petName = 'Fido';
const result = await cursor.executeMany3([
['INSERT INTO persons (uid, name) VALUES (?, ?)', [personUid, personName]],
[
`
INSERT INTO pets (name, ownerId)
SELECT
?, persons.id
FROM persons
WHERE
uid = ?
`,
[petName, personUid],
],
]);
if (result[0]?.rowsAffected !== 1 || result[1]?.rowsAffected !== 1) {
throw new Error(
`Expected exactly 1 person/pet inserted, got: ${inspect(result)}`
);
}
}
main();
Additional Features
Explain
Quickly get a formatted query plan from the current leader for a query, with basic highlighting of the most salient parts
import { RqliteConnection } from 'rqdb';
import crypto from 'crypto';
import { inspect } from 'util';
const conn = new RqliteConnection(['http://127.0.0.1:4001']);
const cursor = conn.cursor('weak');
await cursor.execute(
`
CREATE TABLE persons (
id INTEGER PRIMARY KEY,
uid TEXT UNIQUE NOT NULL,
given_name TEXT NOT NULL,
family_name TEXT NOT NULL
)
`
);
await cursor.explain(
"SELECT id FROM persons WHERE TRIM(given_name || ' ' || family_name) LIKE ?",
['john d%'],
{
out: console.log,
}
);
// --SCAN persons
await cursor.execute(
"CREATE INDEX persons_name_idx ON persons(TRIM(given_name || ' ' || family_name) COLLATE NOCASE)"
);
await cursor.explain(
"SELECT id FROM persons WHERE TRIM(given_name || ' ' || family_name) LIKE ?",
['john d%'],
{
out: console.log,
}
);
// --SEARCH persons USING INDEX persons_name_idx (<expr>>? AND <expr><?)
Read Consistency
Selecting read consistency is done at the cursor level, either by passing
readConsistency
to the cursor constructor (conn.cursor()
), in the options
to execute
/executeMany2
/executeMany3
, or by setting the instance variable
readConsistency
directly. The available consistencies are strong
, weak
,
and none
. You may also indicate the freshness
value at the cursor level.
See CONSISTENCY.md for details.
The default consistency is weak
.
Foreign Keys
Foreign key support in rqlite is disabled by default, to match sqlite. This is a common source of confusion. It cannot be configured by the client reliably. Foreign key support is enabled as described in FOREIGN_KEY_CONSTRAINTS.md
Nulls
Substituting NULL
in parametrized queries can be error-prone.
const name: string | null = null;
// never matches a row since name is null, even if the rows name is null
await cursor.execute('SELECT * FROM persons WHERE name = ?', [name]);
// works as expected
await cursor.execute(
'SELECT * FROM persons WHERE ((? IS NULL AND name IS NULL) OR name = ?)',
[name, name]
);
Backup
Backups can be initiated using await conn.backupToFile('binary', 'database.bak')
.
The download will be streamed to the given filepath. Both the sql format and a
compressed sqlite format are supported. If more control is required, such as
compressing mid flight, use RqliteConnection.backup
directly.
Logging
By default this will log using console
. If chalk
is installed, it will also
color with chalk by default. This can be disabled by using { log: false }
in
the connect
call. If logging is desired but needs to be handled differently,
it can be done as follows:
import { RqliteConnection } from 'rqdb';
import chalk from 'chalk';
import { inspect } from 'util';
// The default formatter includes the error in the message
const logMessage = (message: string, error?: any) => console.log(message);
conn = new RqliteConnection(['http://127.0.0.1:4001'], {
// defaults shown here unless otherwise noted
log: {
meta: {
format: (
level: 'debug' | 'info' | 'warning' | 'error',
msg: string,
error?: any
) => {
const colorsByLevel = {
debug: chalk.gray,
info: chalk.white,
warning: chalk.yellowBright,
error: chalk.redBright,
} as const;
return `${chalk.green(new Date().toLocaleString())} ${colorsByLevel[
level
](message)}${
error === undefined ? '' : '\n' + chalk.gray(inspect(error))
}`;
},
},
readStart: {
// the method is slightly simplified here as we also handle coloring commands
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
readResponse: {
method: logMessage,
level: 'debug',
maxLength: 1024,
enabled: true,
},
readStale: {
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
writeStart: {
// the method is slightly simplified here as we also handle coloring commands
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
writeResponse: {
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
followRedirect: {
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: false,
},
fetchError: {
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
connectTimeout: {
method: logMessage,
level: 'debug',
maxLength: undefined,
enabled: true,
},
readTimeout: {
method: logMessage,
level: 'error',
maxLength: undefined,
enabled: true,
},
hostsExhausted: {
method: logMessage,
level: 'error',
maxLength: undefined,
enabled: true,
},
nonOkResponse: {
method: logMessage,
level: 'warning',
maxLength: undefined,
enabled: true,
},
backupStart: {
method: logMessage,
level: 'info',
maxLength: undefined,
enabled: true,
},
backupEnd: {
method: logMessage,
level: 'info',
maxLength: undefined,
enabled: true,
},
},
});
Limitations
Slow Transactions
The primary limitations is that by the connectionless nature of rqlite, while transactions are possible, the entire transaction must be specified upfront. That is, you cannot open a transaction, perform a query, and then use the result of that query to perform another query before closing the transaction.
This can also be seen as a blessing, as these types of transactions are the most common source of performance issues in traditional applications. They require long-held locks that can easily lead to N^2 performance. The same behavior can almost always be achieved with uids, as shown in the example. The repeated UID lookup causes a consistent overhead, which is highly preferable to the unpredictable negative feedback loop nature of long transactions.
See Also
- Python rqdb with the same maintainer and extremely similar API