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

org.ufoss.kotysa.DefaultSqlClient.kt Maven / Gradle / Ivy

/*
 * This is free and unencumbered software released into the public domain, following 
 */

package org.ufoss.kotysa

import org.ufoss.kolog.Logger

private val logger = Logger.of()

public interface DefaultSqlClient {
    public val tables: Tables
    public val module: Module

    public fun createTableSql(table: Table<*>, ifNotExists: Boolean): CreateTableResult {
        val kotysaTable = tables.getTable(table)

        val columns = createTableColumns(kotysaTable)

        val pk = kotysaTable.primaryKey
        var primaryKey = if (pk.name != null) {
            "CONSTRAINT ${pk.name} "
        } else {
            ""
        }
        primaryKey += "PRIMARY KEY (${pk.columns.joinToString { it.name }})"

        val foreignKeys = createTableForeignKeys(kotysaTable)

        var suffix = ""
        val prefix = if (ifNotExists) {
            if (DbType.MSSQL == tables.dbType) {
                suffix = "\nEND"

                """IF NOT EXISTS(SELECT name FROM sys.sysobjects WHERE Name = N'${kotysaTable.name}' AND xtype = N'U')
                BEGIN
                CREATE TABLE"""
            } else {
                "CREATE TABLE IF NOT EXISTS"
            }
        } else {
            "CREATE TABLE"
        }

        val createTableSql = "$prefix ${kotysaTable.name} ($columns, $primaryKey$foreignKeys)$suffix"
        logger.debug { "Exec SQL (${tables.dbType.name}) : $createTableSql" }

        // create indexes
        val createIndexes = kotysaTable.kotysaIndexes.map { index ->
            val indexTypeLabel = index.type?.name ?: ""
            var indexName = index.name
                ?:
                // build custom index name
                index.columns
                    .joinToString("_", "${kotysaTable.name}_") { column ->
                        column.name
                    }
            if (indexTypeLabel.isNotEmpty()) {
                indexName += "_$indexTypeLabel"
            }
            val indexColumns = index.columns.joinToString { column -> column.name }
            val createIndexSql = when(index.type) {
                IndexType.GIN, IndexType.GIST ->
                    "CREATE INDEX $indexName ON ${kotysaTable.name} USING $indexTypeLabel ($indexColumns)"
                else -> "CREATE $indexTypeLabel INDEX $indexName ON ${kotysaTable.name} ($indexColumns)"
            }
            logger.debug { "Exec SQL (${tables.dbType.name}) : $createIndexSql" }
            CreateIndexResult(indexName, createIndexSql)
        }

        return CreateTableResult(createTableSql, createIndexes)
    }

    public fun createTableForeignKeys(kotysaTable: KotysaTable): String =
        if (kotysaTable.foreignKeys.isEmpty()) {
            ""
        } else {
            kotysaTable.foreignKeys.joinToString(prefix = ", ") { foreignKey ->
                var foreignKeyStatement = if (foreignKey.name != null) {
                    "CONSTRAINT ${foreignKey.name} "
                } else {
                    ""
                }
                val referencedTable = tables.allColumns[foreignKey.references.values.first()]?.table
                    ?: error("Referenced table of column ${foreignKey.references.values.first()} is not mapped")
                foreignKeyStatement += "FOREIGN KEY (${foreignKey.references.keys.joinToString { it.name }})" +
                        " REFERENCES ${referencedTable.name}" +
                        " (${foreignKey.references.values.joinToString { it.name }})"
                foreignKeyStatement
            }
        }

    public fun createTableColumns(kotysaTable: KotysaTable): String {
        val createDbColumns = kotysaTable.dbColumns
            .joinToString { column ->
                if (tables.dbType == DbType.MYSQL && column.sqlType == SqlType.VARCHAR) {
                    requireNotNull(column.size) { "Column ${column.name} : Varchar size is required in MySQL" }
                }
                var parameters = ""
                if (column.size != null) {
                    parameters = "(${column.size}"
                    if (column.decimals != null) {
                        parameters += ",${column.decimals}"
                    }
                    parameters += ")"
                }
                val nullability = if (column.isNullable) "NULL" else "NOT NULL"
                val autoIncrement = if (column.isAutoIncrement && DbType.SQLITE != tables.dbType) {
                    // SQLITE : The AUTOINCREMENT keyword imposes extra CPU, memory, disk space, and disk I/O overhead and should be avoided if not strictly needed.
                    // It is usually not needed -> https://sqlite.org/autoinc.html
                    // if this needs to be added later, sqlite syntax MUST be "column INTEGER PRIMARY KEY AUTOINCREMENT"
                    if (DbType.MSSQL == tables.dbType) {
                        " IDENTITY"
                    } else {
                        " AUTO_INCREMENT"
                    }
                } else {
                    ""
                }
                val default = if (column.defaultValue != null) {
                    " DEFAULT ${column.defaultValue.defaultValue(tables.dbType)}"
                } else {
                    ""
                }
                "${column.name} ${column.sqlType.fullType}$parameters $nullability$autoIncrement$default"
            }

        val otherColumns = kotysaTable.columns
            .filterIsInstance>()
        val createOtherColumns = if (otherColumns.isEmpty()) {
            ""
        } else {
            otherColumns
                .joinToString(prefix = ", ") { column ->
                    val columns = column.kotysaColumns
                        .joinToString(" || ' ' || ") { col ->
                            if (col.isNullable) {
                                // Use coalesce to ensure that field will be indexed
                                "coalesce(${col.name}, '')"
                            } else {
                                col.name
                            }
                        }
                    // see https://www.postgresql.org/docs/current/textsearch-tables.html#TEXTSEARCH-TABLES-INDEX
                    "${column.name} ${column.sqlType.fullType} GENERATED ALWAYS " +
                            "AS(to_tsvector('${column.tsvectorType}', $columns)) STORED"
                }
        }

        return "$createDbColumns$createOtherColumns"
    }

    public fun  insertSql(row: T, withReturn: Boolean = false): String {
        val insertSqlQuery = insertSqlQuery(row, withReturn)
        logger.debug { "Exec SQL (${tables.dbType.name}) : $insertSqlQuery" }
        return insertSqlQuery
    }

    // fixme 24/05/21 : does not work if set to private (fails in demo-kotlin project)
    public fun  insertSqlQuery(row: T, withReturn: Boolean): String {
        val kotysaTable = tables.getTable(row::class)
        val columnNames = mutableSetOf()
        val counter = Counter()
        val values = kotysaTable.dbColumns
            // filter out null values with default value or Serial types
            .filterNot { column ->
                column.entityGetter(row) == null
                        && (column.defaultValue != null
                        || column.isAutoIncrement
                        || SqlType.SERIAL == column.sqlType
                        || SqlType.BIGSERIAL == column.sqlType)
            }
            .joinToString { column ->
                columnNames.add(column.name)
                variable(counter)
            }
        var prefix = ""
        var suffix = ""
        // on MSSQL identity cannot be set a value, must activate IDENTITY_INSERT
        if (tables.dbType == DbType.MSSQL
            && kotysaTable.columns
                .filterIsInstance>()
                .any { column -> column.isAutoIncrement && column.entityGetter(row) != null }
        ) {
            prefix = "SET IDENTITY_INSERT ${kotysaTable.name} ON\n"
            suffix = "\nSET IDENTITY_INSERT ${kotysaTable.name} OFF"
        }

        if (!withReturn || tables.dbType == DbType.MYSQL) {
            // If no return requested, or MySQL that does not provide native RETURNING style feature
            return "${prefix}INSERT INTO ${kotysaTable.name} (${columnNames.joinToString()}) VALUES ($values)$suffix"
        }

        val allTableColumnNames = kotysaTable.columns
            .joinToString { column ->
                if (tables.dbType == DbType.MSSQL) {
                    "INSERTED." + column.name
                } else {
                    column.name
                }
            }

        return when (tables.dbType) {
            DbType.H2 -> "SELECT $allTableColumnNames FROM FINAL TABLE (INSERT INTO ${kotysaTable.name} (${columnNames.joinToString()}) VALUES ($values))"
            DbType.MSSQL -> "${prefix}INSERT INTO ${kotysaTable.name} (${columnNames.joinToString()}) OUTPUT $allTableColumnNames VALUES ($values)$suffix"
            else -> "INSERT INTO ${kotysaTable.name} (${columnNames.joinToString()}) VALUES ($values) RETURNING $allTableColumnNames"
        }
    }

    public fun  lastInsertedSql(row: T): String {
        val lastInsertedQuery = lastInsertedQuery(row)
        logger.debug { "Exec SQL (${tables.dbType.name}) : $lastInsertedQuery" }
        return lastInsertedQuery
    }

    public fun  lastInsertedQuery(row: T): String {
        val kotysaTable = tables.getTable(row::class)

        val pkColumns = kotysaTable.primaryKey.columns

        val allTableColumnNames = kotysaTable.columns
            .joinToString { column -> column.name }

        val pkFirstColumn = pkColumns.elementAt(0)
        val wheres = if (
            pkColumns.size == 1 &&
            pkFirstColumn.isAutoIncrement &&
            pkFirstColumn.entityGetter(row) == null
        ) {
            val selected = if (tables.dbType == DbType.MYSQL) {
                "(SELECT LAST_INSERT_ID())"
            } else {
                "?"
            }
            "${pkFirstColumn.name} = $selected"
        } else {
            val counter = Counter()
            pkColumns
                .joinToString(" AND ") { column ->
                    "${column.name} = ${variable(counter)}"
                }
        }

        return "SELECT $allTableColumnNames FROM ${kotysaTable.name} WHERE $wheres"
    }

    public fun variable(counter: Counter): String =
        when {
            module == Module.SQLITE || module == Module.JDBC
                    || (module == Module.R2DBC && tables.dbType == DbType.MYSQL)
                    || (module == Module.VERTX_SQL_CLIENT && (tables.dbType == DbType.MYSQL || tables.dbType == DbType.MARIADB)) -> "?"

            module.isR2dbcOrVertxSqlClient() && (tables.dbType == DbType.H2 || tables.dbType == DbType.POSTGRESQL) -> "$${++counter.index}"
            module.isR2dbcOrVertxSqlClient() && tables.dbType == DbType.MSSQL -> "@p${++counter.index}"
            else -> ":k${counter.index++}"
        }
}

public class Counter {
    internal var index = 0
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy