Skip to content

Commit e24fb20

Browse files
Merge pull request #3 from raw-labs/partial-pushdowns
Ensure we handle partial pushdowns correctly
2 parents 68e65bf + 601bb07 commit e24fb20

File tree

2 files changed

+143
-58
lines changed

2 files changed

+143
-58
lines changed

src/main/scala/com/rawlabs/das/sqlite/DASSqliteTable.scala

Lines changed: 55 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,11 @@ class DASSqliteTable(backend: DASSqliteBackend, defn: TableDefinition, maybePrim
4747
*/
4848
override def tableEstimate(quals: Seq[Qual], columns: Seq[String]): TableEstimate = {
4949
// 1) Build the same WHERE clause used in `execute(...)`.
50+
val supportedQuals = quals.flatMap(qualToSql)
51+
5052
val whereClause =
51-
if (quals.isEmpty) ""
52-
else "\nWHERE " + quals.map(qualToSql).mkString(" AND ")
53+
if (supportedQuals.isEmpty) ""
54+
else "\nWHERE " + supportedQuals.mkString(" AND ")
5355

5456
// 2) Possibly use columns if you want to estimate only the subset of columns,
5557
// or just use "*" or "1" to get an overall row count approximation.
@@ -210,9 +212,10 @@ class DASSqliteTable(backend: DASSqliteBackend, defn: TableDefinition, maybePrim
210212
else columns.map(quoteIdentifier).mkString(", ")
211213

212214
// Build WHERE from `quals`
215+
val supportedQuals = quals.flatMap(qualToSql)
213216
val whereClause =
214-
if (quals.isEmpty) ""
215-
else "\nWHERE " + quals.map(qualToSql).mkString(" AND ")
217+
if (supportedQuals.isEmpty) ""
218+
else "\nWHERE " + supportedQuals.mkString(" AND ")
216219

217220
// Build ORDER BY
218221
val orderByClause =
@@ -315,76 +318,81 @@ class DASSqliteTable(backend: DASSqliteBackend, defn: TableDefinition, maybePrim
315318
str.replace("'", "''") // naive approach for single quotes
316319

317320
/**
318-
* Maps an Operator enum to the corresponding SQL string. Some operators like ILIKE are not native to SQLite, so we
319-
* provide a naive fallback or throw an exception.
321+
* Maps an Operator enum to the corresponding SQL string. Some operators like ILIKE are not native to SQLite, so we do
322+
* not handle them.
320323
*/
321-
private def operatorToSql(op: Operator): String = {
324+
private def operatorToSql(op: Operator): Option[String] = {
322325
op match {
323-
case Operator.EQUALS => "="
324-
case Operator.NOT_EQUALS => "<>"
325-
case Operator.LESS_THAN => "<"
326-
case Operator.LESS_THAN_OR_EQUAL => "<="
327-
case Operator.GREATER_THAN => ">"
328-
case Operator.GREATER_THAN_OR_EQUAL => ">="
329-
case Operator.LIKE => "LIKE"
330-
case Operator.NOT_LIKE => "NOT LIKE"
331-
332-
// SQLite does not have native ILIKE support. We can fallback to "LIKE" or fail.
333-
case Operator.ILIKE => throw new IllegalArgumentException("SQLite does not support ILIKE.")
334-
case Operator.NOT_ILIKE => throw new IllegalArgumentException("SQLite does not support NOT ILIKE.")
335-
336-
// Arithmetic operators might not be typical in a WHERE Qual
337-
case Operator.PLUS => "+"
338-
case Operator.MINUS => "-"
339-
case Operator.TIMES => "*"
340-
case Operator.DIV => "/"
341-
case Operator.MOD => "%"
342-
case Operator.AND => "AND"
343-
case Operator.OR => "OR"
344-
345-
case _ => throw new IllegalArgumentException(s"Unsupported operator: $op")
326+
case Operator.EQUALS => Some("=")
327+
case Operator.NOT_EQUALS => Some("<>")
328+
case Operator.LESS_THAN => Some("<")
329+
case Operator.LESS_THAN_OR_EQUAL => Some("<=")
330+
case Operator.GREATER_THAN => Some(">")
331+
case Operator.GREATER_THAN_OR_EQUAL => Some(">=")
332+
case Operator.LIKE => Some("LIKE")
333+
case Operator.NOT_LIKE => Some("NOT LIKE")
334+
335+
// May be less typical in a WHERE clause
336+
case Operator.PLUS => Some("+")
337+
case Operator.MINUS => Some("-")
338+
case Operator.TIMES => Some("*")
339+
case Operator.DIV => Some("/")
340+
case Operator.MOD => Some("%")
341+
case Operator.AND => Some("AND")
342+
case Operator.OR => Some("OR")
343+
344+
case _ => None
346345
}
347346
}
348347

349348
/**
350349
* `IsAllQual` means "col op ALL these values", we interpret as multiple AND clauses
351350
*/
352-
private def isAllQualToSql(colName: String, iq: IsAllQual): String = {
353-
val opStr = operatorToSql(iq.getOperator)
354-
val clauses = iq.getValuesList.asScala.map(v => s"$colName $opStr ${valueToSql(v)}")
355-
// Combine with AND
356-
clauses.mkString("(", " AND ", ")")
351+
private def isAllQualToSql(colName: String, iq: IsAllQual): Option[String] = {
352+
operatorToSql(iq.getOperator) match {
353+
case Some(opStr) =>
354+
val clauses = iq.getValuesList.asScala.map(v => s"$colName $opStr ${valueToSql(v)}")
355+
// Combine with AND
356+
Some(clauses.mkString("(", " AND ", ")"))
357+
case None => None
358+
}
357359
}
358360

359361
/**
360362
* `IsAnyQual` means "col op ANY of these values", we interpret as multiple OR clauses
361363
*/
362-
private def isAnyQualToSql(colName: String, iq: IsAnyQual): String = {
363-
val opStr = operatorToSql(iq.getOperator)
364-
val clauses = iq.getValuesList.asScala.map(v => s"$colName $opStr ${valueToSql(v)}")
365-
// Combine with OR
366-
clauses.mkString("(", " OR ", ")")
364+
private def isAnyQualToSql(colName: String, iq: IsAnyQual): Option[String] = {
365+
operatorToSql(iq.getOperator) match {
366+
case Some(opStr) =>
367+
val clauses = iq.getValuesList.asScala.map(v => s"$colName $opStr ${valueToSql(v)}")
368+
// Combine with OR
369+
Some(clauses.mkString("(", " OR ", ")"))
370+
case None => None
371+
}
367372
}
368373

369374
/**
370375
* `SimpleQual` is a single condition: "col op value"
371376
*/
372-
private def simpleQualToSql(colName: String, sq: SimpleQual): String = {
377+
private def simpleQualToSql(colName: String, sq: SimpleQual): Option[String] = {
373378
if (sq.getValue.hasNull && sq.getOperator == Operator.EQUALS) {
374-
s"$colName IS NULL"
379+
Some(s"$colName IS NULL")
375380
} else if (sq.getValue.hasNull && sq.getOperator == Operator.NOT_EQUALS) {
376-
s"$colName IS NOT NULL"
381+
Some(s"$colName IS NOT NULL")
377382
} else {
378-
val opStr = operatorToSql(sq.getOperator)
379-
val valStr = valueToSql(sq.getValue)
380-
s"$colName $opStr $valStr"
383+
operatorToSql(sq.getOperator) match {
384+
case Some(opStr) =>
385+
val valStr = valueToSql(sq.getValue)
386+
Some(s"$colName $opStr $valStr")
387+
case None => None
388+
}
381389
}
382390
}
383391

384392
/**
385393
* Converts any `Qual` to a SQL snippet. We handle `SimpleQual`, `IsAnyQual`, or `IsAllQual`.
386394
*/
387-
private def qualToSql(q: Qual): String = {
395+
private def qualToSql(q: Qual): Option[String] = {
388396
val colName = quoteIdentifier(q.getName)
389397
if (q.hasSimpleQual) {
390398
simpleQualToSql(colName, q.getSimpleQual)

src/test/scala/com/rawlabs/das/sqlite/DASSqliteSimpleTest.scala

Lines changed: 88 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,27 @@ import scala.jdk.CollectionConverters._
1717
import org.scalatest.BeforeAndAfterAll
1818
import org.scalatest.funsuite.AnyFunSuite
1919

20+
import com.rawlabs.das.sdk.DASExecuteResult
21+
import com.rawlabs.protocol.das.v1.query.{Operator, Qual, SimpleQual}
2022
import com.rawlabs.protocol.das.v1.tables.{Column, Row}
2123
import com.rawlabs.protocol.das.v1.types.{Value, ValueDouble, ValueInt, ValueString}
2224
import com.typesafe.scalalogging.StrictLogging
2325

2426
class DASSqliteSimpleTest extends AnyFunSuite with BeforeAndAfterAll with StrictLogging {
2527

26-
test("read mydb file") {
27-
val resourceUrl = getClass.getResource("/mydb")
28-
val file = new java.io.File(resourceUrl.toURI)
29-
val fullPath = file.getAbsolutePath
28+
private var sdk: DASSqlite = _
29+
30+
override def beforeAll(): Unit = {
31+
super.beforeAll()
32+
sdk = buildSdk()
33+
}
3034

31-
val sdk = new DASSqlite(Map("database" -> fullPath))
35+
override def afterAll(): Unit = {
36+
sdk.close()
37+
super.afterAll()
38+
}
39+
40+
test("read mydb file") {
3241
val defs = sdk.tableDefinitions
3342
assert(defs.nonEmpty, "tableDefinitions should not be empty.")
3443
val names = defs.map(_.getTableId.getName)
@@ -39,11 +48,7 @@ class DASSqliteSimpleTest extends AnyFunSuite with BeforeAndAfterAll with Strict
3948

4049
val rs =
4150
sdk.getTable("COMPANY").get.execute(Seq.empty, Seq("ID", "NAME", "AGE", "ADDRESS", "SALARY"), Seq.empty, None)
42-
val buf = scala.collection.mutable.ListBuffer[Row]()
43-
while (rs.hasNext) {
44-
buf += rs.next()
45-
}
46-
rs.close()
51+
val buf = collectAllRows(rs)
4752

4853
assert(
4954
buf.toList == List(
@@ -53,8 +58,80 @@ class DASSqliteSimpleTest extends AnyFunSuite with BeforeAndAfterAll with Strict
5358
buildMyDbRow(4, "Mark", 25, "Rich-Mond ", 65000.0),
5459
buildMyDbRow(5, "David", 27, "Texas", 85000.0),
5560
buildMyDbRow(6, "Kim", 22, "South-Hall", 45000.0)))
61+
}
5662

57-
sdk.close()
63+
test("filter mydb with operation that pushes down") {
64+
val rs =
65+
sdk
66+
.getTable("COMPANY")
67+
.get
68+
.execute(
69+
Seq(
70+
Qual
71+
.newBuilder()
72+
.setName("ID")
73+
.setSimpleQual(
74+
SimpleQual
75+
.newBuilder()
76+
.setOperator(Operator.EQUALS)
77+
.setValue(Value.newBuilder().setInt(ValueInt.newBuilder().setV(1)))
78+
.build())
79+
.build()),
80+
Seq("ID", "NAME", "AGE", "ADDRESS", "SALARY"),
81+
Seq.empty,
82+
None)
83+
val buf = collectAllRows(rs)
84+
assert(buf.toList == List(buildMyDbRow(1, "Paul", 32, "California", 20000.0)))
85+
}
86+
87+
test("filter mydb with operation that does NOT push down") {
88+
val rs =
89+
sdk
90+
.getTable("COMPANY")
91+
.get
92+
.execute(
93+
Seq(
94+
Qual
95+
.newBuilder()
96+
.setName("NAME")
97+
.setSimpleQual(
98+
SimpleQual
99+
.newBuilder()
100+
.setOperator(Operator.ILIKE)
101+
.setValue(Value.newBuilder().setString(ValueString.newBuilder().setV("PAUL")))
102+
.build())
103+
.build()),
104+
Seq("ID", "NAME", "AGE", "ADDRESS", "SALARY"),
105+
Seq.empty,
106+
None)
107+
val buf = collectAllRows(rs)
108+
109+
// Since we do NOT push down, we return the entire table
110+
assert(
111+
buf.toList == List(
112+
buildMyDbRow(1, "Paul", 32, "California", 20000.0),
113+
buildMyDbRow(2, "Allen", 25, "Texas", 15000.0),
114+
buildMyDbRow(3, "Teddy", 23, "Norway", 20000.0),
115+
buildMyDbRow(4, "Mark", 25, "Rich-Mond ", 65000.0),
116+
buildMyDbRow(5, "David", 27, "Texas", 85000.0),
117+
buildMyDbRow(6, "Kim", 22, "South-Hall", 45000.0)))
118+
}
119+
120+
private def buildSdk(): DASSqlite = {
121+
val resourceUrl = getClass.getResource("/mydb")
122+
val file = new java.io.File(resourceUrl.toURI)
123+
val fullPath = file.getAbsolutePath
124+
125+
new DASSqlite(Map("database" -> fullPath))
126+
}
127+
128+
private def collectAllRows(rs: DASExecuteResult): Seq[Row] = {
129+
val buf = scala.collection.mutable.ListBuffer[Row]()
130+
while (rs.hasNext) {
131+
buf += rs.next()
132+
}
133+
rs.close()
134+
buf.toList
58135
}
59136

60137
private def buildMyDbRow(id: Int, name: String, age: Int, address: String, salary: Double): Row = {

0 commit comments

Comments
 (0)