|
21 | 21 | from typing import Any, Callable, Optional, Union |
22 | 22 | from uuid import uuid4 |
23 | 23 |
|
24 | | -import sqlalchemy as sa |
25 | 24 | from alembic import op |
26 | | -from sqlalchemy import inspect |
| 25 | +from sqlalchemy import Column, inspect |
27 | 26 | from sqlalchemy.dialects.mysql.base import MySQLDialect |
28 | 27 | from sqlalchemy.dialects.postgresql.base import PGDialect |
| 28 | +from sqlalchemy.dialects.sqlite.base import SQLiteDialect # noqa: E402 |
| 29 | +from sqlalchemy.engine.reflection import Inspector |
29 | 30 | from sqlalchemy.exc import NoSuchTableError |
30 | 31 | from sqlalchemy.orm import Query, Session |
| 32 | +from sqlalchemy.sql.schema import SchemaItem |
31 | 33 |
|
32 | 34 | from superset.utils import json |
33 | 35 |
|
34 | | -logger = logging.getLogger(__name__) |
| 36 | +GREEN = "\033[32m" |
| 37 | +RESET = "\033[0m" |
| 38 | +YELLOW = "\033[33m" |
| 39 | +RED = "\033[31m" |
| 40 | +LRED = "\033[91m" |
| 41 | + |
| 42 | +logger = logging.getLogger("alembic") |
35 | 43 |
|
36 | 44 | DEFAULT_BATCH_SIZE = int(os.environ.get("BATCH_SIZE", 1000)) |
37 | 45 |
|
@@ -185,15 +193,208 @@ def has_table(table_name: str) -> bool: |
185 | 193 | return table_exists |
186 | 194 |
|
187 | 195 |
|
188 | | -def add_column_if_not_exists(table_name: str, column: sa.Column) -> None: |
| 196 | +def drop_fks_for_table(table_name: str) -> None: |
| 197 | + """ |
| 198 | + Drop all foreign key constraints for a table if it exist and the database |
| 199 | + is not sqlite. |
| 200 | +
|
| 201 | + :param table_name: The table name to drop foreign key constraints for |
| 202 | + """ |
| 203 | + connection = op.get_bind() |
| 204 | + inspector = Inspector.from_engine(connection) |
| 205 | + |
| 206 | + if isinstance(connection.dialect, SQLiteDialect): |
| 207 | + return # sqlite doesn't like constraints |
| 208 | + |
| 209 | + if has_table(table_name): |
| 210 | + foreign_keys = inspector.get_foreign_keys(table_name) |
| 211 | + for fk in foreign_keys: |
| 212 | + logger.info( |
| 213 | + f"Dropping foreign key {GREEN}{fk['name']}{RESET} from table {GREEN}{table_name}{RESET}..." |
| 214 | + ) |
| 215 | + op.drop_constraint(fk["name"], table_name, type_="foreignkey") |
| 216 | + |
| 217 | + |
| 218 | +def create_table(table_name: str, *columns: SchemaItem) -> None: |
| 219 | + """ |
| 220 | + Creates a database table with the specified name and columns. |
| 221 | +
|
| 222 | + This function checks if a table with the given name already exists in the database. |
| 223 | + If the table already exists, it logs an informational. |
| 224 | + Otherwise, it proceeds to create a new table using the provided name and schema columns. |
| 225 | +
|
| 226 | + :param table_name: The name of the table to be created. |
| 227 | + :param columns: A variable number of arguments representing the schema just like when calling alembic's method create_table() |
| 228 | + """ |
| 229 | + |
| 230 | + if has_table(table_name=table_name): |
| 231 | + logger.info(f"Table {LRED}{table_name}{RESET} already exists. Skipping...") |
| 232 | + return |
| 233 | + |
| 234 | + logger.info(f"Creating table {GREEN}{table_name}{RESET}...") |
| 235 | + op.create_table(table_name, *columns) |
| 236 | + logger.info(f"Table {GREEN}{table_name}{RESET} created.") |
| 237 | + |
| 238 | + |
| 239 | +def drop_table(table_name: str) -> None: |
189 | 240 | """ |
190 | | - Adds a column to a table if it does not already exist. |
| 241 | + Drops a database table with the specified name. |
191 | 242 |
|
192 | | - :param table_name: Name of the table. |
193 | | - :param column: SQLAlchemy Column object. |
| 243 | + This function checks if a table with the given name exists in the database. |
| 244 | + If the table does not exist, it logs an informational message and skips the dropping process. |
| 245 | + If the table exists, it first attempts to drop all foreign key constraints associated with the table |
| 246 | + (handled by `drop_fks_for_table`) and then proceeds to drop the table. |
| 247 | +
|
| 248 | + :param table_name: The name of the table to be dropped. |
194 | 249 | """ |
195 | | - if not table_has_column(table_name, column.name): |
196 | | - print(f"Adding column '{column.name}' to table '{table_name}'.\n") |
197 | | - op.add_column(table_name, column) |
198 | | - else: |
199 | | - print(f"Column '{column.name}' already exists in table '{table_name}'.\n") |
| 250 | + |
| 251 | + if not has_table(table_name=table_name): |
| 252 | + logger.info(f"Table {GREEN}{table_name}{RESET} doesn't exist. Skipping...") |
| 253 | + return |
| 254 | + |
| 255 | + logger.info(f"Dropping table {GREEN}{table_name}{RESET}...") |
| 256 | + drop_fks_for_table(table_name) |
| 257 | + op.drop_table(table_name=table_name) |
| 258 | + logger.info(f"Table {GREEN}{table_name}{RESET} dropped.") |
| 259 | + |
| 260 | + |
| 261 | +def batch_operation( |
| 262 | + callable: Callable[[int, int], None], count: int, batch_size: int |
| 263 | +) -> None: |
| 264 | + """ |
| 265 | + Executes an operation by dividing a task into smaller batches and tracking progress. |
| 266 | +
|
| 267 | + This function is designed to process a large number of items in smaller batches. It takes a callable |
| 268 | + that performs the operation on each batch. The function logs the progress of the operation as it processes |
| 269 | + through the batches. |
| 270 | +
|
| 271 | + If count is set to 0 or lower, it logs an informational message and skips the batch process. |
| 272 | +
|
| 273 | + :param callable: A callable function that takes two integer arguments: |
| 274 | + the start index and the end index of the current batch. |
| 275 | + :param count: The total number of items to process. |
| 276 | + :param batch_size: The number of items to process in each batch. |
| 277 | + """ |
| 278 | + if count <= 0: |
| 279 | + logger.info( |
| 280 | + f"No records to process in batch {LRED}(count <= 0){RESET} for callable {LRED}other_callable_example{RESET}. Skipping..." |
| 281 | + ) |
| 282 | + return |
| 283 | + for offset in range(0, count, batch_size): |
| 284 | + percentage = (offset / count) * 100 if count else 0 |
| 285 | + logger.info(f"Progress: {offset:,}/{count:,} ({percentage:.2f}%)") |
| 286 | + callable(offset, min(offset + batch_size, count)) |
| 287 | + |
| 288 | + logger.info(f"Progress: {count:,}/{count:,} (100%)") |
| 289 | + logger.info( |
| 290 | + f"End: {GREEN}{callable.__name__}{RESET} batch operation {GREEN}succesfully{RESET} executed." |
| 291 | + ) |
| 292 | + |
| 293 | + |
| 294 | +def add_columns(table_name: str, *columns: Column) -> None: |
| 295 | + """ |
| 296 | + Adds new columns to an existing database table. |
| 297 | +
|
| 298 | + If a column already exists, it logs an informational message and skips the adding process. |
| 299 | + Otherwise, it proceeds to add the new column to the table. |
| 300 | +
|
| 301 | + The operation is performed using Alembic's batch_alter_table. |
| 302 | +
|
| 303 | + :param table_name: The name of the table to which the columns will be added. |
| 304 | + :param columns: A list of SQLAlchemy Column objects that define the name, type, and other attributes of the columns to be added. |
| 305 | + """ |
| 306 | + |
| 307 | + cols_to_add = [] |
| 308 | + for col in columns: |
| 309 | + if table_has_column(table_name=table_name, column_name=col.name): |
| 310 | + logger.info( |
| 311 | + f"Column {LRED}{col.name}{RESET} already present on table {LRED}{table_name}{RESET}. Skipping..." |
| 312 | + ) |
| 313 | + else: |
| 314 | + cols_to_add.append(col) |
| 315 | + |
| 316 | + with op.batch_alter_table(table_name) as batch_op: |
| 317 | + for col in cols_to_add: |
| 318 | + logger.info( |
| 319 | + f"Adding column {GREEN}{col.name}{RESET} to table {GREEN}{table_name}{RESET}..." |
| 320 | + ) |
| 321 | + batch_op.add_column(col) |
| 322 | + |
| 323 | + |
| 324 | +def drop_columns(table_name: str, *columns: str) -> None: |
| 325 | + """ |
| 326 | + Drops specified columns from an existing database table. |
| 327 | +
|
| 328 | + If a column does not exist, it logs an informational message and skips the dropping process. |
| 329 | + Otherwise, it proceeds to remove the column from the table. |
| 330 | +
|
| 331 | + The operation is performed using Alembic's batch_alter_table. |
| 332 | +
|
| 333 | + :param table_name: The name of the table from which the columns will be removed. |
| 334 | + :param columns: A list of column names to be dropped. |
| 335 | + """ |
| 336 | + |
| 337 | + cols_to_drop = [] |
| 338 | + for col in columns: |
| 339 | + if not table_has_column(table_name=table_name, column_name=col): |
| 340 | + logger.info( |
| 341 | + f"Column {LRED}{col}{RESET} is not present on table {LRED}{table_name}{RESET}. Skipping..." |
| 342 | + ) |
| 343 | + else: |
| 344 | + cols_to_drop.append(col) |
| 345 | + |
| 346 | + with op.batch_alter_table(table_name) as batch_op: |
| 347 | + for col in cols_to_drop: |
| 348 | + logger.info( |
| 349 | + f"Dropping column {GREEN}{col}{RESET} from table {GREEN}{table_name}{RESET}..." |
| 350 | + ) |
| 351 | + batch_op.drop_column(col) |
| 352 | + |
| 353 | + |
| 354 | +def create_index(table_name: str, index_name: str, *columns: str) -> None: |
| 355 | + """ |
| 356 | + Creates an index on specified columns of an existing database table. |
| 357 | +
|
| 358 | + If the index already exists, it logs an informational message and skips the creation process. |
| 359 | + Otherwise, it proceeds to create a new index with the specified name on the given columns of the table. |
| 360 | +
|
| 361 | + :param table_name: The name of the table on which the index will be created. |
| 362 | + :param index_name: The name of the index to be created. |
| 363 | + :param columns: A list column names where the index will be created |
| 364 | + """ |
| 365 | + |
| 366 | + if table_has_index(table=table_name, index=index_name): |
| 367 | + logger.info( |
| 368 | + f"Table {LRED}{table_name}{RESET} already has index {LRED}{index_name}{RESET}. Skipping..." |
| 369 | + ) |
| 370 | + return |
| 371 | + |
| 372 | + logger.info( |
| 373 | + f"Creating index {GREEN}{index_name}{RESET} on table {GREEN}{table_name}{RESET}" |
| 374 | + ) |
| 375 | + |
| 376 | + op.create_index(table_name=table_name, index_name=index_name, columns=columns) |
| 377 | + |
| 378 | + |
| 379 | +def drop_index(table_name: str, index_name: str) -> None: |
| 380 | + """ |
| 381 | + Drops an index from an existing database table. |
| 382 | +
|
| 383 | + If the index does not exists, it logs an informational message and skips the dropping process. |
| 384 | + Otherwise, it proceeds with the removal operation. |
| 385 | +
|
| 386 | + :param table_name: The name of the table from which the index will be dropped. |
| 387 | + :param index_name: The name of the index to be dropped. |
| 388 | + """ |
| 389 | + |
| 390 | + if not table_has_index(table=table_name, index=index_name): |
| 391 | + logger.info( |
| 392 | + f"Table {LRED}{table_name}{RESET} doesn't have index {LRED}{index_name}{RESET}. Skipping..." |
| 393 | + ) |
| 394 | + return |
| 395 | + |
| 396 | + logger.info( |
| 397 | + f"Dropping index {GREEN}{index_name}{RESET} from table {GREEN}{table_name}{RESET}..." |
| 398 | + ) |
| 399 | + |
| 400 | + op.drop_index(table_name=table_name, index_name=index_name) |
0 commit comments