Skip to content

Commit

Permalink
feat: add query builder package (#77)
Browse files Browse the repository at this point in the history
Co-authored-by: Steven Ontong <[email protected]>
Co-authored-by: stevensJourney <[email protected]>
Co-authored-by: DominicGBauer <[email protected]>
  • Loading branch information
4 people authored Feb 27, 2024
1 parent 749dc80 commit 0ff3228
Show file tree
Hide file tree
Showing 23 changed files with 935 additions and 39 deletions.
5 changes: 5 additions & 0 deletions .changeset/pretty-islands-breathe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@powersync/kysely-driver": minor
---

Initial release of Kysely driver
141 changes: 141 additions & 0 deletions packages/kysely-driver/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<p align="center">
<a href="https://www.powersync.com" target="_blank"><img src="https://github.com/powersync-ja/.github/assets/19345049/602bafa0-41ce-4cee-a432-56848c278722"/></a>
</p>

# PowerSync Kysely Driver

[PowerSync](https://powersync.com) is a service and set of SDKs that keeps Postgres databases in sync with on-device SQLite databases.

This package (`packages/kysely-driver`) brings the benefits of an ORM through our maintained [Kysely](https://kysely.dev/) driver to PowerSync.

## Getting started

Setup the PowerSync Database and wrap it with Kysely.

```js
import { wrapPowerSyncWithKysely } from '@powersync/kysely-driver';
import { WASQLitePowerSyncDatabaseOpenFactory } from "@journeyapps/powersync-sdk-web";
import { appSchema } from "./schema";
import { Database } from "./types";

const factory = new WASQLitePowerSyncDatabaseOpenFactory({
schema: appSchema,
dbFilename: "test.sqlite",
});

export const powerSyncDb = factory.getInstance();

export const db = wrapPowerSyncWithKysely<Database>(powerSyncDb)
```

For more information on Kysely typing [here](https://kysely.dev/docs/getting-started#types).

Now you are able to use Kysely queries:

### Select

* In Kysely

```js
const result = await db.selectFrom('users').selectAll().execute();

// {id: '1', name: 'user1', id: '2', name: 'user2'}
```

* In PowerSync

```js
const result = await powerSyncDb.getAll('SELECT * from users')

// {id: '1', name: 'user1', id: '2', name: 'user2'}
```

### Insert

* In Kysely

```js
await db.insertInto('users').values({ id: '1', name: 'John' }).execute();
const result = await db.selectFrom('users').selectAll().execute();

// {id: '1', name: 'John'}
```

* In PowerSync

```js
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(1, ?)', ['John']);
const result = await powerSyncDb.getAll('SELECT * from users')

// {id: '1', name: 'John'}
```

### Delete

* In Kysely

```js
await db.insertInto('users').values({ id: '2', name: 'Ben' }).execute();
await db.deleteFrom('users').where('name', '=', 'Ben').execute();
const result = await db.selectFrom('users').selectAll().execute();

// { }
```

* In PowerSync

```js
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(2, ?)', ['Ben']);
await powerSyncDb.execute(`DELETE FROM users WHERE name = ?`, ['Ben']);
const result = await powerSyncDb.getAll('SELECT * from users')

// { }
```

### Update

* In Kysely

```js
await db.insertInto('users').values({ id: '3', name: 'Lucy' }).execute();
await db.updateTable('users').where('name', '=', 'Lucy').set('name', 'Lucy Smith').execute();
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow();

// { id: '3', name: 'Lucy Smith' }
```

* In PowerSync

```js
await powerSyncDb.execute('INSERT INTO users (id, name) VALUES(3, ?)', ['Lucy']);
await powerSyncDb.execute("UPDATE users SET name = ? WHERE name = ?", ['Lucy Smith', 'Lucy']);
const result = await powerSyncDb.getAll('SELECT * from users')

// { id: '3', name: 'Lucy Smith' }
```

### Transaction

* In Kysely

```js
await db.transaction().execute(async (transaction) => {
await transaction.insertInto('users').values({ id: '4', name: 'James' }).execute();
await transaction.updateTable('users').where('name', '=', 'James').set('name', 'James Smith').execute();
});
const result = await db.selectFrom('users').select('name').executeTakeFirstOrThrow();

// { id: '4', name: 'James Smith' }
```

* In PowerSync

```js
await powerSyncDb.writeTransaction((transaction) => {
await transaction.execute('INSERT INTO users (id, name) VALUES(4, ?)', ['James']);
await transaction.execute("UPDATE users SET name = ? WHERE name = ?", ['James Smith', 'James']);
})
const result = await powerSyncDb.getAll('SELECT * from users')

// { id: '4', name: 'James Smith' }
```
45 changes: 45 additions & 0 deletions packages/kysely-driver/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
{
"name": "@powersync/kysely-driver",
"version": "0.0.0",
"description": "Kysely driver for PowerSync",
"main": "lib/src/index.js",
"types": "lib/src/index.d.ts",
"author": "JOURNEYAPPS",
"license": "Apache-2.0",
"files": [
"lib"
],
"repository": "https://github.com/powersync-ja/powersync-js",
"bugs": {
"url": "https://github.com/powersync-ja/powersync-js/issues"
},
"publishConfig": {
"registry": "https://registry.npmjs.org/",
"access": "public"
},
"homepage": "https://docs.powersync.com",
"scripts": {
"build": "tsc --build",
"clean": "rm -rf dist tsconfig.tsbuildinfo",
"watch": "tsc --build -w",
"test": "pnpm build && vitest"
},
"dependencies": {
"@journeyapps/powersync-sdk-common": "workspace:*",
"kysely": "^0.27.2"
},
"devDependencies": {
"@journeyapps/powersync-sdk-web": "workspace:*",
"@journeyapps/wa-sqlite": "^0.1.1",
"@types/node": "^20.11.17",
"@vitest/browser": "^1.3.1",
"ts-loader": "^9.5.1",
"ts-node": "^10.9.2",
"typescript": "^5.3.3",
"vite": "^5.1.1",
"vite-plugin-top-level-await": "^1.4.1",
"vite-plugin-wasm": "^3.3.0",
"vitest": "^1.3.0",
"webdriverio": "^8.32.3"
}
}
23 changes: 23 additions & 0 deletions packages/kysely-driver/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { wrapPowerSyncWithKysely } from './sqlite/db';
import {
type ColumnType,
type Insertable,
type Selectable,
type Updateable,
type JSONColumnType,
type KyselyConfig,
type Kysely,
sql
} from 'kysely';

export {
ColumnType,
Insertable,
Selectable,
Updateable,
JSONColumnType,
KyselyConfig,
sql,
Kysely,
wrapPowerSyncWithKysely
};
12 changes: 12 additions & 0 deletions packages/kysely-driver/src/sqlite/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { PowerSyncDialect } from './sqlite-dialect';
import { Kysely, type KyselyConfig } from 'kysely';
import { type AbstractPowerSyncDatabase } from '@journeyapps/powersync-sdk-common';

export const wrapPowerSyncWithKysely = <T>(db: AbstractPowerSyncDatabase, options?: KyselyConfig) => {
return new Kysely<T>({
dialect: new PowerSyncDialect({
db
}),
...options
});
};
121 changes: 121 additions & 0 deletions packages/kysely-driver/src/sqlite/sqlite-connection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { type AbstractPowerSyncDatabase, type Transaction } from '@journeyapps/powersync-sdk-common';
import { CompiledQuery, DatabaseConnection, QueryResult } from 'kysely';

/**
* Represent a Kysely connection to the PowerSync database.
*
* The actual locks are acquired on-demand when a transaction is started.
*
* When not using transactions, we rely on the automatic locks.
*
* This allows us to bypass write locks when doing pure select queries outside a transaction.
*/
export class PowerSyncConnection implements DatabaseConnection {
readonly #db: AbstractPowerSyncDatabase;
#completeTransaction: (() => void) | null;
#tx: Transaction | null;

constructor(db: AbstractPowerSyncDatabase) {
this.#db = db;
this.#tx = null;
this.#completeTransaction = null;
}

async executeQuery<O>(compiledQuery: CompiledQuery): Promise<QueryResult<O>> {
const { sql, parameters, query } = compiledQuery;

const context = this.#tx ?? this.#db;

if (query.kind === 'SelectQueryNode') {
// Optimizaton: use getAll() instead of execute() if it's a select query
const rows = await context.getAll(sql, parameters as unknown[]);
return {
rows: rows as O[]
};
}

const result = await context.execute(sql, parameters as unknown[]);

return {
insertId: result.insertId ? BigInt(result.insertId!) : undefined,
numAffectedRows: BigInt(result.rowsAffected),
rows: result.rows?._array ?? []
};
}

async *streamQuery<R>(compiledQuery: CompiledQuery): AsyncIterableIterator<QueryResult<R>> {
// Not actually streamed
const results = await this.executeQuery<R>(compiledQuery);
yield {
rows: results.rows
};
}

async beginTransaction(): Promise<void> {
// TODO: Check if there is already an active transaction?

/**
* Returns a promise which resolves once a transaction has been started.
* Rejects if any errors occur in obtaining the lock.
*/
return new Promise<void>((resolve, reject) => {
/**
* Starts a transaction, resolves the `beginTransaction` promise
* once it's started. The transaction waits until the `this.#release`
* callback is executed.
*/
this.#db
.writeTransaction(async (tx) => {
// Set the current active transaction
this.#tx = tx;

/**
* Wait for this transaction to be completed
* Rejecting would cause any uncommitted changes to be
* rolled back.
*/
const transactionCompleted = new Promise<void>((resolve) => {
this.#completeTransaction = resolve;
});

// Allow this transaction to be used externally
resolve();

await transactionCompleted;
})
.catch(reject);
});
}

async commitTransaction(): Promise<void> {
if (!this.#tx) {
throw new Error('Transaction is not defined');
}

await this.#tx.commit();
this.releaseTransaction();
}

async rollbackTransaction(): Promise<void> {
if (!this.#tx) {
throw new Error('Transaction is not defined');
}

await this.#tx.rollback();
this.releaseTransaction();
}

async releaseConnection(): Promise<void> {
this.#db.close();
}

private releaseTransaction() {
if (!this.#completeTransaction) {
throw new Error(`Not able to release transaction`);
}

this.#completeTransaction();
this.#completeTransaction = null;
this.#tx = null;
}
}
36 changes: 36 additions & 0 deletions packages/kysely-driver/src/sqlite/sqlite-dialect.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {
DatabaseIntrospector,
Dialect,
DialectAdapter,
Driver,
Kysely,
QueryCompiler,
SqliteAdapter,
SqliteIntrospector,
SqliteQueryCompiler
} from 'kysely';
import { PowerSyncDialectConfig, PowerSyncDriver } from './sqlite-driver';

export class PowerSyncDialect implements Dialect {
readonly #config: PowerSyncDialectConfig;

constructor(config: PowerSyncDialectConfig) {
this.#config = Object.freeze({ ...config });
}

createDriver(): Driver {
return new PowerSyncDriver(this.#config);
}

createQueryCompiler(): QueryCompiler {
return new SqliteQueryCompiler();
}

createAdapter(): DialectAdapter {
return new SqliteAdapter();
}

createIntrospector(db: Kysely<unknown>): DatabaseIntrospector {
return new SqliteIntrospector(db);
}
}
Loading

0 comments on commit 0ff3228

Please sign in to comment.