From 696470c358c1da13d31a78d8e7a6189c85a29ce4 Mon Sep 17 00:00:00 2001 From: FabianLars Date: Thu, 15 Dec 2022 17:15:49 +0100 Subject: [PATCH] feat(sql): better db specific types Co-authored-by: Ken Snyder --- plugins/sql/guest-js/dist/index.min.js | 222 ++++++++-------- plugins/sql/guest-js/dist/index.min.js.map | 2 +- plugins/sql/guest-js/dist/index.mjs | 222 ++++++++-------- plugins/sql/guest-js/dist/index.mjs.map | 2 +- plugins/sql/guest-js/index.ts | 45 ++-- plugins/sql/src/deserialize.rs | 294 +++++++++++++++++++++ plugins/sql/src/lib.rs | 57 +++- plugins/sql/src/plugin.rs | 190 ++++++++----- 8 files changed, 707 insertions(+), 327 deletions(-) create mode 100644 plugins/sql/src/deserialize.rs diff --git a/plugins/sql/guest-js/dist/index.min.js b/plugins/sql/guest-js/dist/index.min.js index f734a03f..48168f3c 100644 --- a/plugins/sql/guest-js/dist/index.min.js +++ b/plugins/sql/guest-js/dist/index.min.js @@ -2,117 +2,117 @@ var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumera var f={};e(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`);},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`);},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r});})}function w(e,r="asset"){let n=encodeURIComponent(e);return navigator.userAgent.includes("Windows")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`} -/** - * **Database** - * - * The `Database` class serves as the primary interface for - * communicating with the rust side of the sql plugin. - */ -class Database { - constructor(path) { - this.path = path; - } - /** - * **load** - * - * A static initializer which connects to the underlying database and - * returns a `Database` instance once a connection to the database is established. - * - * # Sqlite - * - * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. - * - * @example - * ```ts - * const db = await Database.load("sqlite:test.db"); - * ``` - */ - static async load(path) { - const _path = await c("plugin:sql|load", { - db: path, - }); - return new Database(_path); - } - /** - * **get** - * - * A static initializer which synchronously returns an instance of - * the Database class while deferring the actual database connection - * until the first invocation or selection on the database. - * - * # Sqlite - * - * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. - * - * @example - * ```ts - * const db = Database.get("sqlite:test.db"); - * ``` - */ - static get(path) { - return new Database(path); - } - /** - * **execute** - * - * Passes a SQL expression to the database for execution. - * - * @example - * ```ts - * const result = await db.execute( - * "UPDATE todos SET title = $1, completed = $2 WHERE id = $3", - * [ todos.title, todos.status, todos.id ] - * ); - * ``` - */ - async execute(query, bindValues) { - const [rowsAffected, lastInsertId] = await c("plugin:sql|execute", { - db: this.path, - query, - values: bindValues !== null && bindValues !== void 0 ? bindValues : [], - }); - return { - lastInsertId, - rowsAffected, - }; - } - /** - * **select** - * - * Passes in a SELECT query to the database for execution. - * - * @example - * ```ts - * const result = await db.select( - * "SELECT * from todos WHERE id = $1", id - * ); - * ``` - */ - async select(query, bindValues) { - const result = await c("plugin:sql|select", { - db: this.path, - query, - values: bindValues !== null && bindValues !== void 0 ? bindValues : [], - }); - return result; - } - /** - * **close** - * - * Closes the database connection pool. - * - * @example - * ```ts - * const success = await db.close() - * ``` - * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. - */ - async close(db) { - const success = await c("plugin:sql|close", { - db, - }); - return success; - } +/** + * **Database** + * + * The `Database` class serves as the primary interface for + * communicating with the rust side of the sql plugin. + * + * @connection is a DB connection string like `sqlite:test.db`, etc. + */ +class Database { + constructor(connection) { + this.connection = connection; + } + /** + * **load** + * + * A static initializer which connects to the underlying database and + * returns a `Database` instance once a connection to the database is established. + * + * # Sqlite + * + * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```ts + * const db = await Database.load("sqlite:test.db"); + * ``` + */ + static async load(connection) { + const _conn = await c("plugin:sql|load", { + db: connection, + }); + return new Database(_conn); + } + /** + * **get** + * + * A static initializer which synchronously returns an instance of + * the Database class while deferring the actual database connection + * until the first invocation or selection on the database. + * + * # Sqlite + * + * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```ts + * const db = Database.get("sqlite:test.db"); + * ``` + */ + static get(connection) { + return new Database(connection); + } + /** + * **execute** + * + * Passes a SQL expression to the database for execution. + * + * @example + * ```ts + * const result = await db.execute( + * "UPDATE todos SET title = $1, completed = $2 WHERE id = $3", + * [ todos.title, todos.status, todos.id ] + * ); + * ``` + */ + async execute(sql, bindValues) { + const [rowsAffected, lastInsertId] = await c("plugin:sql|execute", { + db: this.connection, + sql, + values: bindValues !== null && bindValues !== void 0 ? bindValues : [], + }); + return { + lastInsertId, + rowsAffected, + }; + } + /** + * **select** + * + * Passes in a SELECT query to the database for execution. + * + * @example + * ```ts + * const result = await db.select( + * "SELECT * from todos WHERE id = $1", id + * ); + * ``` + */ + async select(sql, bindValues) { + return await c("plugin:sql|select", { + db: this.connection, + sql, + values: bindValues !== null && bindValues !== void 0 ? bindValues : [], + }); + } + /** + * **close** + * + * Closes the database connection pool. + * + * @example + * ```ts + * const success = await db.close() + * ``` + * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. + */ + async close() { + return await c("plugin:sql|close", { + db: this.connection, + }); + } } export { Database as default }; diff --git a/plugins/sql/guest-js/dist/index.min.js.map b/plugins/sql/guest-js/dist/index.min.js.map index 7e3466c5..4d011809 100644 --- a/plugins/sql/guest-js/dist/index.min.js.map +++ b/plugins/sql/guest-js/dist/index.min.js.map @@ -1 +1 @@ -{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.min.js","sources":["../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-FEIY7W7S.js","../../../../node_modules/.pnpm/@tauri-apps+api@1.2.0/node_modules/@tauri-apps/api/chunk-RCPA6UVN.js","../index.ts"],"sourcesContent":["var d=Object.defineProperty;var e=(c,a)=>{for(var b in a)d(c,b,{get:a[b],enumerable:!0})};export{e as a};\n","import{a as d}from\"./chunk-FEIY7W7S.js\";var f={};d(f,{convertFileSrc:()=>w,invoke:()=>c,transformCallback:()=>s});function u(){return window.crypto.getRandomValues(new Uint32Array(1))[0]}function s(e,r=!1){let n=u(),t=`_${n}`;return Object.defineProperty(window,t,{value:o=>(r&&Reflect.deleteProperty(window,t),e==null?void 0:e(o)),writable:!1,configurable:!0}),n}async function c(e,r={}){return new Promise((n,t)=>{let o=s(i=>{n(i),Reflect.deleteProperty(window,`_${a}`)},!0),a=s(i=>{t(i),Reflect.deleteProperty(window,`_${o}`)},!0);window.__TAURI_IPC__({cmd:e,callback:o,error:a,...r})})}function w(e,r=\"asset\"){let n=encodeURIComponent(e);return navigator.userAgent.includes(\"Windows\")?`https://${r}.localhost/${n}`:`${r}://localhost/${n}`}export{s as a,c as b,w as c,f as d};\n",null],"names":["d","invoke"],"mappings":"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"} \ No newline at end of file diff --git a/plugins/sql/guest-js/dist/index.mjs b/plugins/sql/guest-js/dist/index.mjs index e255900f..b3bea80e 100644 --- a/plugins/sql/guest-js/dist/index.mjs +++ b/plugins/sql/guest-js/dist/index.mjs @@ -1,116 +1,116 @@ import { invoke } from '@tauri-apps/api/tauri'; -/** - * **Database** - * - * The `Database` class serves as the primary interface for - * communicating with the rust side of the sql plugin. - */ -class Database { - constructor(path) { - this.path = path; - } - /** - * **load** - * - * A static initializer which connects to the underlying database and - * returns a `Database` instance once a connection to the database is established. - * - * # Sqlite - * - * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. - * - * @example - * ```ts - * const db = await Database.load("sqlite:test.db"); - * ``` - */ - static async load(path) { - const _path = await invoke("plugin:sql|load", { - db: path, - }); - return new Database(_path); - } - /** - * **get** - * - * A static initializer which synchronously returns an instance of - * the Database class while deferring the actual database connection - * until the first invocation or selection on the database. - * - * # Sqlite - * - * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. - * - * @example - * ```ts - * const db = Database.get("sqlite:test.db"); - * ``` - */ - static get(path) { - return new Database(path); - } - /** - * **execute** - * - * Passes a SQL expression to the database for execution. - * - * @example - * ```ts - * const result = await db.execute( - * "UPDATE todos SET title = $1, completed = $2 WHERE id = $3", - * [ todos.title, todos.status, todos.id ] - * ); - * ``` - */ - async execute(query, bindValues) { - const [rowsAffected, lastInsertId] = await invoke("plugin:sql|execute", { - db: this.path, - query, - values: bindValues !== null && bindValues !== void 0 ? bindValues : [], - }); - return { - lastInsertId, - rowsAffected, - }; - } - /** - * **select** - * - * Passes in a SELECT query to the database for execution. - * - * @example - * ```ts - * const result = await db.select( - * "SELECT * from todos WHERE id = $1", id - * ); - * ``` - */ - async select(query, bindValues) { - const result = await invoke("plugin:sql|select", { - db: this.path, - query, - values: bindValues !== null && bindValues !== void 0 ? bindValues : [], - }); - return result; - } - /** - * **close** - * - * Closes the database connection pool. - * - * @example - * ```ts - * const success = await db.close() - * ``` - * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. - */ - async close(db) { - const success = await invoke("plugin:sql|close", { - db, - }); - return success; - } +/** + * **Database** + * + * The `Database` class serves as the primary interface for + * communicating with the rust side of the sql plugin. + * + * @connection is a DB connection string like `sqlite:test.db`, etc. + */ +class Database { + constructor(connection) { + this.connection = connection; + } + /** + * **load** + * + * A static initializer which connects to the underlying database and + * returns a `Database` instance once a connection to the database is established. + * + * # Sqlite + * + * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```ts + * const db = await Database.load("sqlite:test.db"); + * ``` + */ + static async load(connection) { + const _conn = await invoke("plugin:sql|load", { + db: connection, + }); + return new Database(_conn); + } + /** + * **get** + * + * A static initializer which synchronously returns an instance of + * the Database class while deferring the actual database connection + * until the first invocation or selection on the database. + * + * # Sqlite + * + * The path is relative to `tauri::api::path::BaseDirectory::App` and must start with `sqlite:`. + * + * @example + * ```ts + * const db = Database.get("sqlite:test.db"); + * ``` + */ + static get(connection) { + return new Database(connection); + } + /** + * **execute** + * + * Passes a SQL expression to the database for execution. + * + * @example + * ```ts + * const result = await db.execute( + * "UPDATE todos SET title = $1, completed = $2 WHERE id = $3", + * [ todos.title, todos.status, todos.id ] + * ); + * ``` + */ + async execute(sql, bindValues) { + const [rowsAffected, lastInsertId] = await invoke("plugin:sql|execute", { + db: this.connection, + sql, + values: bindValues !== null && bindValues !== void 0 ? bindValues : [], + }); + return { + lastInsertId, + rowsAffected, + }; + } + /** + * **select** + * + * Passes in a SELECT query to the database for execution. + * + * @example + * ```ts + * const result = await db.select( + * "SELECT * from todos WHERE id = $1", id + * ); + * ``` + */ + async select(sql, bindValues) { + return await invoke("plugin:sql|select", { + db: this.connection, + sql, + values: bindValues !== null && bindValues !== void 0 ? bindValues : [], + }); + } + /** + * **close** + * + * Closes the database connection pool. + * + * @example + * ```ts + * const success = await db.close() + * ``` + * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. + */ + async close() { + return await invoke("plugin:sql|close", { + db: this.connection, + }); + } } export { Database as default }; diff --git a/plugins/sql/guest-js/dist/index.mjs.map b/plugins/sql/guest-js/dist/index.mjs.map index a2499c4f..25534818 100644 --- a/plugins/sql/guest-js/dist/index.mjs.map +++ b/plugins/sql/guest-js/dist/index.mjs.map @@ -1 +1 @@ -{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAgBA;;;;;AAKG;AACW,MAAO,QAAQ,CAAA;AAE3B,IAAA,WAAA,CAAY,IAAY,EAAA;AACtB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;KAClB;AAED;;;;;;;;;;;;;;AAcG;AACH,IAAA,aAAa,IAAI,CAAC,IAAY,EAAA;AAC5B,QAAA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAS,iBAAiB,EAAE;AACpD,YAAA,EAAE,EAAE,IAAI;AACT,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,IAAI,QAAQ,CAAC,KAAK,CAAC,CAAC;KAC5B;AAED;;;;;;;;;;;;;;;AAeG;IACH,OAAO,GAAG,CAAC,IAAY,EAAA;AACrB,QAAA,OAAO,IAAI,QAAQ,CAAC,IAAI,CAAC,CAAC;KAC3B;AAED;;;;;;;;;;;;AAYG;AACH,IAAA,MAAM,OAAO,CAAC,KAAa,EAAE,UAAsB,EAAA;QACjD,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,MAAM,CAC/C,oBAAoB,EACpB;YACE,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CACF,CAAC;QAEF,OAAO;YACL,YAAY;YACZ,YAAY;SACb,CAAC;KACH;AAED;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,MAAM,CAAI,KAAa,EAAE,UAAsB,EAAA;AACnD,QAAA,MAAM,MAAM,GAAG,MAAM,MAAM,CAAI,mBAAmB,EAAE;YAClD,EAAE,EAAE,IAAI,CAAC,IAAI;YACb,KAAK;AACL,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,MAAM,CAAC;KACf;AAED;;;;;;;;;;AAUG;IACH,MAAM,KAAK,CAAC,EAAW,EAAA;AACrB,QAAA,MAAM,OAAO,GAAG,MAAM,MAAM,CAAU,kBAAkB,EAAE;YACxD,EAAE;AACH,SAAA,CAAC,CAAC;AACH,QAAA,OAAO,OAAO,CAAC;KAChB;AACF;;;;"} \ No newline at end of file +{"version":3,"file":"index.mjs","sources":["../index.ts"],"sourcesContent":[null],"names":[],"mappings":";;AAkBA;;;;;;;AAOG;AACW,MAAO,QAAQ,CAAA;AAE3B,IAAA,WAAA,CAAY,UAAwB,EAAA;AAClC,QAAA,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;KAC9B;AAED;;;;;;;;;;;;;;AAcG;AACH,IAAA,aAAa,IAAI,CAAyB,UAAa,EAAA;AACrD,QAAA,MAAM,KAAK,GAAG,MAAM,MAAM,CAAS,iBAAiB,EAAE;AACpD,YAAA,EAAE,EAAE,UAAU;AACf,SAAA,CAAC,CAAC;AAEH,QAAA,OAAO,IAAI,QAAQ,CAAC,KAAqB,CAAC,CAAC;KAC5C;AAED;;;;;;;;;;;;;;;AAeG;IACH,OAAO,GAAG,CAAC,UAAwB,EAAA;AACjC,QAAA,OAAO,IAAI,QAAQ,CAAC,UAAU,CAAC,CAAC;KACjC;AAED;;;;;;;;;;;;AAYG;AACH,IAAA,MAAM,OAAO,CAAC,GAAW,EAAE,UAAsB,EAAA;QAC/C,MAAM,CAAC,YAAY,EAAE,YAAY,CAAC,GAAG,MAAM,MAAM,CAC/C,oBAAoB,EACpB;YACE,EAAE,EAAE,IAAI,CAAC,UAAU;YACnB,GAAG;AACH,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CACF,CAAC;QAEF,OAAO;YACL,YAAY;YACZ,YAAY;SACb,CAAC;KACH;AAED;;;;;;;;;;;AAWG;AACH,IAAA,MAAM,MAAM,CAAgB,GAAW,EAAE,UAAsB,EAAA;AAC7D,QAAA,OAAO,MAAM,MAAM,CAAI,mBAAmB,EAAE;YAC1C,EAAE,EAAE,IAAI,CAAC,UAAU;YACnB,GAAG;AACH,YAAA,MAAM,EAAE,UAAU,KAAA,IAAA,IAAV,UAAU,KAAV,KAAA,CAAA,GAAA,UAAU,GAAI,EAAE;AACzB,SAAA,CAAC,CAAC;KACJ;AAED;;;;;;;;;;AAUG;AACH,IAAA,MAAM,KAAK,GAAA;AACT,QAAA,OAAO,MAAM,MAAM,CAAU,kBAAkB,EAAE;YAC/C,EAAE,EAAE,IAAI,CAAC,UAAU;AACpB,SAAA,CAAC,CAAC;KACJ;AACF;;;;"} \ No newline at end of file diff --git a/plugins/sql/guest-js/index.ts b/plugins/sql/guest-js/index.ts index a574e72e..d019b2a2 100644 --- a/plugins/sql/guest-js/index.ts +++ b/plugins/sql/guest-js/index.ts @@ -14,16 +14,20 @@ export interface QueryResult { lastInsertId: number; } +export type DbConnection = `${`sqlite` | `postgres` | `mysql`}:${string}`; + /** * **Database** * * The `Database` class serves as the primary interface for * communicating with the rust side of the sql plugin. + * + * @connection is a DB connection string like `sqlite:test.db`, etc. */ export default class Database { - path: string; - constructor(path: string) { - this.path = path; + connection: DbConnection; + constructor(connection: DbConnection) { + this.connection = connection; } /** @@ -41,12 +45,12 @@ export default class Database { * const db = await Database.load("sqlite:test.db"); * ``` */ - static async load(path: string): Promise { - const _path = await invoke("plugin:sql|load", { - db: path, + static async load(connection: C): Promise { + const _conn = await invoke("plugin:sql|load", { + db: connection, }); - return new Database(_path); + return new Database(_conn as DbConnection); } /** @@ -65,8 +69,8 @@ export default class Database { * const db = Database.get("sqlite:test.db"); * ``` */ - static get(path: string): Database { - return new Database(path); + static get(connection: DbConnection): Database { + return new Database(connection); } /** @@ -82,12 +86,12 @@ export default class Database { * ); * ``` */ - async execute(query: string, bindValues?: unknown[]): Promise { + async execute(sql: string, bindValues?: unknown[]): Promise { const [rowsAffected, lastInsertId] = await invoke<[number, number]>( "plugin:sql|execute", { - db: this.path, - query, + db: this.connection, + sql, values: bindValues ?? [], } ); @@ -110,14 +114,12 @@ export default class Database { * ); * ``` */ - async select(query: string, bindValues?: unknown[]): Promise { - const result = await invoke("plugin:sql|select", { - db: this.path, - query, + async select(sql: string, bindValues?: unknown[]): Promise { + return await invoke("plugin:sql|select", { + db: this.connection, + sql, values: bindValues ?? [], }); - - return result; } /** @@ -131,10 +133,9 @@ export default class Database { * ``` * @param db - Optionally state the name of a database if you are managing more than one. Otherwise, all database pools will be in scope. */ - async close(db?: string): Promise { - const success = await invoke("plugin:sql|close", { - db, + async close(): Promise { + return await invoke("plugin:sql|close", { + db: this.connection, }); - return success; } } diff --git a/plugins/sql/src/deserialize.rs b/plugins/sql/src/deserialize.rs new file mode 100644 index 00000000..fb93c65a --- /dev/null +++ b/plugins/sql/src/deserialize.rs @@ -0,0 +1,294 @@ +use crate::plugin::Error; +use serde_json::Value as JsonValue; +use tracing::debug; + +#[allow(unused_imports)] +use sqlx::{Column, Row, TypeInfo}; + +/// ensures consistent conversion of a binary +/// blob in the database to an array of JsonValue::number's +fn blob(b: Vec) -> JsonValue { + JsonValue::Array(b.into_iter().map(|n| JsonValue::Number(n.into())).collect()) +} + +#[cfg(feature = "sqlite")] +pub fn deserialize_col<'a>( + row: &'a sqlx::sqlite::SqliteRow, + col: &'a sqlx::sqlite::SqliteColumn, + i: &'a usize, +) -> Result { + let info = col.type_info(); + debug!("Deserializing column of type {}", info.name()); + + if info.is_null() { + Ok(JsonValue::Null) + } else { + let v = match info.name().to_uppercase().as_str() { + "TEXT" | "STRING" | "VARCHAR" | "DATETIME" => { + JsonValue::String(row.try_get::(i)?) + } + "BLOB" => { + let v = row.try_get::, &usize>(i)?; + blob(v) + } + "INTEGER" | "INT" => { + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + + return Err(Error::NumericDecoding( + info.name().to_string(), + String::from("Sqlite"), + )); + } + "BOOL" | "BOOLEAN" => { + // booleans in sqlite are represented as an integer + if let Ok(b) = row.try_get::(i) { + let b: JsonValue = match b { + 0_i8 => JsonValue::Bool(false), + 1_i8 => JsonValue::Bool(true), + _ => { + return Err(Error::BooleanDecoding( + b.to_string(), + info.name().to_string(), + )); + } + }; + + return Ok(b); + } + + // but they can also be represented with "TRUE" / "FALSE" symbols too + if let Ok(b) = row.try_get::(i) { + JsonValue::Bool(&b.to_lowercase() == "true") + } else { + return Err(Error::BooleanDecoding( + i.to_string(), + info.name().to_string(), + )); + } + } + "REAL" | "FLOAT" | "DOUBLE" | "NUMERIC" => { + let v: f64 = row.try_get(i)?; + JsonValue::from(v) + } + _ => { + tracing::info!( + "an unknown type \"{}\" encountered by Sqlite DB, returning NULL value", + info.name().to_string() + ); + JsonValue::Null + } + }; + + Ok(v) + } +} + +#[cfg(feature = "postgres")] +pub fn deserialize_col<'a>( + row: &'a sqlx::postgres::PgRow, + col: &'a sqlx::postgres::PgColumn, + i: &'a usize, +) -> Result { + let info = col.type_info(); + debug!("Deserializing column of type {}", info.name()); + + if info.is_null() { + Ok(JsonValue::Null) + } else { + Ok(match info.name().to_uppercase().as_str() { + "TEXT" | "VARCHAR" | "NAME" => JsonValue::String(row.try_get(i)?), + "JSON" => JsonValue::String(row.try_get(i)?), + "BOOL" => JsonValue::Bool(row.try_get(i)?), + "DATE" => JsonValue::String(row.try_get(i)?), + "TIME" => JsonValue::String(row.try_get(i)?), + "TIMESTAMP" => JsonValue::String(row.try_get(i)?), + "TIMESTAMPTZ" => JsonValue::String(row.try_get(i)?), + "BLOB" => { + let v = row.try_get::, &usize>(i)?; + blob(v) + } + "BYTEA" => { + // try to encode into numeric array + let v = row.try_get::, &usize>(i)?; + JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect()) + } + "CHAR" => JsonValue::Number(row.try_get::(i)?.into()), + "INT2" | "SMALLINT" | "SMALLSERIAL" => { + JsonValue::Number(row.try_get::(i)?.into()) + } + "INT4" | "INT" | "SERIAL" => JsonValue::Number(row.try_get::(i)?.into()), + "INT8" | "BIGINT" | "BIGSERIAL" => { + JsonValue::Number(row.try_get::(i)?.into()) + } + + "FLOAT4" | "REAL" => { + let v = row.try_get::(i)?; + JsonValue::from(v) + } + "FLOAT8" | "DOUBLE PRECISION" => { + let v = row.try_get::(i)?; + JsonValue::from(v) + } + "NUMERIC" => { + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + if let Ok(v) = row.try_get::(i) { + return Ok(JsonValue::Number(v.into())); + } + + return Err(Error::NumericDecoding( + info.name().to_string(), + String::from("Postgres"), + )); + } + _ => { + tracing::info!( + "an unknown type \"{}\" encountered by Postgres DB, returning NULL value", + info.name().to_string() + ); + JsonValue::Null + } + }) + } +} + +#[cfg(feature = "mysql")] +pub fn deserialize_col<'a>( + row: &'a sqlx::mysql::MySqlRow, + col: &'a sqlx::mysql::MySqlColumn, + i: &'a usize, +) -> Result { + let info = col.type_info(); + debug!("Deserializing column of type {}", info.name()); + + if info.is_null() { + Ok(JsonValue::Null) + } else { + let v = match info.name().to_uppercase().as_str() { + "TIMESTAMP" => JsonValue::String(row.try_get(i)?), + "DATE" => JsonValue::String(row.try_get(i)?), + "TIME" => JsonValue::String(row.try_get(i)?), + "DATETIME" => JsonValue::String(row.try_get(i)?), + "NEWDATE" => JsonValue::String(row.try_get(i)?), + "VARCHAR" | "TEXT" | "CHAR" => JsonValue::String(row.try_get(i)?), + "JSON" => JsonValue::String(row.try_get(i)?), + "VAR_STRING" => JsonValue::String(row.try_get(i)?), + "STRING" => JsonValue::String(row.try_get(i)?), + "BLOB" | "TINY_BLOB" | "MEDIUM_BLOB" | "LONG_BLOB" => { + let v = row.try_get::, &usize>(i)?; + blob(v) + } + "ENUM" => JsonValue::String(row.try_get(i)?), + "SET" => JsonValue::String(row.try_get(i)?), + "GEOMETRY" => { + // try to encode into numeric array + let v = row.try_get::, &usize>(i)?; + JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect()) + } + "TINY" | "TINYINT" => JsonValue::Number(row.try_get::(i)?.into()), + "SMALL" | "SMALLINT" => JsonValue::Number(row.try_get::(i)?.into()), + "YEAR" => JsonValue::Number(row.try_get::(i)?.into()), + // really only takes 24-bits + "MEDIUM" | "MEDIUMINT" => JsonValue::Number(row.try_get::(i)?.into()), + // 32-bit primitive + "INT" => JsonValue::Number(row.try_get::(i)?.into()), + "BIGINT" => JsonValue::Number(row.try_get::(i)?.into()), + "REAL" | "FLOAT" => { + let v = row.try_get::(i)?; + JsonValue::from(v) + } + "DOUBLE" => JsonValue::Number(row.try_get::(i)?.into()), + "BIT" => JsonValue::Number(row.try_get::(i)?.into()), + _ => { + tracing::info!( + "an unknown type \"{}\" encountered by MySql database, returning NULL value", + info.name().to_string() + ); + JsonValue::Null + } + }; + + Ok(v) + } +} + +#[cfg(feature = "mssql")] +pub fn deserialize_col<'a>( + row: &'a sqlx::mssql::MssqlRow, + col: &'a sqlx::mssql::MssqlColumn, + i: &'a usize, +) -> Result { + let info = col.type_info(); + debug!("Deserializing column of type {}", info.name()); + + if info.is_null() { + Ok(JsonValue::Null) + } else { + let v = match info.name().to_uppercase().as_str() { + "TIMESTAMP" => JsonValue::String(row.try_get(i)?), + "DATE" => JsonValue::String(row.try_get(i)?), + "TIME" => JsonValue::String(row.try_get(i)?), + "DATETIME" => JsonValue::String(row.try_get(i)?), + "NEWDATE" => JsonValue::String(row.try_get(i)?), + "VARCHAR" => JsonValue::String(row.try_get(i)?), + "VAR_STRING" => JsonValue::String(row.try_get(i)?), + "STRING" => JsonValue::String(row.try_get(i)?), + "BLOB" | "TINY_BLOB" | "MEDIUM_BLOB" | "LONG_BLOB" => { + let v = row.try_get::(i)?; + let v = v.as_bytes().to_vec(); + blob(v) + } + "ENUM" => JsonValue::String(row.try_get(i)?), + "SET" => JsonValue::String(row.try_get(i)?), + "GEOMETRY" => JsonValue::from(row.try_get::(i)?), + "GEOGRAPHY" => JsonValue::from(row.try_get::(i)?), + "TINY" | "TINYINT" => JsonValue::Number(row.try_get::(i)?.into()), + "SMALL" | "SMALLINT" => JsonValue::Number(row.try_get::(i)?.into()), + // really only takes 24-bits + "MEDIUM" | "MEDIUMINT" => JsonValue::Number(row.try_get::(i)?.into()), + // 32-bit primitive + "INT" => JsonValue::Number(row.try_get::(i)?.into()), + // 64-bit int + "BIGINT" => JsonValue::Number(row.try_get::(i)?.into()), + "YEAR" => JsonValue::Number(row.try_get::(i)?.into()), + "BIT" => JsonValue::Number(row.try_get::(i)?.into()), + "DOUBLE" => JsonValue::Number(row.try_get::(i)?.into()), + + "REAL" => { + let v = row.try_get::(i)?; + JsonValue::from(v) + } + "FLOAT" => { + let v = row.try_get::(i)?; + JsonValue::from(v) + } + _ => { + tracing::info!( + "an unknown type \"{}\" encountered by MS SQL database, returning NULL value", + info.name().to_string() + ); + JsonValue::Null + } + }; + + Ok(v) + } +} diff --git a/plugins/sql/src/lib.rs b/plugins/sql/src/lib.rs index aebeeecc..db71aafe 100644 --- a/plugins/sql/src/lib.rs +++ b/plugins/sql/src/lib.rs @@ -5,24 +5,65 @@ #[cfg(any( all(feature = "sqlite", feature = "mysql"), all(feature = "sqlite", feature = "postgres"), - all(feature = "mysql", feature = "postgres") + all(feature = "sqlite", feature = "mssql"), + all(feature = "mysql", feature = "postgres"), + all(feature = "mysql", feature = "mssql"), ))] compile_error!("Only one database driver can be enabled. Use `default-features = false` and set the feature flag for the driver of your choice."); -#[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgres")))] +#[cfg(not(any( + feature = "sqlite", + feature = "mysql", + feature = "postgres", + feature = "mssql" +)))] compile_error!( "Database driver not defined. Please set the feature flag for the driver of your choice." ); #[cfg(any( - all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))), - all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))), - all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))), + feature = "sqlite", + feature = "mssql", + feature = "mysql", + feature = "postgres" +))] +pub mod deserialize; + +#[cfg(any( + all( + feature = "sqlite", + not(any(feature = "mssql", feature = "mysql", feature = "postgres")) + ), + all( + feature = "mysql", + not(any(feature = "sqlite", feature = "mssql", feature = "postgres")) + ), + all( + feature = "postgres", + not(any(feature = "sqlite", feature = "mysql", feature = "mysql")) + ), + all( + feature = "mssql", + not(any(feature = "sqlite", feature = "mysql", feature = "postgres")) + ), ))] mod plugin; #[cfg(any( - all(feature = "sqlite", not(any(feature = "mysql", feature = "postgres"))), - all(feature = "mysql", not(any(feature = "sqlite", feature = "postgres"))), - all(feature = "postgres", not(any(feature = "sqlite", feature = "mysql"))), + all( + feature = "sqlite", + not(any(feature = "mssql", feature = "mysql", feature = "postgres")) + ), + all( + feature = "mysql", + not(any(feature = "sqlite", feature = "mssql", feature = "postgres")) + ), + all( + feature = "postgres", + not(any(feature = "sqlite", feature = "mysql", feature = "mysql")) + ), + all( + feature = "mssql", + not(any(feature = "sqlite", feature = "mysql", feature = "postgres")) + ), ))] pub use plugin::*; diff --git a/plugins/sql/src/plugin.rs b/plugins/sql/src/plugin.rs index 5440c926..d05dccd9 100644 --- a/plugins/sql/src/plugin.rs +++ b/plugins/sql/src/plugin.rs @@ -1,25 +1,37 @@ // Copyright 2021 Tauri Programme within The Commons Conservancy // SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: MIT - -use futures::future::BoxFuture; use serde::{ser::Serializer, Deserialize, Serialize}; use serde_json::Value as JsonValue; + +#[cfg(feature = "mssql")] +use sqlx::mssql::MssqlArguments; +#[cfg(feature = "mysql")] +type SqlArguments<'a> = sqlx::mysql::MySqlArguments; +#[cfg(feature = "postgres")] +type SqlArguments<'a> = sqlx::postgres::PgArguments; +#[cfg(feature = "sqlite")] +type SqlArguments<'a> = sqlx::sqlite::SqliteArguments<'a>; + +#[cfg(not(feature = "mssql"))] +use futures::future::BoxFuture; +#[cfg(not(feature = "mssql"))] use sqlx::{ error::BoxDynError, migrate::{ MigrateDatabase, Migration as SqlxMigration, MigrationSource, MigrationType, Migrator, }, - Column, Pool, Row, TypeInfo, }; + +use sqlx::{query::Query, Column, Pool, Row}; +use std::collections::HashMap; use tauri::{ command, plugin::{Plugin, Result as PluginResult}, AppHandle, Invoke, Manager, RunEvent, Runtime, State, }; use tokio::sync::Mutex; - -use std::collections::HashMap; +use tracing::info; #[cfg(feature = "sqlite")] use std::{fs::create_dir_all, path::PathBuf}; @@ -30,6 +42,8 @@ type Db = sqlx::sqlite::Sqlite; type Db = sqlx::mysql::MySql; #[cfg(feature = "postgres")] type Db = sqlx::postgres::Postgres; +#[cfg(feature = "mssql")] +type Db = sqlx::mssql::Mssql; #[cfg(feature = "sqlite")] type LastInsertId = i64; @@ -44,6 +58,12 @@ pub enum Error { Migration(#[from] sqlx::migrate::MigrateError), #[error("database {0} not loaded")] DatabaseNotLoaded(String), + #[error("Could not decode the numeric column {0} into a type for {1} database.")] + NumericDecoding(String, String), + #[error("Sqlite doesn't have a native Boolean type but represents boolean values as an integer value of 0 or 1, however we received a value of {0} for the column {1}")] + BooleanDecoding(String, String), + #[error("Non-string based query is not allowed with this database")] + NonStringQuery, } impl Serialize for Error { @@ -55,11 +75,13 @@ impl Serialize for Error { } } +use crate::deserialize::deserialize_col; + type Result = std::result::Result; #[cfg(feature = "sqlite")] -/// Resolves the App's **file path** from the `AppHandle` context -/// object +/// Resolves the App's **file path** from the `AppHandle` +/// context object fn app_path(app: &AppHandle) -> PathBuf { #[allow(deprecated)] // FIXME: Change to non-deprecated function in Tauri v2 app.path_resolver() @@ -89,7 +111,10 @@ fn path_mapper(mut app_path: PathBuf, connection_string: &str) -> String { #[derive(Default)] struct DbInstances(Mutex>>); +#[cfg(not(feature = "mssql"))] struct Migrations(Mutex>); +#[cfg(feature = "mssql")] +struct Migrations(); #[derive(Default, Deserialize)] struct PluginConfig { @@ -98,11 +123,13 @@ struct PluginConfig { } #[derive(Debug)] +#[cfg(not(feature = "mssql"))] pub enum MigrationKind { Up, Down, } +#[cfg(not(feature = "mssql"))] impl From for MigrationType { fn from(kind: MigrationKind) -> Self { match kind { @@ -114,6 +141,7 @@ impl From for MigrationType { /// A migration definition. #[derive(Debug)] +#[cfg(not(feature = "mssql"))] pub struct Migration { pub version: i64, pub description: &'static str, @@ -122,9 +150,12 @@ pub struct Migration { } #[derive(Debug)] +#[cfg(not(feature = "mssql"))] struct MigrationList(Vec); +#[cfg(not(feature = "mssql"))] impl MigrationSource<'static> for MigrationList { + #[tracing::instrument] fn resolve(self) -> BoxFuture<'static, std::result::Result, BoxDynError>> { Box::pin(async move { let mut migrations = Vec::new(); @@ -158,17 +189,23 @@ async fn load( #[cfg(feature = "sqlite")] create_dir_all(app_path(&app)).expect("Problem creating App directory!"); + // currently sqlx can not create a mssql database + #[cfg(not(feature = "mssql"))] if !Db::database_exists(&fqdb).await.unwrap_or(false) { Db::create_database(&fqdb).await?; } + let pool = Pool::connect(&fqdb).await?; + #[cfg(not(feature = "mssql"))] if let Some(migrations) = migrations.0.lock().await.remove(&db) { let migrator = Migrator::new(migrations).await?; migrator.run(&pool).await?; } db_instances.0.lock().await.insert(db.clone(), pool); + info!("Database pool \"{}\" has been loaded", db.clone()); + Ok(db) } @@ -185,6 +222,11 @@ async fn close(db_instances: State<'_, DbInstances>, db: Option) -> Resu instances.keys().cloned().collect() }; + info!( + "{} databases closed explicitly in close() call.", + pools.len().to_string() + ); + for pool in pools { let db = instances .get_mut(&pool) // @@ -200,108 +242,92 @@ async fn close(db_instances: State<'_, DbInstances>, db: Option) -> Resu async fn execute( db_instances: State<'_, DbInstances>, db: String, - query: String, + sql: String, values: Vec, ) -> Result<(u64, LastInsertId)> { let mut instances = db_instances.0.lock().await; - - let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?; - let mut query = sqlx::query(&query); + let db = instances + .get_mut(&db) // + .ok_or(Error::DatabaseNotLoaded(db))?; + let mut query = sqlx::query(&sql); for value in values { - if value.is_string() { - query = query.bind(value.as_str().unwrap().to_owned()) - } else { - query = query.bind(value); - } + query = bind_query(query, value)?; } let result = query.execute(&*db).await?; + info!("successful database execute() command: {}", &sql); + #[cfg(feature = "sqlite")] let r = Ok((result.rows_affected(), result.last_insert_rowid())); #[cfg(feature = "mysql")] let r = Ok((result.rows_affected(), result.last_insert_id())); #[cfg(feature = "postgres")] let r = Ok((result.rows_affected(), 0)); + #[cfg(feature = "mssql")] + let r = Ok((result.rows_affected(), 0)); + r } +#[cfg(feature = "mssql")] +fn bind_query( + mut query: Query, + value: JsonValue, +) -> Result> { + if value.is_string() { + query = query.bind(value.as_str().unwrap().to_owned()); + Ok(query) + } else { + Err(Error::NonStringQuery) + } +} +#[cfg(not(feature = "mssql"))] +fn bind_query<'a>( + mut query: Query<'a, Db, SqlArguments<'a>>, + value: JsonValue, +) -> Result>> { + if value.is_string() { + query = query.bind(value.as_str().unwrap().to_owned()); + Ok(query) + } else { + query = query.bind(value); + Ok(query) + } +} + #[command] async fn select( db_instances: State<'_, DbInstances>, db: String, - query: String, + sql: String, values: Vec, ) -> Result>> { let mut instances = db_instances.0.lock().await; let db = instances.get_mut(&db).ok_or(Error::DatabaseNotLoaded(db))?; - let mut query = sqlx::query(&query); + let mut query = sqlx::query(&sql); + for value in values { - if value.is_string() { - query = query.bind(value.as_str().unwrap().to_owned()) - } else { - query = query.bind(value); - } + query = bind_query(query, value)?; } + let rows = query.fetch_all(&*db).await?; let mut values = Vec::new(); for row in rows { let mut value = HashMap::default(); for (i, column) in row.columns().iter().enumerate() { - let info = column.type_info(); - let v = if info.is_null() { - JsonValue::Null - } else { - match info.name() { - "VARCHAR" | "STRING" | "TEXT" | "DATETIME" => { - if let Ok(s) = row.try_get(i) { - JsonValue::String(s) - } else { - JsonValue::Null - } - } - "BOOL" | "BOOLEAN" => { - if let Ok(b) = row.try_get(i) { - JsonValue::Bool(b) - } else { - let x: String = row.get(i); - JsonValue::Bool(x.to_lowercase() == "true") - } - } - "INT" | "NUMBER" | "INTEGER" | "BIGINT" | "INT8" => { - if let Ok(n) = row.try_get::(i) { - JsonValue::Number(n.into()) - } else { - JsonValue::Null - } - } - "REAL" => { - if let Ok(n) = row.try_get::(i) { - JsonValue::from(n) - } else { - JsonValue::Null - } - } - // "JSON" => JsonValue::Object(row.get(i)), - "BLOB" => { - if let Ok(n) = row.try_get::, usize>(i) { - JsonValue::Array( - n.into_iter().map(|n| JsonValue::Number(n.into())).collect(), - ) - } else { - JsonValue::Null - } - } - _ => JsonValue::Null, - } - }; + let v = deserialize_col(&row, column, &i)?; value.insert(column.name().to_string(), v); } values.push(value); } + + info!("successful select() query: {}", sql); + Ok(values) } /// Tauri SQL plugin. pub struct TauriSql { + #[cfg(not(feature = "mssql"))] migrations: Option>, invoke_handler: Box) + Send + Sync>, } @@ -309,6 +335,7 @@ pub struct TauriSql { impl Default for TauriSql { fn default() -> Self { Self { + #[cfg(not(feature = "mssql"))] migrations: Some(Default::default()), invoke_handler: Box::new(tauri::generate_handler![load, execute, select, close]), } @@ -318,11 +345,15 @@ impl Default for TauriSql { impl TauriSql { /// Add migrations to a database. #[must_use] + #[cfg(not(feature = "mssql"))] pub fn add_migrations(mut self, db_url: &str, migrations: Vec) -> Self { self.migrations .as_mut() .unwrap() .insert(db_url.to_string(), MigrationList(migrations)); + + info!("migrations on database have finished"); + self } } @@ -332,12 +363,16 @@ impl Plugin for TauriSql { "sql" } - fn initialize(&mut self, app: &AppHandle, config: serde_json::Value) -> PluginResult<()> { + fn initialize( + &mut self, + app: &AppHandle, + user_config: serde_json::Value, + ) -> PluginResult<()> { tauri::async_runtime::block_on(async move { - let config: PluginConfig = if config.is_null() { + let config: PluginConfig = if user_config.is_null() { Default::default() } else { - serde_json::from_value(config)? + serde_json::from_value(user_config.clone())? }; #[cfg(feature = "sqlite")] @@ -351,20 +386,27 @@ impl Plugin for TauriSql { #[cfg(not(feature = "sqlite"))] let fqdb = db.clone(); + #[cfg(not(feature = "mssql"))] if !Db::database_exists(&fqdb).await.unwrap_or(false) { Db::create_database(&fqdb).await?; } let pool = Pool::connect(&fqdb).await?; + // TODO: currently sqlx does not support migrations for mssql + #[cfg(not(feature = "mssql"))] if let Some(migrations) = self.migrations.as_mut().unwrap().remove(&db) { let migrator = Migrator::new(migrations).await?; migrator.run(&pool).await?; } + lock.insert(db, pool); } drop(lock); app.manage(instances); + #[cfg(not(feature = "mssql"))] app.manage(Migrations(Mutex::new(self.migrations.take().unwrap()))); + + info!("tauri-sql-plugin is initialized: [config: {}]", user_config); Ok(()) }) } @@ -373,7 +415,9 @@ impl Plugin for TauriSql { (self.invoke_handler)(message) } + /// gracefully close all DB pools on application exit fn on_event(&mut self, app: &AppHandle, event: &RunEvent) { + info!("closing all DB pools due to application exit"); if let RunEvent::Exit = event { tauri::async_runtime::block_on(async move { let instances = &*app.state::();