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

org.partiql.lang.eval.Bindings.kt Maven / Gradle / Ivy

There is a newer version: 1.0.0-perf.1
Show newest version
/*
 * Copyright 2019 Amazon.com, Inc. or its affiliates.  All rights reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 *  You may not use this file except in compliance with the License.
 * A copy of the License is located at:
 *
 *      http://aws.amazon.com/apache2.0/
 *
 *  or in the "license" file accompanying this file. This file 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 org.partiql.lang.eval
import com.amazon.ion.IonStruct
import com.amazon.ion.IonSystem
import com.amazon.ion.IonValue
import org.partiql.lang.domains.PartiqlAst
import org.partiql.lang.errors.ErrorCode
import org.partiql.lang.util.errAmbiguousBinding
import org.partiql.lang.util.isBindingNameEquivalent
import org.partiql.lang.util.stringValue

/** Indicates if the lookup of a particular binding should be case-sensitive or not. */
enum class BindingCase {
    SENSITIVE, INSENSITIVE;

    companion object {
        fun fromIonValue(sym: IonValue): BindingCase =
            when (sym.stringValue()) {
                "case_sensitive" -> SENSITIVE
                "case_insensitive" -> INSENSITIVE
                else -> errNoContext(
                    "Unable to convert ion value '${sym.stringValue()}' to a BindingCase instance",
                    errorCode = ErrorCode.EVALUATOR_INVALID_CONVERSION,
                    internal = true
                )
            }
    }

    fun toSymbol(ions: IonSystem) =
        ions.newSymbol(
            when (this) {
                SENSITIVE -> "case_sensitive"
                INSENSITIVE -> "case_insensitive"
            }
        )
}

/**
 * Converts a [CaseSensitivity] to a [BindingCase].
 */
fun PartiqlAst.CaseSensitivity.toBindingCase(): BindingCase = when (this) {
    is PartiqlAst.CaseSensitivity.CaseInsensitive -> BindingCase.INSENSITIVE
    is PartiqlAst.CaseSensitivity.CaseSensitive -> BindingCase.SENSITIVE
}

/**
 * Encapsulates the data necessary to perform a binding lookup.
 */
data class BindingName(val name: String, val bindingCase: BindingCase) {
    val loweredName: String by lazy(LazyThreadSafetyMode.PUBLICATION) { name.toLowerCase() }
    /**
     * Compares [name] to [otherName] using the rules specified by [bindingCase].
     */
    fun isEquivalentTo(otherName: String?) = otherName != null && name.isBindingNameEquivalent(otherName, bindingCase)
}

/**
 * A mapping of name to [ExprValue].
 *
 * Due to the need to throw a consistent [EvaluationException] in the event of an ambiguous
 * case-insensitive binding lookup, customers should avoid implementing this interface directly
 * and should instead use one of the static factory methods or the lazy bindings builder to obtain
 * an instance of [Bindings] that correctly complies with the exception contract.  See
 * [org.partiql.lang.examples.Evaluation] for examples.
 */
interface Bindings {

    /**
     * Looks up a name within the environment.
     *
     * Implementations should respect the value of [BindingName.bindingCase].
     *
     * @param bindingName The binding to look up.
     *
     * @return The value mapped to the binding, or `null` if no such binding exists.
     * @throws [EvaluationException] If multiple bindings matching the specified [BindingName] are found.
     * Clients should use [BindingHelper.throwAmbiguousBindingEvaluationException] to throw this exception.
     */
    @Throws(EvaluationException::class)
    operator fun get(bindingName: BindingName): T?

    companion object {
        private val EMPTY = over { _ -> null }

        @Suppress("UNCHECKED_CAST")
        fun  empty(): Bindings = EMPTY as Bindings

        /**
         * A SAM conversion for [Bindings] from a function object.
         *
         * This is necessary as Kotlin currently doesn't support SAM conversions to
         * Kotlin defined interfaces (only Java defined interfaces).
         */
        fun  over(func: (BindingName) -> T?): Bindings = object : Bindings {
            override fun get(bindingName: BindingName): T? = func(bindingName)
        }

        /**
         * Returns an instance of [LazyBindingsBuilder].  If calling from Kotlin, prefer to use [buildLazyBindings]
         * instead.
         */
        @JvmStatic
        fun  lazyBindingsBuilder(): LazyBindingBuilder = LazyBindingBuilder()

        /**
         * Invokes [block], passing an instance of [LazyBindingBuilder]. If calling from Java, prefer to use
         * [lazyBindingsBuilder] instead.
         */
        fun  buildLazyBindings(block: LazyBindingBuilder.() -> Unit): Bindings = LazyBindingBuilder().apply(block).build()

        // TODO This API needs to be fleshed out with respect to mutable maps (the Map interface doesn't guaranteed immutability)
        /**
         * Returns an instance of [Bindings] that is backed by a `Map`.
         *
         * A current limitation of this factory method is that [backingMap] must not change.  In other words,
         * any [Map] that is passed that can be mutated, is not guaranteed to work correctly with this API.
         */
        @JvmStatic
        fun  ofMap(backingMap: Map): Bindings = MapBindings(backingMap)

        /**
         * Returns an instance of [Bindings] that is backed by an [IonStruct].
         */
        @JvmStatic
        fun ofIonStruct(struct: IonStruct, valueFactory: ExprValueFactory): Bindings = IonStructBindings(valueFactory, struct)
    }

    /** An implementation of the builder pattern for instances of [Bindings]. */
    class LazyBindingBuilder {
        private val bindings = HashMap> ()

        fun addBinding(name: String, getter: () -> T): LazyBindingBuilder =
            this.apply { bindings[name] = lazy(getter) }

        fun build(): Bindings =
            LazyBindings(bindings)
    }
}

/**
 * A [Bindings] implementation that is backed by a [Map].
 *
 * [originalCaseMap] is the backing [Map].  Important note:
 * *this must be immutable!  Changes to this map will not be reflected in [loweredCaseMap]
 * after it has been instantiated.
 *
 * [loweredCaseMap] is based on [originalCaseMap] and is lazily calculated the first time
 * a case-insensitive lookup is performed.
 *
 * If an ambiguous binding match is found during a case-insensitive lookup, [errAmbiguousBinding]
 * is invoked to report the error to the customer.
 */
class MapBindings(val originalCaseMap: Map) : Bindings {
    private val loweredCaseMap: Map>> by lazy {
        originalCaseMap.entries.groupBy { it.key.toLowerCase() }
    }

    override fun get(bindingName: BindingName): T? =
        when (bindingName.bindingCase) {
            BindingCase.SENSITIVE -> originalCaseMap[bindingName.name]
            BindingCase.INSENSITIVE -> {
                val foundBindings = loweredCaseMap[bindingName.loweredName]
                when {
                    foundBindings == null -> null
                    foundBindings.size == 1 -> foundBindings.first().value
                    else ->
                        errAmbiguousBinding(bindingName.name, foundBindings.map { it.key })
                }
            }
        }
}

/** A [Bindings] implementation that lazily materializes the values of the bindings contained within. */
private class LazyBindings(originalCaseMap: Map>) : Bindings {
    private val delegate: Bindings> = MapBindings(originalCaseMap)

    override fun get(bindingName: BindingName): T? = delegate[bindingName]?.value
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy