blob: 06eb260695782688cb68e8b9c8bc9454eed0e3cc [file]
#
# Licensed to the Apache Software Foundation (ASF) under one or more
# contributor license agreements. See the NOTICE file distributed with
# this work for additional information regarding copyright ownership.
# The ASF licenses this file to You under the Apache License, Version 2.0
# (the "License"); you may not use this file except in compliance with
# the License. You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import unittest
from pyspark.errors import AnalysisException
from pyspark.sql import functions as sf
from pyspark.sql.tests.test_column import ColumnTestsMixin
from pyspark.testing.connectutils import ReusedConnectTestCase
class ColumnParityTests(ColumnTestsMixin, ReusedConnectTestCase):
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.spark.conf.set("spark.sql.analyzer.strictDataFrameColumnResolution", "true")
@classmethod
def tearDownClass(cls):
try:
cls.spark.conf.unset("spark.sql.analyzer.strictDataFrameColumnResolution")
finally:
super().tearDownClass()
@unittest.skip("Requires JVM access.")
def test_validate_column_types(self):
super().test_validate_column_types()
def test_resolve_after_union(self):
# Connect diverges from Classic here: Union is treated as a leaf when
# walking the plan tree for plan-id resolution, so the left-side plan
# id is never found and CANNOT_RESOLVE_DATAFRAME_COLUMN is thrown
# before any name-based fallback - in both strict and lenient modes.
df1 = self.spark.sql("SELECT 1 AS c")
df2 = self.spark.sql("SELECT 2 AS c")
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
df1.union(df2).select(df1.c).collect()
# zip merges the two column-projected sides into a single plan, so the
# per-DataFrame plan-id tags do not survive ResolveZip. A tagged left/right
# reference can no longer be found and raises in both strict and lenient
# modes - the throw precedes any name-based fallback - unlike Classic, which
# resolves by the attribute id that ResolveZip preserves. A base-side
# reference (df.zip(right).select(df.a)) raises too when the base's tagged
# plan root is a Project that the rewrite dissolves (createDataFrame,
# overridden below); only a bare relation root like range survives as the
# merged base and resolves (test_resolve_after_zip_bare_base_side,
# inherited with no override).
def test_resolve_after_zip(self):
df = self.spark.createDataFrame([(1, 10), (2, 20), (3, 30)], ["a", "b"])
left = df.select((df.a + 1).alias("x"))
right = df.select((df.b * 2).alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select(left.x).collect()
def test_resolve_after_zip_reordered(self):
df = self.spark.createDataFrame([(1, 10), (2, 20)], ["a", "b"])
left = df.select(df.a.alias("x"))
right = df.select(df.b.alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select(right.y, left.x).collect()
def test_resolve_after_zip_duplicate_names(self):
df = self.spark.createDataFrame([(1, 2), (3, 4)], ["a", "b"])
left = df.select(df.a.alias("v"))
right = df.select(df.b.alias("v"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select(left.v).collect()
def test_resolve_after_zip_shared_producer(self):
df = self.spark.createDataFrame([(1, 2), (3, 4)], ["a", "b"])
left = df.select(df.a.alias("v"))
right = df.select(df.a.alias("v"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select(right.v).collect()
def test_resolve_after_zip_in_expression(self):
df = self.spark.createDataFrame([(1, 10), (2, 20), (3, 30)], ["a", "b"])
left = df.select(df.a.alias("x"))
right = df.select(df.b.alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select((left.x + right.y).alias("s")).collect()
def test_resolve_after_zip_in_filter(self):
df = self.spark.createDataFrame([(1, 10), (2, 20), (3, 30)], ["a", "b"])
left = df.select(df.a.alias("x"))
right = df.select(df.b.alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).filter(left.x >= 2).collect()
def test_resolve_after_zip_chained(self):
df = self.spark.createDataFrame([(1, 2, 3), (4, 5, 6)], ["a", "b", "c"])
left0 = df.select("a", "b")
left = left0.select((left0.a + 1).alias("x"))
right0 = df.select("b", "c")
right = right0.select((right0.c * 2).alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
left.zip(right).select(left.x, right.y).collect()
def test_resolve_after_zip_base_side(self):
df = self.spark.createDataFrame([(1, 10), (2, 20), (3, 30)], ["a", "b"])
right = df.select((df.b * 2).alias("y"))
with self.assertRaisesRegex(AnalysisException, "CANNOT_RESOLVE_DATAFRAME_COLUMN"):
df.zip(right).select(df.a).collect()
def test_df_col_resolution_mode(self):
self.assertEqual(
self.spark.conf.get("spark.sql.analyzer.strictDataFrameColumnResolution"),
"true",
)
class ColumnParityTestsWithNonStrictDFColResolution(ColumnParityTests):
"""Re-run the Column parity tests with
`spark.sql.analyzer.strictDataFrameColumnResolution=false` to exercise the
name-based fallback path for tagged UnresolvedAttributes."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.spark.conf.set("spark.sql.analyzer.strictDataFrameColumnResolution", "false")
@classmethod
def tearDownClass(cls):
try:
cls.spark.conf.unset("spark.sql.analyzer.strictDataFrameColumnResolution")
finally:
super().tearDownClass()
def test_df_col_resolution_mode(self):
self.assertEqual(
self.spark.conf.get("spark.sql.analyzer.strictDataFrameColumnResolution"),
"false",
)
# The shadowing trio diverges in lenient mode: where Classic and Connect
# strict raise, lenient resolves the tagged reference by name against the
# current (shadowed) output.
def test_resolve_after_chained_withcolumn_shadow(self):
df = self.spark.sql("SELECT 1 AS c")
rows = (
df.withColumn("c", sf.col("c").cast("string"))
.withColumn("c", sf.col("c").cast("int"))
.select(df.c)
.collect()
)
self.assertEqual([r.c for r in rows], [1])
def test_resolve_after_select_alias_shadow(self):
df = self.spark.sql("SELECT 1 AS c")
rows = df.select(df.c.cast("string").alias("c")).select(df.c).collect()
self.assertEqual([r.c for r in rows], ["1"])
def test_resolve_after_agg_alias_shadow(self):
df = self.spark.sql("SELECT 1 AS c")
rows = df.groupBy().agg(sf.sum("c").alias("c")).select(df.c).collect()
self.assertEqual([r.c for r in rows], [1])
if __name__ == "__main__":
from pyspark.testing import main
main()