@@ -15,14 +15,17 @@ export type DBWorkerInterface = {
1515 // Close is only exposed when used in a single non shared webworker
1616 close ?: ( ) => void ;
1717 execute : WASQLiteExecuteMethod ;
18+ executeBatch : WASQLiteExecuteBatchMethod ;
1819 registerOnTableChange : ( callback : OnTableChangeCallback ) => void ;
1920} ;
2021
2122export type WASQLiteExecuteMethod = ( sql : string , params ?: any [ ] ) => Promise < WASQLExecuteResult > ;
22-
23+ export type WASQLiteExecuteBatchMethod = ( sql : string , params ?: any [ ] ) => Promise < WASQLExecuteResult > ;
2324export type OnTableChangeCallback = ( opType : number , tableName : string , rowId : number ) => void ;
2425export type OpenDB = ( dbFileName : string ) => DBWorkerInterface ;
2526
27+ export type SQLBatchTuple = [ string ] | [ string , Array < any > | Array < Array < any > > ] ;
28+
2629export async function _openDB ( dbFileName : string ) : Promise < DBWorkerInterface > {
2730 const { default : moduleFactory } = await import ( '@journeyapps/wa-sqlite/dist/wa-sqlite-async.mjs' ) ;
2831 const module = await moduleFactory ( ) ;
@@ -52,64 +55,142 @@ export async function _openDB(dbFileName: string): Promise<DBWorkerInterface> {
5255 } ;
5356
5457 /**
55- * This executes SQL statements.
58+ * This executes single SQL statements inside a requested lock .
5659 */
5760 const execute = async ( sql : string | TemplateStringsArray , bindings ?: any [ ] ) : Promise < WASQLExecuteResult > => {
5861 // Running multiple statements on the same connection concurrently should not be allowed
59- return navigator . locks . request ( `db-execute-${ dbFileName } ` , async ( ) => {
60- const results = [ ] ;
61- for await ( const stmt of sqlite3 . statements ( db , sql as string ) ) {
62- let columns ;
63- const wrappedBindings = bindings ? [ bindings ] : [ [ ] ] ;
64- for ( const binding of wrappedBindings ) {
65- // TODO not sure why this is needed currently, but booleans break
66- binding . forEach ( ( b , index , arr ) => {
67- if ( typeof b == 'boolean' ) {
68- arr [ index ] = b ? 1 : 0 ;
69- }
70- } ) ;
62+ return _acquireExecuteLock ( async ( ) => {
63+ return executeSingleStatement ( sql , bindings ) ;
64+ } ) ;
65+ } ;
7166
72- sqlite3 . reset ( stmt ) ;
73- if ( bindings ) {
74- sqlite3 . bind_collection ( stmt , binding ) ;
75- }
67+ /**
68+ * This requests a lock for executing statements.
69+ * Should only be used interanlly.
70+ */
71+ const _acquireExecuteLock = ( callback : ( ) => Promise < any > ) : Promise < any > => {
72+ return navigator . locks . request ( `db-execute-${ dbFileName } ` , callback ) ;
73+ } ;
7674
77- const rows = [ ] ;
78- while ( ( await sqlite3 . step ( stmt ) ) === SQLite . SQLITE_ROW ) {
79- const row = sqlite3 . row ( stmt ) ;
80- rows . push ( row ) ;
75+ /**
76+ * This executes a single statement using SQLite3.
77+ */
78+ const executeSingleStatement = async (
79+ sql : string | TemplateStringsArray ,
80+ bindings ?: any [ ]
81+ ) : Promise < WASQLExecuteResult > => {
82+ const results = [ ] ;
83+ for await ( const stmt of sqlite3 . statements ( db , sql as string ) ) {
84+ let columns ;
85+ const wrappedBindings = bindings ? [ bindings ] : [ [ ] ] ;
86+ for ( const binding of wrappedBindings ) {
87+ // TODO not sure why this is needed currently, but booleans break
88+ binding . forEach ( ( b , index , arr ) => {
89+ if ( typeof b == 'boolean' ) {
90+ arr [ index ] = b ? 1 : 0 ;
8191 }
92+ } ) ;
8293
83- columns = columns ?? sqlite3 . column_names ( stmt ) ;
84- if ( columns . length ) {
85- results . push ( { columns, rows } ) ;
86- }
94+ sqlite3 . reset ( stmt ) ;
95+ if ( bindings ) {
96+ sqlite3 . bind_collection ( stmt , binding ) ;
8797 }
8898
89- // When binding parameters, only a single statement is executed.
90- if ( bindings ) {
91- break ;
99+ const rows = [ ] ;
100+ while ( ( await sqlite3 . step ( stmt ) ) === SQLite . SQLITE_ROW ) {
101+ const row = sqlite3 . row ( stmt ) ;
102+ rows . push ( row ) ;
92103 }
93- }
94104
95- let rows : Record < string , any > [ ] = [ ] ;
96- for ( let resultset of results ) {
97- for ( let row of resultset . rows ) {
98- let outRow : Record < string , any > = { } ;
99- resultset . columns . forEach ( ( key , index ) => {
100- outRow [ key ] = row [ index ] ;
101- } ) ;
102- rows . push ( outRow ) ;
105+ columns = columns ?? sqlite3 . column_names ( stmt ) ;
106+ if ( columns . length ) {
107+ results . push ( { columns, rows } ) ;
103108 }
104109 }
105110
106- const result = {
107- insertId : sqlite3 . last_insert_id ( db ) ,
108- rowsAffected : sqlite3 . changes ( db ) ,
109- rows : {
110- _array : rows ,
111- length : rows . length
111+ // When binding parameters, only a single statement is executed.
112+ if ( bindings ) {
113+ break ;
114+ }
115+ }
116+
117+ let rows : Record < string , any > [ ] = [ ] ;
118+ for ( let resultset of results ) {
119+ for ( let row of resultset . rows ) {
120+ let outRow : Record < string , any > = { } ;
121+ resultset . columns . forEach ( ( key , index ) => {
122+ outRow [ key ] = row [ index ] ;
123+ } ) ;
124+ rows . push ( outRow ) ;
125+ }
126+ }
127+
128+ const result = {
129+ insertId : sqlite3 . last_insert_id ( db ) ,
130+ rowsAffected : sqlite3 . changes ( db ) ,
131+ rows : {
132+ _array : rows ,
133+ length : rows . length
134+ }
135+ } ;
136+
137+ return result ;
138+ } ;
139+
140+ /**
141+ * This executes SQL statements in a batch.
142+ */
143+ const executeBatch = async ( sql : string , bindings ?: any [ ] [ ] ) : Promise < WASQLExecuteResult > => {
144+ return _acquireExecuteLock ( async ( ) => {
145+ let affectedRows = 0 ;
146+
147+ const str = sqlite3 . str_new ( db , sql ) ;
148+ const query = sqlite3 . str_value ( str ) ;
149+ try {
150+ await executeSingleStatement ( 'BEGIN TRANSACTION' ) ;
151+
152+ //Prepare statement once
153+ let prepared = await sqlite3 . prepare_v2 ( db , query ) ;
154+ if ( prepared === null ) {
155+ return {
156+ rowsAffected : 0
157+ } ;
158+ }
159+ const wrappedBindings = bindings ? bindings : [ ] ;
160+ for ( const binding of wrappedBindings ) {
161+ // TODO not sure why this is needed currently, but booleans break
162+ for ( let i = 0 ; i < binding . length ; i ++ ) {
163+ let b = binding [ i ] ;
164+ if ( typeof b == 'boolean' ) {
165+ binding [ i ] = b ? 1 : 0 ;
166+ }
167+ }
168+
169+ //Reset bindings
170+ sqlite3 . reset ( prepared . stmt ) ;
171+ if ( bindings ) {
172+ sqlite3 . bind_collection ( prepared . stmt , binding ) ;
173+ }
174+
175+ let result = await sqlite3 . step ( prepared . stmt ) ;
176+ if ( result === SQLite . SQLITE_DONE ) {
177+ //The value returned by sqlite3_changes() immediately after an INSERT, UPDATE or DELETE statement run on a view is always zero.
178+ affectedRows += sqlite3 . changes ( db ) ;
179+ }
112180 }
181+ //Finalize prepared statement
182+ await sqlite3 . finalize ( prepared . stmt ) ;
183+ await executeSingleStatement ( 'COMMIT' ) ;
184+ } catch ( err ) {
185+ await executeSingleStatement ( 'ROLLBACK' ) ;
186+ return {
187+ rowsAffected : 0
188+ } ;
189+ } finally {
190+ sqlite3 . str_finish ( str ) ;
191+ }
192+ const result = {
193+ rowsAffected : affectedRows
113194 } ;
114195
115196 return result ;
@@ -118,6 +199,7 @@ export async function _openDB(dbFileName: string): Promise<DBWorkerInterface> {
118199
119200 return {
120201 execute : Comlink . proxy ( execute ) ,
202+ executeBatch : Comlink . proxy ( executeBatch ) ,
121203 registerOnTableChange : Comlink . proxy ( registerOnTableChange ) ,
122204 close : Comlink . proxy ( ( ) => {
123205 sqlite3 . close ( db ) ;
0 commit comments