Support RawQuery in paging data source

This CL fixes a bug where paged list data source would not generate
proper code when a RawQuery is provided.

To overcome this, I've created a RoomSQLiteQuery helper method that
creates a query from a given support query.

I've also added getArgCount to the SupportSQLiteQuery API, which was
previously requested (b/67038952).

Bug: 67038952
Bug: 72600425
Test: DataSourceFactoryTest
Change-Id: I76183d6f02e9809bdbdad2d24159900497828b1b
diff --git a/persistence/db/api/current.txt b/persistence/db/api/current.txt
index f96f17a..e0abe20 100644
--- a/persistence/db/api/current.txt
+++ b/persistence/db/api/current.txt
@@ -5,6 +5,7 @@
     ctor public SimpleSQLiteQuery(java.lang.String);
     method public static void bind(android.arch.persistence.db.SupportSQLiteProgram, java.lang.Object[]);
     method public void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+    method public int getArgCount();
     method public java.lang.String getSql();
   }
 
@@ -96,6 +97,7 @@
 
   public abstract interface SupportSQLiteQuery {
     method public abstract void bindTo(android.arch.persistence.db.SupportSQLiteProgram);
+    method public abstract int getArgCount();
     method public abstract java.lang.String getSql();
   }
 
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
index bcf4f49..d16045f 100644
--- a/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
+++ b/persistence/db/src/main/java/android/arch/persistence/db/SimpleSQLiteQuery.java
@@ -54,6 +54,11 @@
         bind(statement, mBindArgs);
     }
 
+    @Override
+    public int getArgCount() {
+        return mBindArgs.length;
+    }
+
     /**
      * Binds the given arguments into the given sqlite statement.
      *
diff --git a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java
index 2007634..03e0a91 100644
--- a/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java
+++ b/persistence/db/src/main/java/android/arch/persistence/db/SupportSQLiteQuery.java
@@ -35,4 +35,12 @@
      * @param statement The compiled statement
      */
     void bindTo(SupportSQLiteProgram statement);
+
+    /**
+     * Returns the number of arguments in this query. This is equal to the number of placeholders
+     * in the query string. See: https://www.sqlite.org/c3ref/bind_blob.html for details.
+     *
+     * @return The number of arguments in the query.
+     */
+    int getArgCount();
 }
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
index 02e299b..3e7dd9c 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/processor/ProcessorErrors.kt
@@ -146,9 +146,10 @@
             " queries."
 
     val OBSERVABLE_QUERY_NOTHING_TO_OBSERVE = "Observable query return type (LiveData, Flowable" +
-            " etc) can only be used with SELECT queries that directly or indirectly (via" +
-            " @Relation, for example) access at least one table. For @RawQuery, you should" +
-            " specify the list of tables to be observed via the observedEntities field."
+            ", DataSource, DataSourceFactory etc) can only be used with SELECT queries that" +
+            " directly or indirectly (via @Relation, for example) access at least one table. For" +
+            " @RawQuery, you should specify the list of tables to be observed via the" +
+            " observedEntities field."
 
     val RECURSIVE_REFERENCE_DETECTED = "Recursive referencing through @Embedded and/or @Relation " +
             "detected: %s"
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
index 4d0a280..dc11428 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceFactoryQueryResultBinderProvider.kt
@@ -19,10 +19,11 @@
 import android.arch.persistence.room.ext.PagingTypeNames
 import android.arch.persistence.room.parser.ParsedQuery
 import android.arch.persistence.room.processor.Context
+import android.arch.persistence.room.processor.ProcessorErrors
 import android.arch.persistence.room.solver.QueryResultBinderProvider
-import android.arch.persistence.room.solver.query.result.PositionalDataSourceQueryResultBinder
 import android.arch.persistence.room.solver.query.result.ListQueryResultAdapter
 import android.arch.persistence.room.solver.query.result.LivePagedListQueryResultBinder
+import android.arch.persistence.room.solver.query.result.PositionalDataSourceQueryResultBinder
 import android.arch.persistence.room.solver.query.result.QueryResultBinder
 import javax.lang.model.type.DeclaredType
 import javax.lang.model.type.TypeMirror
@@ -34,6 +35,9 @@
     }
 
     override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+        if (query.tables.isEmpty()) {
+            context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+        }
         val typeArg = declared.typeArguments[1]
         val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
             ListQueryResultAdapter(it)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
index c13354e..0fc9dce 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/binderprovider/DataSourceQueryResultBinderProvider.kt
@@ -39,6 +39,9 @@
     }
 
     override fun provide(declared: DeclaredType, query: ParsedQuery): QueryResultBinder {
+        if (query.tables.isEmpty()) {
+            context.logger.e(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+        }
         val typeArg = declared.typeArguments.last()
         val listAdapter = context.typeAdapterStore.findRowAdapter(typeArg, query)?.let {
             ListQueryResultAdapter(it)
diff --git a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
index ac5a256..4530332 100644
--- a/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
+++ b/room/compiler/src/main/kotlin/android/arch/persistence/room/solver/query/result/PositionalDataSourceQueryResultBinder.kt
@@ -42,8 +42,11 @@
                                   dbField: FieldSpec,
                                   inTransaction: Boolean,
                                   scope: CodeGenScope) {
-        val tableNamesList = tableNames.joinToString(",") { "\"$it\"" }
-        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L, $L",
+        // first comma for table names comes from the string since it might be empty in which case
+        // we don't need a comma. If list is empty, this prevents generating bad code (it is still
+        // an error to have empty list but that is already reported while item is processed)
+        val tableNamesList = tableNames.joinToString { ",\"$it\"" }
+        val spec = TypeSpec.anonymousClassBuilder("$N, $L, $L $L",
                 dbField, roomSQLiteQueryVar, inTransaction, tableNamesList).apply {
             superclass(typeName)
             addMethod(createConvertRowsMethod(scope))
diff --git a/room/compiler/src/test/data/common/input/PositionalDataSource.java b/room/compiler/src/test/data/common/input/PositionalDataSource.java
new file mode 100644
index 0000000..7cff0d7
--- /dev/null
+++ b/room/compiler/src/test/data/common/input/PositionalDataSource.java
@@ -0,0 +1,5 @@
+package android.arch.paging;
+
+public abstract class PositionalDataSource<T> extends DataSource<Integer, T> {
+
+}
\ No newline at end of file
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
index 3eb629a..17e8ad9 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/processor/RawQueryMethodProcessorTest.kt
@@ -24,6 +24,7 @@
 import android.arch.persistence.room.Query
 import android.arch.persistence.room.RawQuery
 import android.arch.persistence.room.ext.CommonTypeNames
+import android.arch.persistence.room.ext.PagingTypeNames
 import android.arch.persistence.room.ext.SupportDbTypeNames
 import android.arch.persistence.room.ext.hasAnnotation
 import android.arch.persistence.room.ext.typeName
@@ -123,6 +124,42 @@
     }
 
     @Test
+    fun observableWithoutEntities_dataSourceFactory() {
+        singleQueryMethod(
+                """
+                @RawQuery
+                abstract public ${PagingTypeNames.DATA_SOURCE_FACTORY}<Integer, User> getOne();
+                """) { _, _ ->
+            // do nothing
+        }.failsToCompile()
+                .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+    }
+
+    @Test
+    fun observableWithoutEntities_positionalDataSource() {
+        singleQueryMethod(
+                """
+                @RawQuery
+                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne();
+                """) { _, _ ->
+            // do nothing
+        }.failsToCompile()
+                .withErrorContaining(ProcessorErrors.OBSERVABLE_QUERY_NOTHING_TO_OBSERVE)
+    }
+
+    @Test
+    fun positionalDataSource() {
+        singleQueryMethod(
+                """
+                @RawQuery(observedEntities = {User.class})
+                abstract public ${PagingTypeNames.POSITIONAL_DATA_SOURCE}<User> getOne(
+                        SupportSQLiteQuery query);
+                """) { _, _ ->
+            // do nothing
+        }.compilesWithoutError()
+    }
+
+    @Test
     fun pojo() {
         val pojo: TypeName = ClassName.get("foo.bar.MyClass", "MyPojo")
         singleQueryMethod(
@@ -204,10 +241,11 @@
                         DAO_PREFIX
                                 + input.joinToString("\n")
                                 + DAO_SUFFIX
-                ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER))
+                ), COMMON.LIVE_DATA, COMMON.COMPUTABLE_LIVE_DATA, COMMON.USER,
+                        COMMON.DATA_SOURCE_FACTORY, COMMON.POSITIONAL_DATA_SOURCE))
                 .processedWith(TestProcessor.builder()
                         .forAnnotations(Query::class, Dao::class, ColumnInfo::class,
-                                Entity::class, PrimaryKey::class, RawQueryMethod::class)
+                                Entity::class, PrimaryKey::class, RawQuery::class)
                         .nextRunHandler { invocation ->
                             val (owner, methods) = invocation.roundEnv
                                     .getElementsAnnotatedWith(Dao::class.java)
diff --git a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt
index b657643..c0167b3 100644
--- a/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt
+++ b/room/compiler/src/test/kotlin/android/arch/persistence/room/testing/test_util.kt
@@ -21,6 +21,7 @@
 import android.arch.persistence.room.Query
 import android.arch.persistence.room.Relation
 import android.arch.persistence.room.ext.LifecyclesTypeNames
+import android.arch.persistence.room.ext.PagingTypeNames
 import android.arch.persistence.room.ext.ReactiveStreamsTypeNames
 import android.arch.persistence.room.ext.RoomRxJava2TypeNames
 import android.arch.persistence.room.ext.RxJava2TypeNames
@@ -88,6 +89,11 @@
     val DATA_SOURCE_FACTORY by lazy {
         loadJavaCode("common/input/DataSource.java", "android.arch.paging.DataSource")
     }
+
+    val POSITIONAL_DATA_SOURCE by lazy {
+        loadJavaCode("common/input/PositionalDataSource.java",
+                PagingTypeNames.POSITIONAL_DATA_SOURCE.toString())
+    }
 }
 fun testCodeGenScope(): CodeGenScope {
     return CodeGenScope(Mockito.mock(ClassWriter::class.java))
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
index 7cb8b60..768f64a 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/dao/UserDao.java
@@ -18,11 +18,13 @@
 
 import android.arch.lifecycle.LiveData;
 import android.arch.paging.DataSource;
+import android.arch.persistence.db.SupportSQLiteQuery;
 import android.arch.persistence.room.Dao;
 import android.arch.persistence.room.Delete;
 import android.arch.persistence.room.Insert;
 import android.arch.persistence.room.OnConflictStrategy;
 import android.arch.persistence.room.Query;
+import android.arch.persistence.room.RawQuery;
 import android.arch.persistence.room.Transaction;
 import android.arch.persistence.room.Update;
 import android.arch.persistence.room.integration.testapp.TestDatabase;
@@ -194,6 +196,10 @@
     @Query("SELECT * FROM user where mAge > :age")
     public abstract DataSource.Factory<Integer, User> loadPagedByAge(int age);
 
+    @RawQuery(observedEntities = User.class)
+    public abstract DataSource.Factory<Integer, User> loadPagedByAgeWithObserver(
+            SupportSQLiteQuery query);
+
     // TODO: switch to PositionalDataSource once Room supports it
     @Query("SELECT * FROM user ORDER BY mAge DESC")
     public abstract DataSource.Factory<Integer, User> loadUsersByAgeDesc();
diff --git a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
index 0f68656..587367f 100644
--- a/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
+++ b/room/integration-tests/testapp/src/androidTest/java/android/arch/persistence/room/integration/testapp/paging/DataSourceFactoryTest.java
@@ -30,6 +30,7 @@
 import android.arch.lifecycle.Observer;
 import android.arch.paging.LivePagedListBuilder;
 import android.arch.paging.PagedList;
+import android.arch.persistence.db.SimpleSQLiteQuery;
 import android.arch.persistence.room.integration.testapp.test.TestDatabaseTest;
 import android.arch.persistence.room.integration.testapp.test.TestUtil;
 import android.arch.persistence.room.integration.testapp.vo.User;
@@ -41,7 +42,6 @@
 import org.junit.Test;
 import org.junit.runner.RunWith;
 
-import java.util.concurrent.Callable;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.FutureTask;
 import java.util.concurrent.TimeUnit;
@@ -69,7 +69,23 @@
                 .build());
     }
 
-    private void validateUsersAsPagedList(LivePagedListFactory factory)
+    @Test
+    public void getUsersAsPagedList_ViaRawQuery_WithObservable()
+            throws InterruptedException, ExecutionException, TimeoutException {
+        SimpleSQLiteQuery query = new SimpleSQLiteQuery(
+                "SELECT * FROM user where mAge > ?",
+                new Object[]{3});
+        validateUsersAsPagedList(() -> new LivePagedListBuilder<>(
+                mUserDao.loadPagedByAgeWithObserver(query),
+                new PagedList.Config.Builder()
+                        .setPageSize(10)
+                        .setPrefetchDistance(1)
+                        .setInitialLoadSizeHint(10).build())
+                .build());
+    }
+
+    private void validateUsersAsPagedList(
+            LivePagedListFactory factory)
             throws InterruptedException, ExecutionException, TimeoutException {
         mDatabase.beginTransaction();
         try {
@@ -135,13 +151,10 @@
 
     private void observe(final LiveData liveData, final LifecycleOwner provider,
             final Observer observer) throws ExecutionException, InterruptedException {
-        FutureTask<Void> futureTask = new FutureTask<>(new Callable<Void>() {
-            @Override
-            public Void call() throws Exception {
-                //noinspection unchecked
-                liveData.observe(provider, observer);
-                return null;
-            }
+        FutureTask<Void> futureTask = new FutureTask<>(() -> {
+            //noinspection unchecked
+            liveData.observe(provider, observer);
+            return null;
         });
         ArchTaskExecutor.getInstance().executeOnMainThread(futureTask);
         futureTask.get();
@@ -167,6 +180,7 @@
 
     private static class PagedListObserver<T> implements Observer<PagedList<T>> {
         private PagedList<T> mList;
+
         void reset() {
             mList = null;
         }
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java b/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java
index a10cc52..c4ff4bd 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/RoomSQLiteQuery.java
@@ -79,6 +79,55 @@
     static final TreeMap<Integer, RoomSQLiteQuery> sQueryPool = new TreeMap<>();
 
     /**
+     * Copies the given SupportSQLiteQuery and converts it into RoomSQLiteQuery.
+     *
+     * @param supportSQLiteQuery The query to copy from
+     * @return A new query copied from the provided one.
+     */
+    public static RoomSQLiteQuery copyFrom(SupportSQLiteQuery supportSQLiteQuery) {
+        final RoomSQLiteQuery query = RoomSQLiteQuery.acquire(
+                supportSQLiteQuery.getSql(),
+                supportSQLiteQuery.getArgCount());
+        supportSQLiteQuery.bindTo(new SupportSQLiteProgram() {
+            @Override
+            public void bindNull(int index) {
+                query.bindNull(index);
+            }
+
+            @Override
+            public void bindLong(int index, long value) {
+                query.bindLong(index, value);
+            }
+
+            @Override
+            public void bindDouble(int index, double value) {
+                query.bindDouble(index, value);
+            }
+
+            @Override
+            public void bindString(int index, String value) {
+                query.bindString(index, value);
+            }
+
+            @Override
+            public void bindBlob(int index, byte[] value) {
+                query.bindBlob(index, value);
+            }
+
+            @Override
+            public void clearBindings() {
+                query.clearBindings();
+            }
+
+            @Override
+            public void close() {
+                // ignored.
+            }
+        });
+        return query;
+    }
+
+    /**
      * Returns a new RoomSQLiteQuery that can accept the given number of arguments and holds the
      * given query.
      *
@@ -152,6 +201,7 @@
         return mQuery;
     }
 
+    @Override
     public int getArgCount() {
         return mArgCount;
     }
diff --git a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
index baa5b43..73777c4 100644
--- a/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
+++ b/room/runtime/src/main/java/android/arch/persistence/room/paging/LimitOffsetDataSource.java
@@ -17,6 +17,7 @@
 package android.arch.persistence.room.paging;
 
 import android.arch.paging.PositionalDataSource;
+import android.arch.persistence.db.SupportSQLiteQuery;
 import android.arch.persistence.room.InvalidationTracker;
 import android.arch.persistence.room.RoomDatabase;
 import android.arch.persistence.room.RoomSQLiteQuery;
@@ -52,6 +53,11 @@
     private final InvalidationTracker.Observer mObserver;
     private final boolean mInTransaction;
 
+    protected LimitOffsetDataSource(RoomDatabase db, SupportSQLiteQuery query,
+            boolean inTransaction, String... tables) {
+        this(db, RoomSQLiteQuery.copyFrom(query), inTransaction, tables);
+    }
+
     protected LimitOffsetDataSource(RoomDatabase db, RoomSQLiteQuery query,
             boolean inTransaction, String... tables) {
         mDb = db;