Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tauri adapter #1758

Draft
wants to merge 6 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions flow-typed/custom/tauri-plugin-sql.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
declare class TauriDB {
execute(query: string, args?: any[]): Promise<any>;
select(query: string, args?: any[]): Promise<any[]>;
close(): Promise<void>;
}
declare module 'tauri-plugin-sql' {
declare export default {
load: (path: string) => Promise<TauriDB>
};
}

5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
"@babel/runtime": "7.21.0",
"@nozbe/simdjson": "3.1.0-wmelon1",
"@nozbe/sqlite": "3.40.1",
"@tauri-apps/api": "^1.5.3",
"hoist-non-react-statics": "^3.3.2",
"lokijs": "npm:@nozbe/[email protected]",
"rxjs": "^7.8.0",
Expand Down Expand Up @@ -164,8 +165,10 @@
"react-test-renderer": "18.2.0",
"rimraf": "^4.1.2",
"semver": "^7.3.8",
"tauri-plugin-sql": "https://github.com/tauri-apps/tauri-plugin-sql#v1",
"tslint": "^5.11.0",
"tslint-config-prettier": "^1.15.0",
"typescript": "^4.5.0"
}
},
"packageManager": "[email protected]"
}
1 change: 0 additions & 1 deletion src/adapters/sqlite/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,7 +153,6 @@ export default class SQLiteAdapter implements DatabaseAdapter {
}

_setUpWithMigrations(databaseVersion: SchemaVersion, callback: ResultCallback<void>): void {
logger.log('[SQLite] Database needs migrations')
invariant(databaseVersion > 0, 'Invalid database schema version')

const migrationSteps = this._migrationSteps(databaseVersion)
Expand Down
2 changes: 1 addition & 1 deletion src/adapters/sqlite/makeDispatcher/index.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// @flow
/* eslint-disable global-require */

import DatabaseBridge from '../sqlite-node/DatabaseBridge'
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This path is hardcoded for node and I just replaced it for now.

import DatabaseBridge from '../sqlite-tauri/DatabaseBridge'
import { type ConnectionTag } from '../../../utils/common'
import { type ResultCallback } from '../../../utils/fp/Result'
import type {
Expand Down
84 changes: 84 additions & 0 deletions src/adapters/sqlite/sqlite-tauri/Database.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/* eslint-disable no-console */
// @flow

const SQLite = require('tauri-plugin-sql').default
const {removeFile} = require('@tauri-apps/api/fs')
const { appConfigDir } = require('@tauri-apps/api/path')


class Database {
instance: TauriDB
path: string

constructor(path: string = ':memory:'): void {
this.path = path
}

async open(): Promise<void> {
try {
this.instance = await SQLite.load(`sqlite:${this.path}`)
} catch (error) {
throw new Error(`Failed to open the database. - ${error}`)
}

if (!this.instance) {
throw new Error('Failed to open the database.')
}
}

async inTransaction(executeBlock: () => Promise<void>): Promise<void> {
try {
const transactionResult = await this.instance.execute('BEGIN TRANSACTION;')
await executeBlock()
await this.instance.execute('COMMIT;')
} catch (error) {
await this.instance.execute('ROLLBACK;')
throw error
}
}

async execute(query: string, args: any[] = []): Promise<any> {
return this.instance.select(query, args)
}

async executeStatements(queries: string): Promise<any> {
return this.instance.execute(queries, [])
}

async queryRaw(query: string, args: any[] = []): Promise<any | any[]> {
return this.instance.select(query, args)
}

async count(query: string, args: any[] = []): Promise<number> {
const results = await this.instance.select(query, args)
if (results.length === 0) {
throw new Error('Invalid count query, can`t find next() on the result')
}

const result = results[0]

return Number.parseInt(result.count, 10)
}

async userVersion(): Promise<number> {
const results = await this.instance.select('PRAGMA user_version')
return results[0].user_version
}

async setUserVersion(version: number): Promise<void> {
await this.instance.execute(`PRAGMA user_version = ${version}`)
}

async unsafeDestroyEverything(): Promise<void> {
await this.instance.close()
const appConfigDirPath = await appConfigDir()
await removeFile(`${appConfigDirPath}${this.path}`)
await this.open()
}

isInMemoryDatabase(): any {
return false
}
}

export default Database
244 changes: 244 additions & 0 deletions src/adapters/sqlite/sqlite-tauri/DatabaseBridge.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
// @flow

import DatabaseDriver from './DatabaseDriver'

type Connection = {
driver: DatabaseDriver,
queue: any[],
status: string,
}

class DatabaseBridge {
connections: { [key: number]: Connection } = {}
_initializationPromiseResolve: () => void = () => {}
_initializationPromise: Promise<any> = new Promise((resolve) => {this._initializationPromiseResolve = resolve})
_operationLock: Promise<any> | null = null

// MARK: - Asynchronous connections

connected(tag: number, driver: DatabaseDriver): void {
this.connections[tag] = { driver, queue: [], status: 'connected' }
}

waiting(tag: number, driver: DatabaseDriver): void {
this.connections[tag] = { driver, queue: [], status: 'waiting' }
}

async initialize(
tag: number,
databaseName: string,
schemaVersion: number,
resolve: (status: { code: string, databaseVersion?: number }) => void,
reject: () => void,
): Promise<void> {
let driver
try {
this.assertNoConnection(tag)
driver = new DatabaseDriver()
await driver.initialize(databaseName, schemaVersion)
this._initializationPromiseResolve()
this.connected(tag, driver)

resolve({ code: 'ok' })
} catch (error) {
if (driver && error.type === 'SchemaNeededError') {
this.waiting(tag, driver)
this._initializationPromiseResolve()
resolve({ code: 'schema_needed' })
} else if (driver && error.type === 'MigrationNeededError') {
this.waiting(tag, driver)
this._initializationPromiseResolve()
resolve({ code: 'migrations_needed', databaseVersion: error.databaseVersion })
} else {
this.sendReject(reject, error, 'initialize')
}
}
}

async setUpWithSchema(
tag: number,
databaseName: string,
schema: string,
schemaVersion: number,
resolve: (boolean) => void,
_reject: () => void,
): Promise<void> {
const driver = new DatabaseDriver()
await driver.setUpWithSchema(databaseName, schema, schemaVersion)
this.connectDriverAsync(tag, driver)
this._initializationPromiseResolve()
resolve(true)
}

async setUpWithMigrations(
tag: number,
databaseName: string,
migrations: string,
fromVersion: number,
toVersion: number,
resolve: (boolean) => void,
reject: () => void,
): Promise<void> {
try {
const driver = new DatabaseDriver()
await driver.setUpWithMigrations(databaseName, {
from: fromVersion,
to: toVersion,
sql: migrations,
})
this.connectDriverAsync(tag, driver)
this._initializationPromiseResolve()
resolve(true)
} catch (error) {
this.disconnectDriver(tag)
this.sendReject(reject, error, 'setUpWithMigrations')
}
}

// MARK: - Asynchronous actions

find(
tag: number,
table: string,
id: string,
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'find', (driver) => driver.find(table, id))
}

query(
tag: number,
table: string,
query: string,
args: any[],
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'query', (driver) =>
driver.cachedQuery(table, query, args),
)
}

queryIds(
tag: number,
query: string,
args: any[],
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'queryIds', (driver) => driver.queryIds(query, args))
}

unsafeQueryRaw(
tag: number,
query: string,
args: any[],
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'unsafeQueryRaw', (driver) =>
driver.unsafeQueryRaw(query, args),
)
}

count(
tag: number,
query: string,
args: any[],
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'count', (driver) => driver.count(query, args))
}

batch(tag: number, operations: any[], resolve: (any) => void, reject: (string) => void): void {
this.withDriver(tag, resolve, reject, 'batch', (driver) => driver.batch(operations))
}

unsafeResetDatabase(
tag: number,
schema: string,
schemaVersion: number,
resolve: (any) => void,
reject: (string) => void,
): void {
this.withDriver(tag, resolve, reject, 'unsafeResetDatabase', (driver) =>
driver.unsafeResetDatabase({ version: schemaVersion, sql: schema }),
)
}

// getLocal(tag: number, key: string, resolve: (any) => void, reject: (string) => void): void {
// this.withDriver(tag, resolve, reject, 'getLocal', (driver) => driver.getLocal(key))
// }

// MARK: - Helpers

async withDriver(
tag: number,
resolve: (any) => void,
reject: (any) => void,
functionName: string,
action: (driver: DatabaseDriver) => Promise<any>,
): Promise<void> {
try {
await this._initializationPromise
const connection = this.connections[tag]

if (!connection) {
throw new Error(`No driver for with tag ${tag} available, called from ${functionName}`)
}

if (connection.status === 'connected') {
if(this._operationLock) {
await this._operationLock
}

this._operationLock = action(connection.driver)
const result = await this._operationLock

resolve(result)
this._operationLock = null
} else if (connection.status === 'waiting') {
// try again when driver is ready
connection.queue.push(() => {
this.withDriver(tag, resolve, reject, functionName, action)
})
}
} catch (error) {
this.sendReject(reject, error, functionName)
}
}

connectDriverAsync(tag: number, driver: DatabaseDriver): void {
const { queue = [] } = this.connections[tag]
this.connections[tag] = { driver, queue: [], status: 'connected' }

queue.forEach((operation) => operation())
}

disconnectDriver(tag: number): void {
const { queue = [] } = this.connections[tag]
delete this.connections[tag]

queue.forEach((operation) => operation())
}

assertNoConnection(tag: number): void {
if (this.connections[tag]) {
throw new Error(`A driver with tag ${tag} already set up`)
}
}

sendReject(reject: (string, string, Error) => void, error: Error, functionName: string): void {
if (reject) {
reject(`db.${functionName}.error`, error.message, error)
} else {
throw new Error(`db.${functionName} missing reject (${error.message})`)
}
}
}

const databaseBridge: DatabaseBridge = new DatabaseBridge()

export default databaseBridge
Loading