From 1f4f616ac7bb3876727e1fb806717893501cd388 Mon Sep 17 00:00:00 2001 From: song <> Date: Sun, 23 Nov 2025 12:27:33 +0700 Subject: [PATCH 1/4] newArch2 --- .../main/java/com/prisma/PrismaModule.java | 12 +- cpp/QueryEngineHostObject.h | 7 +- ios/Prisma.mm | 115 ++++++++++++++---- 3 files changed, 108 insertions(+), 26 deletions(-) diff --git a/android/src/main/java/com/prisma/PrismaModule.java b/android/src/main/java/com/prisma/PrismaModule.java index 97d5d43d..2b858092 100644 --- a/android/src/main/java/com/prisma/PrismaModule.java +++ b/android/src/main/java/com/prisma/PrismaModule.java @@ -122,7 +122,17 @@ public void createDir(File dir) throws IOException public void install() { ReactApplicationContext context = this.getReactApplicationContext(); long jsContextPointer = context.getJavaScriptContextHolder().get(); - CallInvokerHolderImpl jsCallInvokerHolder = (CallInvokerHolderImpl)context.getCatalystInstance().getJSCallInvokerHolder(); + if (jsContextPointer == 0) { + throw new RuntimeException("JSI runtime pointer is null. Make sure Hermes is enabled and the runtime is initialized before calling install()."); + } + CallInvokerHolderImpl jsCallInvokerHolder; + if (context.getJSCallInvokerHolder() != null) { + jsCallInvokerHolder = (CallInvokerHolderImpl) context.getJSCallInvokerHolder(); + } else if (context.getCatalystInstance() != null) { + jsCallInvokerHolder = (CallInvokerHolderImpl) context.getCatalystInstance().getJSCallInvokerHolder(); + } else { + throw new RuntimeException("JSCallInvokerHolder is not available yet."); + } String dbPath = context.getDatabasePath("defaultDatabase").getAbsolutePath().replace("defaultDatabase", ""); String migrationsPath; try { diff --git a/cpp/QueryEngineHostObject.h b/cpp/QueryEngineHostObject.h index ec18d023..fa78dada 100644 --- a/cpp/QueryEngineHostObject.h +++ b/cpp/QueryEngineHostObject.h @@ -2,7 +2,12 @@ #ifndef query_engine_host_object_h #define query_engine_host_object_h -#include "query_engine.h" +#include +#if TARGET_OS_SIMULATOR +#include "../engines/ios/QueryEngine.xcframework/ios-arm64_x86_64-simulator/Headers/query_engine.h" +#else +#include "../engines/ios/QueryEngine.xcframework/ios-arm64/Headers/query_engine.h" +#endif #include #include #include diff --git a/ios/Prisma.mm b/ios/Prisma.mm index 5898a16b..1bdd2331 100644 --- a/ios/Prisma.mm +++ b/ios/Prisma.mm @@ -1,52 +1,119 @@ #import "Prisma.h" #import +#import #import +#import +#import #import +#import #import +#ifdef RCT_NEW_ARCH_ENABLED +#import "RNPrismaSpecJSI.h" +#endif + +// Forward declare runtimeExecutor to silence selector warnings on older headers +@interface RCTBridge (RuntimeExecutorForwardDecl) +- (facebook::react::RuntimeExecutor)runtimeExecutor; +@end + +static NSString *PrismaLibraryPath() { + NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true); + return [paths objectAtIndex:0]; +} + +static NSString *PrismaMigrationsPath() { + auto bundleURL = NSBundle.mainBundle.bundleURL; + auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"]; + return [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""]; +} + +static inline void InstallInRuntime(facebook::jsi::Runtime &runtime, + std::shared_ptr callInvoker) { + NSString *libraryPath = PrismaLibraryPath(); + NSString *migrationsPath = PrismaMigrationsPath(); + prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrationsPath UTF8String]); +} + +static RCTBridge *ResolveBridge(id module) { + RCTBridge *bridge = nil; + if ([module respondsToSelector:@selector(bridge)]) { + bridge = ((id)module).bridge; + } + if (bridge == nil) { + bridge = [RCTBridge currentBridge]; + } + return bridge; +} + @implementation Prisma @synthesize bridge=_bridge; RCT_EXPORT_MODULE() +// Old-arch sync install for classic bridge +#if !RCT_NEW_ARCH_ENABLED RCT_EXPORT_BLOCKING_SYNCHRONOUS_METHOD(install) { - RCTCxxBridge *cxxBridge = (RCTCxxBridge *)_bridge; - if (cxxBridge == nil) { - return @false; +#if DEBUG + std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl; + std::cout << "▲ Library Path:\n" << [PrismaLibraryPath() UTF8String] << std::endl; + std::cout << "▲ Migrations Path:\n" << [PrismaMigrationsPath() UTF8String] << std::endl; +#endif + + BOOL ok = NO; + auto okPtr = &ok; + RCTBridge *bridge = ResolveBridge(self); + + // Try new-arch path first if runtimeExecutor is available + if (bridge && [bridge respondsToSelector:@selector(runtimeExecutor)]) { + facebook::react::RuntimeExecutor executor = RCTRuntimeExecutorFromBridge(bridge); + dispatch_semaphore_t sema = dispatch_semaphore_create(0); + executor([&](facebook::jsi::Runtime &runtime) { + InstallInRuntime(runtime, bridge.jsCallInvoker); + *okPtr = YES; + dispatch_semaphore_signal(sema); + }); + dispatch_semaphore_wait(sema, DISPATCH_TIME_FOREVER); + return @(ok); } - - auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime; - if (jsiRuntime == nil) { + + // Fallback to old-arch bridge access + RCTBridge *legacyBridge = _bridge ?: bridge; + RCTCxxBridge *cxxBridge = (RCTCxxBridge *)legacyBridge; + if (cxxBridge == nil || cxxBridge.runtime == nil) { + NSLog(@"[Prisma] no runtime available to install cxx"); return @false; } + + auto jsiRuntime = (facebook::jsi::Runtime *)cxxBridge.runtime; auto &runtime = *jsiRuntime; - auto callInvoker = _bridge.jsCallInvoker; - - // get migrations folder - auto bundleURL = NSBundle.mainBundle.bundleURL; - auto migrations_path_absolute = [NSString stringWithFormat:@"%@%@", bundleURL.absoluteString, @"migrations"]; - auto migrations_path = [migrations_path_absolute stringByReplacingOccurrencesOfString:@"file://" withString:@""]; - - NSArray *paths = NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, true); - NSString *libraryPath = [paths objectAtIndex:0]; - -#if DEBUG - std::cout << "▲ NSHomeDirectory:\n" << [NSHomeDirectory() UTF8String] << std::endl; - std::cout << "▲ Library Path:\n" << [libraryPath UTF8String] << std::endl; - std::cout << "▲ Migrations Path:\n" << [migrations_path UTF8String] << std::endl; -#endif + auto callInvoker = _bridge ? _bridge.jsCallInvoker : legacyBridge.jsCallInvoker; - prisma::install_cxx(runtime, callInvoker, [libraryPath UTF8String], [migrations_path UTF8String]); - return nil; + InstallInRuntime(runtime, callInvoker); + return @true; } +#else +// Required by NativePrismaSpec protocol in new-arch; TurboModule path uses C++ spec below. +- (void)install {} +#endif #ifdef RCT_NEW_ARCH_ENABLED - (std::shared_ptr)getTurboModule: (const facebook::react::ObjCTurboModule::InitParams &)params { - return std::make_shared(params); + class PrismaCxxTurboModule final : public facebook::react::NativePrismaCxxSpec { + public: + explicit PrismaCxxTurboModule(std::shared_ptr jsInvoker) + : NativePrismaCxxSpec(std::move(jsInvoker)) {} + + void install(facebook::jsi::Runtime &rt) { + InstallInRuntime(rt, this->jsInvoker_); + } + }; + + return std::make_shared(params.jsInvoker); } #endif From 8909194eaecf671aaf27136ec4a6cb7d3b3710a1 Mon Sep 17 00:00:00 2001 From: song <> Date: Mon, 24 Nov 2025 11:44:48 +0700 Subject: [PATCH 2/4] fix: migration form /prisma/migrations by default --- copy-migrations.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/copy-migrations.sh b/copy-migrations.sh index 43205c26..e97a21dd 100755 --- a/copy-migrations.sh +++ b/copy-migrations.sh @@ -5,7 +5,6 @@ echo "Copying prisma migration files..." MIGRATIONS_TARGET=${CONFIGURATION_BUILD_DIR}/${UNLOCALIZED_RESOURCES_FOLDER_PATH} rm -rf "$MIGRATIONS_TARGET/migrations" -mkdir "$MIGRATIONS_TARGET/migrations" -cp -r ${SRCROOT}/../migrations ${MIGRATIONS_TARGET} +cp -r ${SRCROOT}/../prisma/migrations "${MIGRATIONS_TARGET}/migrations" echo "migration files copied ✅" \ No newline at end of file From 28e3245e233a17483e88c5861453f6a50c4038ce Mon Sep 17 00:00:00 2001 From: song <> Date: Sat, 14 Mar 2026 17:34:21 +0800 Subject: [PATCH 3/4] fix: capture prisma async state by value --- cpp/react-native-prisma.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cpp/react-native-prisma.cpp b/cpp/react-native-prisma.cpp index 387fbd0b..c8e8c775 100644 --- a/cpp/react-native-prisma.cpp +++ b/cpp/react-native-prisma.cpp @@ -81,7 +81,7 @@ void install_cxx(jsi::Runtime &rt, } auto log_callback_fn = [&rt, js_log_callback](std::string msg) { - call_invoker->invokeAsync([&rt, msg, &js_log_callback] { + call_invoker->invokeAsync([&rt, msg, js_log_callback] { js_log_callback->asObject(rt).asFunction(rt).call( rt, jsi::String::createFromUtf8(rt, msg)); }); @@ -130,7 +130,7 @@ void install_cxx(jsi::Runtime &rt, auto resolve = std::make_shared(rt, args[0]); auto reject = std::make_shared(rt, args[1]); - auto task = [&rt, &queryEngineHostObject, body = std::move(body), + auto task = [&rt, queryEngineHostObject, body = std::move(body), trace = std::move(trace), tx_id = std::move(tx_id), resolve, reject]() { const char *response; From 8124a95ea6b7734ba463be85c4e03f3c635a50ce Mon Sep 17 00:00:00 2001 From: song <> Date: Sat, 27 Jun 2026 14:32:43 +0700 Subject: [PATCH 4/4] Add synchronous React Native reads --- cpp/react-native-prisma.cpp | 56 +++++++++++++++++++-- package.json | 2 + scripts/patch-prisma-runtime.cjs | 59 ++++++++++++++++++++++ src/SyncQueriesExtension.ts | 86 ++++++++++++++++++++++++++++++++ src/index.ts | 10 +++- 5 files changed, 208 insertions(+), 5 deletions(-) create mode 100644 scripts/patch-prisma-runtime.cjs create mode 100644 src/SyncQueriesExtension.ts diff --git a/cpp/react-native-prisma.cpp b/cpp/react-native-prisma.cpp index c8e8c775..8cdd1ef0 100644 --- a/cpp/react-native-prisma.cpp +++ b/cpp/react-native-prisma.cpp @@ -4,6 +4,7 @@ #include "macros.h" #include "query_engine.h" #include "utils.h" +#include #include #include @@ -18,6 +19,12 @@ std::unordered_map> engine_map; ThreadPool thread_pool; +static void free_engine_string(const char *value) { + if (value != nullptr) { + free(const_cast(value)); + } +} + // Pure C function that is used by Rust to call the log callback extern void log_callback(const char *id, const char *msg) { if (engine_map.count(id)) { @@ -147,8 +154,10 @@ void install_cxx(jsi::Runtime &rt, call_invoker->invokeAsync([&rt, response = std::move(response), error_ptr, resolve, reject]() { if (error_ptr == nullptr) { + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); resolve->asObject(rt).asFunction(rt).call( - rt, jsi::String::createFromUtf8(rt, response)); + rt, std::move(js_response)); } else { auto errCtr = rt.global().getPropertyAsFunction(rt, "Error"); std::string error_message(error_ptr); @@ -170,6 +179,38 @@ void install_cxx(jsi::Runtime &rt, return promise; }); + auto execute_sync = HOSTFN("executeSync", 4) { + std::shared_ptr queryEngineHostObject = + args[0].asObject(rt).asHostObject(rt); + std::string body = args[1].asString(rt).utf8(rt); + std::string trace = args[2].asString(rt).utf8(rt); + std::string tx_id; + if (count > 3 && args[3].isString()) { + tx_id = args[3].asString(rt).utf8(rt); + } + + const char *response = nullptr; + char *error_ptr = nullptr; + + if (!tx_id.empty()) { + response = prisma_query(queryEngineHostObject->engine, body.c_str(), + trace.c_str(), tx_id.c_str(), &error_ptr); + } else { + response = prisma_query(queryEngineHostObject->engine, body.c_str(), + trace.c_str(), nullptr, &error_ptr); + } + + if (error_ptr == nullptr) { + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; + } + + std::string error_message(error_ptr); + free(error_ptr); + throw std::runtime_error(error_message); + }); + auto start_transaction = HOSTFN("startTransaction", 3) { std::shared_ptr queryEngineHostObject = args[0].asObject(rt).asHostObject(rt); @@ -183,7 +224,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not start transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto commit_transaction = HOSTFN("commitTransaction", 3) { @@ -199,7 +242,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not commit transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto rollback_transaction = HOSTFN("rollbackTransaction", 3) { @@ -215,7 +260,9 @@ void install_cxx(jsi::Runtime &rt, throw std::runtime_error("prisma engine did not rollback transaction"); } - return jsi::String::createFromUtf8(rt, std::string(response)); + auto js_response = jsi::String::createFromUtf8(rt, response); + free_engine_string(response); + return js_response; }); auto disconnect = HOSTFN("disconnect", 2) { @@ -253,6 +300,7 @@ void install_cxx(jsi::Runtime &rt, module.setProperty(rt, "create", std::move(create)); module.setProperty(rt, "connect", std::move(connect)); module.setProperty(rt, "execute", std::move(execute)); + module.setProperty(rt, "executeSync", std::move(execute_sync)); module.setProperty(rt, "startTransaction", std::move(start_transaction)); module.setProperty(rt, "commitTransaction", std::move(commit_transaction)); module.setProperty(rt, "rollbackTransaction", diff --git a/package.json b/package.json index 3845e2ca..470d0d24 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "!**/__mocks__", "!**/.*", "copy-migrations.sh", + "scripts/patch-prisma-runtime.cjs", "react-native-prisma.gradle", "plugin", "app.plugin.js", @@ -42,6 +43,7 @@ "lint": "eslint \"**/*.{js,ts,tsx}\"", "clean": "del-cli android/build example/android/build example/android/app/build example/ios/build lib", "prepare": "yarn download-engine && bob build", + "postinstall": "node ./scripts/patch-prisma-runtime.cjs", "ios:full": "yarn qe:sim && cd example && yarn ios", "clang": "clang-format -i ./cpp/*.cpp ./cpp/*.h && git add .", "ios": "cd example && yarn ios", diff --git a/scripts/patch-prisma-runtime.cjs b/scripts/patch-prisma-runtime.cjs new file mode 100644 index 00000000..7be33148 --- /dev/null +++ b/scripts/patch-prisma-runtime.cjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node + +const fs = require('node:fs'); + +function resolveRuntime() { + const paths = [process.env.INIT_CWD, process.cwd()].filter(Boolean); + return require.resolve('@prisma/client/runtime/react-native.js', { paths }); +} + +function patchRuntime(runtimePath) { + let source = fs.readFileSync(runtimePath, 'utf8'); + let patched = false; + + const queryMethod = + 'query(t,r,n,i){return __PrismaProxy.execute(this.engineObject,t,r,n,i)}compile(){throw new Error("not implemented")}'; + const querySyncMethod = + 'query(t,r,n,i){return __PrismaProxy.execute(this.engineObject,t,r,n,i)}querySync(t,r,n,i){return globalThis.__PrismaProxy.executeSync(this.engineObject,t,r,n,i)}compile(){throw new Error("not implemented")}'; + + if (source.includes(queryMethod)) { + source = source.replace(queryMethod, querySyncMethod); + patched = true; + } else if (!source.includes(querySyncMethod)) { + throw new Error( + `Unsupported @prisma/client react-native QueryEngine shape: ${runtimePath}` + ); + } + + const wrapEngineQuery = + 'metrics:t.metrics?.bind(t),query:this.withRequestId(t.query.bind(t)),rollbackTransaction:this.withRequestId(t.rollbackTransaction.bind(t))'; + const wrapEngineQuerySync = + 'metrics:t.metrics?.bind(t),query:this.withRequestId(t.query.bind(t)),querySync:t.querySync?.bind(t),rollbackTransaction:this.withRequestId(t.rollbackTransaction.bind(t))'; + + if (source.includes(wrapEngineQuery)) { + source = source.replace(wrapEngineQuery, wrapEngineQuerySync); + patched = true; + } else if (!source.includes(wrapEngineQuerySync)) { + throw new Error( + `Unsupported @prisma/client react-native wrapEngine shape: ${runtimePath}` + ); + } + + if (patched) { + fs.writeFileSync(runtimePath, source); + } + return patched; +} + +try { + const runtimePath = resolveRuntime(); + if (patchRuntime(runtimePath)) { + console.log(`Patched Prisma React Native runtime: ${runtimePath}`); + } +} catch (error) { + console.warn( + `Could not patch Prisma React Native runtime: ${ + error instanceof Error ? error.message : String(error) + }` + ); +} diff --git a/src/SyncQueriesExtension.ts b/src/SyncQueriesExtension.ts new file mode 100644 index 00000000..9d56681d --- /dev/null +++ b/src/SyncQueriesExtension.ts @@ -0,0 +1,86 @@ +import { + type Action, + serializeJsonQuery, +} from '@prisma/client/runtime/react-native'; + +const requestSync = ( + client: any, + modelName: string | undefined, + action: Action, + args: any, + clientMethod: string +) => { + const engine = client._engine; + if ( + !modelName || + !engine?.libraryStarted || + !engine.engine?.querySync || + !globalThis.__PrismaProxy?.executeSync + ) { + return undefined; + } + + const protocolQuery = serializeJsonQuery({ + modelName, + runtimeDataModel: client._runtimeDataModel, + action, + args, + clientMethod, + callsite: undefined, + extensions: client._extensions, + errorFormat: client._errorFormat, + clientVersion: client._clientVersion, + previewFeatures: client._previewFeatures, + globalOmit: client._globalOmit, + }); + const response = engine.parseEngineResponse( + engine.engine.querySync( + JSON.stringify(protocolQuery), + JSON.stringify({ + traceparent: client._tracingHelper.getTraceParent(), + }), + undefined + ) + ); + + if (response.errors) { + throw response.errors.length === 1 + ? engine.buildQueryError(response.errors[0]) + : new Error(JSON.stringify(response.errors)); + } + if (engine.loggerRustPanic) { + throw engine.loggerRustPanic; + } + + return client._requestHandler.mapQueryEngineResult( + { + protocolQuery, + modelName, + action, + clientMethod, + dataPath: [], + args, + extensions: client._extensions, + transaction: undefined, + unpacker: undefined, + otelParentCtx: undefined, + otelChildCtx: client._tracingHelper.getActiveContext(), + globalOmit: client._globalOmit, + customDataProxyFetch: undefined, + }, + { data: response } + ); +}; + +export const findManySync = ( + client: unknown, + modelName: string, + args?: unknown +): T[] | undefined => + requestSync( + client, + modelName, + 'findMany', + args, + `${modelName}.findManySync` + ); diff --git a/src/index.ts b/src/index.ts index a448579c..06dfa613 100644 --- a/src/index.ts +++ b/src/index.ts @@ -7,6 +7,7 @@ global.btoa = btoa; export { reactiveHooksExtension } from './ReactiveHooksExtension'; export { reactiveQueriesExtension } from './ReactiveQueriesExtension'; +export { findManySync } from './SyncQueriesExtension'; declare global { // eslint-disable-next-line no-var @@ -32,6 +33,7 @@ if (!global.__PrismaProxy) { // Wrap the create function to stringify the env variables if necessary const ogCreate = __PrismaProxy!.create; + global.__PrismaProxy = { ...global.__PrismaProxy, create: (options: PrismaCreateOptions): QueryEngineObject => { @@ -64,8 +66,14 @@ type PrismaProxy = { engine: QueryEngineObject, body: string, headers: string, - txId: string + txId?: string ) => Promise; + executeSync?: ( + engine: QueryEngineObject, + body: string, + headers: string, + txId?: string + ) => string; startTransaction: ( engine: QueryEngineObject, body: string,