@@ -28,21 +28,43 @@ import io.delta.storage.uc.UCDeltaClient
2828import io .unitycatalog .client .ApiException
2929import io .unitycatalog .client .auth .TokenProvider
3030import io .unitycatalog .client .delta .model .{
31+ CreateTableRequest ,
3132 CredentialOperation ,
3233 CredentialsResponse ,
3334 DataSourceFormat => DeltaDataSourceFormat ,
35+ DeltaProtocol => DeltaRestProtocol ,
3436 StorageCredential ,
37+ StagingTableResponse ,
38+ StagingTableResponseRequiredProtocol ,
3539 TableType => DeltaTableType
3640}
3741
3842import org .apache .spark .sql .SparkSession
3943import org .apache .spark .sql .catalyst .analysis .NoSuchTableException
4044import org .apache .spark .sql .catalyst .TableIdentifier
41- import org .apache .spark .sql .catalyst .catalog .{CatalogStorageFormat , CatalogTable , CatalogTableType , CatalogUtils }
42- import org .apache .spark .sql .connector .catalog .{CatalogPlugin , Identifier , Table , TableCatalog , V1Table }
45+ import org .apache .spark .sql .catalyst .catalog .{
46+ CatalogStorageFormat ,
47+ CatalogTable ,
48+ CatalogTableType ,
49+ CatalogUtils
50+ }
51+ import org .apache .spark .sql .connector .catalog .{
52+ CatalogPlugin ,
53+ Identifier ,
54+ Table ,
55+ TableCatalog ,
56+ V1Table
57+ }
58+ import org .apache .spark .sql .delta .Snapshot
59+ import org .apache .spark .sql .delta .actions .Protocol
4360import org .apache .spark .sql .delta .coordinatedcommits .UCCommitCoordinatorBuilder
4461import org .apache .spark .sql .delta .sources .DeltaSourceUtils
4562
63+ private [catalog] case class PreparedDeltaRestCreate (
64+ location : URI ,
65+ tableProperties : Map [String , String ],
66+ storageProperties : Map [String , String ])
67+
4668private class DeltaCatalogClient private (
4769 private val ucDeltaClient : Option [UCDeltaClient ],
4870 delegate : TableCatalog ,
@@ -80,6 +102,61 @@ private class DeltaCatalogClient private (
80102 }
81103 }
82104
105+ def prepareCreateTable (
106+ ident : Identifier ,
107+ tableType : CatalogTableType ,
108+ location : Option [URI ]): Option [PreparedDeltaRestCreate ] = {
109+ ucDeltaClient match {
110+ case Some (client) if ident.namespace().length == 1 =>
111+ val schemaName = ident.namespace().head
112+ val tableName = ident.name()
113+ (tableType, location) match {
114+ case (CatalogTableType .MANAGED , None ) =>
115+ val staging = client.createStagingTable(catalogName, schemaName, tableName)
116+ val stagingLocation = CatalogUtils .stringToURI(staging.getLocation)
117+ Some (PreparedDeltaRestCreate (
118+ location = stagingLocation,
119+ tableProperties = toTableProperties(staging),
120+ storageProperties = toCredentialProperties(
121+ staging.getLocation,
122+ Option (staging.getStorageCredentials).map(_.asScala.toSeq).getOrElse(Nil ),
123+ stagingLocation.getScheme)))
124+ case (CatalogTableType .EXTERNAL , Some (locationUri))
125+ if isCloudScheme(locationUri.getScheme) =>
126+ val locationString = locationUri.toString
127+ val credentials = client.getTemporaryPathCredentials(
128+ locationString,
129+ CredentialOperation .READ_WRITE )
130+ Some (PreparedDeltaRestCreate (
131+ location = locationUri,
132+ tableProperties = Map .empty,
133+ storageProperties = toCredentialProperties(
134+ locationString,
135+ getStorageCredentials(credentials),
136+ locationUri.getScheme)))
137+ case _ =>
138+ None
139+ }
140+ case _ =>
141+ None
142+ }
143+ }
144+
145+ def createTable (
146+ ident : Identifier ,
147+ table : CatalogTable ,
148+ snapshot : Snapshot ): Unit = {
149+ ucDeltaClient match {
150+ case Some (client) if ident.namespace().length == 1 =>
151+ client.createTable(
152+ catalogName,
153+ ident.namespace().head,
154+ toCreateTableRequest(ident, table, snapshot))
155+ case _ =>
156+ throw new IllegalStateException (s " Delta REST createTable is not available for $ident. " )
157+ }
158+ }
159+
83160 private def translateLoadTableException (ident : Identifier , e : IOException ): Throwable = {
84161 e.getCause match {
85162 case api : ApiException if api.getCode == 404 =>
@@ -128,25 +205,94 @@ private class DeltaCatalogClient private (
128205 metadata : io.unitycatalog.client.delta.model.TableMetadata ,
129206 credentials : Option [CredentialsResponse ],
130207 locationScheme : String ): Map [String , String ] = {
131- val storageCredentials = credentials.toSeq.flatMap(getStorageCredentials)
132- val credentialProperties =
133- if (! isCloudScheme(locationScheme)) {
134- Map .empty[String , String ]
135- } else if (storageCredentials.isEmpty) {
136- throw new IllegalArgumentException (
137- s " Delta REST returned no storage credentials for cloud location ${metadata.getLocation}. " )
138- } else {
139- selectStorageCredential(metadata.getLocation, storageCredentials)
140- .map(storageCredentialToProperties)
141- .map(withOptionPrefix)
142- .getOrElse {
143- throw new IllegalArgumentException (
144- s " No storage credential matched Delta REST location ${metadata.getLocation}. " )
145- }
146- }
208+ val credentialProperties = withOptionPrefix(
209+ toCredentialProperties(
210+ metadata.getLocation,
211+ credentials.toSeq.flatMap(getStorageCredentials),
212+ locationScheme))
147213 Map (UC_TABLE_ID_KEY -> metadata.getTableUuid.toString) ++ credentialProperties
148214 }
149215
216+ private def toCredentialProperties (
217+ location : String ,
218+ storageCredentials : Seq [StorageCredential ],
219+ locationScheme : String ): Map [String , String ] = {
220+ if (! isCloudScheme(locationScheme)) {
221+ Map .empty[String , String ]
222+ } else if (storageCredentials.isEmpty) {
223+ throw new IllegalArgumentException (
224+ s " Delta REST returned no storage credentials for cloud location $location. " )
225+ } else {
226+ selectStorageCredential(location, storageCredentials)
227+ .map(storageCredentialToProperties)
228+ .getOrElse {
229+ throw new IllegalArgumentException (
230+ s " No storage credential matched Delta REST location $location. " )
231+ }
232+ }
233+ }
234+
235+ private def toTableProperties (staging : StagingTableResponse ): Map [String , String ] = {
236+ protocolFeatureProperties(staging.getRequiredProtocol) ++
237+ Option (staging.getRequiredProperties)
238+ .map(_.asScala.collect { case (key, value) if value != null => key -> value }.toMap)
239+ .getOrElse(Map .empty) ++
240+ Map (
241+ TableCatalog .PROP_IS_MANAGED_LOCATION -> " true" ,
242+ UC_TABLE_ID_KEY -> staging.getTableId.toString)
243+ }
244+
245+ private def protocolFeatureProperties (
246+ protocol : StagingTableResponseRequiredProtocol ): Map [String , String ] = {
247+ Option (protocol).map { p =>
248+ (Option (p.getReaderFeatures).map(_.asScala).getOrElse(Nil ) ++
249+ Option (p.getWriterFeatures).map(_.asScala).getOrElse(Nil ))
250+ .map(feature => s " delta.feature. $feature" -> " supported" )
251+ .toMap
252+ }.getOrElse(Map .empty)
253+ }
254+
255+ private def toCreateTableRequest (
256+ ident : Identifier ,
257+ table : CatalogTable ,
258+ snapshot : Snapshot ): CreateTableRequest = {
259+ new CreateTableRequest ()
260+ .name(ident.name())
261+ .location(table.storage.locationUri
262+ .getOrElse {
263+ throw new IllegalArgumentException (
264+ s " Delta REST createTable requires a location for ${ident.toString}. " )
265+ }
266+ .toString)
267+ .tableType(toDeltaTableType(table.tableType))
268+ .dataSourceFormat(DeltaDataSourceFormat .DELTA )
269+ .comment(table.comment.orNull)
270+ .columns(DeltaRestSchemaConverter .toDeltaType(snapshot.schema))
271+ .partitionColumns(snapshot.metadata.partitionColumns.asJava)
272+ .protocol(toDeltaProtocol(snapshot.protocol))
273+ .properties(toDeltaCreateTableProperties(snapshot.metadata.configuration).asJava)
274+ }
275+
276+ private def toDeltaTableType (tableType : CatalogTableType ): DeltaTableType = tableType match {
277+ case CatalogTableType .MANAGED => DeltaTableType .MANAGED
278+ case CatalogTableType .EXTERNAL => DeltaTableType .EXTERNAL
279+ case other =>
280+ throw new IllegalArgumentException (s " Unsupported Delta REST table type: $other" )
281+ }
282+
283+ private def toDeltaProtocol (protocol : Protocol ): DeltaRestProtocol = {
284+ new DeltaRestProtocol ()
285+ .minReaderVersion(protocol.minReaderVersion)
286+ .minWriterVersion(protocol.minWriterVersion)
287+ .readerFeatures(protocol.readerFeatureNames.toSeq.sorted.asJava)
288+ .writerFeatures(protocol.writerFeatureNames.toSeq.sorted.asJava)
289+ }
290+
291+ private def toDeltaCreateTableProperties (
292+ properties : Map [String , String ]): Map [String , String ] = {
293+ properties -- DeltaCatalogClient .V2CreateTableProperties
294+ }
295+
150296 private def getStorageCredentials (credentials : CredentialsResponse ): Seq [StorageCredential ] = {
151297 Option (credentials)
152298 .flatMap(c => Option (c.getStorageCredentials))
@@ -219,6 +365,14 @@ private class DeltaCatalogClient private (
219365
220366private object DeltaCatalogClient {
221367 private val CloudSchemes = Set (" s3" , " s3a" , " gs" , " abfs" , " abfss" )
368+ private val V2CreateTableProperties = Set (
369+ TableCatalog .PROP_COMMENT ,
370+ TableCatalog .PROP_EXTERNAL ,
371+ TableCatalog .PROP_IS_MANAGED_LOCATION ,
372+ TableCatalog .PROP_LOCATION ,
373+ TableCatalog .PROP_OWNER ,
374+ TableCatalog .PROP_PROVIDER ,
375+ " path" )
222376
223377 private def isCloudScheme (scheme : String ): Boolean = {
224378 Option (scheme).exists(s => CloudSchemes .contains(s.toLowerCase(Locale .ROOT )))
0 commit comments