[SPARK-49828][SQL] Make Column(expression) usable outside of the org.apache.spark package - #57759
[SPARK-49828][SQL] Make Column(expression) usable outside of the org.apache.spark package#57759peter-toth wants to merge 3 commits into
Conversation
…apache.spark package `ClassicConversions.ColumnConstructorExt` is a public `@DeveloperApi` that adds a `Column(e: Expression)` factory to the `Column` companion, but it cannot be used outside of `org.apache.spark`, because `object Column` itself is `private[spark]`. `ExpressionUtils`, `ExpressionColumnNode` and `ColumnNode` are package private too, so since SPARK-49022 removed the public `new Column(expr: Expression)` constructor there has been no public Expression -> Column path at all. The reverse direction is public and works: `ColumnConversions.expression(col)`. This makes `object Column` public. Every member of it is already `private[spark]` or narrower, so the already shipped extension resolves while no new member becomes visible; the two `apply` overloads that relied on the object's own visibility are now marked `private[spark]` explicitly. It also adds a named `ClassicConversions.column(e)` for callers that prefer not to rely on an implicit. The named factory is on the object rather than on the `ClassicConversions` trait on purpose: on the trait it shadows `functions.column(colName: String)` for everyone who mixes the trait in, which breaks existing call sites. Prior art: apache#48306 by holdenk, which took the broader approach of exposing the ColumnNode AST types and was closed by the stale bot.
|
LGTM |
sql/api is a scalafmt governed module with maxColumn = 98, one line was 99.
| * `org.apache.spark.sql.classic.ClassicConversions.ColumnConstructorExt` usable outside of the | ||
| * `org.apache.spark` package. All of its members intentionally stay internal. | ||
| */ | ||
| object Column { |
There was a problem hiding this comment.
If we change the visibility, we need annotations like we did for class Column.
spark/sql/api/src/main/scala/org/apache/spark/sql/Column.scala
Lines 139 to 140 in 496d20f
In this case, which one do you propose in this PR? Stable or DeveloperApi, @peter-toth ?
There was a problem hiding this comment.
Good catch, thanks. I went with @DeveloperApi (plus @since 4.4.0) in f4b2d23.
Reasoning: everything that makes Column(expression) work is already @DeveloperApi — ClassicConversions and ColumnConstructorExt — so the path is only as stable as those, and @Stable on the companion would over-promise. The object also has no public members; it is public purely so the type can be named as a value.
The counter-argument, in case you'd rather have @Stable: @DeveloperApi does let us make the object private again in a minor release, which would re-break the use case this PR fixes. Happy to switch if you prefer that guarantee.
| /** | ||
| * The companion object is public so that extension developers can reference the `Column` type as | ||
| * a value, which is what makes the `Column(expression)` factory provided by | ||
| * `org.apache.spark.sql.classic.ClassicConversions.ColumnConstructorExt` usable outside of the |
There was a problem hiding this comment.
This new documentation might be misleading to the Spark Connect users because org.apache.spark.sql.classic.* is a classic-specific API.
When a Spark Connect user traverses API doc, he/she reaches this object Column doc and will see this. Can we revise this doc into more nuetural way?
There was a problem hiding this comment.
Agreed, that was a mistake on my side — Column.scala is in sql/api, which Connect shares, so pointing at a classic-only API from there is wrong. Dropped the ClassicConversions.ColumnConstructorExt reference in f4b2d23; the doc now just says an implementation can add the factory via an extension method.
dongjoon-hyun
left a comment
There was a problem hiding this comment.
+1 for the direction. LGTM (with two minor comments).
- Mark `object Column` `@DeveloperApi` with `@since 4.4.0`, matching the audience of the conversions that make `Column(expression)` work, which are `@DeveloperApi` themselves. - Drop the classic-specific `ClassicConversions.ColumnConstructorExt` reference from the scaladoc. `Column.scala` lives in `sql/api`, which Spark Connect shares, so a Connect user browsing the API docs would have been pointed at a classic-only API.
What changes were proposed in this pull request?
This makes the intended
Column(expression)conversion usable from outside theorg.apache.sparkpackage:object Column(sql/api) becomes public. Every member of it is alreadyprivate[spark]or narrower, so no new member becomes visible -- the object simply becomes nameable, which is all the extension in point 2 needs. The twoapplyoverloads that relied on the object's own visibility are now markedprivate[spark]explicitly.@DeveloperApi ClassicConversions.column(e: Expression): Columnis added, for callers who would rather not rely on an implicit.ExpressionToColumnSuitein packagetest.org.apache.spark.sql, i.e. outsideorg.apache.spark, pins both entry points and the round trip.Why are the changes needed?
ClassicConversions.ColumnConstructorExtis already a public@DeveloperApi:It is meant to let extension developers write
Column(expr), but it cannot be used outsideorg.apache.spark, becauseobject Columnis itselfprivate[spark]. SoColumn(expr)does not even compile there:ExpressionUtils,ExpressionColumnNodeandColumnNodeare package private as well, and SPARK-49022 removed the publicnew Column(expr: Expression)constructor, so there is currently no public Expression -> Column path at all. The reverse direction is public and works fine (ColumnConversions.expression(col),col.expr), which makes the gap asymmetric. Today the only workaround is to put the helper in a package underorg.apache.spark, which is whatmllib'sml/stat/Summarizer.scaladoes.This also makes an existing review comment on #48306 true: @hvanhovell suggested "You could call
org.apache.spark.sql.classic.ClassicConversions.columndirectly if you want to avoid implicits" -- that method did not exist until now.Prior art: #48306 by @holdenk took the broader approach of exposing the
ColumnNodeAST types (ExpressionColumnNode,ColumnNodeToExpressionConverter) and movingcolumnNodeSupport.scalaout ofsql.internal. It was approved by @hvanhovell, then the approval was dismissed over the package choice, and the stale bot closed it. Most of that diff is now obsolete, since the file has meanwhile moved toorg.apache.spark.sql.classic. This PR intentionally stays narrower and leaves the AST types internal, so Spark keeps freedom over their shape.Three points reviewers may want to weigh in on:
object ClassicConversionsrather than on theClassicConversionstrait. On the trait it shadowsfunctions.column(colName: String)for anyone who mixes the trait in, which breaks existing call sites -- 6 of them insql/core's own tests, viaQueryTest.testImplicits. The trade-off is that a cross-version shim implementing the trait does not inherit it.import ClassicConversions._combined withimport functions.columnis now ambiguous. It is opt-in and a compile error rather than anything silent, but happy to rename (columnOf,toColumn) if preferred;columnwas chosen to mirrorColumnConversions.expression.object Columnhas no public members, so it shows up as an empty entry in the generated API docs. I left it without an@sincetag, since the object itself has existed since 4.0, just not publicly.Does this PR introduce any user-facing change?
Yes, it adds public API. No existing behavior changes.
object Columnbecomes public (no new members).ClassicConversions.column(e: Expression): Columnis new, tagged@since 4.4.0.Extension developers outside
org.apache.sparkcan now write either:How was this patch tested?
New
sql/core/src/test/scala/test/org/apache/spark/sql/ExpressionToColumnSuite.scala. Its package is outsideorg.apache.spark, so compiling it is as much of the test as running it -- that is the piece #48306 lacked, which is how the visibility gap went unnoticed in the first place.masterwithnot found: value Columnat bothColumn(...)sites, which is exactly the error a user hits.ColumnExpressionSuite,JavaColumnExpressionSuite,DataFrameSuite,DatasetSuite,JavaDatasetSuiteand the fullsql-apitest suite pass (~900 tests).spark-sql_2.13:4.0.0andspark-sql-api_2.13:4.0.0, andcompileis clean across all modules, includingmllib, which consumes theColumn(node)overload.Was this patch authored or co-authored using generative AI tooling?
Generated-by: Claude Code