1.0.16 • Published 8 months ago

@jakobsaadbye/teilen-sql v1.0.16

Weekly downloads
-
License
MIT
Repository
github
Last release
8 months ago

Teilen SQL

Replicate your sqlite3 database tables and sync them to other clients with automatic conflict resolution.

Upgrade tables to be crr's (Conflict-Free Replicated Relations) https://inria.hal.science/hal-02983557v1/document

This project is heavily inspired by the cr-sqlite project by Matt Wonlaw so check that out: https://github.com/vlcn-io/cr-sqlite

This framework is purely in javascript/typescript working on-top of wa-sqlite to have sqlite in the browser. Being in javascript rather than a compiled wasm-module means its easier to debug and understand what is going on. Ofc, this has the disadvantage of tying any use of this framework to also be in javascript. Roadmap is to port the core into a C or Rust library at some point for performance and just provide simple database wrappers for a handful of different languages to make it more interoperable.

***Disclaimer***

This library is not really made for production use yet as its still very experimental. Use it at your own will

Features

āš”ļø Reactive sql queries through a React hook

🚸 Automatic conflict-resolution of inserts, updates and deletes

šŸ”€ Distributed fractional indicies for ordering of rows

āŒ›ļø User-defined timings on tables for when deletes takes precedence over new changes

šŸ›œ Network agnostic. Changes can be sent over the network to be merged or they can stay local till connectivity is back

Tables as CRR's

Tables can be updated to be crr's or replicated tables. The second parameter deleteWinsAfter, specifies when deletions should take precedence over 'new' updates. If no updates have been made after deleteWinsAfter amount of time on a row or any row that references it, then it gets removed.

The checking of updates is done transatively, that means if you have a relationship like boards <-- columns <-- todos, deleting the board only happens if no new colums or todo changes have been made since the boards deleteWinsAfter timeframe.

// db setup ...

// Make the tables into crr's with deletions taking precedence when no updates have been made after 10s.
await db.upgradeTableToCrr("boards", "10s");
await db.upgradeTableToCrr("todos", "10s");
await db.upgradeTableToCrr("columns", "10s");

await db.finalizeUpgrades();

Distributed Fractional indicies

Columns in a table can be updated to be fractional indicies to put an ordering to rows.

Fractional indicies are great as they only require an update to the single row that needs to be moved or inserted. If we have two rows with order 0.0 and 1.0 and we wanted a third row in-between, we give the new row order 0.5.

Fractional indicies in teilen-sql uses the same principle with a few additions to make it distributed.

  • The fraction is represented as a base52 string to make it arbitrary precision.
  • Two rows with the same ordering (conflicting) are automatically resolved on inserts
// Update the 'position' field to be a fractional index
await db.upgradeColumnToFractionalIndex("todos", "position", "column_id");
-- The 'position' column now accepts three values:
--   1       : Append to the list
--  -1       : Prepend to the list
--  after_id : The id to place this row after 
INSERT INTO "todos" VALUES (1, 'Buy milk', '1')
INSERT INTO "todos" VALUES (2, 'Eat lunch', '1')
INSERT INTO "todos" VALUES (3, 'Plan dinner', 1) -- place after row 1

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│ id │      title      │ position │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 1  │ 'Buy milk'      │   'a'    │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 3  │ 'Plan dinner'   │   'h'    │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│ 2  │ 'Eat lunch'     │   'n'    │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

Figma has a great blog-post that explains fractional indicies: https://www.figma.com/blog/realtime-editing-of-ordered-sequences/

Also Matt Wonlaw has an execellent video on how fractional indicies work in cr-sqlite: https://www.youtube.com/watch?v=BghFgK6VJIE

āš›ļø React

SqliteContext

Teilen-sql provides the SqliteContext to give child components access to the db instance.

import { createDb } from "@jakobsaadbye/teilen-sql"
import { SqliteContext } from "@jakobsaadbye/teilen-sql/react"

const db = await createDb("example.db");

createRoot(document.getElementById('root') as HTMLElement).render(
  <StrictMode>
    <SqliteContext.Provider value={db}>
      <App />
    </SqliteContext.Provider>
  </StrictMode>,
)

useQuery\

The useQuery hook is the primary way to query data from the database. The query re-runs everytime there is a change to the affected table of the query.

export const TodoList = () => {
    const todos = useQuery<Todo[]>(`SELECT * FROM "todos"`, []).data;

    return (
        <div className="flex flex-col items-center justify-center">
            {todos && todos.map(todo => <p>{todo.title}</p>)}
        </div>
    )
}
useQuery<T>(sql: string | QueryFunc, params: any[], options: UseQueryOptions)

// A QueryFunc is just any function that accepts the db as the first argument and returns data. 
// All the values in the 'params' list are passed to the function.
// Mostly used if you want to delegate out complex queries into a function that live elsewhere. 
// NOTE: You need to specify the list of table dependencies for the function to re-run in the query options
type QueryFunc<T> = (db: SqliteDB, ...params: any) => Promise<T>;

type UseQueryOptions = {
    fireIf?: boolean        // A condition to be true before executing
    once?: boolean          // If the query only should run once when the component mounts
    first?: boolean         // Get the first matching result, undefined if no result
    dependencies?: string[] // List of table names that if updated re-runs the query. Only needed to be specified if passed a function. Otherwise the affected table is infered from the sql query by sqlite EXPLAIN QUERY PLAN
}

šŸ” Inspector

Press ctrl+i anywhere to open the inspector. The inspector lets you inspect and modify the sqlite database that's running in the application.

The design is heavily inspired by TablePlus, with most of the same keybindings and features.

Its build on the same reactivity system as the rest of the components, so any changes done via the inspector, is immediately reflected in the application.

Inspector

1.0.16

8 months ago

1.0.15

8 months ago

1.0.14

8 months ago

1.0.13

8 months ago

1.0.12

8 months ago

1.0.11

8 months ago

1.0.10

8 months ago

1.0.9

8 months ago

1.0.8

8 months ago

1.0.7

8 months ago

1.0.6

8 months ago

1.0.5

8 months ago

1.0.4

8 months ago

1.0.3

8 months ago

1.0.2

8 months ago

1.0.1

8 months ago

1.0.0

8 months ago