Skip to content

Latest commit

 

History

History
89 lines (65 loc) · 4.16 KB

File metadata and controls

89 lines (65 loc) · 4.16 KB

perry-postgres

Perry native package: Prisma ORM backed by sqlx + PostgreSQL.

Architecture

Drop-in replacement for @prisma/client using sqlx with PostgreSQL. Sibling of perry-prisma (MySQL) and perry-sqlite — same architecture, ~95% identical code.

TypeScript → JSON string → FFI → Rust dispatcher → sqlx (PostgreSQL) → JSON result → TypeScript

Key files

  • native/src/lib.rs — All Rust logic: query builder, row-to-JSON conversion, FFI exports
  • native/build.rs — Reads prisma/schema.prisma at build time, generates generated_models.rs with model metadata
  • native/Cargo.toml — Crate config; sqlx with postgres feature
  • src/index.ts — TypeScript layer: PrismaClient, ModelClient, type definitions, FFI declarations
  • perry.config.ts — Perry compiler configuration
  • package.json — Package metadata, FFI function declarations, platform lib requirements

Schema path resolution (build.rs)

  1. PRISMA_SCHEMA_PATH env var
  2. ../../../chainblick/api/prisma/schema.prisma (relative to native/)
  3. Falls back to empty model list with compile warning

PostgreSQL-specific differences (vs perry-prisma / MySQL)

  • Pool type: PgPool (not MySqlPool)
  • Identifier quoting: "col" double quotes (not `col` backticks)
  • Placeholders: $1, $2, $3... via ParamCounter (not ?) — this is the main complexity difference
  • ParamCounter struct: Threaded through build_where and all query-building functions as &mut ParamCounter; call pc.next() to get the next $N placeholder
  • INSERT returning: Uses RETURNING * to get the inserted row directly (no separate SELECT after last_insert_id())
  • Transactions: BEGIN / COMMIT / ROLLBACK (not SET autocommit=0; START TRANSACTION)
  • Boolean binding: q.bind(*b) native bool (not q.bind(*b as i8))
  • Skip duplicates: ON CONFLICT DO NOTHING (not INSERT IGNORE)
  • Unlimited offset: OFFSET n without LIMIT (not LIMIT 18446744073709551615 OFFSET n)
  • TLS libs: Same as perry-prisma — needs Security/SystemConfiguration on iOS, full crypto libs on Windows

FFI functions

All exported as postgres_*:

Function Signature
postgres_connect (config_json: StringHeader*) -> Promise*
postgres_disconnect (handle: i64) -> Promise*
postgres_execute (handle: i64, query_json: StringHeader*) -> Promise*
postgres_transaction_begin (handle: i64) -> Promise*
postgres_transaction_end (tx_handle: i64, options_json: StringHeader*) -> Promise*

Build & verify

# Host check
cd native && cargo check

# iOS cross-compile check
cd native && cargo check --target aarch64-apple-ios

# Perry check (from package root)
perry check

ParamCounter pattern

The ParamCounter is the key difference from the MySQL and SQLite siblings. Every function that generates SQL with bound parameters must accept &mut ParamCounter:

struct ParamCounter { n: usize }
impl ParamCounter {
    fn new() -> Self { Self { n: 0 } }
    fn next(&mut self) -> String { self.n += 1; format!("${}", self.n) }
}

Functions that use it:

  • build_where(where_val, model_def, params, pc) — 4th arg added vs MySQL/SQLite
  • find_many, delete_record, delete_many, update_record, update_many, count_records — all create a local ParamCounter::new()
  • create_record, create_many — also use ParamCounter for INSERT placeholders

When adding new query operations, always create a fresh ParamCounter::new() at the top of the function and pass &mut pc into build_where and any placeholder generation.

Common tasks

  • Adding a new query operation: Add handler in execute_query match, implement async fn taking &PgPool. Create ParamCounter::new() and thread &mut pc through all SQL generation.
  • Changing SQL dialect: All SQL generation is in build_where, build_order_by, build_select_cols, and the operation functions — grep for format! with SQL strings
  • Syncing with perry-prisma: Diff native/src/lib.rs against perry-prisma's version; apply non-dialect changes, remembering to add &mut pc parameter to any new build_where calls