Drop-in replacement for @prisma/client that runs natively — no Node.js, no Prisma engine, no network overhead. Built for Perry apps targeting macOS, iOS, Linux, Android, and Windows.
Uses sqlx with SQLite under the hood. Same API as perry-prisma (MySQL) and perry-postgres, ~95% identical Rust code.
TypeScript → JSON string → FFI → Rust (sqlx + SQLite) → JSON result → TypeScript
import{PrismaClient}from'perry-sqlite';constprisma=newPrismaClient();awaitprisma.$connect();constuser=awaitprisma.user.create({data: {id: 'u1',email: 'alice@example.com',passwordHash: 'hash'},});constusers=awaitprisma.user.findMany({where: {email: {contains: '@example.com'}},orderBy: {email: 'asc'},take: 10,});awaitprisma.$disconnect();Set DATABASE_URL to a SQLite URL before connecting:
export DATABASE_URL=sqlite:/path/to/app.db| Operation | Notes |
|---|---|
findMany | where, orderBy, take, skip, select, distinct |
findFirst | Returns null if not found |
findUnique | Returns null if not found |
findUniqueOrThrow | Throws if not found |
create | Returns created record |
createMany | skipDuplicates via INSERT OR IGNORE |
update | Returns updated record |
updateMany | Returns { count } |
upsert | Create-or-update |
delete | Returns deleted record |
deleteMany | Returns { count } |
count | Supports where filters |
$transaction | Serializable via dedicated single-connection pool |
$executeRaw | Returns affected row count |
$queryRaw | Returns array of row objects |
// Equality{where: {email: 'alice@example.com'}}// String operators{where: {email: {contains: 'example',startsWith: 'alice',endsWith: '.com'}}}// Numeric operators{where: {chainId: {gt: 1,lte: 100}}}// Null checks{where: {lastActiveAt: {isNull: true}}}{where: {lastActiveAt: {isNotNull: true}}}// Set membership{where: {id: {in: ['u1','u2']}}}{where: {id: {notIn: ['u3']}}}// Logical combinators{where: {AND: [{email: 'a@b.com'},{passwordHash: 'x'}]}}{where: {OR: [{email: 'a@b.com'},{email: 'b@b.com'}]}}{where: {NOT: {email: 'alice@example.com'}}}build.rs reads your Prisma schema at compile time to generate model metadata. Point it at your schema:
# Option 1: env varexport PRISMA_SCHEMA_PATH=/path/to/prisma/schema.prisma
# Option 2: default location (relative to native/)# ../../chainblick/api/prisma/schema.prismaOnly scalar fields are mapped to columns. Relation fields are tracked as metadata but not queried.
# Type-check the Rust cratecd native && cargo check
# Cross-compile for iOScd native && cargo check --target aarch64-apple-ios
# Perry type-check (from package root)
perry checkcd tests
./run.sh
# or: DATABASE_URL=sqlite:/tmp/test.db ./run.shTests cover all 26 operation categories: CRUD, filters, orderBy, pagination, upsert, transactions, raw SQL, and more (45 assertions total).
perry-sqlite/
├── src/index.ts # PrismaClient, ModelClient, FFI declarations
├── native/
│ ├── src/lib.rs # Query builder, row→JSON conversion, FFI exports
│ ├── build.rs # Prisma schema parser → generated_models.rs
│ └── Cargo.toml
├── tests/
│ ├── test.ts # Integration test suite
│ └── run.sh
└── perry.config.ts # Perry compiler config (targets, FFI)
Query flow:
- TypeScript serializes query args to JSON
- JSON string crosses the FFI boundary to Rust
- Rust parses the query, builds SQL with bound parameters, executes via sqlx
- Result rows are serialized back to JSON
- TypeScript receives the JSON string and parses it
Transactions use a dedicated max_connections(1)SqlitePool per transaction, ensuring BEGIN, all queries, and COMMIT/ROLLBACK run on the same physical connection.
| Behavior | SQLite | MySQL (perry-prisma) |
|---|---|---|
| Identifier quoting | "col" | `col` |
| Skip duplicates | INSERT OR IGNORE | INSERT IGNORE |
| Unlimited offset | LIMIT -1 OFFSET n | LIMIT 18446744073709551615 OFFSET n |
| Last insert ID | last_insert_rowid() | last_insert_id() |
| Boolean binding | native bool | i8 |
| TLS deps | none | Security.framework / OpenSSL |
MIT