diff --git a/.changeset/sweet-eagles-drop.md b/.changeset/sweet-eagles-drop.md
new file mode 100644
index 0000000..972e0c7
--- /dev/null
+++ b/.changeset/sweet-eagles-drop.md
@@ -0,0 +1,5 @@
+---
+"@crbroughton/sibyl": minor
+---
+
+Add libSQL support - Sibyl now supports the libSQL implementation of SQLite
diff --git a/.github/workflows/vitest.yml b/.github/workflows/bun.yml
similarity index 91%
rename from .github/workflows/vitest.yml
rename to .github/workflows/bun.yml
index baa2ac9..8ec5df1 100644
--- a/.github/workflows/vitest.yml
+++ b/.github/workflows/bun.yml
@@ -26,6 +26,8 @@ jobs:
run: cd src/sqljs && bun install
- name: Install dependencies for Bun
run: cd src/bun && bun install
+ - name: Install dependencies for libSQL
+ run: cd src/libsql && bun install
- name: Run unit tests
run: bun test
- uses: actions/upload-artifact@v3
diff --git a/README.md b/README.md
index 106a73d..3f3e277 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# Sibyl
-Sibyl is a lightweight SQLite query builder for SQL.js and Bun's sqlite3 driver, providing a Prisma-like query builder. Sibyl is in early development,
+Sibyl is a lightweight SQLite query builder for libSQL, Bun's sqlite3 driver, and libSQL, providing a Prisma-like query builder. Sibyl is in early development,
so expect breaking changes and rapid development.
## Getting Started
@@ -29,11 +29,22 @@ Bun documentation. The Bun implemenation of Sibyl can be installed
with the following command:
```bash
-bun install @crbroughton/sibyl:bun
+bun install @crbroughton/sibyl_bun
```
Sibyl will then accept the native Bun SQLite `Database`, again, see the
Bun documentation.
+#### libSQL Installation
+
+The libSQL implemenation of Sibyl can be installed
+with the following command:
+
+```bash
+bun install @crbroughton/sibyl_libsql libsql
+```
+Sibyl will then accept libSQL `Database`, then see the
+libSQL Getting Started Guide.
+
#### Getting Started
To start off with Sibyl, you'll first have to ensure Sibyl is able to be run inside
diff --git a/libsql-playground/.gitignore b/libsql-playground/.gitignore
new file mode 100644
index 0000000..9b1ee42
--- /dev/null
+++ b/libsql-playground/.gitignore
@@ -0,0 +1,175 @@
+# Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore
+
+# Logs
+
+logs
+_.log
+npm-debug.log_
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Caches
+
+.cache
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+
+report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
+
+# Runtime data
+
+pids
+_.pid
+_.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+
+lib-cov
+
+# Coverage directory used by tools like istanbul
+
+coverage
+*.lcov
+
+# nyc test coverage
+
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+
+bower_components
+
+# node-waf configuration
+
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+
+build/Release
+
+# Dependency directories
+
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+
+web_modules/
+
+# TypeScript cache
+
+*.tsbuildinfo
+
+# Optional npm cache directory
+
+.npm
+
+# Optional eslint cache
+
+.eslintcache
+
+# Optional stylelint cache
+
+.stylelintcache
+
+# Microbundle cache
+
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+
+.node_repl_history
+
+# Output of 'npm pack'
+
+*.tgz
+
+# Yarn Integrity file
+
+.yarn-integrity
+
+# dotenv environment variable files
+
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+
+.parcel-cache
+
+# Next.js build output
+
+.next
+out
+
+# Nuxt.js build / generate output
+
+.nuxt
+dist
+
+# Gatsby files
+
+# Comment in the public line in if your project uses Gatsby and not Next.js
+
+# https://nextjs.org/blog/next-9-1#public-directory-support
+
+# public
+
+# vuepress build output
+
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+
+.temp
+
+# Docusaurus cache and generated files
+
+.docusaurus
+
+# Serverless directories
+
+.serverless/
+
+# FuseBox cache
+
+.fusebox/
+
+# DynamoDB Local files
+
+.dynamodb/
+
+# TernJS port file
+
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+
+.vscode-test
+
+# yarn v2
+
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
+
+# IntelliJ based IDEs
+.idea
+
+# Finder (MacOS) folder config
+.DS_Store
diff --git a/libsql-playground/README.md b/libsql-playground/README.md
new file mode 100644
index 0000000..56b6ca7
--- /dev/null
+++ b/libsql-playground/README.md
@@ -0,0 +1,15 @@
+# libsql-playground
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+To run:
+
+```bash
+bun run index.ts
+```
+
+This project was created using `bun init` in bun v1.1.0. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/libsql-playground/bun.lockb b/libsql-playground/bun.lockb
new file mode 100755
index 0000000..ba1c3bd
Binary files /dev/null and b/libsql-playground/bun.lockb differ
diff --git a/libsql-playground/index.ts b/libsql-playground/index.ts
new file mode 100644
index 0000000..5da6268
--- /dev/null
+++ b/libsql-playground/index.ts
@@ -0,0 +1,86 @@
+import Sibyl from '@crbroughton/sibyl_libsql'
+import Database from 'libsql'
+
+const db = new Database(':memory:')
+
+// Create table schema
+interface Tables {
+ firstTable: {
+ id: number
+ name: string
+ location: string
+ hasReadTheReadme: boolean
+ }
+}
+const { createTable, Insert, Select, All } = await Sibyl(db)
+
+createTable('firstTable', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ name: {
+ type: 'char',
+ },
+ hasReadTheReadme: {
+ type: 'bool',
+ },
+ location: {
+ type: 'char',
+ },
+})
+
+Insert('firstTable', [
+ {
+ id: 1,
+ hasReadTheReadme: true,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 2,
+ hasReadTheReadme: false,
+ location: 'Leeds',
+ name: 'Bob',
+ },
+ {
+ id: 3,
+ hasReadTheReadme: true,
+ location: 'Brighton',
+ name: 'David',
+ },
+])
+
+const allResponse = All('firstTable')
+console.log(allResponse)
+
+const selectedResponse = Select('firstTable', {
+ where: {
+ id: 1,
+ },
+})
+console.log(selectedResponse)
+
+const selectedResponseWithMultiple = Select('firstTable', {
+ where: {
+ id: 1,
+ location: 'Brighton',
+ },
+})
+console.log(selectedResponseWithMultiple)
+
+const selectedREsponseWithORStatement = Select('firstTable', {
+ where: {
+ OR: [
+ {
+ name: 'Craig',
+ },
+ {
+ hasReadTheReadme: 1,
+ },
+ ],
+ },
+})
+console.log('here', selectedREsponseWithORStatement)
diff --git a/libsql-playground/package.json b/libsql-playground/package.json
new file mode 100644
index 0000000..a10a74d
--- /dev/null
+++ b/libsql-playground/package.json
@@ -0,0 +1,15 @@
+{
+ "name": "libsql-playground",
+ "type": "module",
+ "module": "index.ts",
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "dependencies": {
+ "@crbroughton/sibyl_libsql": "^2.1.2",
+ "libsql": "^0.3.11"
+ },
+ "devDependencies": {
+ "@types/bun": "latest"
+ }
+}
diff --git a/libsql-playground/tsconfig.json b/libsql-playground/tsconfig.json
new file mode 100644
index 0000000..238655f
--- /dev/null
+++ b/libsql-playground/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ // Enable latest features
+ "lib": ["ESNext", "DOM"],
+ "target": "ESNext",
+ "module": "ESNext",
+ "moduleDetection": "force",
+ "jsx": "react-jsx",
+ "allowJs": true,
+
+ // Bundler mode
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "noEmit": true,
+
+ // Best practices
+ "strict": true,
+ "skipLibCheck": true,
+ "noFallthroughCasesInSwitch": true,
+
+ // Some stricter flags (disabled by default)
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "noPropertyAccessFromIndexSignature": false
+ }
+}
diff --git a/src/bun/README.md b/src/bun/README.md
index e14e23d..48baef0 100644
--- a/src/bun/README.md
+++ b/src/bun/README.md
@@ -1,6 +1,6 @@
# Sibyl
-Sibyl is a lightweight SQLite query builder for SQL.js and Bun's sqlite3 driver, providing a Prisma-like query builder. Sibyl is in early development,
+Sibyl is a lightweight SQLite query builder for libSQL, Bun's sqlite3 driver, and libSQL, providing a Prisma-like query builder. Sibyl is in early development,
so expect breaking changes and rapid development.
## Getting Started
@@ -34,6 +34,17 @@ bun install @crbroughton/sibyl_bun
Sibyl will then accept the native Bun SQLite `Database`, again, see the
Bun documentation.
+#### libSQL Installation
+
+The libSQL implemenation of Sibyl can be installed
+with the following command:
+
+```bash
+bun install @crbroughton/sibyl_libsql libsql
+```
+Sibyl will then accept libSQL `Database`, then see the
+libSQL Getting Started Guide.
+
#### Getting Started
To start off with Sibyl, you'll first have to ensure Sibyl is able to be run inside
@@ -89,12 +100,13 @@ createTable('firstTable', { // inferred table name and entry
id: {
autoincrement: true,
type: 'INTEGER', // only allows for known data types ('int', 'char', 'blob')
- nullable: false,
primary: true,
unique: true,
},
job: {
- type: 'char',
+ type: 'varchar',
+ size: 100, // specify the size of the varchar
+ nullable: true
},
name: {
type: 'char',
@@ -222,6 +234,14 @@ const updatedEntry = Update('firstTable', { // infers the table and response typ
}
})
```
+### Primary type
+
+Sibyl offers a custom type, called the 'primary' type. When using
+this type, Sibyl will automatically set the entry to a primary key,
+not nullable and unique. Sibyl will also ensure that the underlying
+type changes, so your editor gives feedback about no longer requiring
+you to manually set these keys. Currently the primary type is only
+available as an integer type.
### Sibyl Responses
@@ -230,6 +250,27 @@ when wanting to convert data types to TypeScript types; At the moment the custom
only support boolean conversions from `boolean` to `0 | 1`. It's recommended to use
this type as a wrapper, if you're ever using boolean values.
+### Working With Reactivity
+
+When working with any front-end framework, you'll want to combine
+Sibyl with your frameworks reactivity engine. I've provided some
+examples in the playground, in this case using Vue, but in general
+you should follow the following rules:
+
+- Sibyl is not responsive by default; You should aim for Sibyls
+responses to end up in a reactive object (see ref for Vue).
+- When working with your reactive state, it's good practice to ensure
+that the states type is the same of that of the response type from
+Sibyl
+- Sibyl provides the `SibylResponse` type; You can use this type
+as a 'wrapper' type like so:
+
+```typescript
+const results = ref[]>([])
+```
+This ensures that when you work with the `results` array, it conforms
+to the shape and type Sibyl will return.
+
## Development
To install dependencies:
diff --git a/src/libsql/README.md b/src/libsql/README.md
new file mode 100644
index 0000000..48baef0
--- /dev/null
+++ b/src/libsql/README.md
@@ -0,0 +1,293 @@
+# Sibyl
+
+Sibyl is a lightweight SQLite query builder for libSQL, Bun's sqlite3 driver, and libSQL, providing a Prisma-like query builder. Sibyl is in early development,
+so expect breaking changes and rapid development.
+
+## Getting Started
+
+### Installation
+
+Dependant on your chosen SQLite driver, you'll want to follow one
+of the following installation methods:
+
+#### SQL.js Installation
+
+If you choose to use Sibyl with `sql.js`, `sql.js` will provide the lower-level API to interact with your
+embedded SQLite database. You'll also need to install the `.wasm` file that `sql.js`
+provides; Please see their documentation at https://sql.js.org.
+
+With the `.wasm` file now available, you can install Sibyl with the following command:
+
+```bash
+bun install sql.js @types/sql.js @crbroughton/sibyl
+```
+
+#### Bun Installation
+
+If you are using Sibyl with Bun, you should already have access to the driver, and can refer to the
+Bun documentation. The Bun implemenation of Sibyl can be installed
+with the following command:
+
+```bash
+bun install @crbroughton/sibyl_bun
+```
+Sibyl will then accept the native Bun SQLite `Database`, again, see the
+Bun documentation.
+
+#### libSQL Installation
+
+The libSQL implemenation of Sibyl can be installed
+with the following command:
+
+```bash
+bun install @crbroughton/sibyl_libsql libsql
+```
+Sibyl will then accept libSQL `Database`, then see the
+libSQL Getting Started Guide.
+
+#### Getting Started
+
+To start off with Sibyl, you'll first have to ensure Sibyl is able to be run inside
+of a top-level async/await file, alongside your `sql.js` database connection. As
+referenced from the `sql.js` documentation, you can provide Sibyl a database instance
+like so:
+
+```typescript
+interface tableRowType {
+ id: number
+ name: string
+ sex: string
+ job: string
+ hasReadTheReadme: boolean
+}
+
+interface secondRowType {
+ id: number
+}
+
+interface Tables {
+ firstTable: tableRowType
+ secondTable: secondRowType
+}
+
+const SQL = await sql({ // sql.js implementation
+ locateFile: () => {
+ return '/sql-wasm.wasm'
+ }
+})
+const db = new SQL.Database()
+
+const { createTable, Insert, Select, All, Create } = await Sibyl(db)
+```
+
+With top-level async/await enabled, you can then use Sibyl. Sibyl provides the following
+functions:
+
+- `createTable` - Allows you to create a table
+- `Create` - Creates and returns a new entry into your selected table
+- `Insert` - Allows you to provide an array of insertable entries into your selected table
+- `Select` - Returns a type-safe array of entries from the selected table
+- `All` - Returns all entries from the selected table
+- `Update` Updates and returns a single entry from the selected table
+- `Delete` - Deletes an entry from a selected table
+
+### Creating the table
+
+To create a new table, use the `createTable` command:
+
+```typescript
+createTable('firstTable', { // inferred table name and entry
+ id: {
+ autoincrement: true,
+ type: 'INTEGER', // only allows for known data types ('int', 'char', 'blob')
+ primary: true,
+ unique: true,
+ },
+ job: {
+ type: 'varchar',
+ size: 100, // specify the size of the varchar
+ nullable: true
+ },
+ name: {
+ type: 'char',
+ },
+ sex: {
+ type: 'char',
+ },
+ hasReadTheReadme: {
+ type: 'bool',
+ },
+})
+```
+
+`createTable` takes two arguments, the first is the name of the table you wish to select, This
+is based off the generic interface you first supplied to Sibyl.
+The second argument will create the specified columns for your database. Sibyl will handle the order and creation of each column you have specified, and only allow known data types.
+
+### Inserting a single entry into the DB
+
+To create a new entry, you can use the `Create` function:
+
+```typescript
+const result = Create('firstTable', { // returns the resulting entry
+ id: faker.number.int(),
+ name: 'Craig',
+ sex: 'male',
+ job: 'Software Engineer',
+ hasReadTheReadme: true,
+})
+```
+
+### Inserting mutiple entries into the DB
+
+To insert new entries into the database, you can use the `Insert` function:
+
+```typescript
+let insertions: SibylResponse[] = []
+for (let index = 0; index < 1000; index++) {
+ insertions.push({
+ id: faker.number.int(),
+ name: faker.person.firstName(),
+ sex: faker.person.sex(),
+ job: faker.person.jobTitle(),
+ hasReadTheReadme: true,
+ })
+}
+// execute the provided instruction - Data will now be in the DB
+const test = Insert('firstTable', insertions)
+```
+
+### Selecting entries from the DB
+
+When selecting entries from the database, you can utilise the `Select` function
+to retrieve an array of type-safe entries, based from the generic interface
+you have supplied to Sybil main function (see above `tableRowType`).
+
+```typescript
+selection.value = Select('firstTable', {
+ where: {
+ id: 1,
+ name: "Craig", // can combine multiple where clauses
+ },
+ limit: 20, // limit the response from Sibyl
+ offset: 10, // offset the response, useful for pagination
+})
+```
+
+#### OR Selection
+
+When selecting entries from the database, the `Select` function, by
+default, uses an AND statement to build you query. You can however,
+include an optional OR array to select entries:
+
+```typescript
+const response = Select('firstTable', { // Returns all entries where name is Craig OR Bob
+ where: {
+ OR: [
+ {
+ name: 'Craig'
+ },
+ {
+ name: 'Bob'
+ }
+ ]
+ }
+})
+```
+
+You can also combine multiple OR statements as part of a single object,
+if the keys do no clash:
+
+```typescript
+const response = Select('firstTable', { // Returns all entries where name is Craig OR Bob OR hasReadTheReadme is false
+ where: {
+ OR: [
+ {
+ name: 'Craig',
+ hasReadTheReadme: 0, // boolean values need to be selected
+ // based on their database values
+ // and will be returned as such
+ },
+ {
+ name: 'Bob'
+ }
+ ]
+ }
+})
+```
+When using the optional OR array to build a query, you can still use
+the optional `offset` and `limit` keys.
+
+### Updating an entry in the DB
+
+To update a single entry in the database, you can use the `Update` function:
+
+```typescript
+const updatedEntry = Update('firstTable', { // infers the table and response type
+ where: { // Can combine multiple where clauses
+ id: 1,
+ name: 'Craig',
+ },
+ updates: {
+ name: 'Bob', // Can update multiple values at once
+ job: 'Engineer',
+ }
+})
+```
+### Primary type
+
+Sibyl offers a custom type, called the 'primary' type. When using
+this type, Sibyl will automatically set the entry to a primary key,
+not nullable and unique. Sibyl will also ensure that the underlying
+type changes, so your editor gives feedback about no longer requiring
+you to manually set these keys. Currently the primary type is only
+available as an integer type.
+
+### Sibyl Responses
+
+Sibyl also offers a custom type the `SibylResponse` type; This type can be helpful
+when wanting to convert data types to TypeScript types; At the moment the custom type
+only support boolean conversions from `boolean` to `0 | 1`. It's recommended to use
+this type as a wrapper, if you're ever using boolean values.
+
+### Working With Reactivity
+
+When working with any front-end framework, you'll want to combine
+Sibyl with your frameworks reactivity engine. I've provided some
+examples in the playground, in this case using Vue, but in general
+you should follow the following rules:
+
+- Sibyl is not responsive by default; You should aim for Sibyls
+responses to end up in a reactive object (see ref for Vue).
+- When working with your reactive state, it's good practice to ensure
+that the states type is the same of that of the response type from
+Sibyl
+- Sibyl provides the `SibylResponse` type; You can use this type
+as a 'wrapper' type like so:
+
+```typescript
+const results = ref[]>([])
+```
+This ensures that when you work with the `results` array, it conforms
+to the shape and type Sibyl will return.
+
+## Development
+
+To install dependencies:
+
+```bash
+bun install
+```
+
+You can then try Sibyl in the playground, first install the dependencies:
+
+```bash
+cd playground && bun install
+```
+
+and then run the playground:
+```bash
+bun run dev
+```
+
+This project was created using `bun init` in bun v1.0.29. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.
diff --git a/src/libsql/build.ts b/src/libsql/build.ts
new file mode 100644
index 0000000..6553d77
--- /dev/null
+++ b/src/libsql/build.ts
@@ -0,0 +1,7 @@
+import dts from 'bun-plugin-dts'
+
+await Bun.build({
+ entrypoints: ['./index.ts'],
+ outdir: 'dist',
+ plugins: [dts()],
+})
diff --git a/src/libsql/bun.lockb b/src/libsql/bun.lockb
new file mode 100755
index 0000000..a2ef185
Binary files /dev/null and b/src/libsql/bun.lockb differ
diff --git a/src/libsql/index.ts b/src/libsql/index.ts
new file mode 100644
index 0000000..c3b6c51
--- /dev/null
+++ b/src/libsql/index.ts
@@ -0,0 +1,94 @@
+import type { Database } from 'libsql'
+import type { DeleteArgs, MappedTable, SelectArgs, SibylResponse, Sort, UpdateArgs } from '../types'
+import {
+ buildSelectQuery,
+ buildUpdateQuery,
+ convertCreateTableStatement,
+ formatInsertStatementLibSQL,
+ objectToWhereClause,
+} from '../sibylLib'
+
+export default async function Sibyl>(db: Database) {
+type TableKeys = keyof T
+type AccessTable = T[I]
+function createTable(table: T, tableRow: MappedTable>) {
+ const statement = convertCreateTableStatement(tableRow)
+ db.exec(`CREATE TABLE ${String(table)} (${statement});`)
+}
+
+function Insert(table: K, rows: AccessTable[]) {
+ const statement = formatInsertStatementLibSQL(String(table), rows)
+ db.exec(statement)
+}
+
+function Select(table: T, args: SelectArgs>>) {
+ const query = buildSelectQuery(String(table), args)
+ const record = db.prepare(query).all() as SibylResponse>[]
+
+ if (record !== undefined)
+ return record
+
+ return undefined
+}
+
+function Create(table: T, entry: AccessTable) {
+ const statement = formatInsertStatementLibSQL(String(table), [entry])
+ db.exec(statement)
+
+ const result = Select(table, {
+ where: entry,
+ })
+
+ if (result !== undefined)
+ return result[0]
+
+ return undefined
+}
+
+function All(table: K, args?: { sort: Sort>> }) {
+ let query = `SELECT * from ${String(table)}`
+
+ if (args !== undefined && args.sort) {
+ const orders: string[] = []
+ query += ' ORDER BY '
+ for (const [key, value] of Object.entries(args.sort))
+ orders.push(`${key} ${value}`)
+ query += orders.join(', ')
+ }
+
+ const record = db.prepare(query).all() as SibylResponse>[]
+
+ if (record !== undefined)
+ return record
+
+ return undefined
+}
+
+function Update(table: K, args: UpdateArgs>) {
+ const query = buildUpdateQuery(table, args)
+ db.exec(query)
+
+ const result = Select(table, {
+ where: args.where,
+ })
+
+ if (result !== undefined)
+ return result[0]
+
+ return undefined
+}
+
+function Delete(table: K, args: DeleteArgs>) {
+ db.exec(`DELETE FROM ${String(table)} WHERE ${objectToWhereClause(args.where)}`)
+}
+
+return {
+ createTable,
+ Select,
+ All,
+ Insert,
+ Create,
+ Update,
+ Delete,
+}
+}
diff --git a/src/libsql/package.json b/src/libsql/package.json
new file mode 100644
index 0000000..640406c
--- /dev/null
+++ b/src/libsql/package.json
@@ -0,0 +1,45 @@
+{
+ "name": "@crbroughton/sibyl_libsql",
+ "type": "module",
+ "version": "2.1.2",
+ "description": "A lightweight query builder for libsql",
+ "author": "Craig R Broughton",
+ "license": "MIT",
+ "homepage": "https://github.com/crbroughton/sibyl",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/crbroughton/sibyl.git"
+ },
+ "keywords": [
+ "query builder",
+ "sqlite",
+ "wasm",
+ "embedded",
+ "typescript",
+ "libsql"
+ ],
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "files": [
+ "dist"
+ ],
+ "scripts": {
+ "build": "bun run build.ts",
+ "lint": "eslint .",
+ "lint:fix": "eslint . --fix",
+ "changeset": "npx changeset",
+ "changeset:status": "npx changeset status --verbose",
+ "changeset:version": "npx changeset version",
+ "publish": "bun run build && npm publish --access=public"
+ },
+ "peerDependencies": {
+ "typescript": "^5.0.0"
+ },
+ "devDependencies": {
+ "@faker-js/faker": "^8.4.1",
+ "@types/bun": "latest",
+ "@types/sql.js": "^1.4.9",
+ "bun-plugin-dts": "^0.2.1",
+ "libsql": "^0.3.10"
+ }
+}
diff --git a/src/libsql/tests/all.test.ts b/src/libsql/tests/all.test.ts
new file mode 100644
index 0000000..e718b12
--- /dev/null
+++ b/src/libsql/tests/all.test.ts
@@ -0,0 +1,171 @@
+import { describe, expect, it } from 'bun:test'
+import Database from 'libsql'
+import Sibyl from '../index'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+}
+
+interface Tables {
+ first: TableRow
+}
+
+describe('all tests', () => {
+ it('returns all data available in the given table', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, All } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ })
+
+ Insert('first', [
+ {
+ id: 1,
+ name: 'Craig',
+ location: 'Brighton',
+ },
+ {
+ id: 2,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ ])
+
+ const actual = All('first')
+
+ const expectation = [
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 2,
+ location: 'Cornwall',
+ name: 'Bob',
+ },
+ ]
+
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('returns all data available in the given table and sorts then in ascending order by ID', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, All } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ })
+
+ Insert('first', [
+ {
+ id: 1,
+ name: 'Craig',
+ location: 'Brighton',
+ },
+ {
+ id: 2,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ ])
+
+ const actual = All('first', {
+ sort: {
+ id: 'ASC',
+ },
+ })
+
+ const expectation = [
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 2,
+ location: 'Cornwall',
+ name: 'Bob',
+ },
+ ]
+
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('returns all data available in the given table and sorts then in descending order by ID', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, All } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ })
+
+ Insert('first', [
+ {
+ id: 1,
+ name: 'Craig',
+ location: 'Brighton',
+ },
+ {
+ id: 2,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ ])
+
+ const actual = All('first', {
+ sort: {
+ id: 'DESC',
+ },
+ })
+
+ const expectation = [
+ {
+ id: 2,
+ location: 'Cornwall',
+ name: 'Bob',
+ },
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ ]
+
+ expect(actual).toStrictEqual(expectation)
+ })
+})
diff --git a/src/libsql/tests/create.test.ts b/src/libsql/tests/create.test.ts
new file mode 100644
index 0000000..c24ee5b
--- /dev/null
+++ b/src/libsql/tests/create.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'bun:test'
+import Database from 'libsql'
+import Sibyl from '../index'
+import type { SibylResponse } from '../../types'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+ booleanTest: boolean
+}
+
+interface Tables {
+ first: TableRow
+}
+
+describe('create tests', () => {
+ it('creates a new entry in the DB', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Create } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ const actual = Create('first', {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ })
+ const expectation: SibylResponse = {
+ id: 2344,
+ location: 'Brighton',
+ name: 'Craig',
+ booleanTest: 1,
+ }
+ expect(actual).toStrictEqual(expectation)
+ })
+})
diff --git a/src/libsql/tests/delete.test.ts b/src/libsql/tests/delete.test.ts
new file mode 100644
index 0000000..e3e7d66
--- /dev/null
+++ b/src/libsql/tests/delete.test.ts
@@ -0,0 +1,79 @@
+import { describe, expect, it } from 'bun:test'
+import Database from 'libsql'
+import Sibyl from '../index'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+}
+
+interface Tables {
+ first: TableRow
+}
+
+describe('delete tests', () => {
+ it('deletes an entry in the DB', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, All, Delete } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ },
+ {
+ id: 1,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ ])
+
+ let actual = All('first')
+
+ let expectation = [
+ {
+ id: 1,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+
+ Delete('first', {
+ where: {
+ id: 2344,
+ name: 'Craig',
+ },
+ })
+ actual = All('first')
+ expectation = [
+ {
+ id: 1,
+ name: 'Bob',
+ location: 'Cornwall',
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+})
diff --git a/src/libsql/tests/formatInsertStatementLibSQL.test.ts b/src/libsql/tests/formatInsertStatementLibSQL.test.ts
new file mode 100644
index 0000000..30fd750
--- /dev/null
+++ b/src/libsql/tests/formatInsertStatementLibSQL.test.ts
@@ -0,0 +1,54 @@
+import { describe, expect, it } from 'bun:test'
+import { formatInsertStatementLibSQL } from '../../sibylLib'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+}
+
+describe('formatInsertStatmentLibSQL tests', () => {
+ it('correctly formats a single INSERT statement for the DB', async () => {
+ const actual = formatInsertStatementLibSQL('test', [
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ ])
+
+ expect(actual).toStrictEqual('INSERT INTO test (id, location, name) VALUES (1,\'Brighton\',\'Craig\');')
+ })
+ it('correctly formats several INSERT statments for the DB', async () => {
+ const actual = formatInsertStatementLibSQL('test', [
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 2,
+ location: 'Cornwall',
+ name: 'Bob',
+ },
+ ])
+
+ expect(actual).toStrictEqual('INSERT INTO test (id, location, name) VALUES (1,\'Brighton\',\'Craig\'), (2,\'Cornwall\',\'Bob\');')
+ })
+ it('catches incorrect insert keys being the wrong way around and fixes itself', async () => {
+ const actual = formatInsertStatementLibSQL('test', [
+ {
+ id: 1,
+ name: 'Craig',
+ location: 'Brighton',
+ },
+ {
+ location: 'Cornwall',
+ id: 2,
+ name: 'Bob',
+ },
+ ])
+
+ expect(actual).toStrictEqual('INSERT INTO test (id, location, name) VALUES (1,\'Brighton\',\'Craig\'), (2,\'Cornwall\',\'Bob\');')
+ })
+})
diff --git a/src/libsql/tests/select.test.ts b/src/libsql/tests/select.test.ts
new file mode 100644
index 0000000..bd8188d
--- /dev/null
+++ b/src/libsql/tests/select.test.ts
@@ -0,0 +1,406 @@
+import { describe, expect, it } from 'bun:test'
+import Database from 'libsql'
+import Sibyl from '../index'
+import type { SibylResponse } from '../../types'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+ booleanTest: boolean
+}
+
+interface Tables {
+ first: TableRow
+}
+
+describe('select tests', () => {
+ it('select an entry in the DB', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Create, Select } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ primary: true,
+ autoincrement: true,
+ type: 'INTEGER',
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Create('first', {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ })
+
+ const actual = Select('first', {
+ where: {
+ id: 2344,
+ },
+ })
+
+ const expectation: SibylResponse[] = [{
+ id: 2344,
+ location: 'Brighton',
+ name: 'Craig',
+ booleanTest: 1,
+ }]
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('selects multiple entries in the DB', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, Select } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ },
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Brighton',
+ booleanTest: false,
+ },
+ ])
+
+ const actual = Select('first', {
+ where: {
+ location: 'Brighton',
+ },
+ })
+
+ const expectation: SibylResponse[] = [
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Brighton',
+ booleanTest: 0,
+ },
+ {
+ id: 2344,
+ location: 'Brighton',
+ name: 'Craig',
+ booleanTest: 1,
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('selects multiple entries with the OR statement', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, Select } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ },
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Leeds',
+ booleanTest: false,
+ },
+ {
+ name: 'Chris',
+ id: 2,
+ location: 'Cornwall',
+ booleanTest: false,
+ },
+ ])
+
+ const actual = Select('first', {
+ where: {
+ OR: [
+ {
+ location: 'Cornwall',
+ },
+ {
+ location: 'Brighton',
+ },
+ ],
+ },
+ })
+
+ const expectation: SibylResponse[] = [
+ {
+ name: 'Chris',
+ id: 2,
+ location: 'Cornwall',
+ booleanTest: 0,
+ },
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: 1,
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('selects multiple entries with the OR statement (mixed object)', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, Select } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ },
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Leeds',
+ booleanTest: false,
+ },
+ {
+ name: 'Chris',
+ id: 2,
+ location: 'Cornwall',
+ booleanTest: false,
+ },
+ ])
+
+ const actual = Select('first', {
+ where: {
+ OR: [
+ {
+ location: 'Cornwall',
+ id: 2344,
+ },
+ ],
+ },
+ })
+
+ const expectation: SibylResponse[] = [
+ {
+ name: 'Chris',
+ id: 2,
+ location: 'Cornwall',
+ booleanTest: 0,
+ },
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: 1,
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('selects multiple entries with the OR statement and sorts them in ascending order by id', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, Select } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ },
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Brighton',
+ booleanTest: false,
+ },
+ {
+ name: 'Chris',
+ id: 2,
+ location: 'Cornwall',
+ booleanTest: false,
+ },
+ ])
+
+ const actual = Select('first', {
+ where: {
+ location: 'Brighton',
+ },
+ sort: {
+ id: 'ASC',
+ },
+ })
+
+ const expectation: SibylResponse[] = [
+ {
+ id: 1,
+ location: 'Brighton',
+ name: 'Bob',
+ booleanTest: 0,
+ },
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: 1,
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+ it('selects using boolean value', async () => {
+ const db = new Database(':memory:')
+ // Create table schema
+ interface firstTable {
+ id: number
+ name: string
+ location: string
+ hasReadTheReadme: boolean
+ }
+ interface Tables {
+ firstTable: firstTable
+ }
+ const { createTable, Insert, Select } = await Sibyl(db)
+
+ createTable('firstTable', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ name: {
+ type: 'char',
+ },
+ hasReadTheReadme: {
+ type: 'bool',
+ },
+ location: {
+ type: 'char',
+ },
+ })
+
+ Insert('firstTable', [
+ {
+ id: 1,
+ hasReadTheReadme: true,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 2,
+ hasReadTheReadme: false,
+ location: 'Leeds',
+ name: 'Bob',
+ },
+ {
+ id: 3,
+ hasReadTheReadme: true,
+ location: 'Brighton',
+ name: 'David',
+ },
+ ])
+
+ const actual = Select('firstTable', {
+ where: {
+ OR: [
+ {
+ name: 'Craig',
+ },
+ {
+ hasReadTheReadme: 1,
+ },
+ ],
+ },
+ })
+ const expectation: SibylResponse[] = [
+ {
+ id: 1,
+ hasReadTheReadme: 1,
+ location: 'Brighton',
+ name: 'Craig',
+ },
+ {
+ id: 3,
+ hasReadTheReadme: 1,
+ location: 'Brighton',
+ name: 'David',
+ },
+ ]
+ expect(actual).toStrictEqual(expectation)
+ })
+})
diff --git a/src/libsql/tests/update.test.ts b/src/libsql/tests/update.test.ts
new file mode 100644
index 0000000..a7c1371
--- /dev/null
+++ b/src/libsql/tests/update.test.ts
@@ -0,0 +1,71 @@
+import { describe, expect, it } from 'bun:test'
+import Database from 'libsql'
+import Sibyl from '../index'
+import type { SibylResponse } from '../../types'
+
+interface TableRow {
+ id: number
+ location: string
+ name: string
+ booleanTest: boolean
+}
+
+interface Tables {
+ first: TableRow
+}
+
+describe('update tests', () => {
+ it('updates an entry in the DB', async () => {
+ const db = new Database(':memory:')
+ const { createTable, Insert, Update } = await Sibyl(db)
+
+ createTable('first', {
+ id: {
+ autoincrement: true,
+ type: 'INTEGER',
+ primary: true,
+ unique: true,
+ },
+ location: {
+ type: 'char',
+ },
+ name: {
+ type: 'char',
+ },
+ booleanTest: {
+ type: 'bool',
+ },
+ })
+ Insert('first', [
+ {
+ name: 'Craig',
+ id: 2344,
+ location: 'Brighton',
+ booleanTest: true,
+ },
+ {
+ name: 'Bob',
+ id: 1,
+ location: 'Cornwall',
+ booleanTest: false,
+ },
+ ])
+
+ const actual = Update('first', {
+ where: {
+ id: 2344,
+ },
+ updates: {
+ name: 'Richard',
+ booleanTest: false,
+ },
+ })
+ const expectation: SibylResponse = {
+ id: 2344,
+ location: 'Brighton',
+ name: 'Richard',
+ booleanTest: 0,
+ }
+ expect(actual).toStrictEqual(expectation)
+ })
+})
diff --git a/src/sibylLib.ts b/src/sibylLib.ts
index cba3ddf..033f457 100644
--- a/src/sibylLib.ts
+++ b/src/sibylLib.ts
@@ -190,3 +190,38 @@ function processNonPrimaryType(columnType: DBEntry>) {
result += ' UNIQUE'
return result
}
+
+export function formatInsertStatementLibSQL>(table: string, structs: T[]) {
+ const sortedStructs = sortKeys(structs)
+ const flattenedInsert = sortedStructs.map(obj => Object.values(obj))
+ const flattenedKeys = sortedStructs.map(obj => Object.keys(obj))[0]
+ let insertions: string = ''
+ insertions += `INSERT INTO ${table} `
+
+ let tableKeys = ''
+ for (const key of flattenedKeys)
+ tableKeys += `${key}, `
+
+ tableKeys = tableKeys.slice(0, -2)
+ tableKeys.trim()
+ tableKeys = `(${tableKeys}) `
+
+ insertions += tableKeys
+ insertions += 'VALUES '
+
+ for (const insert of flattenedInsert) {
+ let row: T | string[] = []
+ for (const cell of insert) {
+ if (typeof cell !== 'string')
+ row = [...row, cell]
+
+ if (typeof cell === 'string')
+ row = [...row, `\'${cell}\'`]
+ }
+ insertions += `(${row}), `
+ }
+ insertions = insertions.slice(0, -2)
+ insertions.trim()
+ insertions += ';'
+ return insertions
+}