Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support performing operations on tables in multiple schemas #1116

Open
wants to merge 29 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
579ae1b
Supporting operations between multiple schemas
hfazai Dec 8, 2020
f2e7405
clean
hfazai Dec 8, 2020
c4ffe6d
introduce cloneSchema util
hfazai Dec 10, 2020
0f3deb7
no need for logging in test
hfazai Dec 10, 2020
719d1b7
remove unused imports
hfazai Dec 10, 2020
19049bd
getRaw index is based on indexOf expression
hfazai Dec 10, 2020
39f8b22
find columns in all schemas
hfazai Dec 11, 2020
92cde1a
user Pair to make join to other schema
hfazai Dec 11, 2020
41b1042
make _columns mutable to change them when coloning table
hfazai Dec 12, 2020
bbc07ae
Fix implementation of columns extraction from database metadata
hfazai Feb 13, 2021
771bd9d
Fix ResultRow.getRaw()
hfazai Feb 13, 2021
85e99a7
Merge branch 'master' into support-tables-in-schemas
hfazai Feb 13, 2021
897bacf
Change catalog name to null instead of %
hfazai Feb 13, 2021
27cad5c
Add SQLite testing db file to .gitignore
hfazai Feb 13, 2021
35f9b63
Add nullCatalogMeansCurrent to connection properties for mysql5.1
hfazai Feb 14, 2021
a27b3b8
Set nullCatalogMeansCurrent to false in connection properties for mys…
hfazai Feb 14, 2021
64acf90
Minor fix: Change '?' to '&' in properties seperator
hfazai Feb 14, 2021
a67b9e0
Column.equals improvement
hfazai Feb 27, 2021
fa0c542
withSchema without reflection
hfazai Jun 7, 2021
531779e
Merge branch master into support-tables-in-schemas
hfazai Jun 7, 2021
b173381
Delete log and generated files by the project
hfazai Jun 7, 2021
d5c5631
Table name already contains a schema name
hfazai Jun 7, 2021
8aaae3f
formatKotlin
hfazai Jun 8, 2021
7b6c6d9
Adding tests for functions
hfazai Jun 8, 2021
9da4643
function tests are not OK
hfazai Jun 8, 2021
413e78d
Fix functions tests
hfazai Jun 9, 2021
7028c90
formatKotlin
hfazai Jun 9, 2021
8d07bd1
Add insertAndGetId DSL function and time function tests for schema table
hfazai Jun 13, 2021
ba9080a
Add get opertor to access a column of a schema table (usefull in slic…
hfazai Jun 13, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
out
build
classes
/exposed-tests/jetbrains.db
15 changes: 13 additions & 2 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Column.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,27 @@ import java.util.*

private val comparator: Comparator<Column<*>> = compareBy({ it.table.tableName }, { it.name })

class SchemaTableColumn<T>(
/** Table where the columns is declared. */
table: Table,
/** Name of the column. */
name: String,
/** the column in original table. */
val idColumn: Column<*>,
/** Data type of the column. */
columnType: IColumnType
) : Column<T>(table, name, columnType)

/**
* Represents a column.
*/
class Column<T>(
open class Column<T>(
/** Table where the columns is declared. */
val table: Table,
/** Name of the column. */
val name: String,
/** Data type of the column. */
override val columnType: IColumnType
final override val columnType: IColumnType
) : ExpressionWithColumnType<T>(), DdlAware, Comparable<Column<*>> {
var foreignKey: ForeignKeyConstraint? = null

Expand Down
10 changes: 10 additions & 0 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Queries.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ fun <Key : Comparable<Key>, T : IdTable<Key>> T.insertAndGetId(body: T.(InsertSt
get(id)
}

/**
* @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testGeneratedKey03
*/
fun <Key : Comparable<Key>, T : IdTable<Key>, V : SchemaTable<T>> V.insertAndGetId(body: V.(InsertStatement<EntityID<Key>>) -> Unit) =
InsertStatement<EntityID<Key>>(this, false).run {
body(this)
execute(TransactionManager.current())
get([email protected])
}

/**
* @sample org.jetbrains.exposed.sql.tests.shared.DMLTests.testBatchInsert01
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ class ResultRow(val fieldIndex: Map<Expression<*>, Int>) {
val index = fieldIndex[c]
?: ((c as? Column<*>)?.columnType as? EntityIDColumnType<*>)?.let { fieldIndex[it.idColumn] }
?: fieldIndex.keys.firstOrNull {
((it as? Column<*>)?.columnType as? EntityIDColumnType<*>)?.idColumn == c
((it as? Column<*>)?.columnType as? EntityIDColumnType<*>)?.idColumn == c ||
(it as? SchemaTableColumn<*>)?.idColumn == c
}?.let { fieldIndex[it] }
?: error("$c is not in record set")

Expand Down
12 changes: 10 additions & 2 deletions exposed-core/src/main/kotlin/org/jetbrains/exposed/sql/Schema.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package org.jetbrains.exposed.sql
import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.sql.transactions.TransactionManager
import org.jetbrains.exposed.sql.vendors.currentDialect
import org.jetbrains.exposed.sql.vendors.inProperCase
import java.lang.StringBuilder

/**
Expand All @@ -20,7 +21,7 @@ import java.lang.StringBuilder
*
*/
data class Schema(
private val name: String,
val name: String,
val authorization: String? = null,
val password: String? = null,
val defaultTablespace: String? = null,
Expand All @@ -29,7 +30,7 @@ data class Schema(
val on: String? = null
) {

val identifier get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name)
val identifier get() = TransactionManager.current().db.identifierManager.cutIfNecessaryAndQuote(name.inProperCase())

val ddl: List<String>
get() = createStatement()
Expand Down Expand Up @@ -63,7 +64,14 @@ data class Schema(

return listOf(currentDialect.setSchema(this))
}

/**
* Returns the schema name in proper case.
* Should be called within transaction or default [schemaName] will be returned.
*/
fun nameInDatabaseCase(): String = name.inProperCase()
}

/** Appends both [str1] and [str2] to the receiver [StringBuilder] if [str2] is not `null`. */
internal fun StringBuilder.appendIfNotNull(str1: String, str2: Any?) = apply {
if (str2 != null) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package org.jetbrains.exposed.sql

open class SchemaTable<T : Table>(
val scheme: Schema,
val delegate: T,
val references: Map<Column<*>, SchemaTable<*>>? = null
) : Table() {

init {
references?.keys?.forEach {
if (!delegate.columns.contains(it)) {
throw Exception("Column ${it.name} doesn't belong to table ${delegate.tableName}")
}
}
}

override val tableName: String = "${scheme.name}.${delegate.tableNameWithoutScheme}"
override val columns: List<SchemaTableColumn<*>> = delegate.columns.map { (it as Column<Comparable<*>>).clone() }
override val primaryKey: PrimaryKey? = delegate.primaryKey?.clone()

private fun PrimaryKey.clone(): PrimaryKey? {
val resolvedPK = columns.map { (it as Column<Comparable<*>>).clone() }.toTypedArray()

val primaryKey = PrimaryKey(*resolvedPK, name = name)
return primaryKey
}

private fun <U : Comparable<U>> Column<U>.clone(): SchemaTableColumn<U> {
val column = SchemaTableColumn<U>(this@SchemaTable, name, this, columnType)
val resolvedReferee = findReferee(referee)
if (resolvedReferee != null) {
column.foreignKey = foreignKey?.copy(target = resolvedReferee)
}
return column
}

fun <U : Comparable<U>> Column<U>.findReferee(referee: Column<*>?): Column<*>? =
if (references == null || references.isEmpty() || referee == null) {
referee
} else {
val reference = references[this]

reference?.columns?.single {
it.idColumn === referee
}
}

operator fun <T> get(column: Column<T>): SchemaTableColumn<T> {
return columns.single {
it.idColumn == column
} as SchemaTableColumn<T>
}
}

/**
* @sample org.jetbrains.exposed.sql.tests.shared.SchemaTests
*
* By default, the table references tables in the default schema.
* If you want to join with tables from other schemas, you can pass them
* in the [references] parameters.
*
* example : tableB.withSchema(schema1, idColumn to tableA.withSchema(schema2))
* will create tableB in schema1 that references tableA in schema2
*
* @param schema the schema of the table
* @param references tables to make join with. Order of tables is not important.
*/
fun <T : Table> T.withSchema(schema: Schema, vararg references: Pair<Column<*>, SchemaTable<*>>): SchemaTable<T> =
SchemaTable(schema, this, references.toMap())
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,9 @@ object SchemaUtils {
for (table in tables) {
// create columns
val thisTableExistingColumns = existingTableColumns[table].orEmpty()
val missingTableColumns = table.columns.filterNot { c -> thisTableExistingColumns.any { it.name.equals(c.name, true) } }
val missingTableColumns = table.columns.filterNot { c ->
thisTableExistingColumns.any { it.name.equals(c.name, true) }
}
missingTableColumns.flatMapTo(statements) { it.ddl }

if (db.supportsAlterTableWithAddColumn) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,7 +149,7 @@ open class InsertStatement<Key : Any>(val table: Table, val isIgnore: Boolean =

protected open var arguments: List<List<Pair<Column<*>, Any?>>>? = null
get() = field ?: run {
val nullableColumns = table.columns.filter { it.columnType.nullable }
val nullableColumns = table.columns.filter { it.columnType.nullable }.map { if (it is SchemaTableColumn<*>) it.idColumn else it }
val valuesAndDefaults = valuesAndDefaults()
val result = (valuesAndDefaults + (nullableColumns - valuesAndDefaults.keys).associate { it to null }).toList().sortedBy { it.first }
listOf(result).apply { field = this }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,57 @@ open class JavaTimeBaseTest : DatabaseTestsBase() {
}
}

@Test
fun javaTimeSchemaFunctions() {
val schema = Schema("schema")
val CitiesTimeInSchema = CitiesTime.withSchema(schema)
withSchemas(schema) {
try {
SchemaUtils.create(CitiesTimeInSchema)
val now = LocalDateTime.now()

val cityID = CitiesTimeInSchema.insertAndGetId {
it[CitiesTime.name] = "Tunisia"
it[CitiesTime.local_time] = now
}

val insertedYear = CitiesTimeInSchema
.slice(CitiesTimeInSchema[CitiesTime.local_time].year())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTimeInSchema[CitiesTime.local_time].year()]
val insertedMonth = CitiesTimeInSchema
.slice(CitiesTime.local_time.month())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTime.local_time.month()]
val insertedDay = CitiesTimeInSchema
.slice(CitiesTimeInSchema[CitiesTime.local_time].day())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTimeInSchema[CitiesTime.local_time].day()]
val insertedHour = CitiesTimeInSchema
.slice(CitiesTimeInSchema[CitiesTime.local_time].hour())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTimeInSchema[CitiesTime.local_time].hour()]
val insertedMinute = CitiesTimeInSchema
.slice(CitiesTimeInSchema[CitiesTime.local_time].minute())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTimeInSchema[CitiesTime.local_time].minute()]
val insertedSecond = CitiesTimeInSchema
.slice(CitiesTimeInSchema[CitiesTime.local_time].second())
.select { CitiesTime.id.eq(cityID) }
.single()[CitiesTimeInSchema[CitiesTime.local_time].second()]

assertEquals(now.year, insertedYear)
assertEquals(now.month.value, insertedMonth)
assertEquals(now.dayOfMonth, insertedDay)
assertEquals(now.hour, insertedHour)
assertEquals(now.minute, insertedMinute)
assertEquals(now.second, insertedSecond)
} finally {
SchemaUtils.drop(CitiesTimeInSchema)
}
}
}

// Checks that old numeric datetime columns works fine with new text representation
@Test
fun testSQLiteDateTimeFieldRegression() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,33 +115,65 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
return schemas.map { identifierManager.inProperCase(it) }
}

private fun ResultSet.extractColumns(tables: Array<out Table>, extract: (ResultSet) -> Pair<String, ColumnMetadata>): Map<Table, List<ColumnMetadata>> {
val mapping = tables.associateBy { it.nameInDatabaseCase() }
override fun columns(vararg tables: Table): Map<Table, List<ColumnMetadata>> {
val useCatalogInsteadOfScheme = currentDialect is MysqlDialect
val result = HashMap<Table, MutableList<ColumnMetadata>>()

while (next()) {
val (tableName, columnMetadata) = extract(this)
mapping[tableName]?.let { t ->
result.getOrPut(t) { arrayListOf() } += columnMetadata
tables.map { table ->
val columns = if (table is SchemaTable<*>) {
columnNames.getValue(table.scheme.nameInDatabaseCase())
} else {
if (useCatalogInsteadOfScheme) {
columnNames.getValue(databaseName)
} else {
columnNames.getValue(currentScheme)
}
}

val columnMetadata = if (table is SchemaTable<*>) {
columns[table.delegate.nameInDatabaseCase()]
} else {
columns[table.nameInDatabaseCase()]
}

columnMetadata?.let {
result.getOrPut(table) { arrayListOf() } += it
}
}

return result
}

override fun columns(vararg tables: Table): Map<Table, List<ColumnMetadata>> {
val rs = metadata.getColumns(databaseName, currentScheme, "%", "%")
val result = rs.extractColumns(tables) {
private val columnNamesCache = HashMap<String, Map<String, List<ColumnMetadata>>>()

private val columnNames: Map<String, Map<String, List<ColumnMetadata>>> = CachableMapWithDefault(
map = columnNamesCache,
default = { schemeName ->
columnNamesFor(schemeName)
}
)

private fun columnNamesFor(scheme: String): Map<String, List<ColumnMetadata>> = with(metadata) {
val result = HashMap<String, MutableList<ColumnMetadata>>()
val useCatalogInsteadOfScheme = currentDialect is MysqlDialect
val (catalogName, schemeName) = when {
useCatalogInsteadOfScheme -> scheme to currentScheme
else -> databaseName to scheme.ifEmpty { "%" }
}
val resultSet = getColumns(catalogName, schemeName, "%", "%")
resultSet.iterate {
// @see java.sql.DatabaseMetaData.getColumns
val columnMetadata = ColumnMetadata(
it.getString("COLUMN_NAME")/*.quoteIdentifierWhenWrongCaseOrNecessary(tr)*/,
it.getInt("DATA_TYPE"),
it.getBoolean("NULLABLE"),
it.getInt("COLUMN_SIZE").takeIf { it != 0 },
it.getString("IS_AUTOINCREMENT") == "YES",
getString("COLUMN_NAME")/*.quoteIdentifierWhenWrongCaseOrNecessary(tr)*/,
getInt("DATA_TYPE"),
getBoolean("NULLABLE"),
getInt("COLUMN_SIZE").takeIf { it != 0 },
getString("IS_AUTOINCREMENT") == "YES",
)
it.getString("TABLE_NAME") to columnMetadata

result.getOrPut(getString("TABLE_NAME")) { arrayListOf() } += columnMetadata
}
rs.close()
resultSet.close()
return result
}

Expand Down Expand Up @@ -215,6 +247,7 @@ class JdbcDatabaseMetadataImpl(database: String, val metadata: DatabaseMetaData)
@Synchronized
override fun cleanCache() {
existingIndicesCache.clear()
columnNamesCache.clear()
}

private fun <T> lazyMetadata(body: DatabaseMetaData.() -> T) = lazy { metadata.body() }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,39 @@ open class JodaTimeBaseTest : DatabaseTestsBase() {
assertEquals(now.secondOfMinute, insertedSecond)
}
}

@Test
fun jodaTimeSchemaFunctions() {
val schema = Schema("schema")
val CitiesTimeInSchema = CitiesTime.withSchema(schema)
withSchemas(schema) {
try {
SchemaUtils.create(CitiesTimeInSchema)
val now = DateTime.now()

val cityID = CitiesTimeInSchema.insertAndGetId {
it[CitiesTime.name] = "St. Petersburg"
it[CitiesTime.local_time] = now.toDateTime()
}

val insertedYear = CitiesTimeInSchema.slice(CitiesTime.local_time.year()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.year()]
val insertedMonth = CitiesTimeInSchema.slice(CitiesTime.local_time.month()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.month()]
val insertedDay = CitiesTimeInSchema.slice(CitiesTime.local_time.day()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.day()]
val insertedHour = CitiesTimeInSchema.slice(CitiesTime.local_time.hour()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.hour()]
val insertedMinute = CitiesTimeInSchema.slice(CitiesTime.local_time.minute()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.minute()]
val insertedSecond = CitiesTimeInSchema.slice(CitiesTime.local_time.second()).select { CitiesTime.id.eq(cityID) }.single()[CitiesTime.local_time.second()]

assertEquals(now.year, insertedYear)
assertEquals(now.monthOfYear, insertedMonth)
assertEquals(now.dayOfMonth, insertedDay)
assertEquals(now.hourOfDay, insertedHour)
assertEquals(now.minuteOfHour, insertedMinute)
assertEquals(now.secondOfMinute, insertedSecond)
} finally {
SchemaUtils.drop(CitiesTimeInSchema)
}
}
}
}

fun assertEqualDateTime(d1: DateTime?, d2: DateTime?) {
Expand Down
Loading