diff --git a/.github/workflows/demo.yaml b/.github/workflows/demo.yaml
new file mode 100644
index 00000000..89bc3dc2
--- /dev/null
+++ b/.github/workflows/demo.yaml
@@ -0,0 +1,115 @@
+name: demo projects
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+on:
+ push:
+ branches: [main]
+ pull_request:
+ branches: [main]
+
+jobs:
+ demo-test:
+ name: Demo project sqlx-ts (${{ matrix.db.name }})
+ if: "!contains(github.event.head_commit.message, 'Release')"
+ runs-on: ubuntu-latest
+ env:
+ BUILD_MODE: release
+ strategy:
+ fail-fast: false
+ matrix:
+ db:
+ - name: postgres-16
+ postgres: "16"
+ mysql: ""
+ - name: postgres-13
+ postgres: "13"
+ mysql: ""
+ - name: mysql-8
+ postgres: ""
+ mysql: "8"
+ - name: sqlite
+ postgres: ""
+ mysql: ""
+
+ steps:
+ - uses: actions/checkout@v4
+
+ - name: Install stable Rust toolchain
+ uses: dtolnay/rust-toolchain@stable
+
+ - uses: Swatinem/rust-cache@v2
+ with:
+ save-if: ${{ github.ref == 'refs/heads/main' }}
+ cache-provider: "github"
+
+ - name: Build sqlx-ts binary
+ run: cargo build --release
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+
+ - name: Start database services
+ if: matrix.db.postgres != '' || matrix.db.mysql != ''
+ run: docker compose -f docker-compose.yml up -d
+ env:
+ PG_VERSION: ${{ matrix.db.postgres || '16' }}
+ MYSQL_VERSION: ${{ matrix.db.mysql || '8' }}
+
+ - name: Wait for databases to be ready
+ if: matrix.db.postgres != '' || matrix.db.mysql != ''
+ run: |
+ echo "Waiting for databases to initialize..."
+ sleep 15
+ if [ -n "${{ matrix.db.postgres }}" ]; then
+ until docker compose exec -T postgres pg_isready -U postgres 2>/dev/null; do
+ echo "Waiting for PostgreSQL..."
+ sleep 2
+ done
+ echo "PostgreSQL is ready"
+ fi
+ if [ -n "${{ matrix.db.mysql }}" ]; then
+ until docker compose exec -T mysql mysqladmin ping -h localhost --silent 2>/dev/null; do
+ echo "Waiting for MySQL..."
+ sleep 2
+ done
+ echo "MySQL is ready"
+ fi
+
+ - name: Create SQLite database
+ if: matrix.db.name == 'sqlite'
+ working-directory: ./demo
+ run: |
+ sudo apt-get update && sudo apt-get install -y sqlite3
+ bash ./create-sqlite.sh
+
+ - name: Install demo dependencies
+ working-directory: ./demo
+ run: npm install
+
+ - name: Generate types (PostgreSQL demos)
+ if: matrix.db.postgres != ''
+ working-directory: ./demo
+ run: |
+ ../target/release/sqlx-ts ./pg --config ./.sqlxrc.json -g
+ ../target/release/sqlx-ts ./sequelize --config ./.sqlxrc.json -g
+
+ - name: Generate types (MySQL demo)
+ if: matrix.db.mysql != ''
+ working-directory: ./demo
+ run: |
+ ../target/release/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g
+
+ - name: Generate types (SQLite demo)
+ if: matrix.db.name == 'sqlite'
+ working-directory: ./demo
+ run: |
+ ../target/release/sqlx-ts ./sqlite --config ./.sqlxrc.json -g
+
+ - name: Type-check generated code
+ working-directory: ./demo
+ run: npm run build
diff --git a/Cargo.lock b/Cargo.lock
index 5e7fc70e..3ead064f 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -531,6 +531,18 @@ version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7"
+[[package]]
+name = "fallible-iterator"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649"
+
+[[package]]
+name = "fallible-streaming-iterator"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
+
[[package]]
name = "fastrand"
version = "2.3.0"
@@ -715,6 +727,15 @@ version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+[[package]]
+name = "hashlink"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ba4ff7128dee98c7dc9794b6a411377e1404dba1c97deb8d1a55297bd25d8af"
+dependencies = [
+ "hashbrown 0.14.5",
+]
+
[[package]]
name = "heck"
version = "0.5.0"
@@ -946,6 +967,17 @@ dependencies = [
"libc",
]
+[[package]]
+name = "libsqlite3-sys"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c10584274047cb335c23d3e61bcef8e323adae7c5c8c760540f73610177fc3f"
+dependencies = [
+ "cc",
+ "pkg-config",
+ "vcpkg",
+]
+
[[package]]
name = "libz-sys"
version = "1.1.25"
@@ -1316,7 +1348,7 @@ dependencies = [
"base64",
"byteorder",
"bytes",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"hmac",
"md-5",
"memchr",
@@ -1332,7 +1364,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b858f82211e84682fecd373f68e1ceae642d8d751a1ebd13f33de6257b3e20"
dependencies = [
"bytes",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"postgres-protocol",
]
@@ -1577,6 +1609,20 @@ version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+[[package]]
+name = "rusqlite"
+version = "0.31.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b838eba278d213a8beaf485bd313fd580ca4505a00d5871caeb1457c55322cae"
+dependencies = [
+ "bitflags",
+ "fallible-iterator 0.3.0",
+ "fallible-streaming-iterator",
+ "hashlink",
+ "libsqlite3-sys",
+ "smallvec",
+]
+
[[package]]
name = "rustc-demangle"
version = "0.1.27"
@@ -1815,6 +1861,7 @@ dependencies = [
"predicates",
"pretty_assertions",
"regex",
+ "rusqlite",
"serde",
"serde_json",
"sqlparser",
@@ -2147,7 +2194,7 @@ dependencies = [
"async-trait",
"byteorder",
"bytes",
- "fallible-iterator",
+ "fallible-iterator 0.2.0",
"futures-channel",
"futures-util",
"log",
diff --git a/Cargo.toml b/Cargo.toml
index 715028a4..1828a87a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@ tokio-postgres = "0.7.16"
tokio = { version = "1.50.0", features = ["rt-multi-thread", "macros", "default"]}
async-recursion = "1.1.1"
bb8 = "0.9.1"
+rusqlite = { version = "0.31", features = ["bundled"] }
log = "0.4.29"
[dev-dependencies]
@@ -39,3 +40,4 @@ predicates = "3.1.4"
tempfile = "3.27.0"
test_utils = { path="test-utils" }
pretty_assertions = "1.4.1"
+rusqlite = { version = "0.31", features = ["bundled"] }
diff --git a/README.md b/README.md
index f1002898..be5ebd2e 100644
--- a/README.md
+++ b/README.md
@@ -11,8 +11,8 @@
SQLx-ts is a CLI application featuring compile-time checked queries without a DSL and generates types against SQLs to keep your code type-safe
- **Compile time checked queries** - never ship a broken SQL query to production (and [sqlx-ts is not an ORM](https://github.com/JasonShin/sqlx-ts#sqlx-ts-is-not-an-orm))
-- **TypeScript type generations** - generates type definitions based on the raw SQLs and you can use them with any MySQL or PostgreSQL driver
-- **Database Agnostic** - support for [PostgreSQL](http://postgresql.org/) and [MySQL](https://www.mysql.com/) (and more DB supports to come)
+- **TypeScript type generations** - generates type definitions based on the raw SQLs and you can use them with any MySQL, PostgreSQL, or SQLite driver
+- **Database Agnostic** - support for [PostgreSQL](http://postgresql.org/), [MySQL](https://www.mysql.com/), and [SQLite](https://www.sqlite.org/)
- **TypeScript and JavaScript** - supports for both [TypeScript](https://jasonshin.github.io/sqlx-ts/reference-guide/4.typescript-types-generation.html) and [JavaScript](https://github.com/JasonShin/sqlx-ts#using-sqlx-ts-in-vanilla-javascript)
@@ -22,7 +22,7 @@ SQLx-ts is a CLI application featuring compile-time checked queries without a DS
|
- 🤓 Demo
+ 🤓 Demo
diff --git a/book/docs/connect/README.md b/book/docs/connect/README.md
index 41768914..32de2392 100644
--- a/book/docs/connect/README.md
+++ b/book/docs/connect/README.md
@@ -46,6 +46,14 @@ $ sqlx-ts --db-type postgres --db-url postgres://user:pass@localhost:5432
$ sqlx-ts --db-type mysql --db-url mysql://user:pass@localhost:3306/mydb
```
+#### SQLite
+
+For SQLite, you only need to provide the database file path. No host, port, or user credentials are required:
+
+```bash
+$ sqlx-ts --db-type sqlite --db-name ./mydb.sqlite
+```
+
**Note:** When `--db-url` is provided, it takes precedence over individual connection parameters (`--db-host`, `--db-port`, `--db-user`, `--db-pass`, `--db-name`).
Run the following command for more details:
diff --git a/book/docs/connect/config-file.md b/book/docs/connect/config-file.md
index a0280fda..15efa981 100644
--- a/book/docs/connect/config-file.md
+++ b/book/docs/connect/config-file.md
@@ -66,6 +66,22 @@ Alternatively, you can use `DB_URL` to specify the connection string directly:
}
```
+For SQLite, only `DB_TYPE` and `DB_NAME` (the file path) are required:
+
+```json
+{
+ "generate_types": {
+ "enabled": true
+ },
+ "connections": {
+ "default": {
+ "DB_TYPE": "sqlite",
+ "DB_NAME": "./mydb.sqlite"
+ }
+ }
+}
+```
+
## Configuration options
### connections (required)
@@ -92,7 +108,7 @@ const postgresSQL = sql`
Supported fields of each connection include
- `DB_URL`: Database connection URL (e.g. `postgres://user:pass@host:port/dbname` or `mysql://user:pass@host:port/dbname`). If provided, this overrides individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`)
-- `DB_TYPE`: type of database connection (mysql | postgres)
+- `DB_TYPE`: type of database connection (mysql | postgres | sqlite)
- `DB_USER`: database user name
- `DB_PASS`: database password
- `DB_HOST`: database host (e.g. 127.0.0.1)
diff --git a/book/docs/connect/environment-variables.md b/book/docs/connect/environment-variables.md
index d57574ad..08210770 100644
--- a/book/docs/connect/environment-variables.md
+++ b/book/docs/connect/environment-variables.md
@@ -7,7 +7,7 @@
| DB_HOST | Primary DB host |
| DB_PASS | Primary DB password |
| DB_PORT | Primary DB port number |
-| DB_TYPE | Type of primary database to connect [default: postgres] [possible values: postgres, mysql] |
+| DB_TYPE | Type of primary database to connect [default: postgres] [possible values: postgres, mysql, sqlite] |
| DB_USER | Primary DB user name |
| DB_NAME | Primary DB name |
| PG_SEARCH_PATH | PostgreSQL schema search path (default is "$user,public") [https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH](https://www.postgresql.org/docs/current/ddl-schemas.html#DDL-SCHEMAS-PATH) |
@@ -47,3 +47,14 @@ sqlx-ts
**Note:** When `DB_URL` is set, it takes precedence over individual connection parameters (`DB_HOST`, `DB_PORT`, `DB_USER`, `DB_PASS`, `DB_NAME`).
+### SQLite
+
+For SQLite, only `DB_TYPE` and `DB_NAME` (file path) are required:
+
+```bash
+export DB_TYPE=sqlite
+export DB_NAME=./mydb.sqlite
+
+sqlx-ts
+```
+
diff --git a/demo/.gitignore b/demo/.gitignore
new file mode 100644
index 00000000..62d9aaee
--- /dev/null
+++ b/demo/.gitignore
@@ -0,0 +1,5 @@
+node_modules
+.idea
+dist
+.DS_Store
+*.db
\ No newline at end of file
diff --git a/demo/.sqlxrc.json b/demo/.sqlxrc.json
new file mode 100644
index 00000000..fa0e49b8
--- /dev/null
+++ b/demo/.sqlxrc.json
@@ -0,0 +1,27 @@
+{
+ "generate_types": {
+ "enabled": true,
+ "convertToCamelCaseColumnName": true
+ },
+ "connections": {
+ "default": {
+ "DB_TYPE": "postgres",
+ "DB_HOST": "127.0.0.1",
+ "DB_PORT": 54321,
+ "DB_USER": "postgres",
+ "DB_PASS": "postgres",
+ "DB_NAME": "postgres"
+ },
+ "db_mysql": {
+ "DB_TYPE": "mysql",
+ "DB_HOST": "127.0.0.1",
+ "DB_PORT": 33306,
+ "DB_USER": "root",
+ "DB_NAME": "sqlx-ts"
+ },
+ "db_sqlite": {
+ "DB_TYPE": "sqlite",
+ "DB_NAME": "./sqlite/demo.db"
+ }
+ }
+ }
\ No newline at end of file
diff --git a/demo/README.md b/demo/README.md
new file mode 100644
index 00000000..1fd51a9c
--- /dev/null
+++ b/demo/README.md
@@ -0,0 +1,25 @@
+# sqlx-ts demo
+
+Usage examples showing sqlx-ts with different database drivers.
+
+## Examples
+
+- [pg](./pg) - PostgreSQL with `pg` driver
+- [mysql2](./mysql2) - MySQL with `mysql2` driver
+- [sequelize](./sequelize) - PostgreSQL with Sequelize ORM
+- [sqlite](./sqlite) - SQLite with `better-sqlite3` (no Docker needed)
+
+## Running locally
+
+```bash
+# Start databases (from repo root)
+docker compose up -d
+
+# Install demo dependencies
+cd demo
+npm install
+
+# Generate types and type-check
+npm run compile:all
+npm run typecheck
+```
diff --git a/demo/create-sqlite.sh b/demo/create-sqlite.sh
new file mode 100755
index 00000000..0c75a415
--- /dev/null
+++ b/demo/create-sqlite.sh
@@ -0,0 +1,9 @@
+#!/bin/bash
+# Creates the SQLite demo database from setup.sql
+set -e
+SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
+DB_PATH="$SCRIPT_DIR/sqlite/demo.db"
+
+rm -f "$DB_PATH"
+sqlite3 "$DB_PATH" < "$SCRIPT_DIR/setup.sql"
+echo "Created SQLite database at $DB_PATH"
diff --git a/demo/mysql2/index.queries.ts b/demo/mysql2/index.queries.ts
new file mode 100644
index 00000000..6d35739c
--- /dev/null
+++ b/demo/mysql2/index.queries.ts
@@ -0,0 +1,47 @@
+export type GetItems2Params = [];
+
+export interface IGetItems2Result {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface IGetItems2Query {
+ params: GetItems2Params;
+ result: IGetItems2Result;
+}
+
+export type TestInsertParams = [[number, string]];
+
+export interface ITestInsertResult {
+
+}
+
+export interface ITestInsertQuery {
+ params: TestInsertParams;
+ result: ITestInsertResult;
+}
+
+export type TestUpdateParams = [string | null];
+
+export interface ITestUpdateResult {
+
+}
+
+export interface ITestUpdateQuery {
+ params: TestUpdateParams;
+ result: ITestUpdateResult;
+}
+
+export type TestDeleteParams = [string | null];
+
+export interface ITestDeleteResult {
+
+}
+
+export interface ITestDeleteQuery {
+ params: TestDeleteParams;
+ result: ITestDeleteResult;
+}
diff --git a/demo/mysql2/index.ts b/demo/mysql2/index.ts
new file mode 100644
index 00000000..a8909cdb
--- /dev/null
+++ b/demo/mysql2/index.ts
@@ -0,0 +1,47 @@
+import { sql } from 'sqlx-ts'
+import * as mysql from 'mysql2/promise'
+import { IGetItems2Result, TestInsertParams, TestUpdateParams, TestDeleteParams } from './index.queries'
+
+type Rows = Array
+
+(async () => {
+
+ const connection = await mysql.createConnection({
+ host: '127.0.0.1',
+ user: 'root',
+ port: 33306,
+ database: 'sqlx-ts',
+ });
+
+ const [rows] = await connection.execute>(sql`
+ -- @name: getItems2
+ SELECT * FROM items
+ `)
+
+ for (const row of rows) {
+ const { id, rarity, name } = row
+ console.log(id, rarity, name)
+ }
+
+ await connection.execute(sql`
+ -- @name: testInsert
+ -- @db: db_mysql
+ INSERT INTO items (id, name, rarity, flavor_text) VALUES (?, ?, 'test', 'test');
+ `, [1, 'test'] as TestInsertParams[0])
+
+ const rarityType = 'test'
+
+ await connection.query(sql`
+ -- @name: testUpdate
+ -- @db: db_mysql
+ UPDATE items SET rarity = ? WHERE id = 1;
+ `, [rarityType] as TestUpdateParams)
+
+ await connection.query(sql`
+ -- @name: testDelete
+ -- @db: db_mysql
+ DELETE FROM items WHERE rarity = ?;
+ `, [rarityType] as TestDeleteParams)
+
+ connection.destroy()
+})()
diff --git a/demo/package-lock.json b/demo/package-lock.json
new file mode 100644
index 00000000..2c09b83b
--- /dev/null
+++ b/demo/package-lock.json
@@ -0,0 +1,606 @@
+{
+ "name": "sqlx-ts-demo",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "sqlx-ts-demo",
+ "version": "1.0.0",
+ "dependencies": {
+ "mysql2": "^3.5.2",
+ "pg": "^8.11.1",
+ "sequelize": "^6.32.1",
+ "sqlx-ts": "^0.45.0"
+ },
+ "devDependencies": {
+ "@types/pg": "^8.10.2",
+ "@types/sequelize": "^4.28.14",
+ "typescript": "^5.1.6"
+ }
+ },
+ "node_modules/@types/bluebird": {
+ "version": "3.5.42",
+ "resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.42.tgz",
+ "integrity": "sha512-Jhy+MWRlro6UjVi578V/4ZGNfeCOcNCp0YaFNIUGFKlImowqwb1O/22wDVk3FDGMLqxdpOV3qQHD5fPEH4hK6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/continuation-local-storage": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/@types/continuation-local-storage/-/continuation-local-storage-3.2.7.tgz",
+ "integrity": "sha512-Q7dPOymVpRG5Zpz90/o26+OAqOG2Sw+FED7uQmTrJNCF/JAPTylclZofMxZKd6W7g1BDPmT9/C/jX0ZcSNTQwQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/@types/debug": {
+ "version": "4.1.13",
+ "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz",
+ "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/ms": "*"
+ }
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.24",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.24.tgz",
+ "integrity": "sha512-gIW7lQLZbue7lRSWEFql49QJJWThrTFFeIMJdp3eH4tKoxm1OvEPg02rm4wCCSHS0cL3/Fizimb35b7k8atwsQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/ms": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz",
+ "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/node": {
+ "version": "25.6.0",
+ "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz",
+ "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==",
+ "license": "MIT",
+ "dependencies": {
+ "undici-types": "~7.19.0"
+ }
+ },
+ "node_modules/@types/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "pg-protocol": "*",
+ "pg-types": "^2.2.0"
+ }
+ },
+ "node_modules/@types/sequelize": {
+ "version": "4.28.20",
+ "resolved": "https://registry.npmjs.org/@types/sequelize/-/sequelize-4.28.20.tgz",
+ "integrity": "sha512-XaGOKRhdizC87hDgQ0u3btxzbejlF+t6Hhvkek1HyphqCI4y7zVBIVAGmuc4cWJqGpxusZ1RiBToHHnNK/Edlw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/bluebird": "*",
+ "@types/continuation-local-storage": "*",
+ "@types/lodash": "*",
+ "@types/validator": "*"
+ }
+ },
+ "node_modules/@types/validator": {
+ "version": "13.15.10",
+ "resolved": "https://registry.npmjs.org/@types/validator/-/validator-13.15.10.tgz",
+ "integrity": "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA==",
+ "license": "MIT"
+ },
+ "node_modules/adm-zip": {
+ "version": "0.5.17",
+ "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.17.tgz",
+ "integrity": "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0"
+ }
+ },
+ "node_modules/aws-ssl-profiles": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz",
+ "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 6.0.0"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.4.3",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
+ "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
+ "license": "MIT",
+ "dependencies": {
+ "ms": "^2.1.3"
+ },
+ "engines": {
+ "node": ">=6.0"
+ },
+ "peerDependenciesMeta": {
+ "supports-color": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/denque": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
+ "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "node_modules/dottie": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.7.tgz",
+ "integrity": "sha512-7lAK2A0b3zZr3UC5aE69CPdCFR4RHW1o2Dr74TqFykxkUCBXSRJum/yPc7g8zRHJqWKomPLHwFLLoUnn8PXXRg==",
+ "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.",
+ "license": "MIT"
+ },
+ "node_modules/generate-function": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
+ "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "is-property": "^1.0.2"
+ }
+ },
+ "node_modules/iconv-lite": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz",
+ "integrity": "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==",
+ "license": "MIT",
+ "dependencies": {
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/express"
+ }
+ },
+ "node_modules/inflection": {
+ "version": "1.13.4",
+ "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.13.4.tgz",
+ "integrity": "sha512-6I/HUDeYFfuNCVS3td055BaXBwKYuzw7K3ExVMStBowKo9oOAMJIXIHvdyR3iboTCp1b+1i5DSkIZTcwIktuDw==",
+ "engines": [
+ "node >= 0.4.0"
+ ],
+ "license": "MIT"
+ },
+ "node_modules/is-property": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
+ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==",
+ "license": "MIT"
+ },
+ "node_modules/lodash": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
+ "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
+ "license": "MIT"
+ },
+ "node_modules/long": {
+ "version": "5.3.2",
+ "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
+ "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/lru.min": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.4.tgz",
+ "integrity": "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=1.30.0",
+ "node": ">=8.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wellwelwel"
+ }
+ },
+ "node_modules/moment": {
+ "version": "2.30.1",
+ "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz",
+ "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/moment-timezone": {
+ "version": "0.5.48",
+ "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.48.tgz",
+ "integrity": "sha512-f22b8LV1gbTO2ms2j2z13MuPogNoh5UzxL3nzNAYKGraILnbGc9NEE6dyiiiLv46DGRb8A4kg8UKWLjPthxBHw==",
+ "license": "MIT",
+ "dependencies": {
+ "moment": "^2.29.4"
+ },
+ "engines": {
+ "node": "*"
+ }
+ },
+ "node_modules/ms": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
+ "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
+ "license": "MIT"
+ },
+ "node_modules/mysql2": {
+ "version": "3.22.2",
+ "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.22.2.tgz",
+ "integrity": "sha512-snC/L6YoCJPFpozZo3p3hiOlt9ItQ7sCnLSziFLlIttEzsPhrdcPT8g21BiQ7Oqif25W4Xq1IFuBzBvoFYDf0Q==",
+ "license": "MIT",
+ "dependencies": {
+ "aws-ssl-profiles": "^1.1.2",
+ "denque": "^2.1.0",
+ "generate-function": "^2.3.1",
+ "iconv-lite": "^0.7.2",
+ "long": "^5.3.2",
+ "lru.min": "^1.1.4",
+ "named-placeholders": "^1.1.6",
+ "sql-escaper": "^1.3.3"
+ },
+ "engines": {
+ "node": ">= 8.0"
+ },
+ "peerDependencies": {
+ "@types/node": ">= 8"
+ }
+ },
+ "node_modules/named-placeholders": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.6.tgz",
+ "integrity": "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w==",
+ "license": "MIT",
+ "dependencies": {
+ "lru.min": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/pg": {
+ "version": "8.20.0",
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.20.0.tgz",
+ "integrity": "sha512-ldhMxz2r8fl/6QkXnBD3CR9/xg694oT6DZQ2s6c/RI28OjtSOpxnPrUCGOBJ46RCUxcWdx3p6kw/xnDHjKvaRA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-connection-string": "^2.12.0",
+ "pg-pool": "^3.13.0",
+ "pg-protocol": "^1.13.0",
+ "pg-types": "2.2.0",
+ "pgpass": "1.0.5"
+ },
+ "engines": {
+ "node": ">= 16.0.0"
+ },
+ "optionalDependencies": {
+ "pg-cloudflare": "^1.3.0"
+ },
+ "peerDependencies": {
+ "pg-native": ">=3.0.1"
+ },
+ "peerDependenciesMeta": {
+ "pg-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/pg-cloudflare": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.3.0.tgz",
+ "integrity": "sha512-6lswVVSztmHiRtD6I8hw4qP/nDm1EJbKMRhf3HCYaqud7frGysPv7FYJ5noZQdhQtN2xJnimfMtvQq21pdbzyQ==",
+ "license": "MIT",
+ "optional": true
+ },
+ "node_modules/pg-connection-string": {
+ "version": "2.12.0",
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.12.0.tgz",
+ "integrity": "sha512-U7qg+bpswf3Cs5xLzRqbXbQl85ng0mfSV/J0nnA31MCLgvEaAo7CIhmeyrmJpOr7o+zm0rXK+hNnT5l9RHkCkQ==",
+ "license": "MIT"
+ },
+ "node_modules/pg-int8": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
+ "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
+ "license": "ISC",
+ "engines": {
+ "node": ">=4.0.0"
+ }
+ },
+ "node_modules/pg-pool": {
+ "version": "3.13.0",
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.13.0.tgz",
+ "integrity": "sha512-gB+R+Xud1gLFuRD/QgOIgGOBE2KCQPaPwkzBBGC9oG69pHTkhQeIuejVIk3/cnDyX39av2AxomQiyPT13WKHQA==",
+ "license": "MIT",
+ "peerDependencies": {
+ "pg": ">=8.0"
+ }
+ },
+ "node_modules/pg-protocol": {
+ "version": "1.13.0",
+ "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.13.0.tgz",
+ "integrity": "sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==",
+ "license": "MIT"
+ },
+ "node_modules/pg-types": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
+ "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
+ "license": "MIT",
+ "dependencies": {
+ "pg-int8": "1.0.1",
+ "postgres-array": "~2.0.0",
+ "postgres-bytea": "~1.0.0",
+ "postgres-date": "~1.0.4",
+ "postgres-interval": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/pgpass": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
+ "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
+ "license": "MIT",
+ "dependencies": {
+ "split2": "^4.1.0"
+ }
+ },
+ "node_modules/postgres-array": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
+ "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/postgres-bytea": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.1.tgz",
+ "integrity": "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-date": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
+ "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/postgres-interval": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
+ "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
+ "license": "MIT",
+ "dependencies": {
+ "xtend": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/retry-as-promised": {
+ "version": "7.1.1",
+ "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-7.1.1.tgz",
+ "integrity": "sha512-hMD7odLOt3LkTjcif8aRZqi/hybjpLNgSk5oF5FCowfCjok6LukpN2bDX7R5wDmbgBQFn7YoBxSagmtXHaJYJw==",
+ "license": "MIT"
+ },
+ "node_modules/safer-buffer": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
+ "license": "MIT"
+ },
+ "node_modules/semver": {
+ "version": "7.7.4",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
+ "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "license": "ISC",
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/sequelize": {
+ "version": "6.37.8",
+ "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.8.tgz",
+ "integrity": "sha512-HJ0IQFqcTsTiqbEgiuioYFMSD00TP6Cz7zoTti+zVVBwVe9fEhev9cH6WnM3XU31+ABS356durAb99ZuOthnKw==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/sequelize"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "@types/debug": "^4.1.8",
+ "@types/validator": "^13.7.17",
+ "debug": "^4.3.4",
+ "dottie": "^2.0.6",
+ "inflection": "^1.13.4",
+ "lodash": "^4.17.21",
+ "moment": "^2.29.4",
+ "moment-timezone": "^0.5.43",
+ "pg-connection-string": "^2.6.1",
+ "retry-as-promised": "^7.0.4",
+ "semver": "^7.5.4",
+ "sequelize-pool": "^7.1.0",
+ "toposort-class": "^1.0.1",
+ "uuid": "^8.3.2",
+ "validator": "^13.9.0",
+ "wkx": "^0.5.0"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ },
+ "peerDependenciesMeta": {
+ "ibm_db": {
+ "optional": true
+ },
+ "mariadb": {
+ "optional": true
+ },
+ "mysql2": {
+ "optional": true
+ },
+ "oracledb": {
+ "optional": true
+ },
+ "pg": {
+ "optional": true
+ },
+ "pg-hstore": {
+ "optional": true
+ },
+ "snowflake-sdk": {
+ "optional": true
+ },
+ "sqlite3": {
+ "optional": true
+ },
+ "tedious": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/sequelize-pool": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-7.1.0.tgz",
+ "integrity": "sha512-G9c0qlIWQSK29pR/5U2JF5dDQeqqHRragoyahj/Nx4KOOQ3CPPfzxnfqFPCSB7x5UgjOgnZ61nSxz+fjDpRlJg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 10.0.0"
+ }
+ },
+ "node_modules/split2": {
+ "version": "4.2.0",
+ "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
+ "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==",
+ "license": "ISC",
+ "engines": {
+ "node": ">= 10.x"
+ }
+ },
+ "node_modules/sql-escaper": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/sql-escaper/-/sql-escaper-1.3.3.tgz",
+ "integrity": "sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==",
+ "license": "MIT",
+ "engines": {
+ "bun": ">=1.0.0",
+ "deno": ">=2.0.0",
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1"
+ }
+ },
+ "node_modules/sqlx-ts": {
+ "version": "0.45.0",
+ "resolved": "https://registry.npmjs.org/sqlx-ts/-/sqlx-ts-0.45.0.tgz",
+ "integrity": "sha512-ArdK6hc2O1KVIqux3mA3KCsXRQXLeVSGMYQgpB8yuVaYVQnol09lRwMTTBWnG61HPo2YePRXHV5APPsrXMTzug==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "adm-zip": "^0.5.16"
+ },
+ "bin": {
+ "sqlx-ts": "sqlx-ts"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/toposort-class": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz",
+ "integrity": "sha512-OsLcGGbYF3rMjPUf8oKktyvCiUxSbqMMS39m33MAjLTC1DVIH6x3WSt63/M77ihI09+Sdfk1AXvfhCEeUmC7mg==",
+ "license": "MIT"
+ },
+ "node_modules/typescript": {
+ "version": "5.9.3",
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "tsc": "bin/tsc",
+ "tsserver": "bin/tsserver"
+ },
+ "engines": {
+ "node": ">=14.17"
+ }
+ },
+ "node_modules/undici-types": {
+ "version": "7.19.2",
+ "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz",
+ "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==",
+ "license": "MIT"
+ },
+ "node_modules/uuid": {
+ "version": "8.3.2",
+ "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz",
+ "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==",
+ "license": "MIT",
+ "bin": {
+ "uuid": "dist/bin/uuid"
+ }
+ },
+ "node_modules/validator": {
+ "version": "13.15.35",
+ "resolved": "https://registry.npmjs.org/validator/-/validator-13.15.35.tgz",
+ "integrity": "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.10"
+ }
+ },
+ "node_modules/wkx": {
+ "version": "0.5.0",
+ "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.5.0.tgz",
+ "integrity": "sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
+ "node_modules/xtend": {
+ "version": "4.0.2",
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4"
+ }
+ }
+ }
+}
diff --git a/demo/package.json b/demo/package.json
new file mode 100644
index 00000000..a3b2b909
--- /dev/null
+++ b/demo/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "sqlx-ts-demo",
+ "version": "1.0.0",
+ "private": true,
+ "description": "Demo project showing sqlx-ts usage with pg, mysql2, and sequelize",
+ "scripts": {
+ "compile:pg": "../target/${BUILD_MODE:-debug}/sqlx-ts ./pg --config ./.sqlxrc.json -g",
+ "compile:mysql": "../target/${BUILD_MODE:-debug}/sqlx-ts ./mysql2 --config ./.sqlxrc.json -g",
+ "compile:sequelize": "../target/${BUILD_MODE:-debug}/sqlx-ts ./sequelize --config ./.sqlxrc.json -g",
+ "compile:sqlite": "npm run sqlite && ../target/${BUILD_MODE:-debug}/sqlx-ts ./sqlite --config ./.sqlxrc.json -g",
+ "compile:all": "npm run compile:pg && npm run compile:mysql && npm run compile:sequelize && npm run compile:sqlite",
+ "sqlite": "rm -f sqlite/demo.db && ./create-sqlite.sh",
+ "build": "npx tsc -p ./tsconfig.json --noEmit"
+ },
+ "dependencies": {
+ "mysql2": "^3.5.2",
+ "pg": "^8.11.1",
+ "sequelize": "^6.32.1",
+ "sqlx-ts": "^0.45.0"
+ },
+ "devDependencies": {
+ "@types/pg": "^8.10.2",
+ "@types/sequelize": "^4.28.14",
+ "typescript": "^5.1.6"
+ }
+}
diff --git a/demo/pg/index.queries.ts b/demo/pg/index.queries.ts
new file mode 100644
index 00000000..dc011bc3
--- /dev/null
+++ b/demo/pg/index.queries.ts
@@ -0,0 +1,58 @@
+export type SomeQueryParams = [];
+
+export interface ISomeQueryResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+
+export type TestInsertParams = [number, string];
+
+export interface ITestInsertResult {
+
+}
+
+export interface ITestInsertQuery {
+ params: TestInsertParams;
+ result: ITestInsertResult;
+}
+
+export type TestUpdateParams = [string | null];
+
+export interface ITestUpdateResult {
+
+}
+
+export interface ITestUpdateQuery {
+ params: TestUpdateParams;
+ result: ITestUpdateResult;
+}
+
+export type TestDeleteParams = [string | null];
+
+export interface ITestDeleteResult {
+
+}
+
+export interface ITestDeleteQuery {
+ params: TestDeleteParams;
+ result: ITestDeleteResult;
+}
+
+export type GetItemsParams = [];
+
+export interface IGetItemsResult {
+ inventoryId: number;
+}
+
+export interface IGetItemsQuery {
+ params: GetItemsParams;
+ result: IGetItemsResult;
+}
diff --git a/demo/pg/index.ts b/demo/pg/index.ts
new file mode 100644
index 00000000..fdc4370d
--- /dev/null
+++ b/demo/pg/index.ts
@@ -0,0 +1,58 @@
+import { sql } from 'sqlx-ts'
+import { Client } from 'pg'
+import {
+ TestInsertParams, ITestInsertResult,
+ TestUpdateParams, ITestUpdateResult,
+ TestDeleteParams, ITestDeleteResult, IGetItemsResult,
+ } from './index.queries'
+
+
+const client = new Client({
+ host: 'localhost',
+ port: 54321,
+ database: 'postgres',
+ user: 'postgres',
+ password: 'postgres',
+});
+
+(async () => {
+ const someQuery = await client.query(sql`
+ SELECT * FROM items;
+ `)
+
+ for (const row of someQuery.rows) {
+ const { id, food_type, points } = row
+ console.log(id, food_type, points)
+ }
+
+ await client.query(sql`
+ -- @name: testInsert
+ INSERT INTO items (id, name, rarity, flavor_text) VALUES ($1, $2, 'test', 'test');
+ `, [1, "hello"])
+
+ const rarityType = 'test'
+
+ await client.query(sql`
+ -- @name: testUpdate
+ UPDATE items SET rarity = $1 WHERE id = (SELECT id FROM items WHERE rarity = 'test' LIMIT 1);
+ `, [rarityType])
+
+ await client.query(sql`
+ -- @name: testDelete
+ DELETE FROM items WHERE rarity = $1;
+ `, [rarityType])
+
+ await client.end()
+
+ class TestQueryRepository {
+ getItems() {
+ return client.query(sql`
+ -- @name: getItems
+ SELECT inventory.id as inventoryId FROM items
+ JOIN inventory ON items.inventory_id = inventory.id;
+ `)
+ }
+ }
+
+ new TestQueryRepository()
+})();
diff --git a/demo/sequelize/index.queries.ts b/demo/sequelize/index.queries.ts
new file mode 100644
index 00000000..a9af09fe
--- /dev/null
+++ b/demo/sequelize/index.queries.ts
@@ -0,0 +1,47 @@
+export type SomeQueryParams = [];
+
+export interface ISomeQueryResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+
+export type TestInsertParams = [number, string];
+
+export interface ITestInsertResult {
+
+}
+
+export interface ITestInsertQuery {
+ params: TestInsertParams;
+ result: ITestInsertResult;
+}
+
+export type TestUpdateParams = [string | null];
+
+export interface ITestUpdateResult {
+
+}
+
+export interface ITestUpdateQuery {
+ params: TestUpdateParams;
+ result: ITestUpdateResult;
+}
+
+export type TestDeleteParams = [string | null];
+
+export interface ITestDeleteResult {
+
+}
+
+export interface ITestDeleteQuery {
+ params: TestDeleteParams;
+ result: ITestDeleteResult;
+}
diff --git a/demo/sequelize/index.ts b/demo/sequelize/index.ts
new file mode 100644
index 00000000..fcf72cab
--- /dev/null
+++ b/demo/sequelize/index.ts
@@ -0,0 +1,60 @@
+import {sql} from 'sqlx-ts'
+import {QueryTypes, Sequelize} from 'sequelize'
+import {
+ ISomeQueryResult,
+ TestInsertParams,
+ TestUpdateParams,
+ TestDeleteParams,
+ } from "./index.queries";
+
+
+const sequelize = new Sequelize('postgres://postgres:postgres@127.0.0.1:54321', {
+ dialect: 'postgres'
+})
+
+async function demo() {
+ const someQuery = await sequelize.query(sql`
+ SELECT * FROM items;
+ `, {
+ type: QueryTypes.SELECT,
+ replacements: [],
+ })
+
+ for (const row of someQuery) {
+ const { id, rarity, name } = row
+ console.log(id, rarity, name)
+ }
+
+ await sequelize.query(sql`
+ -- @name: testInsert
+ INSERT INTO items (id, name, rarity, flavor_text) VALUES ($1, $2, 'test', 'test');
+ `, {
+ type: QueryTypes.INSERT,
+ // Unfortunately sequelize query does not allow you to type binding params for INSERT
+ bind: [1, 'test'] as TestInsertParams,
+ })
+
+ const rarityType = 'rare'
+
+ await sequelize.query(sql`
+ -- @name: testUpdate
+ UPDATE items SET rarity = $1 WHERE id = (SELECT id FROM items WHERE rarity = 'test' LIMIT 1);
+ `, {
+ type: QueryTypes.UPDATE,
+ // Unfortunately sequelize query does not allow you to type binding params for UPDATE
+ bind: [rarityType] as TestUpdateParams,
+ })
+
+ await sequelize.query(sql`
+ -- @name: testDelete
+ DELETE FROM items WHERE rarity = $1;
+ `, {
+ type: QueryTypes.DELETE,
+ bind: [rarityType] as TestDeleteParams,
+ })
+}
+
+(async () => {
+ await demo()
+ await sequelize.close()
+})();
diff --git a/demo/setup.sql b/demo/setup.sql
new file mode 100644
index 00000000..4ee50656
--- /dev/null
+++ b/demo/setup.sql
@@ -0,0 +1,39 @@
+CREATE TABLE IF NOT EXISTS tables (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ number INTEGER NOT NULL,
+ occupied BOOLEAN NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ food_type TEXT NOT NULL,
+ time_takes_to_cook INTEGER NOT NULL,
+ table_id INTEGER NOT NULL,
+ points INTEGER NOT NULL,
+ FOREIGN KEY (table_id) REFERENCES tables (id)
+);
+
+CREATE TABLE IF NOT EXISTS events (
+ id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ start_date DATETIME,
+ is_active BOOLEAN NOT NULL DEFAULT 1,
+ score REAL,
+ metadata JSON
+);
+
+INSERT INTO tables (number) VALUES (1), (2), (3), (4), (5);
+
+INSERT INTO items (food_type, time_takes_to_cook, table_id, points)
+VALUES
+ ('korean', 10, 1, 2),
+ ('chinese', 10, 1, 2),
+ ('japanese', 10, 1, 2),
+ ('italian', 10, 1, 2),
+ ('french', 10, 1, 2);
+
+INSERT INTO events (name, description, start_date, is_active, score)
+VALUES
+ ('Lunch Special', 'Daily lunch menu', '2024-01-15 12:00:00', 1, 4.5),
+ ('Happy Hour', NULL, '2024-01-15 17:00:00', 1, 3.8);
diff --git a/demo/sqlite/index.queries.ts b/demo/sqlite/index.queries.ts
new file mode 100644
index 00000000..f8736182
--- /dev/null
+++ b/demo/sqlite/index.queries.ts
@@ -0,0 +1,92 @@
+export type GetAllItemsParams = [];
+
+export interface IGetAllItemsResult {
+ food_type: string;
+ id: number;
+ points: number;
+ table_id: number;
+ time_takes_to_cook: number;
+}
+
+export interface IGetAllItemsQuery {
+ params: GetAllItemsParams;
+ result: IGetAllItemsResult;
+}
+
+export type GetItemsByFoodTypeParams = [string];
+
+export interface IGetItemsByFoodTypeResult {
+ food_type: string;
+ id: number;
+ points: number;
+ table_id: number;
+ time_takes_to_cook: number;
+}
+
+export interface IGetItemsByFoodTypeQuery {
+ params: GetItemsByFoodTypeParams;
+ result: IGetItemsByFoodTypeResult;
+}
+
+export type InsertItemParams = [[string, number, number, number]];
+
+export interface IInsertItemResult {
+
+}
+
+export interface IInsertItemQuery {
+ params: InsertItemParams;
+ result: IInsertItemResult;
+}
+
+export type UpdateItemParams = [string, number];
+
+export interface IUpdateItemResult {
+
+}
+
+export interface IUpdateItemQuery {
+ params: UpdateItemParams;
+ result: IUpdateItemResult;
+}
+
+export type DeleteItemParams = [string];
+
+export interface IDeleteItemResult {
+
+}
+
+export interface IDeleteItemQuery {
+ params: DeleteItemParams;
+ result: IDeleteItemResult;
+}
+
+export type GetItemsWithTableParams = [];
+
+export interface IGetItemsWithTableResult {
+ items_food_type: string;
+ items_id: number;
+ table_number: number;
+}
+
+export interface IGetItemsWithTableQuery {
+ params: GetItemsWithTableParams;
+ result: IGetItemsWithTableResult;
+}
+
+export type GetEventsWithScoreParams = [number | null];
+
+export interface IGetEventsWithScoreResult {
+ description: string | null;
+ id: number;
+ is_active: boolean;
+ metadata: object | null;
+ name: string;
+ score: number | null;
+ start_date: Date | null;
+}
+
+export interface IGetEventsWithScoreQuery {
+ params: GetEventsWithScoreParams;
+ result: IGetEventsWithScoreResult;
+}
diff --git a/demo/sqlite/index.ts b/demo/sqlite/index.ts
new file mode 100644
index 00000000..0dd168a7
--- /dev/null
+++ b/demo/sqlite/index.ts
@@ -0,0 +1,62 @@
+import { sql } from 'sqlx-ts'
+import {
+ IGetAllItemsResult,
+ IGetItemsByFoodTypeResult, GetItemsByFoodTypeParams,
+ InsertItemParams, IInsertItemResult,
+ UpdateItemParams, IUpdateItemResult,
+ DeleteItemParams, IDeleteItemResult,
+ IGetEventsWithScoreResult, GetEventsWithScoreParams,
+ IGetItemsWithTableResult,
+} from './index.queries'
+
+// SELECT all items
+const getAllItems = sql`
+ -- @name: getAllItems
+ -- @db: db_sqlite
+ SELECT * FROM items;
+`
+
+// SELECT with WHERE clause using ? placeholder
+const getItemsByFoodType = sql`
+ -- @name: getItemsByFoodType
+ -- @db: db_sqlite
+ SELECT * FROM items WHERE food_type = ?;
+`
+
+// INSERT with parameters
+const insertItem = sql`
+ -- @name: insertItem
+ -- @db: db_sqlite
+ INSERT INTO items (food_type, time_takes_to_cook, table_id, points)
+ VALUES (?, ?, ?, ?);
+`
+
+// UPDATE with parameters
+const updateItem = sql`
+ -- @name: updateItem
+ -- @db: db_sqlite
+ UPDATE items SET food_type = ? WHERE id = ?;
+`
+
+// DELETE with parameter
+const deleteItem = sql`
+ -- @name: deleteItem
+ -- @db: db_sqlite
+ DELETE FROM items WHERE food_type = ?;
+`
+
+// SELECT with JOIN
+const getItemsWithTable = sql`
+ -- @name: getItemsWithTable
+ -- @db: db_sqlite
+ SELECT items.id, items.food_type, tables.number as table_number
+ FROM items
+ JOIN tables ON items.table_id = tables.id;
+`
+
+// Query with multiple types (nullable, boolean, real, datetime, json)
+const getEventsWithScore = sql`
+ -- @name: getEventsWithScore
+ -- @db: db_sqlite
+ SELECT * FROM events WHERE score > ?;
+`
diff --git a/demo/tsconfig.json b/demo/tsconfig.json
new file mode 100644
index 00000000..8764e095
--- /dev/null
+++ b/demo/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "outDir": "dist",
+ "module": "CommonJS",
+ "strict": true
+ },
+ "exclude": [
+ "node_modules"
+ ]
+}
diff --git a/node/package-lock.json b/node/package-lock.json
index f0a6ffcf..1be31f36 100644
--- a/node/package-lock.json
+++ b/node/package-lock.json
@@ -26,7 +26,7 @@
"typescript": "^4.6.3"
},
"engines": {
- "node": ">=12"
+ "node": ">=16"
}
},
"node_modules/@ampproject/remapping": {
diff --git a/node/package.json b/node/package.json
index 067e855b..01eace98 100644
--- a/node/package.json
+++ b/node/package.json
@@ -1,8 +1,35 @@
{
"name": "sqlx-ts",
"version": "0.45.0",
- "description": "sqlx-ts ensures your raw SQLs are compile-time checked",
+ "description": "Compile-time checked raw SQL queries with TypeScript type generation for PostgreSQL, MySQL, and SQLite",
"main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "keywords": [
+ "sql",
+ "typescript",
+ "type-safety",
+ "codegen",
+ "postgres",
+ "postgresql",
+ "mysql",
+ "sqlite",
+ "database",
+ "query",
+ "raw-sql",
+ "compile-time",
+ "type-generation",
+ "sql-validation",
+ "tagged-template",
+ "orm-alternative"
+ ],
+ "homepage": "https://jasonshin.github.io/sqlx-ts",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/JasonShin/sqlx-ts"
+ },
+ "bugs": {
+ "url": "https://github.com/JasonShin/sqlx-ts/issues"
+ },
"maintainers": [
"visualbbasic@gmail.com"
],
@@ -33,6 +60,6 @@
"typescript": "^4.6.3"
},
"engines": {
- "node": ">=12"
+ "node": ">=16"
}
}
diff --git a/playpen/db/sqlite_migration.sql b/playpen/db/sqlite_migration.sql
new file mode 100644
index 00000000..827ae379
--- /dev/null
+++ b/playpen/db/sqlite_migration.sql
@@ -0,0 +1,124 @@
+-- SQLite Migration
+-- This migration creates the same tables as the PostgreSQL and MySQL migrations
+-- but using SQLite-compatible syntax.
+
+-- Factions Table
+CREATE TABLE IF NOT EXISTS factions (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL,
+ description TEXT
+);
+
+-- Races Table
+CREATE TABLE IF NOT EXISTS races (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL,
+ faction_id INTEGER REFERENCES factions(id) ON DELETE CASCADE
+);
+
+-- Classes Table
+CREATE TABLE IF NOT EXISTS classes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL,
+ specialization TEXT
+);
+
+-- Characters Table
+CREATE TABLE IF NOT EXISTS characters (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ race_id INTEGER REFERENCES races(id),
+ class_id INTEGER REFERENCES classes(id),
+ level INTEGER DEFAULT 1,
+ experience INTEGER DEFAULT 0,
+ gold REAL DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Guilds Table
+CREATE TABLE IF NOT EXISTS guilds (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL,
+ description TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+-- Guild Members Table
+CREATE TABLE IF NOT EXISTS guild_members (
+ guild_id INTEGER REFERENCES guilds(id) ON DELETE CASCADE,
+ character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE,
+ rank TEXT,
+ joined_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (guild_id, character_id)
+);
+
+-- Inventory Table
+CREATE TABLE IF NOT EXISTS inventory (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE,
+ quantity INTEGER DEFAULT 1
+);
+
+-- Items Table
+CREATE TABLE IF NOT EXISTS items (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ rarity TEXT,
+ flavor_text TEXT,
+ inventory_id INTEGER REFERENCES inventory(id) ON DELETE CASCADE
+);
+
+-- Quests Table
+CREATE TABLE IF NOT EXISTS quests (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ description TEXT,
+ rewards TEXT,
+ completed BOOLEAN DEFAULT 0,
+ required_level INTEGER DEFAULT 1
+);
+
+-- Character Quests Table
+CREATE TABLE IF NOT EXISTS character_quests (
+ character_id INTEGER REFERENCES characters(id) ON DELETE CASCADE,
+ quest_id INTEGER REFERENCES quests(id) ON DELETE CASCADE,
+ status TEXT DEFAULT 'In Progress',
+ PRIMARY KEY (character_id, quest_id)
+);
+
+-- Random types table for testing SQLite type mappings
+CREATE TABLE IF NOT EXISTS random (
+ int1 INTEGER,
+ real1 REAL,
+ text1 TEXT,
+ blob1 BLOB,
+ numeric1 NUMERIC,
+ bool1 BOOLEAN,
+ date1 DATE,
+ datetime1 DATETIME,
+ float1 FLOAT,
+ double1 DOUBLE,
+ varchar1 VARCHAR(100),
+ char1 CHAR(10),
+ json1 JSON
+);
+
+--- SEED DATA
+
+INSERT INTO factions (name, description) VALUES
+('alliance', 'The noble and righteous faction'),
+('horde', 'The fierce and battle-hardened faction');
+
+INSERT INTO races (name, faction_id) VALUES
+('human', 1),
+('night elf', 1),
+('dwarf', 1),
+('gnome', 1),
+('orc', 2),
+('troll', 2),
+('tauren', 2),
+('undead', 2);
+
+INSERT INTO classes (name, specialization) VALUES
+('warrior', '{"role": "tank", "weapon": "sword", "abilities": ["charge", "slam", "shield block"]}'),
+('hunter', '{"role": "ranged", "weapon": "bow", "abilities": ["aimed shot", "multi-shot", "trap"]}');
diff --git a/src/common/config.rs b/src/common/config.rs
index 02adc67f..0bbad139 100644
--- a/src/common/config.rs
+++ b/src/common/config.rs
@@ -234,7 +234,9 @@ impl Config {
.or_else(|| default_config.map(|x| x.db_host.clone()))
};
- let db_host = match (db_url.is_some(), db_host_chain()) {
+ let is_sqlite = matches!(db_type, DatabaseType::Sqlite);
+
+ let db_host = match (db_url.is_some() || is_sqlite, db_host_chain()) {
(true, Some(v)) => v,
(true, None) => String::new(),
(false, Some(v)) => v,
@@ -254,7 +256,7 @@ impl Config {
.or_else(|| default_config.map(|x| x.db_port))
};
- let db_port = match (db_url.is_some(), db_port_chain()) {
+ let db_port = match (db_url.is_some() || is_sqlite, db_port_chain()) {
(true, Some(v)) => v,
(true, None) => 0,
(false, Some(v)) => v,
@@ -275,7 +277,7 @@ impl Config {
.or_else(|| default_config.map(|x| x.db_user.clone()))
};
- let db_user = match (db_url.is_some(), db_user_chain()) {
+ let db_user = match (db_url.is_some() || is_sqlite, db_user_chain()) {
(true, Some(v)) => v,
(true, None) => String::new(),
(false, Some(v)) => v,
@@ -381,6 +383,19 @@ impl Config {
.to_string()
}
+ /// Returns the file path for a SQLite database connection.
+ /// If DB_URL is provided, it's used directly. Otherwise DB_NAME is used as the file path.
+ pub fn get_sqlite_path(&self, conn: &DbConnectionConfig) -> String {
+ if let Some(db_url) = &conn.db_url {
+ return db_url.to_owned();
+ }
+
+ conn
+ .db_name
+ .clone()
+ .unwrap_or_else(|| panic!("DB_NAME (file path) is required for SQLite connections"))
+ }
+
pub fn get_postgres_cred(&self, conn: &DbConnectionConfig) -> String {
// If custom DB_URL is provided, use it directly
if let Some(db_url) = &conn.db_url {
diff --git a/src/common/dotenv.rs b/src/common/dotenv.rs
index 17b65472..effcaea1 100644
--- a/src/common/dotenv.rs
+++ b/src/common/dotenv.rs
@@ -32,13 +32,11 @@ impl Dotenv {
Dotenv {
db_type: match Self::get_var("DB_TYPE") {
None => None,
- Some(val) => {
- if val == "mysql" {
- Some(DatabaseType::Mysql)
- } else {
- Some(DatabaseType::Postgres)
- }
- }
+ Some(val) => match val.as_str() {
+ "mysql" => Some(DatabaseType::Mysql),
+ "sqlite" => Some(DatabaseType::Sqlite),
+ _ => Some(DatabaseType::Postgres),
+ },
},
db_user: Self::get_var("DB_USER"),
db_host: Self::get_var("DB_HOST"),
diff --git a/src/common/lazy.rs b/src/common/lazy.rs
index 75e561e6..3df1faa3 100644
--- a/src/common/lazy.rs
+++ b/src/common/lazy.rs
@@ -4,6 +4,7 @@ use crate::common::types::DatabaseType;
use crate::core::connection::{DBConn, DBConnections};
use crate::core::mysql::pool::MySqlConnectionManager;
use crate::core::postgres::pool::PostgresConnectionManager;
+use crate::core::sqlite::pool::SqliteConnectionManager;
use crate::ts_generator::information_schema::DBSchema;
use clap::Parser;
use std::sync::LazyLock;
@@ -49,6 +50,20 @@ pub static DB_CONN_CACHE: LazyLock>>> = LazyLo
DBConn::MySQLPooledConn(Mutex::new(pool))
})
}),
+ DatabaseType::Sqlite => task::block_in_place(|| {
+ Handle::current().block_on(async {
+ let sqlite_path = CONFIG.get_sqlite_path(connection_config);
+ let manager = SqliteConnectionManager::new(sqlite_path, connection.to_string());
+ let pool = bb8::Pool::builder()
+ .max_size(connection_config.pool_size)
+ .connection_timeout(std::time::Duration::from_secs(connection_config.connection_timeout))
+ .build(manager)
+ .await
+ .expect(&ERR_DB_CONNECTION_ISSUE);
+
+ DBConn::SqliteConn(Mutex::new(pool))
+ })
+ }),
DatabaseType::Postgres => task::block_in_place(|| {
Handle::current().block_on(async {
let postgres_cred = CONFIG.get_postgres_cred(connection_config);
diff --git a/src/common/types.rs b/src/common/types.rs
index 88a536b8..bc7f0268 100644
--- a/src/common/types.rs
+++ b/src/common/types.rs
@@ -18,6 +18,7 @@ pub enum FileExtension {
pub enum DatabaseType {
Postgres,
Mysql,
+ Sqlite,
}
#[derive(ValueEnum, Debug, Clone, Serialize, Deserialize)]
diff --git a/src/core/connection.rs b/src/core/connection.rs
index b9fa51cc..01129926 100644
--- a/src/core/connection.rs
+++ b/src/core/connection.rs
@@ -3,6 +3,7 @@ use crate::common::types::DatabaseType;
use crate::common::SQL;
use crate::core::mysql::prepare as mysql_explain;
use crate::core::postgres::prepare as postgres_explain;
+use crate::core::sqlite::prepare as sqlite_explain;
use crate::ts_generator::types::ts_query::TsQuery;
use bb8::Pool;
use std::collections::HashMap;
@@ -11,14 +12,17 @@ use tokio::sync::Mutex;
use super::mysql::pool::MySqlConnectionManager;
use super::postgres::pool::PostgresConnectionManager;
+use super::sqlite::pool::SqliteConnectionManager;
use crate::common::errors::DB_CONN_FROM_LOCAL_CACHE_ERROR;
use color_eyre::Result;
use swc_common::errors::Handler;
/// Enum to hold a specific database connection instance
+#[allow(clippy::enum_variant_names)]
pub enum DBConn {
MySQLPooledConn(Mutex>),
PostgresConn(Mutex>),
+ SqliteConn(Mutex>),
}
impl DBConn {
@@ -31,6 +35,7 @@ impl DBConn {
let (explain_failed, ts_query) = match &self {
DBConn::MySQLPooledConn(_conn) => mysql_explain::prepare(self, sql, should_generate_types, handler).await?,
DBConn::PostgresConn(_conn) => postgres_explain::prepare(self, sql, should_generate_types, handler).await?,
+ DBConn::SqliteConn(_conn) => sqlite_explain::prepare(self, sql, should_generate_types, handler).await?,
};
Ok((explain_failed, ts_query))
@@ -41,6 +46,7 @@ impl DBConn {
match self {
DBConn::MySQLPooledConn(_) => DatabaseType::Mysql,
DBConn::PostgresConn(_) => DatabaseType::Postgres,
+ DBConn::SqliteConn(_) => DatabaseType::Sqlite,
}
}
}
diff --git a/src/core/mod.rs b/src/core/mod.rs
index bc280965..f1d630d6 100644
--- a/src/core/mod.rs
+++ b/src/core/mod.rs
@@ -2,3 +2,4 @@ pub mod connection;
pub mod execute;
pub mod mysql;
pub mod postgres;
+pub mod sqlite;
diff --git a/src/core/sqlite/mod.rs b/src/core/sqlite/mod.rs
new file mode 100644
index 00000000..42fe7aa8
--- /dev/null
+++ b/src/core/sqlite/mod.rs
@@ -0,0 +1,2 @@
+pub mod pool;
+pub mod prepare;
diff --git a/src/core/sqlite/pool.rs b/src/core/sqlite/pool.rs
new file mode 100644
index 00000000..24b4ab53
--- /dev/null
+++ b/src/core/sqlite/pool.rs
@@ -0,0 +1,82 @@
+use rusqlite::Connection;
+use std::sync::{Arc, Mutex};
+use tokio::task;
+
+/// A connection manager for SQLite that wraps rusqlite's synchronous Connection
+/// behind an Arc> for thread-safe access with bb8 connection pooling.
+#[derive(Clone, Debug)]
+pub struct SqliteConnectionManager {
+ db_path: String,
+ connection_name: String,
+}
+
+/// Wrapper around rusqlite::Connection to make it Send + Sync for bb8
+pub struct SqliteConnection {
+ pub conn: Arc>,
+}
+
+// Safety: rusqlite::Connection is not Send by default, but we protect it with Mutex
+// and only access it via spawn_blocking
+unsafe impl Send for SqliteConnection {}
+unsafe impl Sync for SqliteConnection {}
+
+impl SqliteConnectionManager {
+ pub fn new(db_path: String, connection_name: String) -> Self {
+ Self {
+ db_path,
+ connection_name,
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct SqlitePoolError(pub String);
+
+impl std::fmt::Display for SqlitePoolError {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "SQLite pool error: {}", self.0)
+ }
+}
+
+impl std::error::Error for SqlitePoolError {}
+
+impl bb8::ManageConnection for SqliteConnectionManager {
+ type Connection = SqliteConnection;
+ type Error = SqlitePoolError;
+
+ async fn connect(&self) -> Result {
+ let db_path = self.db_path.clone();
+ let connection_name = self.connection_name.clone();
+
+ let conn = task::spawn_blocking(move || {
+ Connection::open(&db_path).unwrap_or_else(|err| {
+ panic!(
+ "Failed to open SQLite database at '{}' for connection '{}': {}",
+ db_path, connection_name, err
+ )
+ })
+ })
+ .await
+ .map_err(|e| SqlitePoolError(format!("Failed to spawn blocking task: {e}")))?;
+
+ Ok(SqliteConnection {
+ conn: Arc::new(Mutex::new(conn)),
+ })
+ }
+
+ async fn is_valid(&self, conn: &mut Self::Connection) -> Result<(), Self::Error> {
+ let inner = conn.conn.clone();
+ task::spawn_blocking(move || {
+ let conn = inner.lock().unwrap();
+ conn
+ .execute_batch("SELECT 1")
+ .map_err(|e| SqlitePoolError(format!("SQLite connection validation failed: {e}")))
+ })
+ .await
+ .map_err(|e| SqlitePoolError(format!("Failed to spawn blocking task: {e}")))?
+ }
+
+ fn has_broken(&self, _conn: &mut Self::Connection) -> bool {
+ false
+ }
+}
diff --git a/src/core/sqlite/prepare.rs b/src/core/sqlite/prepare.rs
new file mode 100644
index 00000000..90fa4da1
--- /dev/null
+++ b/src/core/sqlite/prepare.rs
@@ -0,0 +1,54 @@
+use crate::common::SQL;
+use crate::core::connection::DBConn;
+use crate::ts_generator::generator::generate_ts_interface;
+use crate::ts_generator::types::ts_query::TsQuery;
+use color_eyre::eyre::Result;
+
+use swc_common::errors::Handler;
+
+/// Runs the prepare statement on the input SQL.
+/// Validates the query is right by directly connecting to the configured SQLite database.
+/// It also processes ts interfaces if the configuration is set to generate_types = true
+pub async fn prepare(
+ db_conn: &DBConn,
+ sql: &SQL,
+ should_generate_types: &bool,
+ handler: &Handler,
+) -> Result<(bool, Option)> {
+ let mut failed = false;
+
+ let conn = match &db_conn {
+ DBConn::SqliteConn(conn) => conn,
+ _ => panic!("Invalid connection type"),
+ };
+
+ {
+ let span = sql.span.to_owned();
+ let query = sql.query.clone();
+ let conn = conn.lock().await;
+ let pool_conn = conn.get().await.unwrap();
+ let inner = pool_conn.conn.clone();
+
+ let result = tokio::task::spawn_blocking(move || {
+ let conn = inner.lock().unwrap();
+ // Use EXPLAIN to validate the SQL without executing it
+ let explain_query = format!("EXPLAIN {}", query);
+ conn.execute_batch(&explain_query)
+ })
+ .await
+ .unwrap();
+
+ if let Err(e) = result {
+ handler.span_bug_no_panic(span, &e.to_string());
+ failed = true;
+ }
+ }
+
+ let mut ts_query = None;
+
+ if should_generate_types == &true {
+ ts_query = Some(generate_ts_interface(sql, db_conn).await?);
+ }
+
+ Ok((failed, ts_query))
+}
diff --git a/src/parser/sql_parser.rs b/src/parser/sql_parser.rs
index f5689693..feb750ab 100644
--- a/src/parser/sql_parser.rs
+++ b/src/parser/sql_parser.rs
@@ -95,25 +95,22 @@ fn split_sql_queries(content: &str) -> Vec {
}
// Handle single-line comments
- '-' if !in_string && !in_comment => {
- if chars.peek() == Some(&'-') {
- in_comment = true;
- current_query.push(ch);
- current_query.push(chars.next().unwrap()); // consume second dash
- } else {
- current_query.push(ch);
- }
+ '-' if !in_string && !in_comment && chars.peek() == Some(&'-') => {
+ in_comment = true;
+ current_query.push(ch);
+ current_query.push(chars.next().unwrap()); // consume second dash
}
// Handle multi-line comments
- '/' if !in_string && !in_comment => {
- if chars.peek() == Some(&'*') {
- in_comment = true;
- current_query.push(ch);
- current_query.push(chars.next().unwrap()); // consume asterisk
- } else {
- current_query.push(ch);
- }
+ '/' if !in_string && !in_comment && chars.peek() == Some(&'*') => {
+ in_comment = true;
+ current_query.push(ch);
+ current_query.push(chars.next().unwrap()); // consume asterisk
+ }
+
+ // Non-comment dash or slash
+ '-' | '/' if !in_string && !in_comment => {
+ current_query.push(ch);
}
'*' if in_comment && !in_string => {
diff --git a/src/ts_generator/generator.rs b/src/ts_generator/generator.rs
index e8265e36..3e0c2bb0 100644
--- a/src/ts_generator/generator.rs
+++ b/src/ts_generator/generator.rs
@@ -17,7 +17,7 @@ use color_eyre::eyre::Result;
use convert_case::{Case, Casing};
use regex::Regex;
use sqlparser::{
- dialect::{Dialect, MySqlDialect, PostgreSqlDialect},
+ dialect::{Dialect, MySqlDialect, PostgreSqlDialect, SQLiteDialect},
parser::Parser,
};
@@ -125,6 +125,7 @@ pub async fn generate_ts_interface(sql: &SQL, db_conn: &DBConn) -> Result = match db_conn.get_db_type() {
DatabaseType::Postgres => Box::new(PostgreSqlDialect {}),
DatabaseType::Mysql => Box::new(MySqlDialect {}),
+ DatabaseType::Sqlite => Box::new(SQLiteDialect {}),
};
let sql_ast = Parser::parse_sql(&*dialect, &sql.query)?;
diff --git a/src/ts_generator/information_schema.rs b/src/ts_generator/information_schema.rs
index 0ae93f07..79fd86ef 100644
--- a/src/ts_generator/information_schema.rs
+++ b/src/ts_generator/information_schema.rs
@@ -3,6 +3,7 @@ use crate::common::logger::*;
use crate::core::connection::DBConn;
use crate::core::mysql::pool::MySqlConnectionManager;
use crate::core::postgres::pool::PostgresConnectionManager;
+use crate::core::sqlite::pool::SqliteConnectionManager;
use bb8::Pool;
use mysql_async::prelude::Queryable;
use std::collections::HashMap;
@@ -54,6 +55,7 @@ impl DBSchema {
let result = match &conn {
DBConn::MySQLPooledConn(conn) => Self::mysql_fetch_table(self, table_name, conn).await,
DBConn::PostgresConn(conn) => Self::postgres_fetch_table(self, &"public".to_string(), table_name, conn).await,
+ DBConn::SqliteConn(conn) => Self::sqlite_fetch_table(self, table_name, conn).await,
};
if let Some(result) = &result {
@@ -210,4 +212,62 @@ impl DBSchema {
None
}
+
+ async fn sqlite_fetch_table(
+ &self,
+ table_names: &Vec<&str>,
+ conn: &Mutex>,
+ ) -> Option {
+ let mut fields: HashMap = HashMap::new();
+ let conn = conn.lock().await;
+ let pool_conn = conn.get().await.expect(DB_CONN_POOL_RETRIEVE_ERROR);
+ let inner = pool_conn.conn.clone();
+
+ let table_names_owned: Vec = table_names.iter().map(|s| s.to_string()).collect();
+
+ let result = tokio::task::spawn_blocking(move || {
+ let conn = inner.lock().unwrap();
+ let mut all_fields: HashMap = HashMap::new();
+
+ for table_name in &table_names_owned {
+ let query = format!("PRAGMA table_info('{}')", table_name);
+ let mut stmt = match conn.prepare(&query) {
+ Ok(stmt) => stmt,
+ Err(_) => continue,
+ };
+
+ let rows = match stmt.query_map([], |row| {
+ let name: String = row.get(1)?;
+ let type_name: String = row.get(2)?;
+ let notnull: bool = row.get(3)?;
+ let pk: i32 = row.get(5)?;
+ Ok((name, type_name, notnull, pk, table_name.clone()))
+ }) {
+ Ok(rows) => rows,
+ Err(_) => continue,
+ };
+
+ for (field_name, field_type, notnull, pk, tbl_name) in rows.flatten() {
+ let field = Field {
+ field_type: TsFieldType::get_ts_field_type_from_sqlite_field_type(field_type, tbl_name, field_name.clone()),
+ is_nullable: !notnull && pk == 0,
+ };
+ all_fields.insert(field_name, field);
+ }
+ }
+
+ all_fields
+ })
+ .await;
+
+ if let Ok(result) = result {
+ if result.is_empty() {
+ return None;
+ }
+ fields.extend(result);
+ return Some(fields);
+ }
+
+ None
+ }
}
diff --git a/src/ts_generator/types/ts_query.rs b/src/ts_generator/types/ts_query.rs
index 65e2a756..3c78ec8f 100644
--- a/src/ts_generator/types/ts_query.rs
+++ b/src/ts_generator/types/ts_query.rs
@@ -242,6 +242,44 @@ impl TsFieldType {
}
}
+ /// Converts SQLite type affinity strings to TsFieldType.
+ /// SQLite uses type affinity rules, so we match common type names.
+ pub fn get_ts_field_type_from_sqlite_field_type(field_type: String, table_name: String, field_name: String) -> Self {
+ let upper = field_type.to_uppercase();
+ // SQLite type affinity rules (see https://www.sqlite.org/datatype3.html)
+ if upper.contains("INT") {
+ return Self::Number;
+ }
+ if upper.contains("CHAR") || upper.contains("CLOB") || upper.contains("TEXT") {
+ return Self::String;
+ }
+ if upper.contains("BLOB") || upper.is_empty() {
+ // Empty type name in SQLite means BLOB affinity
+ return Self::String;
+ }
+ if upper.contains("REAL") || upper.contains("FLOA") || upper.contains("DOUB") {
+ return Self::Number;
+ }
+ if upper.contains("BOOL") {
+ return Self::Boolean;
+ }
+ if upper.contains("DATE") || upper.contains("TIME") {
+ return Self::Date;
+ }
+ if upper.contains("NUMERIC") || upper.contains("DECIMAL") {
+ return Self::Number;
+ }
+ if upper.contains("JSON") {
+ return Self::Object;
+ }
+ // Default: SQLite NUMERIC affinity
+ let message = format!(
+ "The column {field_name} of type {field_type} in table {table_name} will be translated as any (unsupported SQLite type)"
+ );
+ info!(message);
+ Self::Any
+ }
+
pub fn get_ts_field_from_annotation(annotated_type: &str) -> Self {
if annotated_type == "string" {
return Self::String;
diff --git a/test-utils/src/sandbox.rs b/test-utils/src/sandbox.rs
index 859e19fa..85f42cc4 100644
--- a/test-utils/src/sandbox.rs
+++ b/test-utils/src/sandbox.rs
@@ -46,6 +46,21 @@ impl TestConfig {
config_file_name,
}
}
+ if db_type == "sqlite" {
+ return TestConfig {
+ db_type: "sqlite".into(),
+ file_extension: "ts".to_string(),
+ db_host: String::new(),
+ db_port: 0,
+ db_user: String::new(),
+ db_pass: None,
+ // db_name will be overridden per-test with the actual temp SQLite file path
+ db_name: ":memory:".to_string(),
+ generate_path,
+ generate_types,
+ config_file_name,
+ }
+ }
TestConfig {
db_type: "postgres".into(),
file_extension: "ts".to_string(),
@@ -148,6 +163,7 @@ $(
let db_name = test_config.db_name;
let config_file_name = test_config.config_file_name;
let generate_path = test_config.generate_path;
+ let is_sqlite = db_type == "sqlite";
// SETUP
let dir = tempdir()?;
@@ -164,11 +180,14 @@ $(
cmd.arg(parent_path.to_str().unwrap())
.arg(format!("--ext={file_extension}"))
.arg(format!("--db-type={db_type}"))
- .arg(format!("--db-host={db_host}"))
- .arg(format!("--db-port={db_port}"))
- .arg(format!("--db-user={db_user}"))
.arg(format!("--db-name={db_name}"));
+ if !is_sqlite {
+ cmd.arg(format!("--db-host={db_host}"))
+ .arg(format!("--db-port={db_port}"))
+ .arg(format!("--db-user={db_user}"));
+ }
+
if &generate_path.is_some() == &true {
let generate_path = generate_path.clone();
let generate_path = generate_path.unwrap();
@@ -190,11 +209,13 @@ $(
cmd.arg(format!("--config={config_path}"));
}
- if (db_pass.is_some()) {
- let db_pass = db_pass.unwrap();
- cmd.arg(format!("--db-pass={db_pass}"));
- } else {
- cmd.arg("--db-pass=");
+ if !is_sqlite {
+ if (db_pass.is_some()) {
+ let db_pass = db_pass.unwrap();
+ cmd.arg(format!("--db-pass={db_pass}"));
+ } else {
+ cmd.arg("--db-pass=");
+ }
}
cmd.assert()
@@ -248,6 +269,7 @@ $(
let db_name = test_config.db_name;
let config_file_name = test_config.config_file_name;
let generate_path = test_config.generate_path;
+ let is_sqlite = db_type == "sqlite";
// SETUP
let dir = tempdir()?;
@@ -264,11 +286,14 @@ $(
cmd.arg(parent_path.to_str().unwrap())
.arg(format!("--ext={file_extension}"))
.arg(format!("--db-type={db_type}"))
- .arg(format!("--db-host={db_host}"))
- .arg(format!("--db-port={db_port}"))
- .arg(format!("--db-user={db_user}"))
.arg(format!("--db-name={db_name}"));
+ if !is_sqlite {
+ cmd.arg(format!("--db-host={db_host}"))
+ .arg(format!("--db-port={db_port}"))
+ .arg(format!("--db-user={db_user}"));
+ }
+
if &generate_path.is_some() == &true {
let generate_path = generate_path.clone();
let generate_path = generate_path.unwrap();
@@ -290,11 +315,13 @@ $(
cmd.arg(format!("--config={config_path}"));
}
- if (db_pass.is_some()) {
- let db_pass = db_pass.unwrap();
- cmd.arg(format!("--db-pass={db_pass}"));
- } else {
- cmd.arg("--db-pass=");
+ if !is_sqlite {
+ if (db_pass.is_some()) {
+ let db_pass = db_pass.unwrap();
+ cmd.arg(format!("--db-pass={db_pass}"));
+ } else {
+ cmd.arg("--db-pass=");
+ }
}
cmd.assert()
diff --git a/tests/demo_happy_path.rs b/tests/demo_happy_path.rs
index 9ce9c394..75bb631b 100644
--- a/tests/demo_happy_path.rs
+++ b/tests/demo_happy_path.rs
@@ -7,6 +7,7 @@ mod demo_happy_path_tests {
use std::fs;
use std::io::Write;
use std::path::Path;
+ use tempfile::tempdir;
use walkdir::WalkDir;
fn run_demo_test(demo_path: &Path) -> Result<(), Box> {
@@ -283,4 +284,83 @@ mod demo_happy_path_tests {
Ok(())
}
+
+ #[test]
+ fn all_demo_sqlite_should_pass() -> Result<(), Box> {
+ let root_path = current_dir().unwrap();
+ let demo_path = root_path.join("tests/demo_sqlite");
+ let migration_path = root_path.join("playpen/db/sqlite_migration.sql");
+
+ // Create a temporary SQLite database and run the migration
+ let tmp_dir = tempdir()?;
+ let db_path = tmp_dir.path().join("demo_test.db");
+ let conn = rusqlite::Connection::open(&db_path)?;
+ let migration_sql = fs::read_to_string(&migration_path)?;
+ conn.execute_batch(&migration_sql)?;
+ drop(conn);
+
+ // Create a temporary config file pointing to the SQLite database
+ let config_path = tmp_dir.path().join(".sqlxrc.json");
+ let config_content = format!(
+ r#"{{
+ "generateTypes": {{
+ "enabled": true
+ }},
+ "connections": {{
+ "default": {{
+ "DB_TYPE": "sqlite",
+ "DB_NAME": "{}"
+ }}
+ }}
+}}"#,
+ db_path.display()
+ );
+ fs::write(&config_path, &config_content)?;
+
+ // Run sqlx-ts against the demo_sqlite directory
+ // Use --db-type and --db-name CLI args to override any .env file values
+ let mut cmd = cargo_bin_cmd!("sqlx-ts");
+ cmd
+ .arg(demo_path.to_str().unwrap())
+ .arg("--ext=ts")
+ .arg(format!("--config={}", config_path.display()))
+ .arg("--db-type=sqlite")
+ .arg(format!("--db-name={}", db_path.display()))
+ .arg("-g");
+
+ cmd
+ .assert()
+ .success()
+ .stdout(predicates::str::contains("No SQL errors detected!"));
+
+ // Verify all generated types match snapshots
+ for entry in WalkDir::new(&demo_path) {
+ if entry.is_ok() {
+ let entry = entry.unwrap();
+ let path = entry.path();
+ let parent = entry.path().parent().unwrap();
+ let file_name = path.file_name().unwrap().to_str().unwrap().to_string();
+
+ if path.is_file() && file_name.ends_with(".queries.ts") {
+ let base_file_name = file_name.split('.').collect::>();
+ let base_file_name = base_file_name.first().unwrap();
+ let snapshot_path = parent.join(format!("{base_file_name}.snapshot.ts"));
+
+ let generated_types = fs::read_to_string(path)?;
+
+ if !snapshot_path.exists() {
+ let mut snapshot_file = fs::File::create(&snapshot_path)?;
+ writeln!(snapshot_file, "{generated_types}")?;
+ }
+
+ assert_eq!(
+ generated_types.trim().to_string().trim(),
+ fs::read_to_string(&snapshot_path)?.to_string().trim(),
+ )
+ }
+ }
+ }
+
+ Ok(())
+ }
}
diff --git a/tests/demo_sqlite/delete_basic.queries.ts b/tests/demo_sqlite/delete_basic.queries.ts
new file mode 100644
index 00000000..de5a2dd7
--- /dev/null
+++ b/tests/demo_sqlite/delete_basic.queries.ts
@@ -0,0 +1,21 @@
+export type DeleteItemParams = [number];
+
+export interface IDeleteItemResult {
+
+}
+
+export interface IDeleteItemQuery {
+ params: DeleteItemParams;
+ result: IDeleteItemResult;
+}
+
+export type DeleteCharacterParams = [number];
+
+export interface IDeleteCharacterResult {
+
+}
+
+export interface IDeleteCharacterQuery {
+ params: DeleteCharacterParams;
+ result: IDeleteCharacterResult;
+}
diff --git a/tests/demo_sqlite/delete_basic.snapshot.ts b/tests/demo_sqlite/delete_basic.snapshot.ts
new file mode 100644
index 00000000..c37449f4
--- /dev/null
+++ b/tests/demo_sqlite/delete_basic.snapshot.ts
@@ -0,0 +1,22 @@
+export type DeleteItemParams = [number];
+
+export interface IDeleteItemResult {
+
+}
+
+export interface IDeleteItemQuery {
+ params: DeleteItemParams;
+ result: IDeleteItemResult;
+}
+
+export type DeleteCharacterParams = [number];
+
+export interface IDeleteCharacterResult {
+
+}
+
+export interface IDeleteCharacterQuery {
+ params: DeleteCharacterParams;
+ result: IDeleteCharacterResult;
+}
+
diff --git a/tests/demo_sqlite/delete_basic.ts b/tests/demo_sqlite/delete_basic.ts
new file mode 100644
index 00000000..f86c04ee
--- /dev/null
+++ b/tests/demo_sqlite/delete_basic.ts
@@ -0,0 +1,11 @@
+import { sql } from 'sqlx-ts'
+
+const deleteItem = sql`
+-- @name: delete item
+DELETE FROM items WHERE id = $1
+`
+
+const deleteCharacter = sql`
+-- @name: delete character
+DELETE FROM characters WHERE id = $1
+`
diff --git a/tests/demo_sqlite/insert_basic.queries.ts b/tests/demo_sqlite/insert_basic.queries.ts
new file mode 100644
index 00000000..ce6c9ba0
--- /dev/null
+++ b/tests/demo_sqlite/insert_basic.queries.ts
@@ -0,0 +1,21 @@
+export type InsertItemParams = [string, string | null, string | null, number | null];
+
+export interface IInsertItemResult {
+
+}
+
+export interface IInsertItemQuery {
+ params: InsertItemParams;
+ result: IInsertItemResult;
+}
+
+export type InsertCharacterParams = [string, number | null, number | null, number | null];
+
+export interface IInsertCharacterResult {
+
+}
+
+export interface IInsertCharacterQuery {
+ params: InsertCharacterParams;
+ result: IInsertCharacterResult;
+}
diff --git a/tests/demo_sqlite/insert_basic.snapshot.ts b/tests/demo_sqlite/insert_basic.snapshot.ts
new file mode 100644
index 00000000..8d833e7f
--- /dev/null
+++ b/tests/demo_sqlite/insert_basic.snapshot.ts
@@ -0,0 +1,22 @@
+export type InsertItemParams = [string, string | null, string | null, number | null];
+
+export interface IInsertItemResult {
+
+}
+
+export interface IInsertItemQuery {
+ params: InsertItemParams;
+ result: IInsertItemResult;
+}
+
+export type InsertCharacterParams = [string, number | null, number | null, number | null];
+
+export interface IInsertCharacterResult {
+
+}
+
+export interface IInsertCharacterQuery {
+ params: InsertCharacterParams;
+ result: IInsertCharacterResult;
+}
+
diff --git a/tests/demo_sqlite/insert_basic.ts b/tests/demo_sqlite/insert_basic.ts
new file mode 100644
index 00000000..1d17e91c
--- /dev/null
+++ b/tests/demo_sqlite/insert_basic.ts
@@ -0,0 +1,11 @@
+import { sql } from 'sqlx-ts'
+
+const insertItem = sql`
+-- @name: insert item
+INSERT INTO items (name, rarity, flavor_text, inventory_id) VALUES ($1, $2, $3, $4)
+`
+
+const insertCharacter = sql`
+-- @name: insert character
+INSERT INTO characters (name, race_id, class_id, level) VALUES ($1, $2, $3, $4)
+`
diff --git a/tests/demo_sqlite/join_basic.queries.ts b/tests/demo_sqlite/join_basic.queries.ts
new file mode 100644
index 00000000..dcc4473b
--- /dev/null
+++ b/tests/demo_sqlite/join_basic.queries.ts
@@ -0,0 +1,13 @@
+export type SelectItemsWithInventoryParams = [number | null];
+
+export interface ISelectItemsWithInventoryResult {
+ inventory_quantity: number | null;
+ items_id: number;
+ items_name: string;
+ items_rarity: string | null;
+}
+
+export interface ISelectItemsWithInventoryQuery {
+ params: SelectItemsWithInventoryParams;
+ result: ISelectItemsWithInventoryResult;
+}
diff --git a/tests/demo_sqlite/join_basic.snapshot.ts b/tests/demo_sqlite/join_basic.snapshot.ts
new file mode 100644
index 00000000..735baf23
--- /dev/null
+++ b/tests/demo_sqlite/join_basic.snapshot.ts
@@ -0,0 +1,14 @@
+export type SelectItemsWithInventoryParams = [number | null];
+
+export interface ISelectItemsWithInventoryResult {
+ inventory_quantity: number | null;
+ items_id: number;
+ items_name: string;
+ items_rarity: string | null;
+}
+
+export interface ISelectItemsWithInventoryQuery {
+ params: SelectItemsWithInventoryParams;
+ result: ISelectItemsWithInventoryResult;
+}
+
diff --git a/tests/demo_sqlite/join_basic.ts b/tests/demo_sqlite/join_basic.ts
new file mode 100644
index 00000000..55e8cb4d
--- /dev/null
+++ b/tests/demo_sqlite/join_basic.ts
@@ -0,0 +1,9 @@
+import { sql } from 'sqlx-ts'
+
+const selectItemsWithInventory = sql`
+-- @name: select items with inventory
+SELECT items.id, items.name, items.rarity, inventory.quantity
+FROM items
+JOIN inventory ON items.inventory_id = inventory.id
+WHERE inventory.quantity > $1
+`
diff --git a/tests/demo_sqlite/select_basic.queries.ts b/tests/demo_sqlite/select_basic.queries.ts
new file mode 100644
index 00000000..e8bb2342
--- /dev/null
+++ b/tests/demo_sqlite/select_basic.queries.ts
@@ -0,0 +1,41 @@
+export type SelectAllItemsParams = [];
+
+export interface ISelectAllItemsResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISelectAllItemsQuery {
+ params: SelectAllItemsParams;
+ result: ISelectAllItemsResult;
+}
+
+export type SelectItemByIdParams = [number];
+
+export interface ISelectItemByIdResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISelectItemByIdQuery {
+ params: SelectItemByIdParams;
+ result: ISelectItemByIdResult;
+}
+
+export type SelectItemsByNameParams = [string];
+
+export interface ISelectItemsByNameResult {
+ id: number;
+ name: string;
+}
+
+export interface ISelectItemsByNameQuery {
+ params: SelectItemsByNameParams;
+ result: ISelectItemsByNameResult;
+}
diff --git a/tests/demo_sqlite/select_basic.snapshot.ts b/tests/demo_sqlite/select_basic.snapshot.ts
new file mode 100644
index 00000000..ce5421f5
--- /dev/null
+++ b/tests/demo_sqlite/select_basic.snapshot.ts
@@ -0,0 +1,42 @@
+export type SelectAllItemsParams = [];
+
+export interface ISelectAllItemsResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISelectAllItemsQuery {
+ params: SelectAllItemsParams;
+ result: ISelectAllItemsResult;
+}
+
+export type SelectItemByIdParams = [number];
+
+export interface ISelectItemByIdResult {
+ flavor_text: string | null;
+ id: number;
+ inventory_id: number | null;
+ name: string;
+ rarity: string | null;
+}
+
+export interface ISelectItemByIdQuery {
+ params: SelectItemByIdParams;
+ result: ISelectItemByIdResult;
+}
+
+export type SelectItemsByNameParams = [string];
+
+export interface ISelectItemsByNameResult {
+ id: number;
+ name: string;
+}
+
+export interface ISelectItemsByNameQuery {
+ params: SelectItemsByNameParams;
+ result: ISelectItemsByNameResult;
+}
+
diff --git a/tests/demo_sqlite/select_basic.ts b/tests/demo_sqlite/select_basic.ts
new file mode 100644
index 00000000..350cd7fc
--- /dev/null
+++ b/tests/demo_sqlite/select_basic.ts
@@ -0,0 +1,16 @@
+import { sql } from 'sqlx-ts'
+
+const selectAllItems = sql`
+-- @name: select all items
+SELECT * FROM items
+`
+
+const selectItemById = sql`
+-- @name: select item by id
+SELECT * FROM items WHERE id = $1
+`
+
+const selectItemsByName = sql`
+-- @name: select items by name
+SELECT id, name FROM items WHERE name = $1
+`
diff --git a/tests/demo_sqlite/update_basic.queries.ts b/tests/demo_sqlite/update_basic.queries.ts
new file mode 100644
index 00000000..094ff6b8
--- /dev/null
+++ b/tests/demo_sqlite/update_basic.queries.ts
@@ -0,0 +1,21 @@
+export type UpdateItemNameParams = [string, number];
+
+export interface IUpdateItemNameResult {
+
+}
+
+export interface IUpdateItemNameQuery {
+ params: UpdateItemNameParams;
+ result: IUpdateItemNameResult;
+}
+
+export type UpdateCharacterLevelParams = [number | null, number | null, number];
+
+export interface IUpdateCharacterLevelResult {
+
+}
+
+export interface IUpdateCharacterLevelQuery {
+ params: UpdateCharacterLevelParams;
+ result: IUpdateCharacterLevelResult;
+}
diff --git a/tests/demo_sqlite/update_basic.snapshot.ts b/tests/demo_sqlite/update_basic.snapshot.ts
new file mode 100644
index 00000000..5072cf08
--- /dev/null
+++ b/tests/demo_sqlite/update_basic.snapshot.ts
@@ -0,0 +1,22 @@
+export type UpdateItemNameParams = [string, number];
+
+export interface IUpdateItemNameResult {
+
+}
+
+export interface IUpdateItemNameQuery {
+ params: UpdateItemNameParams;
+ result: IUpdateItemNameResult;
+}
+
+export type UpdateCharacterLevelParams = [number | null, number | null, number];
+
+export interface IUpdateCharacterLevelResult {
+
+}
+
+export interface IUpdateCharacterLevelQuery {
+ params: UpdateCharacterLevelParams;
+ result: IUpdateCharacterLevelResult;
+}
+
diff --git a/tests/demo_sqlite/update_basic.ts b/tests/demo_sqlite/update_basic.ts
new file mode 100644
index 00000000..8e7d3493
--- /dev/null
+++ b/tests/demo_sqlite/update_basic.ts
@@ -0,0 +1,11 @@
+import { sql } from 'sqlx-ts'
+
+const updateItemName = sql`
+-- @name: update item name
+UPDATE items SET name = $1 WHERE id = $2
+`
+
+const updateCharacterLevel = sql`
+-- @name: update character level
+UPDATE characters SET level = $1, experience = $2 WHERE id = $3
+`
diff --git a/tests/sqlite_query_parameters.rs b/tests/sqlite_query_parameters.rs
new file mode 100644
index 00000000..b21c5abd
--- /dev/null
+++ b/tests/sqlite_query_parameters.rs
@@ -0,0 +1,255 @@
+#[cfg(test)]
+mod sqlite_query_parameters_tests {
+ use std::env;
+ use std::fs;
+ use std::io::Write;
+ use tempfile::tempdir;
+
+ use assert_cmd::cargo::cargo_bin_cmd;
+ use pretty_assertions::assert_eq;
+ use test_utils::test_utils::TSString;
+
+ /// Helper: creates a temporary SQLite database with the given schema,
+ /// then runs sqlx-ts on the given TS content, and returns the generated types.
+ fn run_sqlite_test(
+ schema_sql: &str,
+ ts_content: &str,
+ generate_types: bool,
+ ) -> Result<(String, String), Box> {
+ let dir = tempdir()?;
+ let parent_path = dir.path();
+
+ // Create the SQLite database and populate it with the schema
+ let db_path = parent_path.join("test.db");
+ let conn = rusqlite::Connection::open(&db_path)?;
+ conn.execute_batch(schema_sql)?;
+ drop(conn);
+
+ // Write the TS file
+ let file_path = parent_path.join("index.ts");
+ let mut temp_file = fs::File::create(&file_path)?;
+ writeln!(temp_file, "{}", ts_content)?;
+
+ // Run sqlx-ts
+ let mut cmd = cargo_bin_cmd!("sqlx-ts");
+ cmd
+ .arg(parent_path.to_str().unwrap())
+ .arg("--ext=ts")
+ .arg("--db-type=sqlite")
+ .arg(format!("--db-name={}", db_path.display()));
+
+ if generate_types {
+ cmd.arg("-g");
+ }
+
+ let output = cmd.output()?;
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
+ let stderr = String::from_utf8_lossy(&output.stderr).to_string();
+
+ assert!(
+ output.status.success(),
+ "sqlx-ts failed!\nstdout: {stdout}\nstderr: {stderr}"
+ );
+ assert!(
+ stdout.contains("No SQL errors detected!"),
+ "Expected success message in stdout: {stdout}"
+ );
+
+ // Read generated types
+ let type_file_path = parent_path.join("index.queries.ts");
+ let type_file = if type_file_path.exists() {
+ fs::read_to_string(type_file_path)?
+ } else {
+ String::new()
+ };
+
+ Ok((stdout, type_file))
+ }
+
+ #[test]
+ fn should_validate_simple_select() -> Result<(), Box> {
+ let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);";
+
+ let ts_content = r#"
+import { sql } from 'sqlx-ts'
+
+const someQuery = sql`SELECT * FROM items`
+"#;
+
+ let (_, type_file) = run_sqlite_test(schema, ts_content, true)?;
+
+ let expected = r#"
+export type SomeQueryParams = [];
+
+export interface ISomeQueryResult {
+ id: number;
+ name: string;
+ price: number | null;
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+"#;
+
+ assert_eq!(
+ expected.trim().to_string().flatten(),
+ type_file.trim().to_string().flatten()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn should_handle_query_params_with_question_mark() -> Result<(), Box> {
+ let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);";
+
+ let ts_content = r#"
+import { sql } from 'sqlx-ts'
+
+const someQuery = sql`SELECT * FROM items WHERE id = ? AND name = ?`
+"#;
+
+ let (_, type_file) = run_sqlite_test(schema, ts_content, true)?;
+
+ let expected = r#"
+export type SomeQueryParams = [number, string];
+
+export interface ISomeQueryResult {
+ id: number;
+ name: string;
+ price: number | null;
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+"#;
+
+ assert_eq!(
+ expected.trim().to_string().flatten(),
+ type_file.trim().to_string().flatten()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn should_handle_insert_with_params() -> Result<(), Box> {
+ let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, price REAL);";
+
+ let ts_content = r#"
+import { sql } from 'sqlx-ts'
+
+const someQuery = sql`INSERT INTO items (name, price) VALUES (?, ?)`
+"#;
+
+ let (_, type_file) = run_sqlite_test(schema, ts_content, true)?;
+
+ let expected = r#"
+export type SomeQueryParams = [[string, number | null]];
+
+export interface ISomeQueryResult {
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+"#;
+
+ assert_eq!(
+ expected.trim().to_string().flatten(),
+ type_file.trim().to_string().flatten()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn should_handle_multiple_types() -> Result<(), Box> {
+ let schema = r"
+ CREATE TABLE events (
+ id INTEGER PRIMARY KEY NOT NULL,
+ name TEXT NOT NULL,
+ description TEXT,
+ start_date DATETIME,
+ is_active BOOLEAN NOT NULL DEFAULT 1,
+ score REAL,
+ metadata JSON
+ );
+ ";
+
+ let ts_content = r#"
+import { sql } from 'sqlx-ts'
+
+const someQuery = sql`SELECT * FROM events WHERE id = ?`
+"#;
+
+ let (_, type_file) = run_sqlite_test(schema, ts_content, true)?;
+
+ let expected = r#"
+export type SomeQueryParams = [number];
+
+export interface ISomeQueryResult {
+ description: string | null;
+ id: number;
+ is_active: boolean;
+ metadata: object | null;
+ name: string;
+ score: number | null;
+ start_date: Date | null;
+}
+
+export interface ISomeQueryQuery {
+ params: SomeQueryParams;
+ result: ISomeQueryResult;
+}
+"#;
+
+ assert_eq!(
+ expected.trim().to_string().flatten(),
+ type_file.trim().to_string().flatten()
+ );
+ Ok(())
+ }
+
+ #[test]
+ fn should_detect_invalid_sql() -> Result<(), Box> {
+ let schema = "CREATE TABLE items (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL);";
+
+ let ts_content = r#"
+import { sql } from 'sqlx-ts'
+
+const someQuery = sql`SELECT * FROM nonexistent_table`
+"#;
+
+ let dir = tempdir()?;
+ let parent_path = dir.path();
+
+ let db_path = parent_path.join("test.db");
+ let conn = rusqlite::Connection::open(&db_path)?;
+ conn.execute_batch(schema)?;
+ drop(conn);
+
+ let file_path = parent_path.join("index.ts");
+ let mut temp_file = fs::File::create(&file_path)?;
+ writeln!(temp_file, "{}", ts_content)?;
+
+ let mut cmd = cargo_bin_cmd!("sqlx-ts");
+ cmd
+ .arg(parent_path.to_str().unwrap())
+ .arg("--ext=ts")
+ .arg("--db-type=sqlite")
+ .arg(format!("--db-name={}", db_path.display()));
+
+ // This should fail because the table doesn't exist
+ let output = cmd.output()?;
+ let stdout = String::from_utf8_lossy(&output.stdout).to_string();
+ // The command should report SQL errors
+ assert!(
+ !stdout.contains("No SQL errors detected!"),
+ "Expected SQL errors but got success: {stdout}"
+ );
+ Ok(())
+ }
+}