npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

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