diff --git a/README.md b/README.md index 576192d..c53750e 100644 --- a/README.md +++ b/README.md @@ -99,7 +99,7 @@ $ deno run -A --unstable https://deno.land/x/nqlite/main.ts --wshost=wss://FQDN ## Coming Soon - [ ] Prometheus exporter -- [X] Transactions +- [ ] Transactions - [ ] API Authentication - [ ] InsertId and affectedRows in response - [ ] Work with Deno Deploy (memory db) @@ -283,19 +283,39 @@ curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ ]' ``` -## Transactions - -A form of transactions are supported where nqlite accepts an array of queries. Only **Write** queries are supported. You can use the same `/db/query` as usual. +## Bulk Queries ### Write ```bash curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d "[ - \"INSERT INTO foo(name) VALUES('fiona')\", - \"INSERT INTO foo(name) VALUES('sinead')\" + \"INSERT INTO foo(name) VALUES('fiona')\", + \"INSERT INTO foo(name) VALUES('sinead')\" ]" ``` +### Parameterized Bulk Queries + +```bash +curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ + ["INSERT INTO foo(name) VALUES(?)", "fiona"], + ["INSERT INTO foo(name) VALUES(?)", "sinead"] +]' +``` + +## Named Parameter Bulk Queries + +```bash +curl -XPOST 'localhost:4001/db/query' -H "Content-Type: application/json" -d '[ + ["INSERT INTO foo(name) VALUES(:name)", {"name": "fiona"}], + ["INSERT INTO foo(name) VALUES(:name)", {"name": "sinead"}] +]' +``` + +## Transactions + +Not implemented yet + ## Error handling nqlite will return a body that looks like this: diff --git a/main.ts b/main.ts index 52251a3..a518648 100644 --- a/main.ts +++ b/main.ts @@ -49,8 +49,16 @@ if (flags.creds && flags.token) { showHelp(); } -import { Nqlite } from "./mod.ts"; +import { Nqlite, Options } from "./mod.ts"; // Startup nqlite const nqlite = new Nqlite(); -await nqlite.init(flags["wshost"], flags["creds"], flags["token"], flags["data-dir"]); + +const opts: Options = { + url: flags["wshost"], + creds: flags["creds"], + token: flags["token"], + dataDir: flags["data-dir"], +}; + +await nqlite.init(opts); diff --git a/mod.ts b/mod.ts index 79331e7..dae10c6 100644 --- a/mod.ts +++ b/mod.ts @@ -14,7 +14,7 @@ import { serve } from "serve"; import { Context, Hono } from "hono"; import { nats, restore, snapshot } from "./util.ts"; import { Database } from "sqlite3"; -import { NatsInit, NatsRes, ParseRes, Res } from "./types.ts"; +import { NatsInit, NatsRes, Options, ParseRes, Res } from "./types.ts"; import { parse } from "./parse.ts"; export class Nqlite { @@ -45,7 +45,8 @@ export class Nqlite { } // Init function to connect to NATS - async init(url: string, creds: string, token: string, dataDir: string): Promise { + async init(opts: Options): Promise { + const { url, creds, token, dataDir } = opts; // Make sure directory exists this.dataDir = dataDir; this.dbFile = `${this.dataDir}/nqlite.db`; @@ -104,14 +105,21 @@ export class Nqlite { return res; } - // Check for transaction - if (s.txItems.length) { - let changes = 0; - for (const p of s.txItems) { - this.db.exec(p); - changes += this.db.changes; + // Check for simple bulk query + if (s.bulkItems.length && s.simple) { + for (const p of s.bulkItems) this.db.exec(p); + res.time = performance.now() - s.t; + res.results[0].last_insert_id = this.db.lastInsertRowId; + return res; + } + + // Check for bulk paramaterized/named query + if (s.bulkParams.length) { + for (const p of s.bulkParams) { + const stmt = this.db.prepare(p.query); + stmt.run(...p.params); } - res.results[0].rows_affected = changes; + res.results[0].last_insert_id = this.db.lastInsertRowId; res.time = performance.now() - s.t; return res; } @@ -128,7 +136,7 @@ export class Nqlite { // Must not be a read statement res.results[0].rows_affected = s.simple ? stmt.all() - : stmt.all(...s.params); + : stmt.run(...s.params); res.results[0].last_insert_id = this.db.lastInsertRowId; res.time = performance.now() - s.t; return res; @@ -314,3 +322,5 @@ export class Nqlite { serve(api.fetch, { port: 4001 }); } } + +export type { Options }; diff --git a/parse.ts b/parse.ts index 012f9e7..9fc73bc 100644 --- a/parse.ts +++ b/parse.ts @@ -9,7 +9,8 @@ export function parse(data: JSON, t: number): ParseRes { t, data, isRead: false, - txItems: [], + bulkItems: [], + bulkParams: [], }; // If this is not an array, return error @@ -19,36 +20,42 @@ export function parse(data: JSON, t: number): ParseRes { return res; } - // If this is not an array of arrays, just a simple query + // If array is empty, return error + if (!data.length) { + res.error = "Empty array"; + console.log(data); + return res; + } + + // Handle simple query if (!Array.isArray(data[0])) { - // Check if this is a transaction (more than 1 item in the array) - if (data.length > 1) { - // Make sure it's not a read query - if (isReadTx(data)) { - res.error = "Invalid Transaction. SELECT query in transaction"; - console.log(data); - return res; - } + // Check if this is really a simple query + if (data.length === 1) { + res.query = data[0] as string; + res.isRead = isReadQuery(res.query); + return res; + } - // Make sure data is an array of strings - for (const d of data) { - if (typeof d !== "string") { - res.error = "Invalid Transaction. Not an array of strings"; - console.log(d); - return res; - } - } + // Must be a bulk query + // Make sure it's not a read query + if (isReadBulk(data)) { + res.error = "Invalid Bulk. SELECT query in bulk request"; + console.log(data); + return res; + } - res.txItems = data; + // Make sure data is an array of strings + if (data.find((d) => typeof d !== "string")) { + res.error = "Invalid Bulk. Not an array of strings"; + console.log(data); return res; } - - res.query = data[0] as string; - res.isRead = isReadQuery(res.query); + + res.bulkItems = data; return res; } - // If array is nested more than 2 levels, return error + // Check for array more than 2 levels deep if (Array.isArray(data[0][0])) { res.error = "Invalid Paramaratized/Named Statement. Array more than 2 levels deep"; @@ -56,25 +63,55 @@ export function parse(data: JSON, t: number): ParseRes { return res; } - // Grab the first item in the array - const item = Array.from(data[0]); + // At this point, we know it's a paramarized/named statement + res.simple = false; - // If item has fewer than 2 items, return error - if (item.length < 2) { - res.error = "Invalid Paramaratized/Named Statement. Not enough items"; - console.log(item); + // Check for bulk paramarized/named statements (second array is an array) + if (data.length > 1 && Array.isArray(data[1])) { + // Build the bulkItems array + for (const i of data) { + const paramRes = paramQueryRes(i); + const { error, query, params, isRead } = paramRes; + + // If error in paramarized/named statement, return error + if (error) { + res.error = error; + console.log(data); + return res; + } + + // If this is a read query, return error + if (isRead) { + res.error = "Invalid Bulk. SELECT query in bulk request"; + console.log(data); + return res; + } + + res.bulkParams.push({ query, params }); + } + + return res; + } + + // Must be regular (non bulk) paramarized/named statement + const paramRes = paramQueryRes(data[0]); + const { error, query, params, isRead } = paramRes; + + // If error in paramarized/named statement, return error + if (error) { + res.error = error; + console.log(data); return res; } // Shift the first item off the array as the SQL statement - res.query = item.shift(); - res.simple = false; - res.isRead = isReadQuery(res.query); - res.params = item; + res.query = query; + res.isRead = isRead; + res.params = params; return res; } -function isReadTx(data: string[]): boolean { +function isReadBulk(data: string[]): boolean { const found = data.find((q) => isReadQuery(q)); return found ? true : false; } @@ -82,3 +119,28 @@ function isReadTx(data: string[]): boolean { function isReadQuery(q: string): boolean { return q.toLowerCase().startsWith("select"); } + +function paramQueryRes(data: string[]) { + const res = { + error: "", + query: "", + params: [] as string[], + isRead: false, + }; + + // Grab the first item in the array + const params = Array.from(data); + + // If item has fewer than 2 items, return error + if (params.length < 2) { + res.error = "Invalid Paramaratized/Named Statement. Not enough items"; + console.log(params); + return res; + } + + // Shift the first item off the array as the SQL statement + res.query = params.shift() as string; + res.isRead = isReadQuery(res.query); + res.params = params; + return res; +} diff --git a/types.ts b/types.ts index 222913b..b5c586a 100644 --- a/types.ts +++ b/types.ts @@ -41,7 +41,13 @@ export type ParseRes = { t: number; data: JSON; isRead: boolean; - txItems: string[]; + bulkItems: string[]; + bulkParams: bulkParams[]; +}; + +type bulkParams = { + query: string; + params: RestBindParameters; }; export type Res = { @@ -49,3 +55,10 @@ export type Res = { results: Array>; time: number; }; + +export type Options = { + url: string; + creds: string; + token: string; + dataDir: string; +};