|
13 | 13 | import sqlmesh.core.dialect as d |
14 | 14 | from sqlmesh.core.engine_adapter import BigQueryEngineAdapter |
15 | 15 | from sqlmesh.core.engine_adapter.bigquery import select_partitions_expr |
16 | | -from sqlmesh.core.engine_adapter.shared import DataObjectType |
| 16 | +from sqlmesh.core.engine_adapter.mixins import ( |
| 17 | + TableAlterClusterByOperation, |
| 18 | + TableAlterDropClusterKeyOperation, |
| 19 | +) |
| 20 | +from sqlmesh.core.engine_adapter.shared import DataObject, DataObjectType |
| 21 | +from sqlmesh.core.schema_diff import TableAlterDropColumnOperation |
17 | 22 | from sqlmesh.core.node import IntervalUnit |
18 | 23 | from sqlmesh.utils import AttributeDict |
19 | 24 | from sqlmesh.utils.errors import SQLMeshError |
@@ -1380,3 +1385,158 @@ def test_sync_grants_config_no_schema( |
1380 | 1385 |
|
1381 | 1386 | with pytest.raises(ValueError, match="Table test_table does not have a schema \\(dataset\\)"): |
1382 | 1387 | adapter.sync_grants_config(relation, new_grants_config) |
| 1388 | + |
| 1389 | + |
| 1390 | +def _mock_bq_data_objects( |
| 1391 | + mocker: MockerFixture, |
| 1392 | + adapter: BigQueryEngineAdapter, |
| 1393 | + current_obj: DataObject, |
| 1394 | + target_obj: DataObject, |
| 1395 | +) -> None: |
| 1396 | + """Patch _get_data_objects on *adapter* to return the supplied DataObjects.""" |
| 1397 | + |
| 1398 | + def _side_effect( |
| 1399 | + schema_name: t.Any, object_names: t.Optional[t.Set[str]] = None |
| 1400 | + ) -> t.List[DataObject]: |
| 1401 | + results = [] |
| 1402 | + for obj in [current_obj, target_obj]: |
| 1403 | + if object_names is None or obj.name in object_names: |
| 1404 | + results.append(obj) |
| 1405 | + return results |
| 1406 | + |
| 1407 | + mocker.patch.object(adapter, "_get_data_objects", side_effect=_side_effect) |
| 1408 | + |
| 1409 | + |
| 1410 | +def test_bigquery_clustering_column_drop_before_clustering_change( |
| 1411 | + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture |
| 1412 | +) -> None: |
| 1413 | + """BigQuery keeps the historical order: DROP COLUMN before clustering key change. |
| 1414 | +
|
| 1415 | + Dropping a column that belongs to the current clustering key must NOT trigger |
| 1416 | + a reorder. BigQuery must emit DROP COLUMN first, then DROP CLUSTERING KEY. |
| 1417 | + """ |
| 1418 | + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter, patch_get_data_objects=False) |
| 1419 | + |
| 1420 | + current_columns = {"tenant_id": "INT64", "partition_month": "INT64"} |
| 1421 | + target_columns = {"tenant_id": "INT64"} |
| 1422 | + adapter.columns = lambda table_name, **kwargs: { |
| 1423 | + k: exp.DataType.build(v, dialect="bigquery") |
| 1424 | + for k, v in ( |
| 1425 | + current_columns if table_name == "project.dataset.current_table" else target_columns |
| 1426 | + ).items() |
| 1427 | + } |
| 1428 | + |
| 1429 | + _mock_bq_data_objects( |
| 1430 | + mocker, |
| 1431 | + adapter, |
| 1432 | + current_obj=DataObject( |
| 1433 | + catalog="project", |
| 1434 | + schema="dataset", |
| 1435 | + name="current_table", |
| 1436 | + type=DataObjectType.TABLE, |
| 1437 | + clustering_key="(partition_month)", |
| 1438 | + ), |
| 1439 | + target_obj=DataObject( |
| 1440 | + catalog="project", |
| 1441 | + schema="dataset", |
| 1442 | + name="target_table", |
| 1443 | + type=DataObjectType.TABLE, |
| 1444 | + clustering_key=None, |
| 1445 | + ), |
| 1446 | + ) |
| 1447 | + |
| 1448 | + operations = adapter.get_alter_operations( |
| 1449 | + "project.dataset.current_table", "project.dataset.target_table" |
| 1450 | + ) |
| 1451 | + |
| 1452 | + assert len(operations) == 2 |
| 1453 | + assert isinstance(operations[0], TableAlterDropColumnOperation) |
| 1454 | + assert isinstance(operations[1], TableAlterDropClusterKeyOperation) |
| 1455 | + |
| 1456 | + |
| 1457 | +def test_bigquery_no_column_change_clustering_drop_only( |
| 1458 | + make_mocked_engine_adapter: t.Callable, mocker: MockerFixture |
| 1459 | +) -> None: |
| 1460 | + """BigQuery: same columns, only clustering key removed — one DROP CLUSTERING KEY.""" |
| 1461 | + adapter = make_mocked_engine_adapter(BigQueryEngineAdapter, patch_get_data_objects=False) |
| 1462 | + |
| 1463 | + columns = {"tenant_id": "INT64", "partition_month": "INT64"} |
| 1464 | + adapter.columns = lambda table_name, **kwargs: { |
| 1465 | + k: exp.DataType.build(v, dialect="bigquery") for k, v in columns.items() |
| 1466 | + } |
| 1467 | + |
| 1468 | + _mock_bq_data_objects( |
| 1469 | + mocker, |
| 1470 | + adapter, |
| 1471 | + current_obj=DataObject( |
| 1472 | + catalog="project", |
| 1473 | + schema="dataset", |
| 1474 | + name="current_table", |
| 1475 | + type=DataObjectType.TABLE, |
| 1476 | + clustering_key="(partition_month)", |
| 1477 | + ), |
| 1478 | + target_obj=DataObject( |
| 1479 | + catalog="project", |
| 1480 | + schema="dataset", |
| 1481 | + name="target_table", |
| 1482 | + type=DataObjectType.TABLE, |
| 1483 | + clustering_key=None, |
| 1484 | + ), |
| 1485 | + ) |
| 1486 | + |
| 1487 | + operations = adapter.get_alter_operations( |
| 1488 | + "project.dataset.current_table", "project.dataset.target_table" |
| 1489 | + ) |
| 1490 | + |
| 1491 | + assert len(operations) == 1 |
| 1492 | + assert isinstance(operations[0], TableAlterDropClusterKeyOperation) |
| 1493 | + |
| 1494 | + |
| 1495 | +def test_unchanged_clustering_key_is_not_altered_when_column_added( |
| 1496 | + adapter: BigQueryEngineAdapter, mocker: MockerFixture |
| 1497 | +): |
| 1498 | + # Adding a column to a clustered table must not emit a clustering change when the key |
| 1499 | + # itself is unchanged. BigQuery applies a new key by rewriting the clustered columns. |
| 1500 | + current_columns = {"c1": exp.DataType.build("INT64", dialect="bigquery")} |
| 1501 | + target_columns = { |
| 1502 | + **current_columns, |
| 1503 | + "c2": exp.DataType.build("INT64", dialect="bigquery"), |
| 1504 | + } |
| 1505 | + |
| 1506 | + def columns(table_name: str, include_pseudo_columns: bool = False) -> t.Dict[str, exp.DataType]: |
| 1507 | + if exp.to_table(table_name).name == "current_table": |
| 1508 | + return current_columns |
| 1509 | + return target_columns |
| 1510 | + |
| 1511 | + mocker.patch.object(adapter, "columns", side_effect=columns) |
| 1512 | + |
| 1513 | + def get_data_objects( |
| 1514 | + schema_name: exp.Table, |
| 1515 | + object_names: t.Optional[t.Set[str]] = None, |
| 1516 | + safe_to_cache: bool = False, |
| 1517 | + ) -> t.List[DataObject]: |
| 1518 | + assert object_names is not None |
| 1519 | + return [ |
| 1520 | + DataObject( |
| 1521 | + schema=schema_name.db, |
| 1522 | + name=next(iter(object_names)), |
| 1523 | + type=DataObjectType.TABLE, |
| 1524 | + clustering_key="(c1)", |
| 1525 | + ) |
| 1526 | + ] |
| 1527 | + |
| 1528 | + mocker.patch.object(adapter, "get_data_objects", side_effect=get_data_objects) |
| 1529 | + # Let a wrongly emitted clustering change surface as the column-rewrite UPDATE. |
| 1530 | + mocker.patch.object(adapter, "_get_table", return_value=mocker.Mock()) |
| 1531 | + mocker.patch.object(adapter, "_db_call") |
| 1532 | + |
| 1533 | + operations = adapter.get_alter_operations( |
| 1534 | + "test_schema.current_table", "test_schema.target_table" |
| 1535 | + ) |
| 1536 | + assert not any(isinstance(operation, TableAlterClusterByOperation) for operation in operations) |
| 1537 | + |
| 1538 | + adapter.alter_table(operations) |
| 1539 | + |
| 1540 | + assert _to_sql_calls(adapter) == [ |
| 1541 | + "ALTER TABLE `test_schema`.`current_table` ADD COLUMN `c2` INT64" |
| 1542 | + ] |
0 commit comments