Entity schema generation.

This CL adds ability to create database schema from Entity classes.

Bug: 32342709
Test: DatabaseWriterTest.kt, SQLiteOpenHelperWriterTest.kt
Change-Id: I13a5d17eabadd2647aa72d24ae45bf7345e83e10
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt b/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
index ea39ae4..aff722a 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/parser/SqlParser.kt
@@ -42,7 +42,6 @@
 
 class SqlParser {
     companion object {
-        val BINDING_REGEX = "\\:[a-zA-Z_-][a-zA-Z0-9_-]+".toRegex()
         fun parse(input: String): ParsedQuery {
             val inputStream = ANTLRInputStream(input)
             val lexer = SQLiteLexer(inputStream)
@@ -62,3 +61,11 @@
         }
     }
 }
+
+enum class SQLTypeAffinity {
+    TEXT,
+    NUMERIC,
+    INTEGER,
+    REAL,
+    BLOB
+}
\ No newline at end of file
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
index d941b02..e40e1ed 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/BoxedPrimitiveColumnTypeAdapter.kt
@@ -27,7 +27,7 @@
  */
 open class BoxedPrimitiveColumnTypeAdapter(boxed : TypeMirror,
                                            val primitiveAdapter : PrimitiveColumnTypeAdapter)
-            : ColumnTypeAdapter(boxed) {
+            : ColumnTypeAdapter(boxed, primitiveAdapter.typeAffinity) {
     companion object {
         fun createBoxedPrimitiveAdapters(processingEnvironment: ProcessingEnvironment,
                                     primitiveAdapters : List<PrimitiveColumnTypeAdapter>)
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/ColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/ColumnTypeAdapter.kt
index ff9b3d7..c3a2728 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/ColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/ColumnTypeAdapter.kt
@@ -16,6 +16,7 @@
 
 package com.android.support.room.solver.types
 
+import com.android.support.room.parser.SQLTypeAffinity
 import com.android.support.room.solver.CodeGenScope
 import com.squareup.javapoet.TypeName
 import javax.lang.model.type.TypeMirror
@@ -23,7 +24,7 @@
 /**
  * A code generator that can read a field from Cursor and write a field to a Statement
  */
-abstract class ColumnTypeAdapter(val out: TypeMirror) {
+abstract class ColumnTypeAdapter(val out: TypeMirror, val typeAffinity: SQLTypeAffinity) {
     val outTypeName by lazy { TypeName.get(out) }
     abstract fun readFromCursor(outVarName : String, cursorVarName: String, indexVarName: String,
                                 scope: CodeGenScope)
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/CompositeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/CompositeAdapter.kt
index 8d9d927..eca25ee 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/CompositeAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/CompositeAdapter.kt
@@ -19,7 +19,6 @@
 import com.android.support.room.ext.L
 import com.android.support.room.ext.T
 import com.android.support.room.solver.CodeGenScope
-import com.squareup.javapoet.TypeName
 import javax.lang.model.type.TypeMirror
 
 /**
@@ -27,7 +26,8 @@
  * a composite one.
  */
 class CompositeAdapter(out: TypeMirror, val columnTypeAdapter: ColumnTypeAdapter,
-                       val typeConverter : TypeConverter) : ColumnTypeAdapter(out) {
+                       val typeConverter : TypeConverter)
+            : ColumnTypeAdapter(out, columnTypeAdapter.typeAffinity) {
     override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
                                 scope: CodeGenScope) {
         scope.builder().apply {
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/PrimitiveColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/PrimitiveColumnTypeAdapter.kt
index dda987b..0baaaaa 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/PrimitiveColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/PrimitiveColumnTypeAdapter.kt
@@ -18,6 +18,8 @@
 
 import com.android.support.room.ext.L
 import com.android.support.room.ext.typeName
+import com.android.support.room.parser.SQLTypeAffinity
+import com.android.support.room.parser.SQLTypeAffinity.REAL
 import com.android.support.room.solver.CodeGenScope
 import javax.annotation.processing.ProcessingEnvironment
 import javax.lang.model.type.PrimitiveType
@@ -35,7 +37,9 @@
  */
 open class PrimitiveColumnTypeAdapter(out: PrimitiveType,
                                       val cursorGetter: String,
-                                      val stmtSetter: String) : ColumnTypeAdapter(out) {
+                                      val stmtSetter: String,
+                                      typeAffinity : SQLTypeAffinity)
+        : ColumnTypeAdapter(out, typeAffinity) {
     val cast =  if (cursorGetter == "get${out.typeName().toString().capitalize()}")
                     ""
                 else
@@ -56,7 +60,12 @@
                 PrimitiveColumnTypeAdapter(
                         out = processingEnvironment.typeUtils.getPrimitiveType(it.first),
                         cursorGetter = it.second,
-                        stmtSetter = it.third
+                        stmtSetter = it.third,
+                        typeAffinity = when(it.first) {
+                            INT, SHORT, BYTE, LONG, CHAR -> SQLTypeAffinity.INTEGER
+                            FLOAT, DOUBLE -> REAL
+                            else -> throw IllegalArgumentException("invalid type")
+                        }
                 )
             }
         }
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/StringColumnTypeAdapter.kt b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/StringColumnTypeAdapter.kt
index 769af59..af799d3 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/solver/types/StringColumnTypeAdapter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/solver/types/StringColumnTypeAdapter.kt
@@ -17,12 +17,13 @@
 package com.android.support.room.solver.types
 
 import com.android.support.room.ext.L
+import com.android.support.room.parser.SQLTypeAffinity.TEXT
 import com.android.support.room.solver.CodeGenScope
 import javax.annotation.processing.ProcessingEnvironment
 
 class StringColumnTypeAdapter(processingEnvironment: ProcessingEnvironment)
     : ColumnTypeAdapter((processingEnvironment.elementUtils.getTypeElement(
-        String::class.java.canonicalName)).asType()) {
+        String::class.java.canonicalName)).asType(), TEXT) {
     override fun readFromCursor(outVarName: String, cursorVarName: String, indexVarName: String,
                                 scope: CodeGenScope) {
         scope.builder()
diff --git a/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt b/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
index 30de7e7..bbd4852 100644
--- a/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
+++ b/room/compiler/src/main/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriter.kt
@@ -16,13 +16,17 @@
 
 package com.android.support.room.writer
 
+import android.support.annotation.VisibleForTesting
 import com.android.support.room.ext.L
 import com.android.support.room.ext.N
+import com.android.support.room.ext.S
 import com.android.support.room.ext.SupportDbTypeNames
 import com.android.support.room.ext.T
+import com.android.support.room.parser.SQLTypeAffinity
 import com.android.support.room.solver.CodeGenScope
 import com.android.support.room.vo.Database
-import com.squareup.javapoet.FieldSpec
+import com.android.support.room.vo.Entity
+import com.android.support.room.vo.Field
 import com.squareup.javapoet.MethodSpec
 import com.squareup.javapoet.ParameterSpec
 import com.squareup.javapoet.TypeName
@@ -71,7 +75,10 @@
         return MethodSpec.methodBuilder("onCreate").apply {
             addModifiers(PUBLIC)
             addParameter(SupportDbTypeNames.DB, "_db")
-            // TODO
+            // this is already called in transaction so no need for a transaction
+            database.entities.forEach {
+                addStatement("_db.execSQL($S)", createQuery(it))
+            }
         }.build()
     }
 
@@ -81,7 +88,10 @@
             addParameter(SupportDbTypeNames.DB, "_db")
             addParameter(TypeName.INT, "_oldVersion")
             addParameter(TypeName.INT, "_newVersion")
-            // TODO
+            database.entities.forEach {
+                addStatement("_db.execSQL($S)", createDropTableQuery(it))
+                addStatement("_db.execSQL($S)", createQuery(it))
+            }
         }.build()
     }
 
@@ -91,7 +101,38 @@
             addParameter(SupportDbTypeNames.DB, "_db")
             addParameter(TypeName.INT, "_oldVersion")
             addParameter(TypeName.INT, "_newVersion")
-            // TODO
+            // TODO better handle this
+            addStatement("onUpgrade(_db, _oldVersion, _newVersion)")
         }.build()
     }
+
+    private fun createDatabaseDefinition(field : Field) : String {
+        val affinity = field.let {
+            val adapter = it.getter.columnAdapter ?: it.setter.columnAdapter
+            adapter?.typeAffinity ?: SQLTypeAffinity.TEXT
+        }
+        return "`${field.columnName}` ${affinity.name}"
+    }
+
+    @VisibleForTesting
+    fun createQuery(entity : Entity) : String {
+        val definitions = entity.fields.map {
+            field -> createDatabaseDefinition(field)
+        } + createPrimaryKeyDefinition(entity)
+        return "CREATE TABLE IF NOT EXISTS `${entity.tableName}` " +
+                "(${definitions.joinToString(", ")})"
+    }
+
+    private fun createPrimaryKeyDefinition(entity: Entity): String {
+        val keys = entity.fields
+                .filter { it.primaryKey }
+                .map { "`${it.columnName}`" }
+                .joinToString(", ")
+        return "PRIMARY KEY($keys)"
+    }
+
+    @VisibleForTesting
+    fun createDropTableQuery(entity: Entity) : String {
+        return "DROP TABLE IF EXISTS `${entity.tableName}`"
+    }
 }
diff --git a/room/compiler/src/test/data/common/input/User.java b/room/compiler/src/test/data/common/input/User.java
index 12ee13b..e82bc4d 100644
--- a/room/compiler/src/test/data/common/input/User.java
+++ b/room/compiler/src/test/data/common/input/User.java
@@ -20,4 +20,15 @@
 public class User {
     @PrimaryKey
     int uid;
+    String name;
+    private String lastName;
+    @ColumnName("ageColumn")
+    public int age;
+
+    public String getLastName() {
+        return lastName;
+    }
+    public void setLastName(String lastName) {
+        this.lastName = lastName;
+    }
 }
\ No newline at end of file
diff --git a/room/compiler/src/test/data/daoWriter/output/WriterDao.java b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
index e5dc0c9..6ca04ca 100644
--- a/room/compiler/src/test/data/daoWriter/output/WriterDao.java
+++ b/room/compiler/src/test/data/daoWriter/output/WriterDao.java
@@ -19,6 +19,7 @@
 import com.android.support.db.SupportSQLiteStatement;
 import com.android.support.room.EntityInsertionAdapter;
 import com.android.support.room.RoomDatabase;
+
 import java.lang.Override;
 import java.lang.String;
 import java.util.List;
@@ -35,24 +36,47 @@
         this.__insertionAdapterOfUser = new EntityInsertionAdapter<User>(__db) {
             @Override
             public String createInsertQuery() {
-                return "INSERT OR ABORT INTO `User`(`uid`) VALUES (?)";
+                return "INSERT OR ABORT INTO `User`(`uid`,`name`,`lastName`,`ageColumn`) VALUES"
+                        + " (?,?,?,?)";
             }
 
             @Override
             public void bind(SupportSQLiteStatement stmt, User value) {
                 stmt.bindLong(0, value.uid);
+                if (value.name == null) {
+                    stmt.bindNull(1);
+                } else {
+                    stmt.bindString(1, value.name);
+                }
+                if (value.getLastName() == null) {
+                    stmt.bindNull(2);
+                } else {
+                    stmt.bindString(2, value.getLastName());
+                }
+                stmt.bindLong(3, value.age);
             }
         };
-
         this.__insertionAdapterOfUser_1 = new EntityInsertionAdapter<User>(__db) {
             @Override
             public String createInsertQuery() {
-                return "INSERT OR REPLACE INTO `User`(`uid`) VALUES (?)";
+                return "INSERT OR REPLACE INTO `User`(`uid`,`name`,`lastName`,`ageColumn`) VALUES"
+                        + " (?,?,?,?)";
             }
 
             @Override
             public void bind(SupportSQLiteStatement stmt, User value) {
                 stmt.bindLong(0, value.uid);
+                if (value.name == null) {
+                    stmt.bindNull(1);
+                } else {
+                    stmt.bindString(1, value.name);
+                }
+                if (value.getLastName() == null) {
+                    stmt.bindNull(2);
+                } else {
+                    stmt.bindString(2, value.getLastName());
+                }
+                stmt.bindLong(3, value.age);
             }
         };
     }
diff --git a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
index aea0829..16652a5 100644
--- a/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
+++ b/room/compiler/src/test/data/databasewriter/output/ComplexDatabase.java
@@ -14,12 +14,18 @@
     protected SupportSQLiteOpenHelper createOpenHelper(DatabaseConfiguration configuration) {
         final SupportSQLiteOpenHelper.Callback _openCallback = new SupportSQLiteOpenHelper.Callback() {
             public void onCreate(SupportSQLiteDatabase _db) {
+                _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER, `name` TEXT,"
+                        + " `lastName` TEXT, `ageColumn` INTEGER, PRIMARY KEY(`uid`))");
             }
 
             public void onUpgrade(SupportSQLiteDatabase _db, int _oldVersion, int _newVersion) {
+                _db.execSQL("DROP TABLE IF EXISTS `User`");
+                _db.execSQL("CREATE TABLE IF NOT EXISTS `User` (`uid` INTEGER, `name` TEXT,"
+                        + " `lastName` TEXT, `ageColumn` INTEGER, PRIMARY KEY(`uid`))");
             }
 
             public void onDowngrade(SupportSQLiteDatabase _db, int _oldVersion, int _newVersion) {
+                onUpgrade(_db, _oldVersion, _newVersion);
             }
         };
         final SupportSQLiteOpenHelper.Configuration _sqliteConfig = SupportSQLiteOpenHelper.Configuration.builder(configuration.context)
@@ -30,4 +36,4 @@
         final SupportSQLiteOpenHelper _helper = configuration.sqliteOpenHelperFactory.create(_sqliteConfig);
         return _helper;
     }
-}
\ No newline at end of file
+}
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/processor/BaseEntityParserTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/processor/BaseEntityParserTest.kt
index f353dfb..26b3056 100644
--- a/room/compiler/src/test/kotlin/com/android/support/room/processor/BaseEntityParserTest.kt
+++ b/room/compiler/src/test/kotlin/com/android/support/room/processor/BaseEntityParserTest.kt
@@ -37,8 +37,7 @@
     }
 
     fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
-                     handler: (Entity, TestInvocation) -> Unit):
-            CompileTester {
+                     handler: (Entity, TestInvocation) -> Unit): CompileTester {
         val attributesReplacement : String
         if (attributes.isEmpty()) {
             attributesReplacement = ""
diff --git a/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt b/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt
new file mode 100644
index 0000000..e6cd006
--- /dev/null
+++ b/room/compiler/src/test/kotlin/com/android/support/room/writer/SQLiteOpenHelperWriterTest.kt
@@ -0,0 +1,111 @@
+/*
+ * Copyright (C) 2016 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.support.room.writer
+
+import com.android.support.room.processor.DatabaseProcessor
+import com.android.support.room.testing.TestInvocation
+import com.android.support.room.testing.TestProcessor
+import com.android.support.room.vo.Database
+import com.google.auto.common.MoreElements
+import com.google.common.truth.Truth
+import com.google.testing.compile.CompileTester
+import com.google.testing.compile.JavaFileObjects
+import com.google.testing.compile.JavaSourcesSubjectFactory
+import org.hamcrest.CoreMatchers.`is`
+import org.hamcrest.MatcherAssert.assertThat
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.JUnit4
+
+@RunWith(JUnit4::class)
+class SQLiteOpenHelperWriterTest {
+    companion object {
+        const val ENTITY_PREFIX = """
+            package foo.bar;
+            import com.android.support.room.*;
+            @Entity
+            public class MyEntity {
+            """
+        const val ENTITY_SUFFIX = "}"
+        const val DATABASE_CODE = """
+            package foo.bar;
+            import com.android.support.room.*;
+            @Database(entities = {MyEntity.class})
+            abstract public class MyDatabase extends RoomDatabase {
+                public MyDatabase(DatabaseConfiguration configuration) {
+                    super(configuration);
+                }
+            }
+            """
+    }
+
+    @Test
+    fun createSimpleEntity() {
+        singleEntity(
+                """
+                @PrimaryKey
+                String uuid;
+                String name;
+                int age;
+                """.trimIndent()
+        ) { database, invocation ->
+            val query = SQLiteOpenHelperWriter(database)
+                    .createQuery(database.entities.first())
+            assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+                    " `MyEntity` (`uuid` TEXT, `name` TEXT, `age` INTEGER, PRIMARY KEY(`uuid`))"))
+        }.compilesWithoutError()
+    }
+
+    @Test
+    fun multiplePrimaryKeys() {
+        singleEntity(
+                """
+                @PrimaryKey
+                String uuid;
+                @PrimaryKey
+                String name;
+                int age;
+                """.trimIndent()
+        ) { database, invocation ->
+            val query = SQLiteOpenHelperWriter(database)
+                    .createQuery(database.entities.first())
+            assertThat(query, `is`("CREATE TABLE IF NOT EXISTS" +
+                    " `MyEntity` (`uuid` TEXT, `name` TEXT, `age` INTEGER," +
+                    " PRIMARY KEY(`uuid`, `name`))"))
+        }.compilesWithoutError()
+    }
+
+    fun singleEntity(input: String, attributes: Map<String, String> = mapOf(),
+                     handler: (Database, TestInvocation) -> Unit): CompileTester {
+        return Truth.assertAbout(JavaSourcesSubjectFactory.javaSources())
+                .that(listOf(JavaFileObjects.forSourceString("foo.bar.MyEntity",
+                        ENTITY_PREFIX + input + ENTITY_SUFFIX
+                ), JavaFileObjects.forSourceString("foo.bar.MyDatabase",
+                        DATABASE_CODE)))
+                .processedWith(TestProcessor.builder()
+                        .forAnnotations(com.android.support.room.Database::class)
+                        .nextRunHandler { invocation ->
+                            val db = MoreElements.asType(invocation.roundEnv
+                                    .getElementsAnnotatedWith(
+                                            com.android.support.room.Database::class.java)
+                                    .first())
+                            handler(DatabaseProcessor(invocation.context).parse(db), invocation)
+                            true
+                        }
+                        .build())
+    }
+}