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

com.zeoflow.depot.parser.expansion.ProjectionExpander.kt Maven / Gradle / Ivy

Go to download

The Depot persistence library provides an abstraction layer over SQLite to allow for more robust database access while using the full power of SQLite.

The newest version!
/*
 * Copyright (C) 2021 ZeoFlow SRL
 *
 * 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.zeoflow.depot.parser.expansion

import androidx.annotation.VisibleForTesting
import com.zeoflow.depot.parser.ParsedQuery
import com.zeoflow.depot.parser.SqlParser
import com.zeoflow.depot.processor.QueryRewriter
import com.zeoflow.depot.solver.query.result.PojoRowAdapter
import com.zeoflow.depot.solver.query.result.RowAdapter
import com.zeoflow.depot.verifier.QueryResultInfo
import com.zeoflow.depot.vo.EmbeddedField
import com.zeoflow.depot.vo.Entity
import com.zeoflow.depot.vo.EntityOrView
import com.zeoflow.depot.vo.Field
import com.zeoflow.depot.vo.Pojo
import com.zeoflow.depot.vo.columnNames
import java.util.Locale

/**
 * Interprets and rewrites SQL queries in the context of the provided entities and views such that
 * star projection (select *) turn into explicit column lists and embedded fields are re-named to
 * avoid conflicts in the response data set.
 */
class ProjectionExpander(
    private val tables: List
) : QueryRewriter {

    private class IdentifierMap : HashMap() {
        override fun put(key: String, value: V): V? {
            return super.put(key.lowercase(Locale.ENGLISH), value)
        }

        override fun get(key: String): V? {
            return super.get(key.lowercase(Locale.ENGLISH))
        }
    }

    /**
     * Rewrites the specified [query] in the context of the provided [pojo]. Expanding its start
     * projection ('SELECT *') and converting its named binding templates to positional
     * templates (i.e. ':VVV' to '?').
     */
    @VisibleForTesting(otherwise = VisibleForTesting.PRIVATE)
    fun interpret(
        query: ParsedQuery,
        pojo: Pojo?
    ) = interpret(
        query = ExpandableSqlParser.parse(query.original).also {
            it.resultInfo = query.resultInfo
        },
        pojo = pojo
    )

    override fun rewrite(
        query: ParsedQuery,
        rowAdapter: RowAdapter
    ) = if (rowAdapter is PojoRowAdapter) {
        interpret(
            query = ExpandableSqlParser.parse(query.original),
            pojo = rowAdapter.pojo
        ).let {
            val reParsed = SqlParser.parse(it)
            if (reParsed.errors.isEmpty()) {
                reParsed
            } else {
                query // return original, expansion somewhat failed
            }
        }
    } else {
        query
    }

    private fun interpret(
        query: ExpandableParsedQuery,
        pojo: Pojo?
    ): String {
        val queriedTableNames = query.tables.map { it.name }
        return query.sections.joinToString("") { section ->
            when (section) {
                is ExpandableSection.Text -> section.text
                is ExpandableSection.BindVar -> "?"
                is ExpandableSection.Newline -> "\n"
                is ExpandableSection.Projection -> {
                    if (pojo == null) {
                        section.text
                    } else {
                        interpretProjection(query, section, pojo, queriedTableNames)
                    }
                }
            }
        }
    }

    private fun interpretProjection(
        query: ExpandableParsedQuery,
        section: ExpandableSection.Projection,
        pojo: Pojo,
        queriedTableNames: List
    ): String {
        val aliasToName = query.tables
            .map { (name, alias) -> alias to name }
            .toMap(IdentifierMap())
        val nameToAlias = query.tables
            .groupBy { it.name.lowercase(Locale.ENGLISH) }
            .filter { (_, pairs) -> pairs.size == 1 }
            .map { (name, pairs) -> name to pairs.first().alias }
            .toMap(IdentifierMap())
        return when (section) {
            is ExpandableSection.Projection.All -> {
                expand(
                    pojo = pojo,
                    ignoredColumnNames = query.explicitColumns,
                    // The columns come directly from the specified table.
                    // We should not prepend the prefix-dot to the columns.
                    shallow = findEntityOrView(pojo)?.tableName in queriedTableNames,
                    nameToAlias = nameToAlias,
                    resultInfo = query.resultInfo
                )
            }
            is ExpandableSection.Projection.Table -> {
                val embedded = findEmbeddedField(pojo, section.tableAlias)
                if (embedded != null) {
                    expandEmbeddedField(
                        embedded = embedded,
                        table = findEntityOrView(embedded.pojo),
                        shallow = false,
                        tableToAlias = nameToAlias
                    ).joinToString(", ")
                } else {
                    val tableName =
                        aliasToName[section.tableAlias] ?: section.tableAlias
                    val table = tables.find { it.tableName == tableName }
                    pojo.fields.filter { field ->
                        field.parent == null &&
                            field.columnName !in query.explicitColumns &&
                            table?.columnNames?.contains(field.columnName) == true
                    }.joinToString(", ") { field ->
                        "`${section.tableAlias}`.`${field.columnName}`"
                    }
                }
            }
        }
    }

    private fun findEntityOrView(pojo: Pojo): EntityOrView? {
        return tables.find { it.typeName == pojo.typeName }
    }

    private fun findEmbeddedField(
        pojo: Pojo,
        tableAlias: String
    ): EmbeddedField? {
        // Try to find by the prefix.
        val matchByPrefix = pojo.embeddedFields.find { it.prefix == tableAlias }
        if (matchByPrefix != null) {
            return matchByPrefix
        }
        // Try to find by the table name.
        return pojo.embeddedFields.find {
            it.prefix.isEmpty() &&
                findEntityOrView(it.pojo)?.tableName == tableAlias
        }
    }

    private fun expand(
        pojo: Pojo,
        ignoredColumnNames: List,
        shallow: Boolean,
        nameToAlias: Map,
        resultInfo: QueryResultInfo?
    ): String {
        val table = findEntityOrView(pojo)
        return (
            pojo.embeddedFields.flatMap {
                expandEmbeddedField(it, findEntityOrView(it.pojo), shallow, nameToAlias)
            } + pojo.fields.filter { field ->
                field.parent == null &&
                    field.columnName !in ignoredColumnNames &&
                    (resultInfo == null || resultInfo.hasColumn(field.columnName))
            }.map { field ->
                if (table != null && table is Entity) {
                    // Should not happen when defining a view
                    val tableAlias = nameToAlias[table.tableName.lowercase(Locale.ENGLISH)]
                        ?: table.tableName
                    "`$tableAlias`.`${field.columnName}` AS `${field.columnName}`"
                } else {
                    "`${field.columnName}`"
                }
            }
            ).joinToString(", ")
    }

    private fun QueryResultInfo.hasColumn(columnName: String): Boolean {
        return columns.any { column -> column.name == columnName }
    }

    private fun expandEmbeddedField(
        embedded: EmbeddedField,
        table: EntityOrView?,
        shallow: Boolean,
        tableToAlias: Map
    ): List {
        val pojo = embedded.pojo
        return if (table != null) {
            if (embedded.prefix.isNotEmpty()) {
                table.fields.map { field ->
                    if (shallow) {
                        "`${embedded.prefix}${field.columnName}`"
                    } else {
                        "`${embedded.prefix}`.`${field.columnName}` " +
                            "AS `${embedded.prefix}${field.columnName}`"
                    }
                }
            } else {
                table.fields.map { field ->
                    if (shallow) {
                        "`${field.columnName}`"
                    } else {
                        val tableAlias = tableToAlias[table.tableName] ?: table.tableName
                        "`$tableAlias`.`${field.columnName}` AS `${field.columnName}`"
                    }
                }
            }
        } else {
            if (!shallow &&
                embedded.prefix.isNotEmpty() &&
                embedded.prefix in tableToAlias.values
            ) {
                pojo.fields.map { field ->
                    "`${embedded.prefix}`.`${field.columnNameWithoutPrefix(embedded.prefix)}` " +
                        "AS `${field.columnName}`"
                }
            } else {
                pojo.fields.map { field ->
                    "`${field.columnName}`"
                }
            }
        }
    }

    private fun Field.columnNameWithoutPrefix(prefix: String): String {
        return if (columnName.startsWith(prefix)) {
            columnName.substring(prefix.length)
        } else {
            columnName
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy