@@ -175,42 +175,48 @@ def _index_key(index: Dict[str, Any]) -> tuple:
175175 unique = bool (index .get ("unique" , False ))
176176 return (name , cols , unique )
177177
178+ def _qi (identifier : str ) -> str :
179+ """Quote a SQL identifier to prevent injection."""
180+ sanitized = identifier .replace ('"' , '""' )
181+ return f'"{ sanitized } "'
182+
178183 def _create_index_sql (dialect : str , name : str , table : str , cols : str , unique : str ) -> str :
179- return f"CREATE { unique } INDEX { name } ON { table } ({ cols } );"
184+ return f"CREATE { unique } INDEX { _qi ( name ) } ON { _qi ( table ) } ({ cols } );"
180185
181186 def _drop_index_sql (dialect : str , name : str , table : str ) -> str :
182187 if dialect == "mysql" :
183- return f"DROP INDEX { name } ON { table } ;"
184- return f"DROP INDEX { name } ;"
188+ return f"DROP INDEX { _qi ( name ) } ON { _qi ( table ) } ;"
189+ return f"DROP INDEX { _qi ( name ) } ;"
185190
186191 def _alter_type_sql (
187192 dialect : str , table : str , column : str , desired : str
188193 ) -> tuple [str | None , str | None ]:
194+ t , c = _qi (table ), _qi (column )
189195 if dialect == "postgresql" :
190- return (f"ALTER TABLE { table } ALTER COLUMN { column } TYPE { desired } ;" , None )
196+ return (f"ALTER TABLE { t } ALTER COLUMN { c } TYPE { desired } ;" , None )
191197 if dialect == "mysql" :
192- return (f"ALTER TABLE { table } MODIFY COLUMN { column } { desired } ;" , None )
198+ return (f"ALTER TABLE { t } MODIFY COLUMN { c } { desired } ;" , None )
193199 if dialect == "sqlite" :
194200 return (None , f"SQLite does not support ALTER COLUMN TYPE for { table } .{ column } " )
195- return (f"ALTER TABLE { table } ALTER COLUMN { column } TYPE { desired } ;" , None )
201+ return (f"ALTER TABLE { t } ALTER COLUMN { c } TYPE { desired } ;" , None )
196202
197203 def _alter_nullable_sql (
198204 dialect : str , table : str , column : str , desired_nullable : bool
199205 ) -> tuple [str | None , str | None ]:
206+ t , c = _qi (table ), _qi (column )
200207 if dialect == "postgresql" :
201208 if desired_nullable :
202- return (f"ALTER TABLE { table } ALTER COLUMN { column } DROP NOT NULL;" , None )
203- return (f"ALTER TABLE { table } ALTER COLUMN { column } SET NOT NULL;" , None )
209+ return (f"ALTER TABLE { t } ALTER COLUMN { c } DROP NOT NULL;" , None )
210+ return (f"ALTER TABLE { t } ALTER COLUMN { c } SET NOT NULL;" , None )
204211 if dialect == "mysql" :
205- # MySQL needs full column definition; warn user.
206212 return (None , f"MySQL requires full column type to change NULL for { table } .{ column } " )
207213 if dialect == "sqlite" :
208214 return (None , f"SQLite does not support ALTER COLUMN NULL for { table } .{ column } " )
209215 return (None , f"Nullable change not supported for { dialect } " )
210216
211217 def _drop_column_sql (dialect : str , table : str , column : str ) -> tuple [str | None , str | None ]:
212218 if dialect in {"postgresql" , "mysql" }:
213- return (f"ALTER TABLE { table } DROP COLUMN { column } ;" , None )
219+ return (f"ALTER TABLE { _qi ( table ) } DROP COLUMN { _qi ( column ) } ;" , None )
214220 if dialect == "sqlite" :
215221 return (None , f"SQLite does not support DROP COLUMN for { table } .{ column } " )
216222 return (None , f"DROP COLUMN not supported for { dialect } " )
@@ -289,15 +295,15 @@ def db_migrate_plan(payload: Dict[str, Any]) -> Dict[str, Any]:
289295 for name , c in columns .items ():
290296 col_type = c .get ("type" , "TEXT" )
291297 nullable = c .get ("nullable" , True )
292- col_defs .append (f"{ name } { col_type } { '' if nullable else ' NOT NULL' } " )
298+ col_defs .append (f"{ _qi ( name ) } { col_type } { '' if nullable else ' NOT NULL' } " )
293299 if col_defs :
294- statements .append (f"CREATE TABLE { table } ({ ', ' .join (col_defs )} );" )
300+ statements .append (f"CREATE TABLE { _qi ( table ) } ({ ', ' .join (col_defs )} );" )
295301
296302 for idx in spec .get ("indexes" , []):
297303 idx_norm = _normalize_index (idx )
298304 idx_name = idx_norm .get ("name" ) or f"idx_{ table } _{ '_' .join (idx_norm ['columns' ])} "
299305 unique = "UNIQUE " if idx_norm .get ("unique" ) else ""
300- cols = ", " .join (idx_norm .get ("columns" , []))
306+ cols = ", " .join (_qi ( c ) for c in idx_norm .get ("columns" , []))
301307 if cols :
302308 statements .append (_create_index_sql (dialect , idx_name , table , cols , unique ))
303309
@@ -307,7 +313,8 @@ def db_migrate_plan(payload: Dict[str, Any]) -> Dict[str, Any]:
307313 col_type = spec .get ("type" , "TEXT" )
308314 nullable = spec .get ("nullable" , True )
309315 not_null = "" if nullable else " NOT NULL"
310- statements .append (f"ALTER TABLE { table } ADD COLUMN { col } { col_type } { not_null } ;" )
316+ stmt = f"ALTER TABLE { _qi (table )} ADD COLUMN { _qi (col )} { col_type } { not_null } ;"
317+ statements .append (stmt )
311318
312319 for table , cols in diff .get ("type_mismatches" , {}).items ():
313320 for item in cols :
@@ -330,13 +337,13 @@ def db_migrate_plan(payload: Dict[str, Any]) -> Dict[str, Any]:
330337 idx_norm = _normalize_index (idx )
331338 idx_name = idx_norm .get ("name" ) or f"idx_{ table } _{ '_' .join (idx_norm ['columns' ])} "
332339 unique = "UNIQUE " if idx_norm .get ("unique" ) else ""
333- cols = ", " .join (idx_norm .get ("columns" , []))
340+ cols = ", " .join (_qi ( c ) for c in idx_norm .get ("columns" , []))
334341 if cols :
335342 statements .append (_create_index_sql (dialect , idx_name , table , cols , unique ))
336343
337344 if destructive :
338345 for table in diff .get ("extra_tables" , []):
339- statements .append (f"DROP TABLE { table } ;" )
346+ statements .append (f"DROP TABLE { _qi ( table ) } ;" )
340347 for table , cols in diff .get ("extra_columns" , {}).items ():
341348 for col in cols :
342349 stmt , warn = _drop_column_sql (dialect , table , col )
0 commit comments