@@ -3,6 +3,7 @@ import { Factory as WaSqliteFactory, SQLITE_ROW } from '@journeyapps/wa-sqlite';
33import { loadModuleAndVfs , WASQLiteVFS } from './vfs.js' ;
44import { TemporaryStorageOption } from '../options.js' ;
55import { RawQueryResult , SqliteValue } from '@powersync/common' ;
6+ import { PreparedStatementCache } from './StatementCache.js' ;
67
78export interface RawWebResult extends Required < RawQueryResult > {
89 autocommit : boolean ;
@@ -23,6 +24,10 @@ export interface RawWaSqliteDatabaseOptions {
2324 encryptionKey : string | undefined ;
2425 temporaryStorage : TemporaryStorageOption ;
2526 cacheSizeKb : number ;
27+ /**
28+ * The amount of prepared statements to cache, or 0 to disable caching.
29+ */
30+ preparedStatementsCache : number ;
2631}
2732
2833/**
@@ -33,12 +38,18 @@ export interface RawWaSqliteDatabaseOptions {
3338 */
3439export class RawSqliteConnection {
3540 private _sqliteAPI : SQLiteAPI | null = null ;
41+ private sqlite3_stmt_isexplain ! : ( stmt : number ) => 0 | 1 | 2 ;
42+
3643 /**
3744 * The `sqlite3*` connection pointer.
3845 */
3946 private db : number = 0 ;
47+ private statementCache : PreparedStatementCache | null ;
4048
41- constructor ( readonly options : RawWaSqliteDatabaseOptions ) { }
49+ constructor ( readonly options : RawWaSqliteDatabaseOptions ) {
50+ this . statementCache =
51+ options . preparedStatementsCache > 0 ? new PreparedStatementCache ( options . preparedStatementsCache ) : null ;
52+ }
4253
4354 get isOpen ( ) : boolean {
4455 return this . db != 0 ;
@@ -62,6 +73,7 @@ export class RawSqliteConnection {
6273
6374 private async openSQLiteAPI ( ) : Promise < SQLiteAPI > {
6475 const { module, vfs } = await loadModuleAndVfs ( this . options ) ;
76+ this . sqlite3_stmt_isexplain = module . cwrap ( 'sqlite3_stmt_isexplain' , 'int' , [ 'int' ] ) ;
6577 const sqlite3 = WaSqliteFactory ( module ) ;
6678 sqlite3 . vfs_register ( vfs , true ) ;
6779 /**
@@ -141,7 +153,7 @@ export class RawSqliteConnection {
141153 async executeRaw ( sql : string , bindings ?: any [ ] ) : Promise < RawResultSet [ ] > {
142154 const results = [ ] ;
143155 const api = this . requireSqlite ( ) ;
144- for await ( const stmt of api . statements ( this . db , sql ) ) {
156+ for await ( const stmt of this . cachedStatements ( api , sql ) ) {
145157 let columns ;
146158
147159 const rs = await this . stepThroughStatement ( api , stmt , bindings ?? [ ] , columns ) ;
@@ -192,8 +204,56 @@ export class RawSqliteConnection {
192204
193205 async close ( ) {
194206 if ( this . isOpen ) {
195- await this . requireSqlite ( ) . close ( this . db ) ;
207+ const api = this . requireSqlite ( ) ;
208+
209+ if ( this . statementCache ) {
210+ for ( const stmt of this . statementCache . drain ( ) ) {
211+ await api . finalize ( stmt ) ;
212+ }
213+ }
214+
215+ await api . close ( this . db ) ;
196216 this . db = 0 ;
197217 }
198218 }
219+
220+ async * cachedStatements ( api : SQLiteAPI , sql : string ) : AsyncIterable < number > {
221+ {
222+ const existing = this . statementCache ?. lookup ( sql ) ;
223+ if ( existing != null ) {
224+ yield existing ;
225+ return ;
226+ }
227+ }
228+
229+ const inner = api . statements ( this . db , sql , { unscoped : true } ) ;
230+ const preparedStatements : number [ ] = [ ] ;
231+
232+ try {
233+ for await ( const stmt of inner ) {
234+ preparedStatements . push ( stmt ) ;
235+ yield stmt ;
236+ }
237+ } finally {
238+ // We can only cache statements if the sql text corresponds to a single statement, otherwise it's not clear what
239+ // portion of the original sql text to use as a key.
240+ if ( preparedStatements . length == 1 && this . statementCache ) {
241+ const stmt = preparedStatements [ 0 ] ;
242+ // Don't cache EXPLAIN statements, their result becomes invalid after schema changes.
243+ if ( this . sqlite3_stmt_isexplain ( stmt ) == 0 ) {
244+ const evicted = this . statementCache . addStatement ( sql , stmt ) ;
245+ if ( evicted != null ) {
246+ await api . finalize ( evicted ) ;
247+ }
248+
249+ return ;
250+ }
251+ }
252+
253+ // We're not caching statements, so finalize them.
254+ for ( const stmt of preparedStatements ) {
255+ await api . finalize ( stmt ) ;
256+ }
257+ }
258+ }
199259}
0 commit comments