@@ -177,6 +177,39 @@ def _driver_type_from_declared(type_expr: str) -> str:
177177 return "TEXT"
178178
179179
180+ def _parse_alter_add_column (statement : str ) -> tuple [str , str , str ] | None :
181+ match = re .match (
182+ r'^ALTER TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+ADD\s+COLUMN\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+(.+?)\s*;?$' ,
183+ statement ,
184+ re .IGNORECASE ,
185+ )
186+ if match is None :
187+ return None
188+ return match .group (1 ), match .group (2 ), match .group (3 )
189+
190+
191+ def _parse_alter_drop_column (statement : str ) -> tuple [str , str ] | None :
192+ match = re .match (
193+ r'^ALTER TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+DROP\s+COLUMN\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s*;?$' ,
194+ statement ,
195+ re .IGNORECASE ,
196+ )
197+ if match is None :
198+ return None
199+ return match .group (1 ), match .group (2 )
200+
201+
202+ def _parse_alter_rename_column (statement : str ) -> tuple [str , str , str ] | None :
203+ match = re .match (
204+ r'^ALTER TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+RENAME\s+COLUMN\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+TO\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s*;?$' ,
205+ statement ,
206+ re .IGNORECASE ,
207+ )
208+ if match is None :
209+ return None
210+ return match .group (1 ), match .group (2 ), match .group (3 )
211+
212+
180213def _statement_for_driver_execution (statement : str ) -> str :
181214 create_match = re .match (
182215 r'^CREATE TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s*\((.*)\)\s*;?$' ,
@@ -202,15 +235,11 @@ def _statement_for_driver_execution(statement: str) -> str:
202235 if simplified :
203236 return f"CREATE TABLE { table_name } ({ ', ' .join (simplified )} )"
204237
205- if not statement .upper ().startswith ("ALTER TABLE " ):
206- return statement
207-
208- tokens = statement .split ()
209- if len (tokens ) < 7 :
210- return statement
211-
212- if tokens [3 ].upper () == "ADD" and tokens [4 ].upper () == "COLUMN" :
213- return " " .join (tokens [:7 ])
238+ add_match = _parse_alter_add_column (statement )
239+ if add_match is not None :
240+ table_name , column_name , remainder = add_match
241+ normalized_type = _driver_type_from_declared (remainder )
242+ return f"ALTER TABLE { table_name } ADD COLUMN { column_name } { normalized_type } "
214243
215244 return statement
216245
@@ -326,10 +355,11 @@ def do_execute(
326355 ) -> None :
327356 """Execute a statement, normalizing whitespace for excel-dbapi."""
328357 normalized = _normalize_statement_whitespace_quote_aware (statement )
358+ pre_alter_meta = self ._read_pre_alter_metadata (cursor , normalized )
329359 cursor .execute (_statement_for_driver_execution (normalized ), parameters )
330360 self ._sync_create_table_metadata (cursor , normalized )
331361 self ._sync_drop_table_metadata (cursor , normalized )
332- self ._sync_alter_table_metadata (cursor , normalized )
362+ self ._sync_alter_table_metadata (cursor , normalized , pre_alter_meta )
333363
334364 def do_execute_no_params (
335365 self ,
@@ -339,10 +369,36 @@ def do_execute_no_params(
339369 ) -> None :
340370 """Execute a statement with no parameters."""
341371 normalized = _normalize_statement_whitespace_quote_aware (statement )
372+ pre_alter_meta = self ._read_pre_alter_metadata (cursor , normalized )
342373 cursor .execute (_statement_for_driver_execution (normalized ), None )
343374 self ._sync_create_table_metadata (cursor , normalized )
344375 self ._sync_drop_table_metadata (cursor , normalized )
345- self ._sync_alter_table_metadata (cursor , normalized )
376+ self ._sync_alter_table_metadata (cursor , normalized , pre_alter_meta )
377+
378+ def _read_pre_alter_metadata (
379+ self , cursor : Any , statement : str
380+ ) -> list [dict [str , Any ]] | None :
381+ if (
382+ _parse_alter_add_column (statement ) is None
383+ and _parse_alter_drop_column (statement ) is None
384+ and _parse_alter_rename_column (statement ) is None
385+ ):
386+ return None
387+
388+ table_match = re .match (
389+ r'^ALTER TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\b' ,
390+ statement ,
391+ re .IGNORECASE ,
392+ )
393+ if table_match is None :
394+ return None
395+
396+ import excel_dbapi
397+
398+ table_name = self ._unquote_identifier (table_match .group (1 ))
399+ raw_conn = cursor .connection
400+ current = excel_dbapi .read_table_metadata (raw_conn , table_name ) or []
401+ return [dict (column ) for column in current ]
346402
347403 @staticmethod
348404 def _unquote_identifier (identifier : str ) -> str :
@@ -404,9 +460,24 @@ def _sync_create_table_metadata(self, cursor: Any, statement: str) -> None:
404460 columns : list [dict [str , Any ]] = []
405461 table_pk_columns : set [str ] = set ()
406462 for part in self ._split_sql_list (column_block ):
407- upper_part = part .upper ()
463+ stripped = part .strip ()
464+ upper_part = stripped .upper ()
465+
466+ if upper_part .startswith ("CONSTRAINT " ) and " PRIMARY KEY" in upper_part :
467+ pk_match = re .search (
468+ r"PRIMARY KEY\s*\((.+)\)\s*$" ,
469+ stripped ,
470+ re .IGNORECASE ,
471+ )
472+ if pk_match is not None :
473+ for name in self ._split_sql_list (pk_match .group (1 )):
474+ table_pk_columns .add (self ._unquote_identifier (name .strip ()))
475+ continue
476+
408477 if upper_part .startswith ("PRIMARY KEY" ):
409- pk_match = re .match (r"^PRIMARY KEY\s*\((.+)\)\s*$" , part , re .IGNORECASE )
478+ pk_match = re .match (
479+ r"^PRIMARY KEY\s*\((.+)\)\s*$" , stripped , re .IGNORECASE
480+ )
410481 if pk_match is not None :
411482 for name in self ._split_sql_list (pk_match .group (1 )):
412483 table_pk_columns .add (self ._unquote_identifier (name .strip ()))
@@ -423,7 +494,7 @@ def _sync_create_table_metadata(self, cursor: Any, statement: str) -> None:
423494
424495 col_match = re .match (
425496 r'^("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\s+(.+)$' ,
426- part ,
497+ stripped ,
427498 )
428499 if col_match is None :
429500 continue
@@ -474,29 +545,45 @@ def _sync_drop_table_metadata(self, cursor: Any, statement: str) -> None:
474545 raw_conn = cursor .connection
475546 excel_dbapi .remove_table_metadata (raw_conn , table_name )
476547
477- def _sync_alter_table_metadata (self , cursor : Any , statement : str ) -> None :
548+ def _sync_alter_table_metadata (
549+ self ,
550+ cursor : Any ,
551+ statement : str ,
552+ pre_alter_meta : list [dict [str , Any ]] | None = None ,
553+ ) -> None :
478554 if not statement .upper ().startswith ("ALTER TABLE " ):
479555 return
480556
557+ add_match = _parse_alter_add_column (statement )
558+ drop_match = _parse_alter_drop_column (statement )
559+ rename_match = _parse_alter_rename_column (statement )
560+ if add_match is None and drop_match is None and rename_match is None :
561+ return
562+
481563 import excel_dbapi
482564
483- tokens = statement .split ()
484- if len (tokens ) < 6 :
565+ table_match = re .match (
566+ r'^ALTER TABLE\s+("[^"]+"|[A-Za-z_][A-Za-z0-9_]*)\b' ,
567+ statement ,
568+ re .IGNORECASE ,
569+ )
570+ if table_match is None :
485571 return
486572
487- table_name = tokens [2 ].strip ('"' )
488- operation = tokens [3 ].upper ()
573+ table_name = self ._unquote_identifier (table_match .group (1 ))
489574
490575 raw_conn = cursor .connection
491- current_meta = excel_dbapi .read_table_metadata (raw_conn , table_name ) or []
576+ current_meta = pre_alter_meta
577+ if current_meta is None :
578+ current_meta = excel_dbapi .read_table_metadata (raw_conn , table_name ) or []
492579
493580 type_map = {col ["name" ]: col ["type_name" ] for col in current_meta }
494581 nullable_map = {col ["name" ]: col .get ("nullable" , True ) for col in current_meta }
495582 pk_map = {col ["name" ]: col .get ("primary_key" , False ) for col in current_meta }
496583
497- if operation == "ADD" and len ( tokens ) >= 7 and tokens [ 4 ]. upper () == "COLUMN" :
498- col_name = tokens [ 5 ]. strip ( '"' )
499- remainder = " " . join ( tokens [ 6 :] )
584+ if add_match is not None :
585+ _table_name , raw_col_name , remainder = add_match
586+ col_name = self . _unquote_identifier ( raw_col_name )
500587 extracted = self ._extract_declared_type_name (remainder )
501588 added_type = self ._normalize_metadata_type_name (extracted or "TEXT" )
502589 type_map [col_name ] = added_type
@@ -507,20 +594,17 @@ def _sync_alter_table_metadata(self, cursor: Any, statement: str) -> None:
507594 nullable_map [col_name ] = "NOT NULL" not in tail and not is_pk
508595 pk_map [col_name ] = is_pk
509596
510- if operation == "DROP" and len (tokens ) == 6 and tokens [4 ].upper () == "COLUMN" :
511- removed = tokens [5 ].strip ('"' )
597+ if drop_match is not None :
598+ _table_name , raw_removed = drop_match
599+ removed = self ._unquote_identifier (raw_removed )
512600 type_map .pop (removed , None )
513601 nullable_map .pop (removed , None )
514602 pk_map .pop (removed , None )
515603
516- if (
517- operation == "RENAME"
518- and len (tokens ) == 8
519- and tokens [4 ].upper () == "COLUMN"
520- and tokens [6 ].upper () == "TO"
521- ):
522- old_name = tokens [5 ].strip ('"' )
523- new_name = tokens [7 ].strip ('"' )
604+ if rename_match is not None :
605+ _table_name , raw_old_name , raw_new_name = rename_match
606+ old_name = self ._unquote_identifier (raw_old_name )
607+ new_name = self ._unquote_identifier (raw_new_name )
524608 if old_name in type_map :
525609 type_map [new_name ] = type_map .pop (old_name )
526610 if old_name in nullable_map :
0 commit comments