feat(sql): better db specific types

Co-authored-by: Ken Snyder <ken@ken.net>
pull/31/head
FabianLars 3 years ago
parent 366e17feae
commit 696470c358
No known key found for this signature in database
GPG Key ID: 3B12BC1DEBF61125

@ -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 };

@ -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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"}
{"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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"}

@ -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 };

@ -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;;;;"}
{"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;;;;"}

@ -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<Database> {
const _path = await invoke<string>("plugin:sql|load", {
db: path,
static async load<C extends DbConnection>(connection: C): Promise<Database> {
const _conn = await invoke<string>("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<QueryResult> {
async execute(sql: string, bindValues?: unknown[]): Promise<QueryResult> {
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<T>(query: string, bindValues?: unknown[]): Promise<T> {
const result = await invoke<T>("plugin:sql|select", {
db: this.path,
query,
async select<T = unknown[]>(sql: string, bindValues?: unknown[]): Promise<T> {
return await invoke<T>("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<boolean> {
const success = await invoke<boolean>("plugin:sql|close", {
db,
async close(): Promise<boolean> {
return await invoke<boolean>("plugin:sql|close", {
db: this.connection,
});
return success;
}
}

@ -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<u8>) -> 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<JsonValue, Error> {
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::<String, &usize>(i)?)
}
"BLOB" => {
let v = row.try_get::<Vec<u8>, &usize>(i)?;
blob(v)
}
"INTEGER" | "INT" => {
if let Ok(v) = row.try_get::<i64, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i32, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i16, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i8, &usize>(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::<i8, &usize>(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::<String, &usize>(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<JsonValue, Error> {
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::<Vec<u8>, &usize>(i)?;
blob(v)
}
"BYTEA" => {
// try to encode into numeric array
let v = row.try_get::<Vec<u8>, &usize>(i)?;
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
}
"CHAR" => JsonValue::Number(row.try_get::<i8, &usize>(i)?.into()),
"INT2" | "SMALLINT" | "SMALLSERIAL" => {
JsonValue::Number(row.try_get::<i16, &usize>(i)?.into())
}
"INT4" | "INT" | "SERIAL" => JsonValue::Number(row.try_get::<i32, &usize>(i)?.into()),
"INT8" | "BIGINT" | "BIGSERIAL" => {
JsonValue::Number(row.try_get::<i64, &usize>(i)?.into())
}
"FLOAT4" | "REAL" => {
let v = row.try_get::<f32, &usize>(i)?;
JsonValue::from(v)
}
"FLOAT8" | "DOUBLE PRECISION" => {
let v = row.try_get::<f64, &usize>(i)?;
JsonValue::from(v)
}
"NUMERIC" => {
if let Ok(v) = row.try_get::<i64, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i32, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i16, &usize>(i) {
return Ok(JsonValue::Number(v.into()));
}
if let Ok(v) = row.try_get::<i8, &usize>(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<JsonValue, Error> {
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::<Vec<u8>, &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::<Vec<u8>, &usize>(i)?;
JsonValue::Array(v.into_iter().map(|n| JsonValue::Number(n.into())).collect())
}
"TINY" | "TINYINT" => JsonValue::Number(row.try_get::<i8, &usize>(i)?.into()),
"SMALL" | "SMALLINT" => JsonValue::Number(row.try_get::<i16, &usize>(i)?.into()),
"YEAR" => JsonValue::Number(row.try_get::<i16, &usize>(i)?.into()),
// really only takes 24-bits
"MEDIUM" | "MEDIUMINT" => JsonValue::Number(row.try_get::<i32, &usize>(i)?.into()),
// 32-bit primitive
"INT" => JsonValue::Number(row.try_get::<i32, &usize>(i)?.into()),
"BIGINT" => JsonValue::Number(row.try_get::<i64, &usize>(i)?.into()),
"REAL" | "FLOAT" => {
let v = row.try_get::<f64, &usize>(i)?;
JsonValue::from(v)
}
"DOUBLE" => JsonValue::Number(row.try_get::<i64, &usize>(i)?.into()),
"BIT" => JsonValue::Number(row.try_get::<i8, &usize>(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<JsonValue, Error> {
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::<String, &usize>(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::<String, &usize>(i)?),
"GEOGRAPHY" => JsonValue::from(row.try_get::<String, &usize>(i)?),
"TINY" | "TINYINT" => JsonValue::Number(row.try_get::<i8, &usize>(i)?.into()),
"SMALL" | "SMALLINT" => JsonValue::Number(row.try_get::<i16, &usize>(i)?.into()),
// really only takes 24-bits
"MEDIUM" | "MEDIUMINT" => JsonValue::Number(row.try_get::<i32, &usize>(i)?.into()),
// 32-bit primitive
"INT" => JsonValue::Number(row.try_get::<i32, &usize>(i)?.into()),
// 64-bit int
"BIGINT" => JsonValue::Number(row.try_get::<i64, &usize>(i)?.into()),
"YEAR" => JsonValue::Number(row.try_get::<i16, &usize>(i)?.into()),
"BIT" => JsonValue::Number(row.try_get::<i8, &usize>(i)?.into()),
"DOUBLE" => JsonValue::Number(row.try_get::<i64, &usize>(i)?.into()),
"REAL" => {
let v = row.try_get::<f64, &usize>(i)?;
JsonValue::from(v)
}
"FLOAT" => {
let v = row.try_get::<f32, &usize>(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)
}
}

@ -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::*;

@ -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<T> = std::result::Result<T, Error>;
#[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<R: Runtime>(app: &AppHandle<R>) -> 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<HashMap<String, Pool<Db>>>);
#[cfg(not(feature = "mssql"))]
struct Migrations(Mutex<HashMap<String, MigrationList>>);
#[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<MigrationKind> for MigrationType {
fn from(kind: MigrationKind) -> Self {
match kind {
@ -114,6 +141,7 @@ impl From<MigrationKind> 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<Migration>);
#[cfg(not(feature = "mssql"))]
impl MigrationSource<'static> for MigrationList {
#[tracing::instrument]
fn resolve(self) -> BoxFuture<'static, std::result::Result<Vec<SqlxMigration>, BoxDynError>> {
Box::pin(async move {
let mut migrations = Vec::new();
@ -158,17 +189,23 @@ async fn load<R: Runtime>(
#[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<String>) -> 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<String>) -> Resu
async fn execute(
db_instances: State<'_, DbInstances>,
db: String,
query: String,
sql: String,
values: Vec<JsonValue>,
) -> 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<Db, MssqlArguments>,
value: JsonValue,
) -> Result<Query<Db, MssqlArguments>> {
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<Query<'a, Db, SqlArguments<'a>>> {
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<JsonValue>,
) -> Result<Vec<HashMap<String, JsonValue>>> {
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::<i64, usize>(i) {
JsonValue::Number(n.into())
} else {
JsonValue::Null
}
}
"REAL" => {
if let Ok(n) = row.try_get::<f64, usize>(i) {
JsonValue::from(n)
} else {
JsonValue::Null
}
}
// "JSON" => JsonValue::Object(row.get(i)),
"BLOB" => {
if let Ok(n) = row.try_get::<Vec<u8>, 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<R: Runtime> {
#[cfg(not(feature = "mssql"))]
migrations: Option<HashMap<String, MigrationList>>,
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
@ -309,6 +335,7 @@ pub struct TauriSql<R: Runtime> {
impl<R: Runtime> Default for TauriSql<R> {
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<R: Runtime> Default for TauriSql<R> {
impl<R: Runtime> TauriSql<R> {
/// Add migrations to a database.
#[must_use]
#[cfg(not(feature = "mssql"))]
pub fn add_migrations(mut self, db_url: &str, migrations: Vec<Migration>) -> 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<R: Runtime> Plugin<R> for TauriSql<R> {
"sql"
}
fn initialize(&mut self, app: &AppHandle<R>, config: serde_json::Value) -> PluginResult<()> {
fn initialize(
&mut self,
app: &AppHandle<R>,
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<R: Runtime> Plugin<R> for TauriSql<R> {
#[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<R: Runtime> Plugin<R> for TauriSql<R> {
(self.invoke_handler)(message)
}
/// gracefully close all DB pools on application exit
fn on_event(&mut self, app: &AppHandle<R>, 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::<DbInstances>();

Loading…
Cancel
Save