|
2 | 2 | # Copyright (c) 2012-2025 Snowflake Computing Inc. All rights reserved. |
3 | 3 | # |
4 | 4 |
|
5 | | -from typing import List |
| 5 | +from typing import Dict, List |
6 | 6 | import copy |
7 | 7 | import pytest |
8 | 8 |
|
9 | 9 | import snowflake.snowpark._internal.analyzer.snowflake_plan as snowflake_plan |
10 | 10 |
|
11 | 11 | from unittest.mock import patch |
12 | 12 |
|
13 | | -from snowflake.snowpark import DataFrame |
| 13 | +from snowflake.snowpark import DataFrame, Row |
14 | 14 | from snowflake.snowpark._internal.analyzer.expression import Attribute, Star |
15 | 15 | from snowflake.snowpark._internal.analyzer.unary_expression import UnresolvedAlias |
16 | 16 | from snowflake.snowpark._internal.analyzer.unary_plan_node import Project |
|
34 | 34 | _PYTHON_SNOWPARK_REDUCE_DESCRIBE_QUERY_ENABLED, |
35 | 35 | Session, |
36 | 36 | ) |
37 | | -from snowflake.snowpark.types import LongType, StructField, StructType |
| 37 | +from snowflake.snowpark.types import LongType, StringType, StructField, StructType |
38 | 38 | from tests.integ.utils.sql_counter import SqlCounter |
39 | | -from tests.utils import IS_IN_STORED_PROC, TestData |
| 39 | +from tests.utils import IS_IN_STORED_PROC, TestData, Utils |
40 | 40 |
|
41 | 41 | pytestmark = [ |
42 | 42 | pytest.mark.skipif( |
@@ -228,6 +228,10 @@ def check_attributes_equality(attrs1: List[Attribute], attrs2: List[Attribute]) |
228 | 228 | assert attr1.nullable == attr2.nullable |
229 | 229 |
|
230 | 230 |
|
| 231 | +def _attrs_by_name(parent_attributes: List[Attribute]) -> Dict[str, Attribute]: |
| 232 | + return {attr.name: attr for attr in parent_attributes} |
| 233 | + |
| 234 | + |
231 | 235 | def has_star_in_projection(df: DataFrame) -> bool: |
232 | 236 | plan = df._plan.source_plan |
233 | 237 | return isinstance(plan, Project) and any( |
@@ -441,6 +445,142 @@ def test_project_alias_infers_attributes_from_parent_metadata(session): |
441 | 445 | check_attributes_equality(df2._plan.attributes, expected_attributes) |
442 | 446 |
|
443 | 447 |
|
| 448 | +def test_swap_column_aliases_infers_types_from_source_names(session): |
| 449 | + """n-1: columns a, b; n: b AS a, a AS b — metadata follows source column types.""" |
| 450 | + df = session.create_dataframe([[1, 2]], schema=["a", "b"]) |
| 451 | + _ = df.schema |
| 452 | + |
| 453 | + parent_attributes = df._plan._metadata.attributes |
| 454 | + assert parent_attributes is not None |
| 455 | + by_name = _attrs_by_name(parent_attributes) |
| 456 | + expected_attributes = [ |
| 457 | + by_name['"B"'].with_name("a"), |
| 458 | + by_name['"A"'].with_name("b"), |
| 459 | + ] |
| 460 | + |
| 461 | + df2 = df.select(col("b").alias("a"), col("a").alias("b")) |
| 462 | + Utils.check_answer(df2, [Row(A=2, B=1)], sort=False) |
| 463 | + |
| 464 | + if session.reduce_describe_query_enabled: |
| 465 | + check_attributes_equality(df2._plan._metadata.attributes, expected_attributes) |
| 466 | + expected_describe_count = 0 |
| 467 | + else: |
| 468 | + assert df2._plan._metadata.attributes is None |
| 469 | + expected_describe_count = 1 |
| 470 | + |
| 471 | + with SqlCounter(query_count=0, describe_count=expected_describe_count): |
| 472 | + check_attributes_equality(df2._plan.attributes, expected_attributes) |
| 473 | + |
| 474 | + |
| 475 | +def test_swap_mixed_column_types_inference_follows_source(session): |
| 476 | + """Swapped output names must take datatypes from the referenced source column.""" |
| 477 | + schema = StructType( |
| 478 | + [ |
| 479 | + StructField("a", LongType(), nullable=True), |
| 480 | + StructField("b", StringType(), nullable=True), |
| 481 | + ] |
| 482 | + ) |
| 483 | + df = session.create_dataframe([(1, "x")], schema=schema) |
| 484 | + _ = df.schema |
| 485 | + |
| 486 | + parent_attributes = df._plan._metadata.attributes |
| 487 | + assert parent_attributes is not None |
| 488 | + by_name = _attrs_by_name(parent_attributes) |
| 489 | + expected_attributes = [ |
| 490 | + by_name['"B"'].with_name("a"), |
| 491 | + by_name['"A"'].with_name("b"), |
| 492 | + ] |
| 493 | + |
| 494 | + df2 = df.select(col("b").alias("a"), col("a").alias("b")) |
| 495 | + Utils.check_answer(df2, [Row(A="x", B=1)], sort=False) |
| 496 | + |
| 497 | + if session.reduce_describe_query_enabled: |
| 498 | + check_attributes_equality(df2._plan._metadata.attributes, expected_attributes) |
| 499 | + expected_describe_count = 0 |
| 500 | + else: |
| 501 | + assert df2._plan._metadata.attributes is None |
| 502 | + expected_describe_count = 1 |
| 503 | + |
| 504 | + with SqlCounter(query_count=0, describe_count=expected_describe_count): |
| 505 | + check_attributes_equality(df2._plan.attributes, expected_attributes) |
| 506 | + |
| 507 | + |
| 508 | +def test_column_permutation_inference_name_keyed_lookup(session): |
| 509 | + """Non-swap rename: c->a, a->x, b->y — each output type matches its source column.""" |
| 510 | + df = session.create_dataframe([[1, 2, 3]], schema=["a", "b", "c"]) |
| 511 | + _ = df.schema |
| 512 | + |
| 513 | + parent_attributes = df._plan._metadata.attributes |
| 514 | + assert parent_attributes is not None |
| 515 | + by_name = _attrs_by_name(parent_attributes) |
| 516 | + expected_attributes = [ |
| 517 | + by_name['"C"'].with_name("a"), |
| 518 | + by_name['"A"'].with_name("x"), |
| 519 | + by_name['"B"'].with_name("y"), |
| 520 | + ] |
| 521 | + |
| 522 | + df2 = df.select(col("c").alias("a"), col("a").alias("x"), col("b").alias("y")) |
| 523 | + Utils.check_answer(df2, [Row(A=3, X=1, Y=2)], sort=False) |
| 524 | + |
| 525 | + if session.reduce_describe_query_enabled: |
| 526 | + check_attributes_equality(df2._plan._metadata.attributes, expected_attributes) |
| 527 | + expected_describe_count = 0 |
| 528 | + else: |
| 529 | + assert df2._plan._metadata.attributes is None |
| 530 | + expected_describe_count = 1 |
| 531 | + |
| 532 | + with SqlCounter(query_count=0, describe_count=expected_describe_count): |
| 533 | + check_attributes_equality(df2._plan.attributes, expected_attributes) |
| 534 | + |
| 535 | + |
| 536 | +def test_chained_simple_renames_infer_from_previous_metadata(session): |
| 537 | + """Second select's parent already has inferred attributes from the first rename.""" |
| 538 | + df = session.create_dataframe([[10, 20]], schema=["a", "b"]) |
| 539 | + _ = df.schema |
| 540 | + |
| 541 | + df1 = df.select(col("a").alias("p"), col("b").alias("q")) |
| 542 | + if session.reduce_describe_query_enabled: |
| 543 | + assert df1._plan._metadata.attributes is not None |
| 544 | + mid_attrs = df1._plan._metadata.attributes |
| 545 | + assert mid_attrs is not None or not session.reduce_describe_query_enabled |
| 546 | + |
| 547 | + df2 = df1.select(col("p").alias("x"), col("q").alias("y")) |
| 548 | + _ = df1.schema |
| 549 | + |
| 550 | + if session.reduce_describe_query_enabled: |
| 551 | + assert df2._plan._metadata.attributes is not None |
| 552 | + by_mid = _attrs_by_name(df1._plan._metadata.attributes or []) |
| 553 | + expected_attributes = [ |
| 554 | + by_mid['"P"'].with_name("x"), |
| 555 | + by_mid['"Q"'].with_name("y"), |
| 556 | + ] |
| 557 | + check_attributes_equality(df2._plan._metadata.attributes, expected_attributes) |
| 558 | + with SqlCounter(query_count=0, describe_count=0): |
| 559 | + check_attributes_equality(df2._plan.attributes, expected_attributes) |
| 560 | + else: |
| 561 | + assert df2._plan._metadata.attributes is None |
| 562 | + with SqlCounter(query_count=0, describe_count=1): |
| 563 | + _ = df2._plan.attributes |
| 564 | + |
| 565 | + |
| 566 | +def test_non_simple_projection_skips_metadata_inference(session): |
| 567 | + """Expressions other than plain column or simple alias(column) do not infer attributes.""" |
| 568 | + df = session.create_dataframe([[1, 2]], schema=["a", "b"]) |
| 569 | + _ = df.schema |
| 570 | + |
| 571 | + df2 = df.select((col("a") + lit(1)).alias("ap1"), "b") |
| 572 | + |
| 573 | + assert df2._plan._metadata.attributes is None |
| 574 | + |
| 575 | + with SqlCounter(query_count=0, describe_count=1): |
| 576 | + _ = df2._plan.attributes |
| 577 | + |
| 578 | + df3 = df.select(col("a"), (col("b") + lit(1)).alias("b")) |
| 579 | + assert df3._plan._metadata.attributes is None |
| 580 | + with SqlCounter(query_count=0, describe_count=1): |
| 581 | + _ = df3._plan.attributes |
| 582 | + |
| 583 | + |
444 | 584 | @pytest.mark.skipif(IS_IN_STORED_PROC, reason="Can't create a session in SP") |
445 | 585 | def test_reduce_describe_query_enabled_on_session(db_parameters): |
446 | 586 | with Session.builder.configs(db_parameters).create() as new_session: |
|
0 commit comments