|
16 | 16 |
|
17 | 17 | from ..transform import ( |
18 | 18 | AssignOperation, BooleanExpression, ComparisonExpression, |
| 19 | + CreateVariableOperation, DeleteVariableOperation, |
19 | 20 | ConditionalAssignOperation, ExecuteOperation, Operand, |
20 | 21 | RecodeOperation, RecodeResult, ReplaceValueLabelsOperation, |
21 | 22 | SetFormatOperation, SetMeasurementLevelOperation, |
@@ -57,7 +58,8 @@ def in_place_transformation_capabilities() -> dict[str, Any]: |
57 | 58 | "parent_kinds": ["core"], |
58 | 59 | "mutation": "same_dataset_same_physical_wide_table", |
59 | 60 | "commands": ["RECODE", "COMPUTE", "IF", "VARIABLE LABELS", |
60 | | - "VALUE LABELS", "FORMATS", "VARIABLE LEVEL", "EXECUTE"], |
| 61 | + "VALUE LABELS", "FORMATS", "VARIABLE LEVEL", |
| 62 | + "STRING", "DELETE VARIABLES", "EXECUTE"], |
61 | 63 | "new_target_column": { |
62 | 64 | "sqlite": True, |
63 | 65 | "postgresql": True, |
@@ -211,6 +213,10 @@ def _input_schema( |
211 | 213 | and str(row["storage_kind"]) == "numeric" else None |
212 | 214 | ), |
213 | 215 | measurement_level=row["measurement_level"], |
| 216 | + declared_string_width=( |
| 217 | + int(row["declared_string_width"]) |
| 218 | + if row["declared_string_width"] is not None else None |
| 219 | + ), |
214 | 220 | ) |
215 | 221 | for row in variables |
216 | 222 | )) |
@@ -301,6 +307,53 @@ def _replace_value_labels( |
301 | 307 | label=item.label, |
302 | 308 | )) |
303 | 309 |
|
| 310 | +def _delete_variable_metadata( |
| 311 | + connection: Any, |
| 312 | + *, |
| 313 | + core: Any, |
| 314 | + variable: dict[str, Any], |
| 315 | +) -> None: |
| 316 | + """Delete one variable and every normative catalog row owned by it.""" |
| 317 | + variable_id = str(variable["variable_id"]) |
| 318 | + label_set_id = connection.execute( |
| 319 | + select(core.variable_value_label_set.c.value_label_set_id).where( |
| 320 | + core.variable_value_label_set.c.variable_id == variable_id |
| 321 | + ) |
| 322 | + ).scalar_one_or_none() |
| 323 | + connection.execute(delete(core.dataset_weight_variable).where( |
| 324 | + core.dataset_weight_variable.c.variable_id == variable_id |
| 325 | + )) |
| 326 | + connection.execute(delete(core.variable_set_member).where( |
| 327 | + core.variable_set_member.c.variable_id == variable_id |
| 328 | + )) |
| 329 | + connection.execute(delete(core.multiple_response_member).where( |
| 330 | + core.multiple_response_member.c.variable_id == variable_id |
| 331 | + )) |
| 332 | + connection.execute(delete(core.variable_attribute).where( |
| 333 | + core.variable_attribute.c.variable_id == variable_id |
| 334 | + )) |
| 335 | + connection.execute(delete(core.missing_rule).where( |
| 336 | + core.missing_rule.c.variable_id == variable_id |
| 337 | + )) |
| 338 | + connection.execute(delete(core.variable_value_label_set).where( |
| 339 | + core.variable_value_label_set.c.variable_id == variable_id |
| 340 | + )) |
| 341 | + if label_set_id is not None: |
| 342 | + still_used = connection.execute( |
| 343 | + select(core.variable_value_label_set.c.variable_id).where( |
| 344 | + core.variable_value_label_set.c.value_label_set_id == label_set_id |
| 345 | + ) |
| 346 | + ).first() |
| 347 | + if still_used is None: |
| 348 | + connection.execute(delete(core.value_label).where( |
| 349 | + core.value_label.c.value_label_set_id == label_set_id |
| 350 | + )) |
| 351 | + connection.execute(delete(core.value_label_set).where( |
| 352 | + core.value_label_set.c.value_label_set_id == label_set_id |
| 353 | + )) |
| 354 | + connection.execute(delete(core.variable).where( |
| 355 | + core.variable.c.variable_id == variable_id |
| 356 | + )) |
304 | 357 |
|
305 | 358 | def _failure_boundary(_name: str) -> None: |
306 | 359 | """Synthetic-test hook for schema/data/catalog/audit failure boundaries.""" |
@@ -382,17 +435,25 @@ def _apply_plan_on_connection( |
382 | 435 | ) |
383 | 436 | create_operations = [ |
384 | 437 | operation for operation in plan.operations |
385 | | - if isinstance(operation, (RecodeOperation, AssignOperation)) |
386 | | - and operation.target_mode == "create" |
| 438 | + if isinstance(operation, CreateVariableOperation) |
| 439 | + or ( |
| 440 | + isinstance(operation, (RecodeOperation, AssignOperation)) |
| 441 | + and operation.target_mode == "create" |
| 442 | + ) |
| 443 | + ] |
| 444 | + schema_operations = create_operations + [ |
| 445 | + operation for operation in plan.operations |
| 446 | + if isinstance(operation, DeleteVariableOperation) |
387 | 447 | ] |
388 | | - if create_operations and not allow_schema_change: |
| 448 | + if schema_operations and not allow_schema_change: |
389 | 449 | raise TransformationError( |
390 | 450 | "schema_change_not_atomic", |
391 | 451 | "This database profile has no coherent new-target strategy.", |
392 | 452 | ) |
393 | 453 | unsupported_targets = [ |
394 | 454 | operation.target for operation in create_operations |
395 | | - if output_by_name[operation.target.casefold()].storage_kind != "numeric" |
| 455 | + if not isinstance(operation, CreateVariableOperation) |
| 456 | + and output_by_name[operation.target.casefold()].storage_kind != "numeric" |
396 | 457 | ] |
397 | 458 | if unsupported_targets: |
398 | 459 | raise TransformationError( |
@@ -449,20 +510,37 @@ def _apply_plan_on_connection( |
449 | 510 | # Compensation is journal-bounded to target columns and catalog identities |
450 | 511 | # created by this apply, without resetting unrelated database state. |
451 | 512 | for operation in create_operations: |
452 | | - target_physical = physical_name(operation.target, used_physical) |
| 513 | + target_name = ( |
| 514 | + operation.variable |
| 515 | + if isinstance(operation, CreateVariableOperation) |
| 516 | + else operation.target |
| 517 | + ) |
| 518 | + target_physical = physical_name(target_name, used_physical) |
| 519 | + storage_kind = ( |
| 520 | + operation.storage_kind |
| 521 | + if isinstance(operation, CreateVariableOperation) |
| 522 | + else "numeric" |
| 523 | + ) |
| 524 | + string_width = ( |
| 525 | + operation.declared_string_width |
| 526 | + if isinstance(operation, CreateVariableOperation) |
| 527 | + else None |
| 528 | + ) |
| 529 | + column_type = numeric_type if storage_kind == "numeric" else f"VARCHAR({string_width})" |
453 | 530 | connection.exec_driver_sql( |
454 | 531 | f"ALTER TABLE {qualified_table} ADD COLUMN " |
455 | | - f"{quote(target_physical)} {numeric_type} NULL" |
| 532 | + f"{quote(target_physical)} {column_type} NULL" |
456 | 533 | ) |
457 | 534 | if mutation_journal is not None: |
458 | 535 | mutation_journal["added_columns"].append(target_physical) |
459 | 536 | target_row = { |
460 | 537 | "variable_id": str(uuid4()), |
461 | 538 | "dataset_id": dataset_id, |
462 | 539 | "source_ordinal": next_ordinal, |
463 | | - "source_name": operation.target, |
| 540 | + "source_name": target_name, |
464 | 541 | "physical_name": target_physical, |
465 | | - "storage_kind": "numeric", |
| 542 | + "storage_kind": storage_kind, |
| 543 | + "declared_string_width": string_width, |
466 | 544 | "variable_label": None, |
467 | 545 | } |
468 | 546 | next_ordinal += 1 |
@@ -549,6 +627,23 @@ def _apply_plan_on_connection( |
549 | 627 | core.variable.c.variable_id == variable["variable_id"] |
550 | 628 | ).values(measurement_level=operation.level)) |
551 | 629 | _failure_boundary("catalog") |
| 630 | + elif isinstance(operation, DeleteVariableOperation): |
| 631 | + variable = by_name[operation.variable.casefold()] |
| 632 | + connection.exec_driver_sql( |
| 633 | + f"ALTER TABLE {qualified_table} DROP COLUMN " |
| 634 | + f"{quote(variable['physical_name'])}" |
| 635 | + ) |
| 636 | + _delete_variable_metadata(connection, core=core, variable=variable) |
| 637 | + by_name.pop(operation.variable.casefold(), None) |
| 638 | + variables = [ |
| 639 | + row for row in variables |
| 640 | + if row["variable_id"] != variable["variable_id"] |
| 641 | + ] |
| 642 | + relation = Table( |
| 643 | + table_name, MetaData(), schema=dataset.get("physical_table_schema"), |
| 644 | + autoload_with=connection, |
| 645 | + ) |
| 646 | + _failure_boundary("schema") |
552 | 647 | elif isinstance(operation, ExecuteOperation): |
553 | 648 | continue |
554 | 649 | else: # pragma: no cover |
|
0 commit comments