@@ -15,23 +15,19 @@ import {
1515
1616type PreparedQueryConfig = Omit < PreparedQueryConfigBase , 'statement' | 'run' > ;
1717
18- /**
19- * Callback which uses a LockContext for database operations.
20- */
2118export type LockCallback < T > = ( ctx : LockContext ) => Promise < T > ;
2219
23- /**
24- * Provider for specific database contexts.
25- * Handlers are provided a context to the provided callback.
26- * This does not necessarily need to acquire a database lock for each call.
27- * Calls might use the same lock context for multiple operations.
28- * The read/write context may relate to a single read OR write context.
29- */
3020export type ContextProvider = {
3121 useReadContext : < T > ( fn : LockCallback < T > ) => Promise < T > ;
3222 useWriteContext : < T > ( fn : LockCallback < T > ) => Promise < T > ;
3323} ;
3424
25+ type ResultMapper = ( rows : unknown [ ] [ ] , mapColumnValue ?: ( value : unknown ) => unknown ) => unknown ;
26+ type RelationalResultMapper = (
27+ rows : Record < string , unknown > [ ] ,
28+ mapColumnValue ?: ( value : unknown ) => unknown
29+ ) => unknown ;
30+
3531export class PowerSyncSQLitePreparedQuery <
3632 T extends PreparedQueryConfig = PreparedQueryConfig
3733> extends SQLitePreparedQuery < {
@@ -46,25 +42,62 @@ export class PowerSyncSQLitePreparedQuery<
4642
4743 private readOnly = false ;
4844
45+ constructor (
46+ contextProvider : ContextProvider ,
47+ query : Query ,
48+ logger : Logger ,
49+ fields : SelectedFieldsOrdered | undefined ,
50+ executeMethod : SQLiteExecuteMethod ,
51+ isResponseInArrayMode : boolean ,
52+ customResultMapper ?: ResultMapper ,
53+ cache ?: Cache | undefined ,
54+ queryMetadata ?:
55+ | {
56+ type : 'select' | 'update' | 'delete' | 'insert' ;
57+ tables : string [ ] ;
58+ }
59+ | undefined ,
60+ cacheConfig ?: WithCacheConfig | undefined ,
61+ relationalQueryMode ?: false
62+ ) ;
63+ constructor (
64+ contextProvider : ContextProvider ,
65+ query : Query ,
66+ logger : Logger ,
67+ fields : SelectedFieldsOrdered | undefined ,
68+ executeMethod : SQLiteExecuteMethod ,
69+ isResponseInArrayMode : boolean ,
70+ customResultMapper : RelationalResultMapper ,
71+ cache : Cache | undefined ,
72+ queryMetadata :
73+ | {
74+ type : 'select' | 'update' | 'delete' | 'insert' ;
75+ tables : string [ ] ;
76+ }
77+ | undefined ,
78+ cacheConfig : WithCacheConfig | undefined ,
79+ relationalQueryMode : true
80+ ) ;
4981 constructor (
5082 private contextProvider : ContextProvider ,
5183 query : Query ,
5284 private logger : Logger ,
5385 private fields : SelectedFieldsOrdered | undefined ,
5486 executeMethod : SQLiteExecuteMethod ,
5587 private _isResponseInArrayMode : boolean ,
56- private customResultMapper ?: ( rows : unknown [ ] [ ] ) => unknown ,
88+ private customResultMapper ?: ResultMapper | RelationalResultMapper ,
5789 cache ?: Cache | undefined ,
5890 queryMetadata ?:
5991 | {
6092 type : 'select' | 'update' | 'delete' | 'insert' ;
6193 tables : string [ ] ;
6294 }
6395 | undefined ,
64- cacheConfig ?: WithCacheConfig | undefined
96+ cacheConfig ?: WithCacheConfig | undefined ,
97+ private relationalQueryMode = false
6598 ) {
6699 super ( 'async' , executeMethod , query , cache , queryMetadata , cacheConfig ) ;
67- this . readOnly = queryMetadata ?. type == 'select' ;
100+ this . readOnly = queryMetadata ?. type == 'select' || relationalQueryMode ;
68101 }
69102
70103 async run ( placeholderValues ?: Record < string , unknown > ) : Promise < QueryResult > {
@@ -85,10 +118,20 @@ export class PowerSyncSQLitePreparedQuery<
85118 } ) ;
86119 }
87120
121+ if ( customResultMapper && this . relationalQueryMode ) {
122+ const params = fillPlaceholders ( query . params , placeholderValues ?? { } ) ;
123+ logger . logQuery ( query . sql , params ) ;
124+ const relationalResultMapper = customResultMapper as RelationalResultMapper ;
125+ return await this . useContext ( async ( ctx ) => {
126+ const rows = ( await ctx . getAll ( this . query . sql , params ) ) as Record < string , unknown > [ ] ;
127+ return relationalResultMapper ( rows ) as T [ 'all' ] ;
128+ } ) ;
129+ }
130+
88131 const rows = ( await this . values ( placeholderValues ) ) as unknown [ ] [ ] ;
89132 if ( customResultMapper ) {
90- const mapped = customResultMapper ( rows ) as T [ 'all' ] ;
91- return mapped ;
133+ const resultMapper = customResultMapper as ResultMapper ;
134+ return resultMapper ( rows ) as T [ 'all' ] ;
92135 }
93136 return rows . map ( ( row ) => mapResultRow ( fields ! , row , ( this as any ) . joinsNotNullableMap ) ) ;
94137 }
@@ -105,6 +148,17 @@ export class PowerSyncSQLitePreparedQuery<
105148 } ) ;
106149 }
107150
151+ if ( customResultMapper && this . relationalQueryMode ) {
152+ const relationalResultMapper = customResultMapper as RelationalResultMapper ;
153+ return this . useContext ( async ( ctx ) => {
154+ const row = ( await ctx . get ( this . query . sql , params ) ) as Record < string , unknown > | undefined ;
155+ if ( ! row ) {
156+ return undefined as T [ 'get' ] ;
157+ }
158+ return relationalResultMapper ( [ row ] ) as T [ 'get' ] ;
159+ } ) ;
160+ }
161+
108162 const rows = ( await this . values ( placeholderValues ) ) as unknown [ ] [ ] ;
109163 const row = rows [ 0 ] ;
110164
@@ -113,7 +167,8 @@ export class PowerSyncSQLitePreparedQuery<
113167 }
114168
115169 if ( customResultMapper ) {
116- return customResultMapper ( rows ) as T [ 'get' ] ;
170+ const resultMapper = customResultMapper as ResultMapper ;
171+ return resultMapper ( rows ) as T [ 'get' ] ;
117172 }
118173
119174 return mapResultRow ( fields ! , row , joinsNotNullableMap ) ;
@@ -141,17 +196,11 @@ export class PowerSyncSQLitePreparedQuery<
141196 }
142197}
143198
144- /**
145- * Maps a flat array of database row values to a result object based on the provided column definitions.
146- * It reconstructs the hierarchical structure of the result by following the specified paths for each field.
147- * It also handles nullification of nested objects when joined tables are nullable.
148- */
149199export function mapResultRow < TResult > (
150200 columns : SelectedFieldsOrdered ,
151201 row : unknown [ ] ,
152202 joinsNotNullableMap : Record < string , boolean > | undefined
153203) : TResult {
154- // Key -> nested object key, value -> table name if all fields in the nested object are from the same table, false otherwise
155204 const nullifyMap : Record < string , string | false > = { } ;
156205
157206 const result = columns . reduce < Record < string , any > > ( ( result , { path, field } , columnIndex ) => {
@@ -178,10 +227,7 @@ export function mapResultRow<TResult>(
178227 return result as TResult ;
179228}
180229
181- /**
182- * Determines the appropriate decoder for a given field.
183- */
184- function getDecoder ( field : SQLiteColumn | SQL < unknown > | SQL . Aliased ) : DriverValueDecoder < unknown , unknown > {
230+ function getDecoder ( field : any ) : DriverValueDecoder < unknown , unknown > {
185231 if ( is ( field , Column ) ) {
186232 return field ;
187233 } else if ( is ( field , SQL ) ) {
@@ -204,15 +250,15 @@ function updateNullifyMap(
204250
205251 const objectName = path [ 0 ] ! ;
206252 if ( ! ( objectName in nullifyMap ) ) {
207- nullifyMap [ objectName ] = value === null ? getTableName ( field . table ) : false ;
208- } else if ( typeof nullifyMap [ objectName ] === 'string' && nullifyMap [ objectName ] !== getTableName ( field . table ) ) {
253+ nullifyMap [ objectName ] = value === null ? getTableName ( ( field as any ) . table ) : false ;
254+ } else if (
255+ typeof nullifyMap [ objectName ] === 'string' &&
256+ nullifyMap [ objectName ] !== getTableName ( ( field as any ) . table )
257+ ) {
209258 nullifyMap [ objectName ] = false ;
210259 }
211260}
212261
213- /**
214- * Nullify all nested objects from nullifyMap that are nullable
215- */
216262function applyNullifyMap (
217263 result : Record < string , any > ,
218264 nullifyMap : Record < string , string | false > ,
0 commit comments