-
Notifications
You must be signed in to change notification settings - Fork 63
Add stored procedure calls with unnamed args #345
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
Changes from 1 commit
922d4a3
6c27840
d218a50
c6aea9d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,130 @@ | ||
package org.partiql.examples | ||
|
||
import com.amazon.ion.IonDecimal | ||
import com.amazon.ion.IonStruct | ||
import com.amazon.ion.system.IonSystemBuilder | ||
import org.partiql.examples.util.Example | ||
import org.partiql.lang.CompilerPipeline | ||
import org.partiql.lang.errors.ErrorCode | ||
import org.partiql.lang.errors.Property | ||
import org.partiql.lang.errors.PropertyValueMap | ||
import org.partiql.lang.eval.BindingCase | ||
import org.partiql.lang.eval.BindingName | ||
import org.partiql.lang.eval.Bindings | ||
import org.partiql.lang.eval.EvaluationException | ||
import org.partiql.lang.eval.EvaluationSession | ||
import org.partiql.lang.eval.ExprValue | ||
import org.partiql.lang.eval.ExprValueFactory | ||
import org.partiql.lang.eval.ExprValueType | ||
import org.partiql.lang.eval.builtins.storedprocedure.StoredProcedure | ||
import org.partiql.lang.eval.builtins.storedprocedure.StoredProcedureSignature | ||
import org.partiql.lang.eval.stringValue | ||
import java.io.PrintStream | ||
import java.math.BigDecimal | ||
import java.math.RoundingMode | ||
|
||
private val ion = IonSystemBuilder.standard().build() | ||
|
||
/** | ||
* A simple custom stored procedure that calculates the moon weight for each crewmate of the given crew, storing the | ||
* moon weight in the [EvaluationSession] global bindings. This procedure also returns the number of crewmates we | ||
* calculated the moon weight for, returning -1 if no crew is found. | ||
* | ||
* This example demonstrates how to create a custom stored procedure, check argument types, and modify the | ||
* [EvaluationSession]. | ||
*/ | ||
class CalculateCrewMoonWeight(private val valueFactory: ExprValueFactory): StoredProcedure { | ||
private val MOON_GRAVITATIONAL_CONSTANT = BigDecimal(1.622 / 9.81) | ||
|
||
// [StoredProcedureSignature] takes two arguments: | ||
// 1. the name of the stored procedure | ||
// 2. the arity of this stored procedure. Checks to arity are taken care of by the evaluator. However, we must | ||
// still check that the passed arguments are of the right type in our implementation of the procedure. | ||
override val signature = StoredProcedureSignature(name = "calculate_crew_moon_weight", arity = 1) | ||
|
||
// `call` is where you define the logic of the stored procedure given an [EvaluationSession] and a list of | ||
// arguments | ||
override fun call(session: EvaluationSession, args: List<ExprValue>): ExprValue { | ||
// We first check that the first argument is a string | ||
val crewName = args.first() | ||
// In the future the evaluator will also verify function argument types, but for now we must verify their type | ||
// manually | ||
if (crewName.type != ExprValueType.STRING) { | ||
val errorContext = PropertyValueMap().also { | ||
it[Property.EXPECTED_ARGUMENT_TYPES] = "STRING" | ||
it[Property.ACTUAL_ARGUMENT_TYPES] = crewName.type.name | ||
it[Property.FUNCTION_NAME] = signature.name | ||
} | ||
throw EvaluationException("First argument to ${signature.name} was not a string", | ||
ErrorCode.EVALUATOR_INCORRECT_TYPE_OF_ARGUMENTS_TO_PROCEDURE_CALL, | ||
errorContext, | ||
internal = false) | ||
} | ||
|
||
// Next we check if the given `crewName` is in the [EvaluationSession]'s global bindings. If not, we return 0. | ||
val sessionGlobals = session.globals | ||
val crewBindings = sessionGlobals[BindingName(crewName.stringValue(), BindingCase.INSENSITIVE)] | ||
?: return valueFactory.newInt(-1) | ||
|
||
// Now that we've confirmed the given `crewName` is in the session's global bindings, we calculate and store | ||
// the moon weight for each crewmate in the crew. | ||
// In addition, we keep a running a tally of how many crewmates we do this for. | ||
var numCalculated = 0 | ||
for (crewmateBinding in crewBindings) { | ||
val crewmate = crewmateBinding.ionValue as IonStruct | ||
val mass = crewmate["mass"] as IonDecimal | ||
val moonWeight = (mass.decimalValue() * MOON_GRAVITATIONAL_CONSTANT).setScale(1, RoundingMode.HALF_UP) | ||
crewmate.add("moonWeight", ion.newDecimal(moonWeight)) | ||
|
||
numCalculated++ | ||
} | ||
return valueFactory.newInt(numCalculated) | ||
} | ||
} | ||
|
||
/** | ||
* Demonstrates the use of custom stored procedure [CalculateCrewMoonWeight] in PartiQL queries. | ||
*/ | ||
class CustomProceduresExample(out: PrintStream) : Example(out) { | ||
override fun run() { | ||
/** | ||
* To make custom stored procedures available to the PartiQL query being executed, they must be passed to | ||
* [CompilerPipeline.Builder.addProcedure]. | ||
*/ | ||
val pipeline = CompilerPipeline.build(ion) { | ||
addProcedure(CalculateCrewMoonWeight(valueFactory)) | ||
} | ||
|
||
// Here, we initialize the crews to be stored in our global session bindings | ||
val initialCrews = Bindings.ofMap( | ||
mapOf( | ||
"crew1" to pipeline.valueFactory.newFromIonValue( | ||
ion.singleValue("""[ { name: "Neil", mass: 80.5 }, | ||
{ name: "Buzz", mass: 72.3 }, | ||
{ name: "Michael", mass: 89.9 } ]""")), | ||
"crew2" to pipeline.valueFactory.newFromIonValue( | ||
ion.singleValue("""[ { name: "James", mass: 77.1 }, | ||
{ name: "Spock", mass: 81.6 } ]""")) | ||
) | ||
) | ||
val session = EvaluationSession.build { globals(initialCrews) } | ||
|
||
val crew1BindingName = BindingName("crew1", BindingCase.INSENSITIVE) | ||
val crew2BindingName = BindingName("crew2", BindingCase.INSENSITIVE) | ||
|
||
out.println("Initial global session bindings:") | ||
print("Crew 1:", "${session.globals[crew1BindingName]}") | ||
print("Crew 2:", "${session.globals[crew2BindingName]}") | ||
|
||
// We call our custom stored procedure using PartiQL's `EXEC` clause. Here we call our stored procedure | ||
// 'calculate_crew_moon_weight' with the arg 'crew1', which outputs the number of crewmates we've calculated | ||
// the moon weight for | ||
val procedureCall = "EXEC calculate_crew_moon_weight 'crew1'" | ||
val procedureCallOutput = pipeline.compile(procedureCall).eval(session) | ||
print("Number of calculated moon weights:", "$procedureCallOutput") | ||
|
||
out.println("Updated global session bindings:") | ||
print("Crew 1:", "${session.globals[crew1BindingName]}") | ||
print("Crew 2:", "${session.globals[crew2BindingName]}") | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
package org.partiql.examples | ||
|
||
import org.partiql.examples.util.Example | ||
import java.io.PrintStream | ||
|
||
class CustomProceduresExampleTest : BaseExampleTest() { | ||
override fun example(out: PrintStream): Example = CustomProceduresExample(out) | ||
|
||
override val expected = """ | ||
|Initial global session bindings: | ||
|Crew 1: | ||
| [{'name': 'Neil', 'mass': 80.5}, {'name': 'Buzz', 'mass': 72.3}, {'name': 'Michael', 'mass': 89.9}] | ||
|Crew 2: | ||
| [{'name': 'James', 'mass': 77.1}, {'name': 'Spock', 'mass': 81.6}] | ||
|Number of calculated moon weights: | ||
| 3 | ||
|Updated global session bindings: | ||
|Crew 1: | ||
| [{'name': 'Neil', 'mass': 80.5, 'moonWeight': 13.3}, {'name': 'Buzz', 'mass': 72.3, 'moonWeight': 12.0}, {'name': 'Michael', 'mass': 89.9, 'moonWeight': 14.9}] | ||
|Crew 2: | ||
| [{'name': 'James', 'mass': 77.1}, {'name': 'Spock', 'mass': 81.6}] | ||
| | ||
""".trimMargin() | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,6 +21,7 @@ import org.partiql.lang.ast.passes.* | |
import org.partiql.lang.domains.PartiqlAst | ||
import org.partiql.lang.errors.* | ||
import org.partiql.lang.eval.binding.* | ||
import org.partiql.lang.eval.builtins.storedprocedure.StoredProcedure | ||
import org.partiql.lang.eval.like.PatternPart | ||
import org.partiql.lang.eval.like.executePattern | ||
import org.partiql.lang.eval.like.parsePattern | ||
|
@@ -54,6 +55,7 @@ import kotlin.collections.* | |
internal class EvaluatingCompiler( | ||
private val valueFactory: ExprValueFactory, | ||
private val functions: Map<String, ExprFunction>, | ||
private val procedures: Map<String, StoredProcedure>, | ||
private val compileOptions: CompileOptions = CompileOptions.standard() | ||
) { | ||
private val thunkFactory = ThunkFactory(compileOptions.thunkOptions) | ||
|
@@ -1926,8 +1928,46 @@ internal class EvaluatingCompiler( | |
} | ||
} | ||
|
||
private fun compileExec(node: ExprNode): ThunkEnv { | ||
TODO() | ||
private fun compileExec(node: Exec): ThunkEnv { | ||
val (procedureName, args, metas: MetaContainer) = node | ||
val procedure = procedures[procedureName.name] ?: err( | ||
"No such stored procedure: ${procedureName.name}", | ||
ErrorCode.EVALUATOR_NO_SUCH_PROCEDURE, | ||
errorContextFrom(metas).also { | ||
it[Property.PROCEDURE_NAME] = procedureName.name | ||
}, | ||
internal = false) | ||
|
||
// Check arity | ||
if (args.size !in procedure.signature.arity) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just found something weird regarding There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm interesting. I don't think that internally This leads me to think that nothing should be done unless we decide not to use |
||
val errorContext = errorContextFrom(metas).also { | ||
it[Property.EXPECTED_ARITY_MIN] = procedure.signature.arity.first | ||
it[Property.EXPECTED_ARITY_MAX] = procedure.signature.arity.last | ||
} | ||
|
||
val message = when { | ||
procedure.signature.arity.first == 1 && procedure.signature.arity.last == 1 -> | ||
"${procedure.signature.name} takes a single argument, received: ${args.size}" | ||
procedure.signature.arity.first == procedure.signature.arity.last -> | ||
"${procedure.signature.name} takes exactly ${procedure.signature.arity.first} arguments, received: ${args.size}" | ||
else -> | ||
"${procedure.signature.name} takes between ${procedure.signature.arity.first} and " + | ||
"${procedure.signature.arity.last} arguments, received: ${args.size}" | ||
} | ||
|
||
throw EvaluationException(message, | ||
ErrorCode.EVALUATOR_INCORRECT_NUMBER_OF_ARGUMENTS_TO_PROCEDURE_CALL, | ||
errorContext, | ||
internal = false) | ||
} | ||
|
||
// Compile the procedure's arguments | ||
val argThunks = args.map { compileExprNode(it) } | ||
|
||
return thunkFactory.thunkEnv(metas) { env -> | ||
val procedureArgValues = argThunks.map { it(env) } | ||
procedure.call(env.session, procedureArgValues) | ||
} | ||
} | ||
|
||
/** A special wrapper for `UNPIVOT` values as a BAG. */ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
According to comment, type check is done in procedure implementation, so usage of this error code depends on user's best intention. As mentioned in the other comment, this could be improved by taking over type check or providing utility to help user defining their own type check.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned in the other reply, we do have plans to add a type-checking utility for sprocs and udfs, so once that's added, throwing of this error should be handled by that utility.