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 +}