Summary
df.write.format("lance").mode("overwrite").save("<new_path>") fails with org.apache.spark.sql.catalyst.analysis.NoSuchTableException when <new_path> does not yet exist. This is the most idiomatic Spark write call, and it diverges from how parquet/delta/iceberg handle the same pattern.
The same call succeeds with SaveMode.ErrorIfExists or SaveMode.Ignore.
Reproduction
import org.apache.spark.sql.SaveMode
import spark.implicits._
val newPath = "s3://bucket/some/brand/new/path" // does not exist yet
val df = Seq((1L, "Alice", 25), (2L, "Bob", 30)).toDF("id", "name", "age")
df.write.mode(SaveMode.Overwrite).format("lance").save(newPath)
Stack trace:
org.apache.spark.sql.catalyst.analysis.NoSuchTableException:
[TABLE_OR_VIEW_NOT_FOUND] The table or view ... cannot be found. SQLSTATE: 42P01
at org.lance.spark.BaseLanceNamespaceSparkCatalog.loadTableFromPath(BaseLanceNamespaceSparkCatalog.java:1509)
at org.lance.spark.BaseLanceNamespaceSparkCatalog.loadTableInternal(BaseLanceNamespaceSparkCatalog.java:1388)
at org.lance.spark.BaseLanceNamespaceSparkCatalog.loadTable(BaseLanceNamespaceSparkCatalog.java:557)
at org.apache.spark.sql.DataFrameWriter.saveInternal(DataFrameWriter.scala:376)
at org.apache.spark.sql.DataFrameWriter.save(DataFrameWriter.scala:302)
Environment
- lance-spark:
0.5.0-beta.1 (also confirmed on v0.4.0)
- Spark: 3.5.2
- Tested on Databricks Runtime 15.4 LTS and 16.4 LTS (Java 17); not Databricks-specific — the failing code path is purely in
BaseLanceNamespaceSparkCatalog + Spark's DataFrameWriter.
Root cause
In Spark 3.5's DataFrameWriter.saveInternal, the two save-mode branches behave differently when the data source implements SupportsCatalogOptions:
| Mode |
loadTable probe |
Next step |
Result |
ErrorIfExists |
not called |
CreateTableAsSelect |
✓ |
Ignore |
not called |
CreateTableAsSelect (ignoreIfExists=true) |
✓ |
Append |
called, unguarded |
n/a |
✗ propagates (arguably correct) |
Overwrite |
called, unguarded |
n/a |
✗ propagates ← this bug |
The Append | Overwrite branch in DataFrameWriter.scala calls catalog.loadTable(ident) directly without catching NoSuchTableException, so lance-spark's loadTableFromPath throwing for a missing path propagates straight to the user.
Parquet/Delta don't hit this because they only implement TableProvider, not SupportsCatalogOptions, and route through saveToV1Source.
Workaround
For first-time writes of a new path, use SaveMode.ErrorIfExists or SaveMode.Ignore:
df.write.mode(SaveMode.ErrorIfExists).format("lance").save(newPath)
Subsequent Append/Overwrite against the now-existing path work normally.
Summary
df.write.format("lance").mode("overwrite").save("<new_path>")fails withorg.apache.spark.sql.catalyst.analysis.NoSuchTableExceptionwhen<new_path>does not yet exist. This is the most idiomatic Spark write call, and it diverges from how parquet/delta/iceberg handle the same pattern.The same call succeeds with
SaveMode.ErrorIfExistsorSaveMode.Ignore.Reproduction
Stack trace:
Environment
0.5.0-beta.1(also confirmed onv0.4.0)BaseLanceNamespaceSparkCatalog+ Spark'sDataFrameWriter.Root cause
In Spark 3.5's
DataFrameWriter.saveInternal, the two save-mode branches behave differently when the data source implementsSupportsCatalogOptions:loadTableprobeErrorIfExistsCreateTableAsSelectIgnoreCreateTableAsSelect (ignoreIfExists=true)AppendOverwriteThe
Append | Overwritebranch inDataFrameWriter.scalacallscatalog.loadTable(ident)directly without catchingNoSuchTableException, so lance-spark'sloadTableFromPaththrowing for a missing path propagates straight to the user.Parquet/Delta don't hit this because they only implement
TableProvider, notSupportsCatalogOptions, and route throughsaveToV1Source.Workaround
For first-time writes of a new path, use
SaveMode.ErrorIfExistsorSaveMode.Ignore:Subsequent
Append/Overwriteagainst the now-existing path work normally.