Skip to content
This repository was archived by the owner on Jun 14, 2024. It is now read-only.

Commit edafcfe

Browse files
authored
Check available index versions for Delta Lake time travel query (#389)
1 parent f971986 commit edafcfe

8 files changed

Lines changed: 74 additions & 17 deletions

File tree

src/main/scala/com/microsoft/hyperspace/index/IndexCollectionManager.scala

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,18 @@ class IndexCollectionManager(
138138
}
139139
}
140140

141+
override def getIndex(indexName: String, logVersion: Int): Option[IndexLogEntry] = {
142+
withLogManager(indexName) { logManager =>
143+
logManager.getLog(logVersion).map(_.asInstanceOf[IndexLogEntry])
144+
}
145+
}
146+
147+
override def getIndexVersions(indexName: String, states: Seq[String]): Seq[Int] = {
148+
withLogManager(indexName) { logManager =>
149+
logManager.getIndexVersions(states)
150+
}
151+
}
152+
141153
private def indexLogManagers: Seq[IndexLogManager] = {
142154
val hadoopConf = spark.sessionState.newHadoopConf()
143155
val rootPath = PathResolver(conf, hadoopConf).systemPath
@@ -162,12 +174,6 @@ class IndexCollectionManager(
162174
}
163175
}
164176

165-
override def getIndex(indexName: String, logVersion: Int): Option[IndexLogEntry] = {
166-
withLogManager(indexName) { logManager =>
167-
logManager.getLog(logVersion).map(_.asInstanceOf[IndexLogEntry])
168-
}
169-
}
170-
171177
private def withLogManager[T](indexName: String)(f: IndexLogManager => T): T = {
172178
getLogManager(indexName) match {
173179
case Some(logManager) => f(logManager)

src/main/scala/com/microsoft/hyperspace/index/IndexLogManager.scala

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@ trait IndexLogManager {
4040
/** Returns the latest LogEntry whose state is STABLE */
4141
def getLatestStableLog(): Option[LogEntry]
4242

43+
/** Returns index log versions whose state is in the given states */
44+
def getIndexVersions(states: Seq[String]): Seq[Int]
45+
4346
/** update latest.json symlink to the given id/path */
4447
def createLatestStableLog(id: Int): Boolean
4548

@@ -108,6 +111,21 @@ class IndexLogManagerImpl(indexPath: Path, hadoopConfiguration: Configuration =
108111
}
109112
}
110113

114+
override def getIndexVersions(states: Seq[String]): Seq[Int] = {
115+
val latestId = getLatestId()
116+
if (latestId.isDefined) {
117+
(latestId.get to 0 by -1).map { id =>
118+
getLog(id) match {
119+
case Some(entry) if states.contains(entry.state) =>
120+
Some(id)
121+
case _ => None
122+
}
123+
}.flatten
124+
} else {
125+
Seq()
126+
}
127+
}
128+
111129
override def createLatestStableLog(id: Int): Boolean = {
112130
getLog(id) match {
113131
case Some(logEntry) if Constants.STABLE_STATES.contains(logEntry.state) =>

src/main/scala/com/microsoft/hyperspace/index/IndexManager.scala

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,13 @@ trait IndexManager {
113113
* @return IndexLogEntry if the index of the given log version exists, otherwise None.
114114
*/
115115
def getIndex(indexName: String, logVersion: Int): Option[IndexLogEntry]
116+
117+
/**
118+
* Get index log version ids of the given index that match any of the given states.
119+
*
120+
* @param indexName Name of the index.
121+
* @param states List of index states of interest.
122+
* @return Index log versions.
123+
*/
124+
def getIndexVersions(indexName: String, states: Seq[String]): Seq[Int]
116125
}

src/main/scala/com/microsoft/hyperspace/index/sources/delta/DeltaLakeRelation.scala

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,10 @@ import org.apache.spark.sql.delta.files.TahoeLogFileIndex
2323
import org.apache.spark.sql.execution.datasources.{HadoopFsRelation, LogicalRelation}
2424

2525
import com.microsoft.hyperspace.Hyperspace
26-
import com.microsoft.hyperspace.index.{Content, FileIdTracker, FileInfo, Hdfs, IndexLogEntry, Relation}
26+
import com.microsoft.hyperspace.actions.Constants
27+
import com.microsoft.hyperspace.index.{Content, FileIdTracker, Hdfs, IndexLogEntry, Relation}
2728
import com.microsoft.hyperspace.index.sources.default.DefaultFileBasedRelation
28-
import com.microsoft.hyperspace.util.PathUtils
29+
import com.microsoft.hyperspace.util.{HyperspaceConf, PathUtils}
2930

3031
/**
3132
* Implementation for file-based relation used by [[DeltaLakeFileBasedSource]]
@@ -184,8 +185,17 @@ class DeltaLakeRelation(spark: SparkSession, override val plan: LogicalRelation)
184185
* @return IndexLogEntry of the closest version among available index versions.
185186
*/
186187
override def closestIndex(index: IndexLogEntry): IndexLogEntry = {
188+
// Only support when Hybrid Scan is enabled for both appended and deleted files.
189+
// TODO: Support time travel utilizing Hybrid Scan append-only.
190+
// See https://github.com/microsoft/hyperspace/issues/408.
191+
if (!(HyperspaceConf.hybridScanEnabled(spark) &&
192+
HyperspaceConf.hybridScanDeleteEnabled(spark) &&
193+
index.hasLineageColumn)) {
194+
return index
195+
}
196+
187197
// Seq of (index log version, delta lake table version)
188-
val versions = deltaLakeVersionHistory(index)
198+
val versionsHistory = deltaLakeVersionHistory(index)
189199

190200
lazy val indexManager = Hyperspace.getContext(spark).indexCollectionManager
191201
def getIndexLogEntry(logVersion: Int): IndexLogEntry = {
@@ -194,9 +204,12 @@ class DeltaLakeRelation(spark: SparkSession, override val plan: LogicalRelation)
194204
.get
195205
.asInstanceOf[IndexLogEntry]
196206
}
197-
// TODO: Currently assume all versions of index data exist.
198-
// Need to check and remove candidate indexes.
199-
// See https://github.com/microsoft/hyperspace/issues/387
207+
208+
val activeLogVersions =
209+
indexManager.getIndexVersions(index.name, Seq(Constants.States.ACTIVE))
210+
val versions = versionsHistory.filter {
211+
case (indexLogVersion, _) => activeLogVersions.contains(indexLogVersion)
212+
}
200213

201214
plan.relation match {
202215
case HadoopFsRelation(location: TahoeLogFileIndex, _, _, _, _, _) =>
@@ -206,7 +219,7 @@ class DeltaLakeRelation(spark: SparkSession, override val plan: LogicalRelation)
206219
if (equalOrLessThanLastIndex == versions.size - 1) {
207220
// The given table version is equal or larger than the latest index's.
208221
// Use the latest version.
209-
index
222+
getIndexLogEntry(versions.last._1)
210223
} else if (equalOrLessThanLastIndex == -1) {
211224
// The given table version is smaller than the version at index creation.
212225
// Use the initial version.

src/test/scala/com/microsoft/hyperspace/index/DeltaLakeIntegrationTest.scala

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ class DeltaLakeIntegrationTest extends QueryTest with HyperspaceSuite {
6868
dfFromSample.write.format("delta").save(dataPath)
6969

7070
val deltaDf = spark.read.format("delta").load(dataPath)
71-
hyperspace.createIndex(deltaDf, IndexConfig("deltaIndex", Seq("clicks"), Seq("Query")))
71+
72+
// Enable lineage column for Hybrid Scan delete support to test time travel.
73+
withSQLConf(IndexConstants.INDEX_LINEAGE_ENABLED -> "true") {
74+
hyperspace.createIndex(deltaDf, IndexConfig("deltaIndex", Seq("clicks"), Seq("Query")))
75+
}
7276

7377
withIndex("deltaIndex") {
7478
def query(version: Option[Long] = None): DataFrame = {
@@ -102,7 +106,8 @@ class DeltaLakeIntegrationTest extends QueryTest with HyperspaceSuite {
102106
// The index should not be applied for the version at index creation.
103107
assert(!isIndexUsed(query(Some(0)).queryExecution.optimizedPlan, "deltaIndex"))
104108

105-
withSQLConf(TestConfig.HybridScanEnabledAppendOnly: _*) {
109+
// Enable Hybrid Scan for time travel query validation.
110+
withSQLConf(TestConfig.HybridScanEnabled: _*) {
106111
// The index should be applied for the updated version.
107112
assert(isIndexUsed(query(Some(0)).queryExecution.optimizedPlan, "deltaIndex/v__=0"))
108113
}

src/test/scala/com/microsoft/hyperspace/index/IndexCollectionManagerTest.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class IndexCollectionManagerTest extends SparkFunSuite with SparkInvolvedSuite {
3737
override def createLatestStableLog(id: Int): Boolean = throw new NotImplementedError
3838
override def deleteLatestStableLog(): Boolean = throw new NotImplementedError
3939
override def writeLog(id: Int, log: LogEntry): Boolean = throw new NotImplementedError
40+
override def getIndexVersions(states: Seq[String]): Seq[Int] = Seq(0)
4041

4142
private val testLogEntry: IndexLogEntry = {
4243
val sourcePlanProperties = SparkPlan.Properties(

src/test/scala/com/microsoft/hyperspace/index/IndexLogManagerImplTest.scala

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,10 @@ class IndexLogManagerImplTest
142142
fs,
143143
new Path(path, s"$HYPERSPACE_LOG/0"),
144144
JsonUtils.toJson(getEntry("CREATING")))
145+
FileUtils.createFile(
146+
fs,
147+
new Path(path, s"$HYPERSPACE_LOG/1"),
148+
JsonUtils.toJson(getEntry("ACTIVE")))
145149
FileUtils.createFile(
146150
fs,
147151
new Path(path, s"$HYPERSPACE_LOG/3"),
@@ -158,6 +162,8 @@ class IndexLogManagerImplTest
158162
val expected = Some(getEntry("ACTIVE"))
159163
val actual = new IndexLogManagerImpl(path).getLatestStableLog()
160164
assert(actual.equals(expected))
165+
val actualActiveVersions = new IndexLogManagerImpl(path).getIndexVersions(Seq("ACTIVE"))
166+
assert(actualActiveVersions.equals(Seq(3, 1)))
161167
}
162168

163169
test("testUpdateLatestStableLog passes if latestStable.json can be created") {

src/test/scala/com/microsoft/hyperspace/index/IndexStatisticsTest.scala

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,7 @@ class IndexStatisticsTest extends QueryTest with HyperspaceSuite {
6262
}
6363
}
6464

65-
test(
66-
"index() on an index refreshed in incremental or quick mode returns correct result.") {
65+
test("index() on an index refreshed in incremental or quick mode returns correct result.") {
6766
Seq("incremental", "quick").foreach { mode =>
6867
withTempPathAsString { testPath =>
6968
withSQLConf(IndexConstants.INDEX_LINEAGE_ENABLED -> "true") {

0 commit comments

Comments
 (0)