@@ -86,26 +86,7 @@ static int database_refresh_snapshot (void);
8686
8787// MARK: - SQL -
8888
89- char * sql_build_drop_table (const char * table_name , char * buffer , int bsize , bool is_meta ) {
90- // Escape the table name (doubles any embedded quotes)
91- char escaped [512 ];
92- sql_escape_name (table_name , escaped , sizeof (escaped ));
93-
94- // Add the surrounding quotes in the format string
95- if (is_meta ) {
96- snprintf (buffer , bsize , "DROP TABLE IF EXISTS \"%s_cloudsync\";" , escaped );
97- } else {
98- snprintf (buffer , bsize , "DROP TABLE IF EXISTS \"%s\";" , escaped );
99- }
100-
101- return buffer ;
102- }
103-
104- char * sql_escape_name (const char * name , char * buffer , size_t bsize ) {
105- // PostgreSQL identifier escaping: double any embedded double quotes
106- // Does NOT add surrounding quotes (caller's responsibility)
107- // Similar to SQLite's %q behavior for escaping
108-
89+ static char * sql_escape_character (const char * name , char * buffer , size_t bsize , char c ) {
10990 if (!name || !buffer || bsize < 1 ) {
11091 if (buffer && bsize > 0 ) buffer [0 ] = '\0' ;
11192 return NULL ;
@@ -114,14 +95,14 @@ char *sql_escape_name (const char *name, char *buffer, size_t bsize) {
11495 size_t i = 0 , j = 0 ;
11596
11697 while (name [i ]) {
117- if (name [i ] == '"' ) {
118- // Need space for 2 chars (escaped quote ) + null
98+ if (name [i ] == c ) {
99+ // Need space for 2 chars (escaped c ) + null
119100 if (j >= bsize - 2 ) {
120101 elog (WARNING , "Identifier name too long for buffer, truncated: %s" , name );
121102 break ;
122103 }
123- buffer [j ++ ] = '"' ;
124- buffer [j ++ ] = '"' ;
104+ buffer [j ++ ] = c ;
105+ buffer [j ++ ] = c ;
125106 } else {
126107 // Need space for 1 char + null
127108 if (j >= bsize - 1 ) {
@@ -137,6 +118,34 @@ char *sql_escape_name (const char *name, char *buffer, size_t bsize) {
137118 return buffer ;
138119}
139120
121+ static char * sql_escape_identifier (const char * name , char * buffer , size_t bsize ) {
122+ // PostgreSQL identifier escaping: double any embedded double quotes
123+ // Does NOT add surrounding quotes (caller's responsibility)
124+ // Similar to SQLite's %q behavior for escaping
125+ return sql_escape_character (name , buffer , bsize , '"' );
126+ }
127+
128+ static char * sql_escape_literal (const char * name , char * buffer , size_t bsize ) {
129+ // Escapes single quotes for use inside SQL string literals: ' → ''
130+ // Does NOT add surrounding quotes (caller's responsibility)
131+ return sql_escape_character (name , buffer , bsize , '\'' );
132+ }
133+
134+ char * sql_build_drop_table (const char * table_name , char * buffer , int bsize , bool is_meta ) {
135+ // Escape the table name (doubles any embedded quotes)
136+ char escaped [512 ];
137+ sql_escape_identifier (table_name , escaped , sizeof (escaped ));
138+
139+ // Add the surrounding quotes in the format string
140+ if (is_meta ) {
141+ snprintf (buffer , bsize , "DROP TABLE IF EXISTS \"%s_cloudsync\";" , escaped );
142+ } else {
143+ snprintf (buffer , bsize , "DROP TABLE IF EXISTS \"%s\";" , escaped );
144+ }
145+
146+ return buffer ;
147+ }
148+
140149char * sql_build_select_nonpk_by_pk (cloudsync_context * data , const char * table_name , const char * schema ) {
141150 UNUSED_PARAMETER (data );
142151 char * qualified = database_build_base_ref (schema , table_name );
@@ -226,7 +235,7 @@ char *sql_build_rekey_pk_and_reset_version_except_col (cloudsync_context *data,
226235 return result ;
227236}
228237
229- char * database_table_schema (const char * table_name ) {
238+ char * database_table_schema (const char * table_name ) {
230239 if (!table_name ) return NULL ;
231240
232241 // Build metadata table name
@@ -279,31 +288,31 @@ char *database_table_schema(const char *table_name) {
279288 return schema ;
280289}
281290
282- char * database_build_meta_ref (const char * schema , const char * table_name ) {
291+ char * database_build_meta_ref (const char * schema , const char * table_name ) {
283292 char escaped_table [512 ];
284- sql_escape_name (table_name , escaped_table , sizeof (escaped_table ));
293+ sql_escape_identifier (table_name , escaped_table , sizeof (escaped_table ));
285294 if (schema ) {
286295 char escaped_schema [512 ];
287- sql_escape_name (schema , escaped_schema , sizeof (escaped_schema ));
296+ sql_escape_identifier (schema , escaped_schema , sizeof (escaped_schema ));
288297 return cloudsync_memory_mprintf ("\"%s\".\"%s_cloudsync\"" , escaped_schema , escaped_table );
289298 }
290299 return cloudsync_memory_mprintf ("\"%s_cloudsync\"" , escaped_table );
291300}
292301
293- char * database_build_base_ref (const char * schema , const char * table_name ) {
302+ char * database_build_base_ref (const char * schema , const char * table_name ) {
294303 char escaped_table [512 ];
295- sql_escape_name (table_name , escaped_table , sizeof (escaped_table ));
304+ sql_escape_identifier (table_name , escaped_table , sizeof (escaped_table ));
296305 if (schema ) {
297306 char escaped_schema [512 ];
298- sql_escape_name (schema , escaped_schema , sizeof (escaped_schema ));
307+ sql_escape_identifier (schema , escaped_schema , sizeof (escaped_schema ));
299308 return cloudsync_memory_mprintf ("\"%s\".\"%s\"" , escaped_schema , escaped_table );
300309 }
301310 return cloudsync_memory_mprintf ("\"%s\"" , escaped_table );
302311}
303312
304313// Schema-aware SQL builder for PostgreSQL: deletes columns not in schema or pkcol.
305314// Schema parameter: pass empty string to fall back to current_schema() via SQL.
306- char * sql_build_delete_cols_not_in_schema_query (const char * schema , const char * table_name , const char * meta_ref , const char * pkcol ) {
315+ char * sql_build_delete_cols_not_in_schema_query (const char * schema , const char * table_name , const char * meta_ref , const char * pkcol ) {
307316 const char * schema_param = schema ? schema : "" ;
308317 return cloudsync_memory_mprintf (
309318 "DELETE FROM %s WHERE col_name NOT IN ("
@@ -316,7 +325,7 @@ char *sql_build_delete_cols_not_in_schema_query(const char *schema, const char *
316325}
317326
318327// Builds query to get comma-separated list of primary key column names.
319- char * sql_build_pk_collist_query (const char * schema , const char * table_name ) {
328+ char * sql_build_pk_collist_query (const char * schema , const char * table_name ) {
320329 const char * schema_param = schema ? schema : "" ;
321330 return cloudsync_memory_mprintf (
322331 "SELECT string_agg(quote_ident(column_name), ',') "
@@ -328,7 +337,7 @@ char *sql_build_pk_collist_query(const char *schema, const char *table_name) {
328337}
329338
330339// Builds query to get SELECT list of decoded primary key columns.
331- char * sql_build_pk_decode_selectlist_query (const char * schema , const char * table_name ) {
340+ char * sql_build_pk_decode_selectlist_query (const char * schema , const char * table_name ) {
332341 const char * schema_param = schema ? schema : "" ;
333342 return cloudsync_memory_mprintf (
334343 "SELECT string_agg("
@@ -342,14 +351,18 @@ char *sql_build_pk_decode_selectlist_query(const char *schema, const char *table
342351}
343352
344353// Builds query to get qualified (schema.table.column) primary key column list.
345- char * sql_build_pk_qualified_collist_query (const char * schema , const char * table_name ) {
354+ char * sql_build_pk_qualified_collist_query (const char * schema , const char * table_name ) {
346355 const char * schema_param = schema ? schema : "" ;
356+
357+ char buffer [1024 ];
358+ char * singlequote_escaped_table_name = sql_escape_literal (table_name , buffer , sizeof (buffer ));
359+ if (!singlequote_escaped_table_name ) return NULL ;
360+
347361 return cloudsync_memory_mprintf (
348362 "SELECT string_agg(quote_ident(column_name), ',' ORDER BY ordinal_position) "
349363 "FROM information_schema.key_column_usage "
350364 "WHERE table_name = '%s' AND table_schema = COALESCE(NULLIF('%s', ''), current_schema()) "
351- "AND constraint_name LIKE '%%_pkey';" ,
352- table_name , schema_param
365+ "AND constraint_name LIKE '%%_pkey';" , singlequote_escaped_table_name , schema_param
353366 );
354367}
355368
@@ -1116,7 +1129,7 @@ static int database_create_insert_trigger_internal (cloudsync_context *data, con
11161129 char trigger_name [1024 ];
11171130 char func_name [1024 ];
11181131 char escaped_tbl [512 ];
1119- sql_escape_name (table_name , escaped_tbl , sizeof (escaped_tbl ));
1132+ sql_escape_identifier (table_name , escaped_tbl , sizeof (escaped_tbl ));
11201133 snprintf (trigger_name , sizeof (trigger_name ), "cloudsync_after_insert_%s" , escaped_tbl );
11211134 snprintf (func_name , sizeof (func_name ), "cloudsync_after_insert_%s_fn" , escaped_tbl );
11221135
@@ -1178,7 +1191,7 @@ static int database_create_update_trigger_gos_internal (cloudsync_context *data,
11781191 char trigger_name [1024 ];
11791192 char func_name [1024 ];
11801193 char escaped_tbl [512 ];
1181- sql_escape_name (table_name , escaped_tbl , sizeof (escaped_tbl ));
1194+ sql_escape_identifier (table_name , escaped_tbl , sizeof (escaped_tbl ));
11821195 snprintf (trigger_name , sizeof (trigger_name ), "cloudsync_before_update_%s" , escaped_tbl );
11831196 snprintf (func_name , sizeof (func_name ), "cloudsync_before_update_%s_fn" , escaped_tbl );
11841197
@@ -1221,7 +1234,7 @@ static int database_create_update_trigger_internal (cloudsync_context *data, con
12211234 char trigger_name [1024 ];
12221235 char func_name [1024 ];
12231236 char escaped_tbl [512 ];
1224- sql_escape_name (table_name , escaped_tbl , sizeof (escaped_tbl ));
1237+ sql_escape_identifier (table_name , escaped_tbl , sizeof (escaped_tbl ));
12251238 snprintf (trigger_name , sizeof (trigger_name ), "cloudsync_after_update_%s" , escaped_tbl );
12261239 snprintf (func_name , sizeof (func_name ), "cloudsync_after_update_%s_fn" , escaped_tbl );
12271240
@@ -1327,7 +1340,7 @@ static int database_create_delete_trigger_gos_internal (cloudsync_context *data,
13271340 char trigger_name [1024 ];
13281341 char func_name [1024 ];
13291342 char escaped_tbl [512 ];
1330- sql_escape_name (table_name , escaped_tbl , sizeof (escaped_tbl ));
1343+ sql_escape_identifier (table_name , escaped_tbl , sizeof (escaped_tbl ));
13311344 snprintf (trigger_name , sizeof (trigger_name ), "cloudsync_before_delete_%s" , escaped_tbl );
13321345 snprintf (func_name , sizeof (func_name ), "cloudsync_before_delete_%s_fn" , escaped_tbl );
13331346
@@ -1370,7 +1383,7 @@ static int database_create_delete_trigger_internal (cloudsync_context *data, con
13701383 char trigger_name [1024 ];
13711384 char func_name [1024 ];
13721385 char escaped_tbl [512 ];
1373- sql_escape_name (table_name , escaped_tbl , sizeof (escaped_tbl ));
1386+ sql_escape_identifier (table_name , escaped_tbl , sizeof (escaped_tbl ));
13741387 snprintf (trigger_name , sizeof (trigger_name ), "cloudsync_after_delete_%s" , escaped_tbl );
13751388 snprintf (func_name , sizeof (func_name ), "cloudsync_after_delete_%s_fn" , escaped_tbl );
13761389
@@ -1470,7 +1483,7 @@ int database_delete_triggers (cloudsync_context *data, const char *table) {
14701483 if (!base_ref ) return DBRES_NOMEM ;
14711484
14721485 char escaped_tbl [512 ];
1473- sql_escape_name (table , escaped_tbl , sizeof (escaped_tbl ));
1486+ sql_escape_identifier (table , escaped_tbl , sizeof (escaped_tbl ));
14741487
14751488 char * sql = cloudsync_memory_mprintf (
14761489 "DROP TRIGGER IF EXISTS \"cloudsync_after_insert_%s\" ON %s;" ,
0 commit comments