-
Notifications
You must be signed in to change notification settings - Fork 7
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit 82e17df
Showing
18 changed files
with
1,196 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
node_modules | ||
/lib-esm | ||
/lib-cjs | ||
package-lock.json | ||
*.tsbuildinfo | ||
Session.vim |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
MIT License | ||
|
||
Copyright 2023 the sqld authors | ||
|
||
Permission is hereby granted, free of charge, to any person obtaining a copy of | ||
this software and associated documentation files (the "Software"), to deal in | ||
the Software without restriction, including without limitation the rights to | ||
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of | ||
the Software, and to permit persons to whom the Software is furnished to do so, | ||
subject to the following conditions: | ||
|
||
The above copyright notice and this permission notice shall be included in all | ||
copies or substantial portions of the Software. | ||
|
||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS | ||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR | ||
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER | ||
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN | ||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,50 @@ | ||
# Hrana client for TypeScript | ||
|
||
This package implements a Hrana client for TypeScript. Hrana is a protocol based on WebSockets that can be used to connect to sqld. It is more efficient than the postgres wire protocol (especially for edge deployments) and it supports interactive stateful SQL connections (called "streams") which are not supported by the HTTP API. | ||
|
||
> This package is intended mostly for internal use. Consider using the [`@libsql/client`][libsql-client] package, which will automatically use Hrana if you connect to a `ws://` or `wss://` URL. | ||
[libsql-client]: https://www.npmjs.com/package/@libsql/client | ||
|
||
## Usage | ||
|
||
```typescript | ||
import * as hrana from "@libsql/hrana-client"; | ||
|
||
// Open a `hrana.Client`, which works like a connection pool in standard SQL | ||
// databases, but it uses just a single network connection internally | ||
const url = process.env.URL ?? "ws://localhost:2023"; // Address of the sqld server | ||
const jwt = process.env.JWT; // JWT token for authentication | ||
const client = hrana.open(url, jwt); | ||
|
||
// Open a `hrana.Stream`, which is an interactive SQL stream. This corresponds | ||
// to a "connection" from other SQL databases | ||
const stream = client.openStream(); | ||
|
||
// Fetch all rows returned by a SQL statement | ||
const books = await stream.query("SELECT title, year FROM book WHERE author = 'Jane Austen'"); | ||
// The rows are returned in an Array... | ||
for (const book of books) { | ||
// every returned row works as an array (`book[1]`) and as an object (`book.year`) | ||
console.log(`${book.title} from ${book.year}`); | ||
} | ||
|
||
// Fetch a single row | ||
const book = await stream.queryRow("SELECT title, MIN(year) FROM book"); | ||
if (book !== undefined) { | ||
console.log(`The oldest book is ${book.title} from year ${book[1]}`); | ||
} | ||
|
||
// Fetch a single value, using a bound parameter | ||
const year = await stream.queryValue(["SELECT MAX(year) FROM book WHERE author = ?", ["Jane Austen"]]); | ||
if (year !== undefined) { | ||
console.log(`Last book from Jane Austen was published in ${year}`); | ||
} | ||
|
||
// Execute a statement that does not return any rows | ||
const res = await stream.execute(["DELETE FROM book WHERE author = ?", ["J. K. Rowling"]]) | ||
console.log(`${res.rowsAffected} books have been cancelled`); | ||
|
||
// When you are done, remember to close the client | ||
client.close(); | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
import * as hrana from "@libsql/hrana-client"; | ||
|
||
const client = hrana.open(process.env.URL ?? "ws://localhost:2023", process.env.JWT); | ||
const stream = client.openStream(); | ||
console.log(await stream.queryValue("SELECT 1")); | ||
client.close(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import * as hrana from "@libsql/hrana-client"; | ||
|
||
// Open a `hrana.Client`, which works like a connection pool in standard SQL | ||
// databases, but it uses just a single network connection internally | ||
const url = process.env.URL ?? "ws://localhost:2023"; // Address of the sqld server | ||
const jwt = process.env.JWT; // JWT token for authentication | ||
const client = hrana.open(url, jwt); | ||
|
||
// Open a `hrana.Stream`, which is an interactive SQL stream. This corresponds | ||
// to a "connection" from other SQL databases | ||
const stream = client.openStream(); | ||
|
||
await stream.execute(`CREATE TABLE book ( | ||
id INTEGER PRIMARY KEY NOT NULL, | ||
author TEXT NOT NULL, | ||
title TEXT NOT NULL, | ||
year INTEGER NOT NULL | ||
)`); | ||
await stream.execute(`INSERT INTO book (author, title, year) VALUES | ||
('Jane Austen', 'Sense and Sensibility', 1811), | ||
('Jane Austen', 'Pride and Prejudice', 1813), | ||
('Jane Austen', 'Mansfield Park', 1814), | ||
('Jane Austen', 'Emma', 1815), | ||
('Jane Austen', 'Persuasion', 1818), | ||
('Jane Austen', 'Lady Susan', 1871), | ||
('Daniel Defoe', 'Robinson Crusoe', 1719), | ||
('Daniel Defoe', 'A Journal of the Plague Year', 1722), | ||
('J. K. Rowling', 'Harry Potter and the Philosopher''s Stone', 1997), | ||
('J. K. Rowling', 'The Casual Vacancy', 2012), | ||
('J. K. Rowling', 'The Ickabog', 2020) | ||
`); | ||
|
||
// Fetch all rows returned by a SQL statement | ||
const books = await stream.query("SELECT title, year FROM book WHERE author = 'Jane Austen'"); | ||
// The rows are returned in an Array... | ||
for (const book of books) { | ||
// every returned row works as an array (`book[1]`) and as an object (`book.year`) | ||
console.log(`${book.title} from ${book.year}`); | ||
} | ||
|
||
// Fetch a single row | ||
const book = await stream.queryRow("SELECT title, MIN(year) FROM book"); | ||
if (book !== undefined) { | ||
console.log(`The oldest book is ${book.title} from year ${book[1]}`); | ||
} | ||
|
||
// Fetch a single value, using a bound parameter | ||
const year = await stream.queryValue(["SELECT MAX(year) FROM book WHERE author = ?", ["Jane Austen"]]); | ||
if (year !== undefined) { | ||
console.log(`Last book from Jane Austen was published in ${year}`); | ||
} | ||
|
||
// Execute a statement that does not return any rows | ||
const res = await stream.execute(["DELETE FROM book WHERE author = ?", ["J. K. Rowling"]]) | ||
console.log(`${res.rowsAffected} books have been cancelled`); | ||
|
||
// When you are done, remember to close the client | ||
client.close(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
export default { | ||
preset: "ts-jest/presets/default-esm", | ||
moduleNameMapper: { | ||
'^(\\.{1,2}/.*)\\.js$': '$1', | ||
}, | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
{ | ||
"type": "commonjs" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
{ | ||
"name": "@libsql/hrana-client", | ||
"version": "0.1.0", | ||
"keywords": [ | ||
"hrana", | ||
"libsql", | ||
"sqld", | ||
"database" | ||
], | ||
"description": "Hrana client for connecting to sqld over a WebSocket", | ||
"repository": { | ||
"type": "git", | ||
"url": "github:libsql/hrana-client-ts" | ||
}, | ||
"homepage": "https://github.com/libsql/hrana-client-ts", | ||
"authors": [ | ||
"Jan Špaček <[email protected]>" | ||
], | ||
"license": "MIT", | ||
|
||
"type": "module", | ||
"main": "lib-cjs/index.js", | ||
"types": "lib-esm/index.d.ts", | ||
"exports": { | ||
".": { | ||
"types": "./lib-esm/index.d.ts", | ||
"import": "./lib-esm/index.js", | ||
"require": "./lib-cjs/index.js" | ||
} | ||
}, | ||
|
||
"scripts": { | ||
"prebuild": "rm -rf ./lib-cjs ./lib-esm", | ||
"build": "npm run build:cjs && npm run build:esm", | ||
"build:cjs": "tsc -p tsconfig.build-cjs.json", | ||
"build:esm": "tsc -p tsconfig.build-esm.json", | ||
"postbuild": "cp package-cjs.json ./lib-cjs/package.json", | ||
|
||
"test": "jest" | ||
}, | ||
|
||
"files": [ | ||
"lib-cjs/**", | ||
"lib-esm/**" | ||
], | ||
|
||
"dependencies": { | ||
"isomorphic-ws": "^5.0.0" | ||
}, | ||
"devDependencies": { | ||
"@types/jest": "^29.4.0", | ||
"@types/ws": "^8.5.4", | ||
"jest": "^29.4.0", | ||
"ts-jest": "^29.0.5", | ||
"typescript": "^4.9.4" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,173 @@ | ||
import * as hrana from ".."; | ||
|
||
function withClient(f: (c: hrana.Client) => Promise<void>): () => Promise<void> { | ||
return async () => { | ||
const c = hrana.open("ws://localhost:2023"); | ||
try { | ||
await f(c); | ||
} finally { | ||
c.close(); | ||
} | ||
}; | ||
} | ||
|
||
test("Stream.queryValue()", withClient(async (c) => { | ||
const s = c.openStream(); | ||
expect(await s.queryValue("SELECT 1")).toStrictEqual(1); | ||
expect(await s.queryValue("SELECT 'elephant'")).toStrictEqual("elephant"); | ||
expect(await s.queryValue("SELECT 42.5")).toStrictEqual(42.5); | ||
expect(await s.queryValue("SELECT NULL")).toStrictEqual(null); | ||
})); | ||
|
||
test("Stream.queryRow()", withClient(async (c) => { | ||
const s = c.openStream(); | ||
|
||
const row = await s.queryRow( | ||
"SELECT 1 AS one, 'elephant' AS two, 42.5 AS three, NULL as four"); | ||
expect(row[0]).toStrictEqual(1); | ||
expect(row[1]).toStrictEqual("elephant"); | ||
expect(row[2]).toStrictEqual(42.5); | ||
expect(row[3]).toStrictEqual(null); | ||
|
||
expect(row[0]).toStrictEqual(row.one); | ||
expect(row[1]).toStrictEqual(row.two); | ||
expect(row[2]).toStrictEqual(row.three); | ||
expect(row[3]).toStrictEqual(row.four); | ||
})); | ||
|
||
test("Stream.query()", withClient(async (c) => { | ||
const s = c.openStream(); | ||
|
||
await s.execute("BEGIN"); | ||
await s.execute("DROP TABLE IF EXISTS t"); | ||
await s.execute("CREATE TABLE t (one, two, three, four)"); | ||
await s.execute( | ||
`INSERT INTO t VALUES | ||
(1, 'elephant', 42.5, NULL), | ||
(2, 'hippopotamus', '123', 0.0)` | ||
); | ||
|
||
const rows = await s.query("SELECT * FROM t ORDER BY one"); | ||
expect(rows.length).toStrictEqual(2); | ||
expect(rows.rowsAffected).toStrictEqual(0); | ||
|
||
const row0 = rows[0]; | ||
expect(row0[0]).toStrictEqual(1); | ||
expect(row0[1]).toStrictEqual("elephant"); | ||
expect(row0["three"]).toStrictEqual(42.5); | ||
expect(row0["four"]).toStrictEqual(null); | ||
|
||
const row1 = rows[1]; | ||
expect(row1["one"]).toStrictEqual(2); | ||
expect(row1["two"]).toStrictEqual("hippopotamus"); | ||
expect(row1[2]).toStrictEqual("123"); | ||
expect(row1[3]).toStrictEqual(0.0); | ||
})); | ||
|
||
test("Stream.execute()", withClient(async (c) => { | ||
const s = c.openStream(); | ||
|
||
let res = await s.execute("BEGIN"); | ||
expect(res.rowsAffected).toStrictEqual(0); | ||
|
||
res = await s.execute("DROP TABLE IF EXISTS t"); | ||
expect(res.rowsAffected).toStrictEqual(0); | ||
|
||
res = await s.execute("CREATE TABLE t (num, word)"); | ||
expect(res.rowsAffected).toStrictEqual(0); | ||
|
||
res = await s.execute("INSERT INTO t VALUES (1, 'one'), (2, 'two'), (3, 'three')"); | ||
expect(res.rowsAffected).toStrictEqual(3); | ||
|
||
const rows = await s.query("SELECT * FROM t ORDER BY num"); | ||
expect(rows.length).toStrictEqual(3); | ||
expect(rows.rowsAffected).toStrictEqual(0); | ||
|
||
res = await s.execute("DELETE FROM t WHERE num >= 2"); | ||
expect(res.rowsAffected).toStrictEqual(2); | ||
|
||
res = await s.execute("UPDATE t SET num = 4, word = 'four'"); | ||
expect(res.rowsAffected).toStrictEqual(1); | ||
|
||
res = await s.execute("DROP TABLE t"); | ||
expect(res.rowsAffected).toStrictEqual(0); | ||
|
||
await s.execute("COMMIT"); | ||
})); | ||
|
||
test("Stream.executeRaw()", withClient(async (c) => { | ||
const s = c.openStream(); | ||
|
||
let res = await s.executeRaw({ | ||
"sql": "SELECT 1 as one, ? as two, NULL as three", | ||
"args": [{"type": "text", "value": "1+1"}], | ||
"want_rows": true, | ||
}); | ||
|
||
expect(res.cols).toStrictEqual([ | ||
{"name": "one"}, | ||
{"name": "two"}, | ||
{"name": "three"}, | ||
]); | ||
expect(res.rows).toStrictEqual([ | ||
[ | ||
{"type": "integer", "value": "1"}, | ||
{"type": "text", "value": "1+1"}, | ||
{"type": "null"}, | ||
], | ||
]); | ||
})); | ||
|
||
test("concurrent streams are separate", withClient(async (c) => { | ||
const s1 = c.openStream(); | ||
await s1.execute("DROP TABLE IF EXISTS t"); | ||
await s1.execute("CREATE TABLE t (number)"); | ||
await s1.execute("INSERT INTO t VALUES (1)"); | ||
|
||
const s2 = c.openStream(); | ||
|
||
await s1.execute("BEGIN"); | ||
|
||
await s2.execute("BEGIN"); | ||
await s2.execute("INSERT INTO t VALUES (10)"); | ||
|
||
expect(await s1.queryValue("SELECT SUM(number) FROM t")).toStrictEqual(1); | ||
expect(await s2.queryValue("SELECT SUM(number) FROM t")).toStrictEqual(11); | ||
})); | ||
|
||
test("concurrent operations are correctly ordered", withClient(async (c) => { | ||
const s = c.openStream(); | ||
await s.execute("DROP TABLE IF EXISTS t"); | ||
await s.execute("CREATE TABLE t (stream, value)"); | ||
|
||
async function stream(streamId: number): Promise<void> { | ||
const s = c.openStream(); | ||
|
||
let value = "s" + streamId; | ||
await s.execute(["INSERT INTO t VALUES (?, ?)", [streamId, value]]); | ||
|
||
const promises: Array<Promise<any>> = []; | ||
const expectedValues = []; | ||
for (let i = 0; i < 10; ++i) { | ||
const promise = s.queryValue([ | ||
"UPDATE t SET value = value || ? WHERE stream = ? RETURNING value", | ||
["_" + i, streamId], | ||
]); | ||
value = value + "_" + i; | ||
promises.push(promise); | ||
expectedValues.push(value); | ||
} | ||
|
||
for (let i = 0; i < promises.length; ++i) { | ||
expect(await promises[i]).toStrictEqual(expectedValues[i]); | ||
} | ||
|
||
s.close(); | ||
} | ||
|
||
const promises = []; | ||
for (let i = 0; i < 10; ++i) { | ||
promises.push(stream(i)); | ||
} | ||
await Promise.all(promises); | ||
})); |
Oops, something went wrong.