Azarattum / CRStore

Conflict-free replicated store.

Home Page:https://npmjs.com/package/crstore

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Better Bun Support (bun:sqlite)

fawaz-alesayi opened this issue Β· comments

Hello! πŸ‘‹

I'm currently using your awesome package to build an internal local-first app at my company. I'm using Bun & SvelteKit to build my project, and so I noticed that this package has experimental support for Bun. Now I'm still a beginner in this whole local-first cr-sqlite CRDT world. But as I understand it, any SQLite instance that works with cr-sqlite should be able to work with CRStore, bun:sqlite included.

Problem

When I try to run this file in Bun:

import { crr, primary } from "crstore";
import { boolean, object, string } from "superstruct";
import { database } from "crstore";
import Os from 'os';
import { Database } from 'bun:sqlite';

const macOS = Os.platform() === "darwin";

if (macOS) {
    /**
     * By default, macOS ships with Apple's proprietary build of SQLite
     * which doesn't support extensions. (therefore does not support cr-sqlite)
     * To use extensions, you'll need to install a vanilla build of SQLite.
     * Reference: https://bun.sh/docs/api/sqlite#loadextension
     */
    Database.setCustomSQLite("/opt/homebrew/opt/sqlite/lib/libsqlite3.dylib");
}

// Struct that represents the table
const todos = object({
  id: string(),
  title: string(),
  text: string(),
  completed: boolean(),
});
crr(todos); // Register table with conflict-free replicated relations
primary(todos, "id"); // Define a primary key (can be multi-column)

const schema = object({ todos });

const { close } = database(schema);

close();

I get this error:

53 |             return Promise.resolve({
54 |                 rows: stmt.all(parameters),
55 |             });
56 |         }
57 |         else {
58 |             const { changes, lastInsertRowid } = stmt.run(parameters);
                                                      ^
TypeError: Right side of assignment cannot be destructured
      at executeQuery (/Users/fawaz/Projects/crstore-bun-issue/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-driver.js:58:50)
      at /Users/fawaz/Projects/crstore-bun-issue/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-driver.js:35:15
      at rollbackTransaction (/Users/fawaz/Projects/crstore-bun-issue/node_modules/kysely/dist/esm/dialect/sqlite/sqlite-driver.js:34:31)
      at /Users/fawaz/Projects/crstore-bun-issue/node_modules/kysely/dist/esm/kysely.js:417:23

Minimal Reproduction: https://github.com/fawaz-alesayi/crstore-bun-issue

Why I think this happens

After a couple of minutes of investigation I noticed that you're using Kysely when creating the new tables defined by superstruct.

As I understand it, Kysely only supports bun:sqlite if you're using a custom adapter like these ones
https://github.com/dylanblokhuis/kysely-bun-sqlite
https://github.com/subframe7536/kysely-sqlite-tools/tree/master/packages/dialect-bun-worker

Solutions

Here are some ways I thought up of making Bun support better:

Solution 1: Allow users to provide their own Kysely adapters

This is the solution I'm using in my fork and it works great. The new API for the database function would be

import { object, string, type Infer } from "superstruct";

    const db = database(schema, {
      name: "test.db",
      kysely: ({ database }) => {
        return new Kysely<Schema<Infer<typeof schema>>>({
          dialect: new SqliteDialect({ database }), // Or BunDialect, LibSQL, whatever works with cr-sqlite
          plugins: [new JSONPlugin()],
        });
      },
    });

Solution 2: Conditional check to load appropriate Kysely Dialect

Would look something like this:

// https://github.com/Azarattum/CRStore/blob/main/src/lib/database/index.ts
async function init<T extends CRSchema>(
  file: string,
  schema: T,
  paths = defaultPaths,
) {
  type DB = Schema<T>;
  if (connections.has(file)) return connections.get(file) as Connection<DB>;

  const { database, browser } = await load(file, paths);
  const Dialect = browser ? CRDialect : process.isBun ? SqliteBunDialect : SqliteDialect;
  const kysely = new Kysely<DB>({
    dialect: new Dialect({ database }),
    plugins: [new JSONPlugin()],
  });

// ... rest of the code

I haven't had much time to dig into the actual error that is produced by Kysely and why it happens with Bun. but my guess is that there's some subtle API differences between bun:sqlite and better-sqlite3 that causes this error.

Hello, @fawaz-alesayi. Thank you for an awesome description of an issue and all your investigation on the problem.

The issue is indeed originates from bun:SQLite having a slightly different API. In particular, run function doesn't return any metadata unlike better-sqlite. In fact we cannot even differentiate whether a prepared statement returns any result or not (because reader property is not implemented in bun:sqlite). kysely-bun-sqlite works around this issue by treating every query as if it returned something (running them with .all()).

I have made a separate runtime entry in CRStore for bun which makes reader always be true in prepared statements therefore making them compatible with default SQLite adapter. That also brought some QOL changes, like now you don't have to do Database.setCustomSQLite nonsense to make it work on MacOS (CRStore does that for you). You can also overwrite that default path with the the same binding path option that you would use in node.

I've published 0.23.0 version with these changes. Although I was able to run CRStore's test suit successfully with bun (even without vitest), let me know if there are any other bun-related difficulties you encounter while building your app. So, that we can finally bring bun support out of the experimental stage.