All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.sxtanna.db.KueryTask.kt Maven / Gradle / Ivy

There is a newer version: 1.6
Show newest version
package com.sxtanna.db

import com.sxtanna.db.ext.PrimaryKey
import com.sxtanna.db.ext.Value
import com.sxtanna.db.ext.value
import com.sxtanna.db.ext.whileNext
import com.sxtanna.db.struct.Resolver
import com.sxtanna.db.struct.Table
import com.sxtanna.db.struct.base.Duplicate
import com.sxtanna.db.struct.base.Order
import com.sxtanna.db.struct.base.Order.*
import com.sxtanna.db.struct.base.Order.Direction.ASCEND
import com.sxtanna.db.struct.base.Where
import com.sxtanna.db.struct.base.Where.Clause.Between
import com.sxtanna.db.struct.statement.*
import com.sxtanna.db.type.Executed
import org.intellij.lang.annotations.Language
import java.sql.Connection
import java.sql.PreparedStatement
import java.sql.ResultSet
import java.util.*
import kotlin.reflect.KParameter
import kotlin.reflect.KProperty1
import kotlin.reflect.full.declaredMemberProperties
import kotlin.reflect.full.findAnnotation
import kotlin.reflect.full.primaryConstructor

/**
 * Main task class
 * * If a call doesn't return [Unit] it requires you to make a call to either
 * * [SelectN.componentN] directly or through destructuring
 * * or
 * * [Executed.execute] for [Delete] and [Update]
 *
 * [kotlin.reflect.KClass.properties]
 */
class KueryTask(private val kuery : Kuery, private val connection : Connection) : Creator, Deleter, Dropper, Inserter, Selector, Truncater, Updater {

    /**
     * Push a statement to the database
     */
    fun push(@Language("MySQL") statement : String, block : PreparedStatement.() -> Unit = {}) {
        kuery.logger.debug("Pushing statement `$statement`")
        with(kuery) { connection.push(statement, block) }
    }

    /**
     * Pull the results of a query from the database
     */
    fun pull(@Language("MySQL") statement : String, block : PreparedStatement.() -> Unit = {}) : ResultSet {
        kuery.logger.debug("Pulling statement `$statement`")
        return with(kuery) { connection.pull(statement, block) }
    }


    //region Table cursors
    /**
     * Open and return a Table task handler
     */
    fun  on(table : Table) = KueryTaskTable(table, this)

    /**
     * Open and use a Table task handler
     */
    fun  on(table : Table, block : KueryTaskTable.() -> R) : R {
        return on(table).block()
    }
    //endregion


    //region Create statement
    override fun  create(table : Table) {
        val columns = table.columns.entries.joinToString { "${it.key} ${it.value}" }
        push("CREATE TABLE IF NOT EXISTS ${table.name}($columns)")
    }
    //endregion


    //region Delete statements
    override fun  delete(table : Table) : Delete {
        return DeleteImpl(table)
    }

    override fun  delete(table : Table, rows : Collection) {
        if (rows.isEmpty()) return

        val statement = "DELETE FROM ${table.name} WHERE ${table.fields.joinToString(" AND ") { "${it.name}=?" }}"
        kuery.logger.debug("Pushing statement `$statement`")

        val state = connection.prepareStatement(statement).apply {

            var index = 1
            rows.forEach { e ->
                table.fields.map { it.get(e) }.forEach { this[index++] = it }
                index = 1

                addBatch()
            }

        }

        state.executeBatch()
    }

    override fun  deleteAllRows(table : Table) {
        push("DELETE FROM ${table.name}")
    }
    //endregion


    //region Drop statement
    override fun  drop(table : Table) {
        push("DROP TABLE ${table.name}")
    }
    //endregion


    //region Insert statements
    override fun  insert(table : Table, rows : Collection) {
        if (rows.isEmpty()) return

        val columns = table.columns.keys
        val values = "(${Array(columns.size) { "?" }.joinToString()})"

        push("INSERT INTO ${table.name} (${columns.joinToString()}) VALUES ${(0 until rows.size).joinToString { values }}") {

            var index = 1
            rows.forEach { e ->
                table.fields.map { it.get(e) }.forEach { this[index++] = it }
            }

        }
    }

    override fun  insert(table : Table, duplicate : Duplicate>, rows : Collection) {
        if (rows.isEmpty()) return

        val columns = table.columns.keys
        val values = "(${Array(columns.size) { "?" }.joinToString()})"

        push("INSERT INTO ${table.name} (${columns.joinToString()}) VALUES ${(0 until rows.size).joinToString { values }} ${duplicate(table)}") {

            var index = 1
            rows.forEach { e ->
                table.fields.map { it.get(e) }.forEach { this[index++] = it }
            }

        }
    }
    //endregion


    //region Select statements
    override fun  select(table : Table) : Select1> {
        return Select1Impl(table, emptyList())
    }

    override fun 
          select(table : Table,
                 prop : KProperty1) : Select1> {
        return Select1Impl(table, listOf(prop))
    }

    override fun 
          select(table : Table,
                 prop1 : KProperty1,
                 prop2 : KProperty1) : Select2, List> {
        return Select2Impl(table, listOf(prop1, prop2))
    }

    override fun 
          select(table : Table,
                 prop1 : KProperty1,
                 prop2 : KProperty1,
                 prop3 : KProperty1) : Select3, List, List> {
        return Select3Impl(table, listOf(prop1, prop2, prop3))
    }

    override fun 
          select(table : Table,
                 prop1 : KProperty1,
                 prop2 : KProperty1,
                 prop3 : KProperty1,
                 prop4 : KProperty1) : Select4, List, List, List> {
        return Select4Impl(table, listOf(prop1, prop2, prop3, prop4))
    }

    override fun 
          select(table : Table,
                 prop1 : KProperty1,
                 prop2 : KProperty1,
                 prop3 : KProperty1,
                 prop4 : KProperty1,
                 prop5 : KProperty1) : Select5, List, List, List, List> {
        return Select5Impl(table, listOf(prop1, prop2, prop3, prop4, prop5))
    }
    //endregion


    //region Truncate statement
    override fun  truncate(table : Table) {
        push("TRUNCATE TABLE ${table.name}")
    }
    //endregion


    //region Update statements
    override fun  update(table : Table) : Update {
        return UpdateImpl(table)
    }

    override fun  update(table : Table, rows : Collection) {
        val key = table.getPrimaryKey() ?: return kuery.logger.error("Cannot update entire rows in a table without a primary key")

        val statement = "UPDATE ${table.name} SET ${table.fields.joinToString { "${it.name}=?" }} WHERE ${key.name}=?"
        kuery.logger.debug("Pushing statement `$statement`")

        connection.prepareStatement(statement).apply {

            var index = 1
            rows.forEach { e ->
                table.fields.map { it.get(e) }.forEach { this[index++] = it }
                this[index++] = key.get(e)

                index = 1

                addBatch()
            }

        }.executeBatch()
    }

    override fun  updateAllRows(table : Table, row : E) {
        if (table.getPrimaryKey() != null) return kuery.logger.error("Cannot update all rows to a single row of a table with a primary key")

        push("UPDATE ${table.name} SET ${table.fields.joinToString { "${it.name}=?" }}") {

            var index = 1
            table.fields.map { it.get(row) }.forEach { this[index++] = it }

        }
    }

    override fun  updateAllRows(table : Table, vararg values : Value) {
        if (values.any { it.prop.findAnnotation() != null }) {
            return kuery.logger.error("Cannot update all rows in a table with a primary key to the same value")
        }

        push("UPDATE ${table.name} SET ${values.joinToString { "${it.prop.name}=?" }}") {

            var index = 1
            values.forEach { this[index++] = it.value }

        }
    }
    //endregion


    //region Select implementations
    internal inner abstract class SelectImpl(protected val table : Table, target : List>)
        : Select1> {

        private val order = mutableListOf()
        private val where = mutableListOf()

        internal val results = mutableMapOf>()

        protected val target = if (target.isEmpty()) "*" else target.joinToString { it.name }


        protected lateinit var lastResult : ResultSet


        override final fun component1() : List {
            if (::lastResult.isInitialized) return first()

            val order = order.isNotEmpty().value(" ORDER BY ${order.joinToString()}")
            val where = where.isNotEmpty().value(" WHERE ${where.joinToString(" AND ")}")

            // pull results from database
            val result = pull("SELECT $target FROM ${table.name}$where$order") {

                var offset = 0
                [email protected] { index, it ->

                    this[index + 1 + offset] = it.data

                    if (it is Between) {
                        this[index + 1 + ++offset] = it.data
                    }

                }

            }


            // return early if empty
            if (result.isBeforeFirst.not()) {
                result.close()
                lastResult = result
                return emptyList()
            }


            // gather column names
            val columns = result.metaData.let { m ->
                (1..m.columnCount).map { m.getColumnName(it) }
            }

            // gather fields
            val fields = table.clazz.declaredMemberProperties.associateBy { it.name }

            // populate results with empty
            columns.forEachIndexed { index, _ ->
                results[index] = mutableListOf()
            }

            // for every result, populate every index
            result.whileNext {

                columns.forEachIndexed { index, s ->

                    val field = fields[s]
                    results[index]?.add(field?.let { Resolver.SqlI[this, it] })

                }

            }

            lastResult = result

            // return first component
            return first()
        }


        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) : Select1> = apply {
            where.addAll(Where().apply { block(prop) }.clauses)
        }

        override fun  order(prop : KProperty1, direction : Direction) : Select1> = apply {
            order.add(if (direction == ASCEND) Ascend(prop.name) else Descend(prop.name))
        }

        protected open fun first() : List {
            return checkNotNull(results[0] as List) {
                table.fields[1].let { "Results for ${it.name} weren't ${it.returnType}" }
            }
        }

    }

    internal open inner class Select1Impl(table : Table, target : List>)
        : SelectImpl(table, target) {

        override final fun first() : List {
            return if (target != "*") super.first() else { // try to create the entire object

                // get primary constructor
                val construct = table.clazz.primaryConstructor ?: return emptyList()

                val params = construct.parameters
                val pNames = params.associateBy { it.name ?: "" }

                // associate columns with indexes
                val target = lastResult.metaData.let { m ->
                    (1..m.columnCount).associate { it to m.getColumnName(it) }
                }

                // associate params with indexes
                val map = results.keys.associateBy { pNames[target[it + 1]] }

                // the least amount of creatable objects (might not actually need this due to nullability)
                val min = results.values.minBy { it.size }?.size ?: 0


                val values = mutableMapOf()

                (0 until min).mapNotNull { ri ->

                    params.forEach {
                        val index = map[it]
                        values[it] = results[index]?.get(ri)
                    }

                    construct.callBy(values).also { values.clear() }
                } as List
            }
        }

    }

    internal open inner class Select2Impl(table : Table, target : List>)
        : Select1Impl(table, target), Select2, List> {

        override final fun component2() : List {
            return checkNotNull(results[1] as List) {
                table.fields[2].let { "Results for ${it.name} weren't ${it.returnType}" }
            }
        }


        override fun  order(prop : KProperty1, direction : Direction) : Select2, List> {
            super.order(prop, direction)
            return this
        }

        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) : Select2, List> {
            super.where(prop, block)
            return this
        }

    }

    internal open inner class Select3Impl(table : Table, target : List>)
        : Select2Impl(table, target), Select3, List, List> {

        override final fun component3() : List {
            return checkNotNull(results[2] as? List) {
                table.fields[3].let { "Results for ${it.name} weren't ${it.returnType}" }
            }
        }


        override fun  order(prop : KProperty1, direction : Direction) : Select3, List, List> {
            super.order(prop, direction)
            return this
        }

        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) : Select3, List, List> {
            super.where(prop, block)
            return this
        }

    }

    internal open inner class Select4Impl(table : Table, target : List>)
        : Select3Impl(table, target), Select4, List, List, List> {

        override final fun component4() : List {
            return checkNotNull(results[3] as List) {
                table.fields[4].let { "Results for ${it.name} weren't ${it.returnType}" }
            }
        }


        override fun  order(prop : KProperty1, direction : Direction) : Select4, List, List, List> {
            super.order(prop, direction)
            return this
        }

        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) : Select4, List, List, List> {
            super.where(prop, block)
            return this
        }

    }

    internal open inner class Select5Impl(table : Table, target : List>)
        : Select4Impl(table, target), Select5, List, List, List, List> {

        override final fun component5() : List {
            return checkNotNull(results[4] as List) {
                table.fields[5].let { "Results for ${it.name} weren't ${it.returnType}" }
            }
        }


        override fun  order(prop : KProperty1, direction : Direction) : Select5, List, List, List, List> {
            super.order(prop, direction)
            return this
        }

        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) : Select5, List, List, List, List> {
            super.where(prop, block)
            return this
        }

    }
    //endregion


    //region Delete implementation
    internal inner class DeleteImpl(private val table : Table) : Delete {

        private val where = mutableListOf()

        private var executed = false

        override fun execute() {
            if (executed) return
            executed = true

            if (where.isEmpty()) {
                return kuery.logger.error("Attempting to run delete with no clauses, this will delete all rows, please use KueryTask#deleteAllRows")
            }

            push("DELETE FROM ${table.name} WHERE ${where.joinToString(" AND ")}") {

                var offset = 0
                where.forEachIndexed { index, it ->

                    this[index + 1 + offset] = it.data

                    if (it is Between) {
                        this[index + 1 + ++offset] = it.data
                    }

                }

            }
        }


        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) = apply {
            where.addAll(Where().apply { block(prop) }.clauses)
        }

    }
    //endregion


    //region Update implementation
    internal inner class UpdateImpl(private val table : Table) : Update {

        private val value = mutableListOf>()
        private val where = mutableListOf()

        private var executed = false

        override fun execute() {
            if (executed) return
            executed = true

            if (value.isEmpty()) {
                return kuery.logger.error("Attempting to run update with no values, this will do nothing")
            }
            if (where.isEmpty()) {
                return kuery.logger.error("Attempting to run update with no clauses, this will update every row, please use KueryTask#updateAllRows")
            }

            push("UPDATE ${table.name} SET ${value.joinToString { "${it.prop.name}=?" }} WHERE ${where.joinToString(" AND ")}") {

                var index = 1
                value.forEach { this[index++] = it.value }

                var offset = 0
                where.forEachIndexed { index, it ->

                    this[index + 1 + offset] = it.data

                    if (it is Between) {
                        this[index + 1 + ++offset] = it.data
                    }

                }

            }
        }


        override fun  set(column : KProperty1, value : R) : Update = apply {
            this.value.add(Value(column, value))
        }


        override fun  where(prop : KProperty1, block : Where.(KProperty1) -> Unit) = apply {
            where.addAll(Where().apply { block(prop) }.clauses)
        }

    }
    //endregion


    private operator fun PreparedStatement.set(index : Int, any : Any?) {
        setObject(index, (any as? UUID)?.toString() ?: any)
    }

}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy