1- import type { DuckDBConnection } from '@duckdb/node-api'
1+ import type { DuckDBConnection , DuckDBValue } from '@duckdb/node-api'
22
33export interface ReplaceOperationParams {
44 table : string
@@ -9,69 +9,95 @@ export interface ReplaceOperationParams {
99 wholeWord : boolean
1010}
1111
12- export interface ReplaceOperationResult {
13- message : string
14- affectedRows : number
15- }
16-
1712export class ReplaceOperationService {
1813 constructor ( private db : DuckDBConnection ) { }
1914
2015 /**
2116 * Performs a replace operation on a column in a project table
2217 */
23- async performReplace ( params : ReplaceOperationParams ) : Promise < ReplaceOperationResult > {
18+ async performReplace ( params : ReplaceOperationParams ) : Promise < number > {
2419 const { table, column, find, replace, caseSensitive, wholeWord } = params
2520
26- // Build the REPLACE operation based on parameters
27- const replaceExpression = this . buildReplaceExpression (
21+ // Count rows that will be affected before the update
22+ const affectedRows = await this . countAffectedRows ( table , column , find , caseSensitive , wholeWord )
23+
24+ // Only proceed if there are rows to update
25+ if ( affectedRows === 0 ) {
26+ return 0
27+ }
28+
29+ // Build and execute the parameterized UPDATE query
30+ const { query, params : queryParams } = this . buildParameterizedUpdateQuery (
31+ table ,
2832 column ,
2933 find ,
3034 replace ,
3135 caseSensitive ,
3236 wholeWord ,
3337 )
3438
35- // Count rows that will be affected before the update
36- const affectedRows = await this . countAffectedRows ( table , column , find , caseSensitive , wholeWord )
37-
38- // Execute the update
39- await this . db . run (
40- `UPDATE "${ table } " SET "${ column } " = ${ replaceExpression } WHERE "${ column } " IS NOT NULL` ,
41- )
39+ await this . db . run ( query , queryParams )
4240
43- return {
44- message : `Successfully replaced '${ find } ' with '${ replace } ' in column '${ column } '` ,
45- affectedRows,
46- }
41+ return affectedRows
4742 }
4843
4944 /**
50- * Builds the appropriate replace expression based on the operation parameters
45+ * Builds a parameterized UPDATE query to safely perform replace operations
5146 */
52- private buildReplaceExpression (
47+ private buildParameterizedUpdateQuery (
48+ table : string ,
5349 column : string ,
5450 find : string ,
5551 replace : string ,
5652 caseSensitive : boolean ,
5753 wholeWord : boolean ,
58- ) : string {
54+ ) {
55+ const params : DuckDBValue [ ] = [ ]
56+
5957 if ( wholeWord ) {
6058 // For whole word replacement, use regex with word boundaries
61- const flags = caseSensitive ? 'g' : 'gi'
59+ const replaceFlags = caseSensitive ? 'g' : 'gi'
60+ const matchFlags = caseSensitive ? '' : 'i'
6261 const pattern = `\\b${ this . escapeRegex ( find ) } \\b`
63- return `regexp_replace("${ column } ", '${ pattern } ', '${ this . escapeSql ( replace ) } ', '${ flags } ')`
62+ params . push ( pattern , replace , replaceFlags )
63+
64+ const query = `
65+ UPDATE "${ table } "
66+ SET "${ column } " = regexp_replace("${ column } ", $1, $2, $3)
67+ WHERE "${ column } " IS NOT NULL
68+ AND regexp_matches("${ column } ", $1, '${ matchFlags } ')
69+ `
70+
71+ return { query, params }
6472 }
6573
6674 // For partial replacement, use simple replace or regex
6775 if ( caseSensitive ) {
68- return `replace("${ column } ", '${ this . escapeSql ( find ) } ', '${ this . escapeSql ( replace ) } ')`
76+ params . push ( find , replace )
77+
78+ const query = `
79+ UPDATE "${ table } "
80+ SET "${ column } " = replace("${ column } ", $1, $2)
81+ WHERE "${ column } " IS NOT NULL
82+ AND position($1 in "${ column } ") > 0
83+ `
84+
85+ return { query, params }
6986 }
7087
7188 // Case-insensitive replacement using regex
72- const flags = 'gi'
89+ const replaceFlags = 'gi'
7390 const pattern = this . escapeRegex ( find )
74- return `regexp_replace("${ column } ", '${ pattern } ', '${ this . escapeSql ( replace ) } ', '${ flags } ')`
91+ params . push ( pattern , replace , replaceFlags )
92+
93+ const query = `
94+ UPDATE "${ table } "
95+ SET "${ column } " = regexp_replace("${ column } ", $1, $2, $3)
96+ WHERE "${ column } " IS NOT NULL
97+ AND regexp_matches("${ column } ", $1, 'i')
98+ `
99+
100+ return { query, params }
75101 }
76102
77103 /**
@@ -84,23 +110,39 @@ export class ReplaceOperationService {
84110 caseSensitive : boolean ,
85111 wholeWord : boolean ,
86112 ) : Promise < number > {
87- let countQuery : string
113+ let query : string
114+ const params : DuckDBValue [ ] = [ ]
88115
89116 if ( wholeWord ) {
90117 // For whole word matching, count rows where the word appears as a whole word
91118 const flags = caseSensitive ? '' : 'i'
92119 const pattern = `\\b${ this . escapeRegex ( find ) } \\b`
93- countQuery = `SELECT COUNT(*) as count FROM "${ table } " WHERE "${ column } " IS NOT NULL AND regexp_matches("${ column } ", '${ pattern } ', '${ flags } ')`
120+ params . push ( pattern , flags )
121+ query = `
122+ SELECT COUNT(*) as count FROM "${ table } "
123+ WHERE "${ column } " IS NOT NULL
124+ AND regexp_matches("${ column } ", $1, $2)
125+ `
94126 } else {
95127 if ( caseSensitive ) {
96- countQuery = `SELECT COUNT(*) as count FROM "${ table } " WHERE "${ column } " IS NOT NULL AND position('${ this . escapeSql ( find ) } ' in "${ column } ") > 0`
128+ params . push ( find )
129+ query = `
130+ SELECT COUNT(*) as count FROM "${ table } "
131+ WHERE "${ column } " IS NOT NULL
132+ AND position($1 in "${ column } ") > 0
133+ `
97134 } else {
98135 const pattern = this . escapeRegex ( find )
99- countQuery = `SELECT COUNT(*) as count FROM "${ table } " WHERE "${ column } " IS NOT NULL AND regexp_matches("${ column } ", '${ pattern } ', 'i')`
136+ params . push ( pattern )
137+ query = `
138+ SELECT COUNT(*) as count FROM "${ table } "
139+ WHERE "${ column } " IS NOT NULL
140+ AND regexp_matches("${ column } ", $1, 'i')
141+ `
100142 }
101143 }
102144
103- const countBeforeReader = await this . db . runAndReadAll ( countQuery )
145+ const countBeforeReader = await this . db . runAndReadAll ( query , params )
104146 const countBeforeResult = countBeforeReader . getRowObjectsJson ( )
105147
106148 return Number ( countBeforeResult [ 0 ] ?. count ?? 0 )
@@ -110,13 +152,6 @@ export class ReplaceOperationService {
110152 * Escapes special regex characters in a string
111153 */
112154 private escapeRegex ( str : string ) : string {
113- return str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' ) . replace ( / ' / g, "''" )
114- }
115-
116- /**
117- * Escapes single quotes for SQL
118- */
119- private escapeSql ( str : string ) : string {
120- return str . replace ( / ' / g, "''" )
155+ return str . replace ( / [ . * + ? ^ $ { } ( ) | [ \] \\ ] / g, '\\$&' )
121156 }
122157}
0 commit comments