11import { WalEngine as BaseWalEngine } from '../../proc/timeline/WalEngine.js' ;
2+ import { ConflictError } from '../../flashql/errors/ConflictError.js' ;
23
34export class MainstreamWalEngine extends BaseWalEngine {
45
@@ -10,52 +11,90 @@ export class MainstreamWalEngine extends BaseWalEngine {
1011 this . #client = client ;
1112 }
1213
14+ _quoteIdent ( name ) {
15+ return `"${ String ( name ) . replace ( / " / g, '""' ) } "` ;
16+ }
17+
18+ _quoteQualifiedRelation ( { namespace, name } ) {
19+ return namespace
20+ ? `${ this . _quoteIdent ( namespace ) } .${ this . _quoteIdent ( name ) } `
21+ : this . _quoteIdent ( name ) ;
22+ }
23+
24+ _serializeValue ( value ) {
25+ if ( value === null ) return 'NULL' ;
26+ if ( typeof value === 'number' ) {
27+ if ( ! Number . isFinite ( value ) ) throw new TypeError ( 'Cannot serialize non-finite number' ) ;
28+ return String ( value ) ;
29+ }
30+ if ( typeof value === 'boolean' ) return value ? 'TRUE' : 'FALSE' ;
31+ if ( typeof value === 'string' ) return `'${ value . replace ( / ' / g, "''" ) } '` ;
32+ if ( value instanceof Date ) return `'${ value . toISOString ( ) . replace ( / ' / g, "''" ) } '` ;
33+ return `'${ JSON . stringify ( value ) . replace ( / ' / g, "''" ) } '` ;
34+ }
35+
1336 #buildOriginPredicate( event , mvccKey ) {
1437 let sql = event . relation . keyColumns . map ( ( col ) => {
1538 if ( ! ( col in event . old ) ) throw new TypeError ( `Missing value for key field ${ col } ` ) ;
1639 return `${ this . _quoteIdent ( col ) } = ${ this . _serializeValue ( event . old [ col ] ) } ` ;
1740 } ) . join ( ' AND ' ) ;
1841
1942 if ( mvccKey ) {
20- if ( ! event . mvccTag ) throw new TypeError ( `Missing event.mvccTag for the specified mvccKey ${ mvccKey } ` ) ;
21- sql += `${ this . _quoteIdent ( mvccKey ) } = ${ this . _serializeValue ( event . mvccTag ) } ` ;
43+ if ( ! event . mvccTag )
44+ throw new TypeError ( `Missing event.mvccTag for the specified mvccKey ${ mvccKey } ` ) ;
45+ const mvccExpr = this . #client. dialect === 'postgres' && mvccKey . toUpperCase ( ) === 'XMIN'
46+ ? `CAST(CAST(${ this . _quoteIdent ( mvccKey ) } AS TEXT) AS INT)`
47+ : this . _quoteIdent ( mvccKey ) ;
48+ sql += ` AND ${ mvccExpr } = ${ this . _serializeValue ( event . mvccTag ) } ` ;
2249 }
2350
2451 return sql ;
2552 }
2653
27- async handleDownstreamCommit ( commit ) {
28- // Steps:
29- // begin transaction
54+ async applyDownstreamCommit ( commit ) {
55+ const applyCommit = async ( tx = null ) => {
56+ for ( const event of commit . entries ) {
57+ const { op, relation } = event ;
58+ let sql ;
3059
31- for ( const event of commit . entries ) {
32- const { op, relation } = event ;
33- let sql ;
60+ if ( op === 'insert' ) {
61+ const entries = Object . entries ( event . new ) ;
62+ sql = `
63+ INSERT INTO ${ this . _quoteQualifiedRelation ( relation ) }
64+ (${ entries . map ( ( [ name ] ) => this . _quoteIdent ( name ) ) . join ( ', ' ) } )
65+ VALUES (${ entries . map ( ( [ , value ] ) => this . _serializeValue ( value ) ) . join ( ', ' ) } )` ;
66+ }
3467
35- if ( op === 'insert' ) {
36- const entries = Object . entries ( event . new ) ;
37- sql = `
38- INSERT INTO ${ this . _quoteQualifiedRelation ( relation ) }
39- (${ entries . map ( ( [ name ] ) => this . _quoteIdent ( name ) ) . join ( ', ' ) } )
40- VALUES (${ entries . map ( ( [ , value ] ) => this . _serializeValue ( value ) ) . join ( ', ' ) } )` ;
41- }
68+ if ( op === 'update' ) {
69+ const assignments = Object . entries ( event . new )
70+ . map ( ( [ name , value ] ) => `${ this . _quoteIdent ( name ) } = ${ this . _serializeValue ( value ) } ` ) ;
71+ sql = `
72+ UPDATE ${ this . _quoteQualifiedRelation ( relation ) }
73+ SET ${ assignments . join ( ', ' ) }
74+ WHERE ${ this . #buildOriginPredicate( event , relation . mvccKey ) } ` ;
75+ }
4276
43- if ( op === 'update' ) {
44- const assignments = Object . entries ( event . new )
45- . map ( ( [ name , value ] ) => `${ this . _quoteIdent ( name ) } = ${ this . _serializeValue ( value ) } ` ) ;
46- sql = `
47- UPDATE ${ this . _quoteQualifiedRelation ( relation ) }
48- SET ${ assignments . join ( ', ' ) }
49- WHERE ${ this . #buildOriginPredicate( event , commit . mvccKey ) } ` ;
50- }
77+ if ( op === 'delete' ) {
78+ sql = `
79+ DELETE FROM ${ this . _quoteQualifiedRelation ( relation ) }
80+ WHERE ${ this . #buildOriginPredicate( event , relation . mvccKey ) } ` ;
81+ }
5182
52- if ( op === 'delete' ) {
53- sql = `
54- DELETE FROM ${ this . _quoteQualifiedRelation ( relation ) }
55- WHERE ${ this . #buildOriginPredicate( event , commit . mvccKey ) } ` ;
83+ if ( ! sql ) continue ;
84+
85+ const result = await this . #client. _query ( sql , { tx } ) ;
86+ if ( ( op === 'update' || op === 'delete' ) && result ?. rowCount === 0 ) {
87+ throw new ConflictError ( `[${ this . _quoteQualifiedRelation ( relation ) } ] Origin row version no longer matches the expected version` ) ;
88+ }
5689 }
90+ } ;
5791
58- if ( sql ) await this . #client. query ( sql , { tx } ) ;
92+ if ( typeof this . #client. transaction === 'function' ) {
93+ return await this . #client. transaction ( async ( tx ) => {
94+ await applyCommit ( tx ) ;
95+ } ) ;
5996 }
97+
98+ return await applyCommit ( ) ;
6099 }
61- }
100+ }
0 commit comments