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())
+ }
+}