Skip to content

Commit

Permalink
Merge pull request #5 from voxoco/remove-tx-add-bulk
Browse files Browse the repository at this point in the history
Remove tx add bulk
  • Loading branch information
jmordica committed Dec 30, 2022
2 parents ba5a6f0 + 46f786e commit ca32f5d
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 53 deletions.
32 changes: 26 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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:
Expand Down
12 changes: 10 additions & 2 deletions main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
30 changes: 20 additions & 10 deletions mod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -45,7 +45,8 @@ export class Nqlite {
}

// Init function to connect to NATS
async init(url: string, creds: string, token: string, dataDir: string): Promise<void> {
async init(opts: Options): Promise<void> {
const { url, creds, token, dataDir } = opts;
// Make sure directory exists
this.dataDir = dataDir;
this.dbFile = `${this.dataDir}/nqlite.db`;
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
Expand Down Expand Up @@ -314,3 +322,5 @@ export class Nqlite {
serve(api.fetch, { port: 4001 });
}
}

export type { Options };
130 changes: 96 additions & 34 deletions parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -19,66 +20,127 @@ 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";
console.log(data);
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;
}

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;
}
15 changes: 14 additions & 1 deletion types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,11 +41,24 @@ export type ParseRes = {
t: number;
data: JSON;
isRead: boolean;
txItems: string[];
bulkItems: string[];
bulkParams: bulkParams[];
};

type bulkParams = {
query: string;
params: RestBindParameters;
};

export type Res = {
error?: string;
results: Array<Record<string, unknown>>;
time: number;
};

export type Options = {
url: string;
creds: string;
token: string;
dataDir: string;
};

0 comments on commit ca32f5d

Please sign in to comment.