Skip to content

Commit

Permalink
Add pluggable functions tutorial and pull from main
Browse files Browse the repository at this point in the history
  • Loading branch information
yuxtang-amazon committed Aug 1, 2023
1 parent 01c1a09 commit 1467673
Show file tree
Hide file tree
Showing 9 changed files with 269 additions and 63 deletions.
8 changes: 4 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,10 @@ Thank you to all who have contributed!
### Deprecated
- Deprecates Map<String, ExprFunction> representation of functions in the `CompilerPipeline`
and experimental `PartiQLCompilerPipeline`. Please use List<ExprFunction> to represent functions instead.
- **Breaking**: Deprecates `Arguments` class, `callWithOptional()` and `callWithVariadic()` methods in the `ExprFunction`
with a Deprecation Level of ERROR. Please invoke `callWithRequired()` instead.
- **Breaking**: Deprecates the `Arguments`, `RequiredArgs`, `RequiredWithOptional`, and `RequiredWithVariadic` classes,
along with the `callWithOptional()`, `callWithVariadic()`, and the overloaded `call()` methods in the `ExprFunction` class,
marking them with a Deprecation Level of ERROR. Now, it's recommended to use
`call(session: EvaluationSession, args: List<ExprValue>)` and `callWithRequired()` instead.
- **Breaking**: Deprecates `optionalParameter` and `variadicParameter` in the `FunctionSignature` with a Deprecation
Level of ERROR. Please use multiple implementations of ExprFunction and use the LIST ExprValue to
represent variadic parameters instead.
Expand All @@ -89,8 +91,6 @@ Thank you to all who have contributed!

## [0.12.0] - 2023-06-14

## [0.12.0] - 2023-06-14

### Added

- Adds support for using EXCLUDED within DML ON-CONFLICT-ACTION conditions. Closes #1111.
Expand Down
136 changes: 136 additions & 0 deletions docs/wiki/tutorials/Pluggable Functions Tutorial.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# Pluggable Functions Tutorial

The PartiQL ecosystem has introduced a new method for integrating custom logic - Pluggable Functions. This system allows PartiQL users to implement and introduce their custom functions into the PartiQL Command Line Interface (CLI) without the need for extensive knowledge of the PartiQL codebase or build system.

This document will guide you on creating custom functions and loading them to the PartiQL CLI. The functions created by you will be pluggable, meaning they can be integrated as needed, enhancing the functionality of the CLI without modifying the underlying code.

## Step 1: Add Dependencies and Build Your Module

To build your module, you can use build automation tools like Maven or Gradle. These tools will handle the dependency management and compilation process for you. Here's how you can specify the necessary dependencies:

```Kotlin
dependencies {
implementation("org.partiql:partiql-spi:<latest_version>")
implementation("org.partiql:partiql-types:<latest_version>")
}
```

## Step 2: Create a Custom Function

To create a custom function, you need to implement the `PartiQLFunction` interface. This interface allows you to define the function's behavior and its signature, including its name, return type, parameters, determinism, and optional description.

Here's a basic template to help you start:

```Kotlin
import org.partiql.spi.connector.ConnectorSession
import org.partiql.spi.function.PartiQLFunction
import org.partiql.spi.function.PartiQLFunctionExperimental
import org.partiql.types.PartiQLValueType
import org.partiql.types.function.FunctionParameter
import org.partiql.types.function.FunctionSignature
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.StringValue
import org.partiql.value.stringValue

@OptIn(PartiQLFunctionExperimental::class)
object TrimLead : PartiQLFunction {
override val signature = FunctionSignature(
name = "trim_lead", // Specify your function name
returns = PartiQLValueType.STRING, // Specify the return type
parameters = listOf(
FunctionParameter.ValueParameter(name = "str", type = PartiQLValueType.STRING) // Specify parameters
),
isDeterministic = true, // Specify determinism
description = "Trims leading whitespace of a [str]." // A brief description of your function
)

@OptIn(PartiQLValueExperimental::class)
override operator fun invoke(session: ConnectorSession, arguments: List<PartiQLValue>): PartiQLValue {
// Implement the function logic here
val str = (arguments[0] as? StringValue)?.string ?: ""
val processed = str.trimStart()
return stringValue(processed)
}
}
```

Ensure that you replace the signature and function invoking with your actual implementations.

## Step 3: Implement the Plugin Interface

Next, you need to implement the `Plugin` interface in your code. This allows you to return a list of all the custom `PartiQLFunction` implementations you've created, using the `getFunctions()` method. This step is crucial as it allows the service loader to retrieve all your custom functions.

Here's an example of a `Plugin` implementation:

```Kotlin
package org.partiql.plugins.mockdb

import org.partiql.spi.Plugin
import org.partiql.spi.connector.Connector
import org.partiql.spi.function.PartiQLFunction

public class LocalPlugin implements Plugin {
override fun getConnectorFactories(): List<Connector.Factory> = listOf()

@PartiQLFunctionExperimental
override fun getFunctions(): List<PartiQLFunction> = listOf(
TrimLead // Specify the functions
)
}
```

## Step 4: Create Service Provider Configuration file

In order for the Java's ServiceLoader to recognize your plugin and load your custom functions, you'll need to specify your `Plugin` implementation in a Service Provider Configuration file:

1. In your project's main resources directory (usually `src/main/resources`), create a new directory named `META-INF/services`.

2. Within this directory, create a new file named after the full interface name as `org.partiql.spi.Plugin`.

3. Inside this file, write the fully qualified name of your Plugin implementation class. If you have multiple implementations, each should be listed on a new line.

Here's an example if your `Plugin` implementation class is `org.partiql.plugins.mockdb.LocalPlugin`:
```Kotlin
org.partiql.plugins.mockdb.LocalPlugin
```

## Step 5: Package Your Functions into a .jar File

Compile your function and `Plugin` implementation into bytecode and package it into a .jar file. This .jar file will act as a plugin for the PartiQL CLI.

For example, if you are using Gradle, you can simply run `./gradlew build` to compile your module. And your .jar file can be found under `build/libs`.

## Step 6: Load the Functions into CLI

Each of your .jar files should be stored in its own subdirectory under the `plugins` directory, which itself is inside the .partiql directory in your home directory. Here's what the directory structure should look like:

```
~/.partiql/plugins
├── firstPlugin
│ └── firstPlugin.jar
└── secondPlugin
└── secondPlugin.jar
```

In the example above, `firstPlugin.jar` and `secondPlugin.jar` are the plugins you've created, each in its own directory under `~/.partiql/plugins`.

By default, the PartiQL CLI will search the `~/.partiql/plugins` directory for plugins. However, you can specify a different directory when starting the CLI with the `--plugins` option:

```shell
partiql --plugins /path/to/your/plugin/directory
```

With this command, the CLI will search for .jar files in the specified directory’s subdirectories and load them as plugins. This feature gives you the flexibility to organize your plugins in a manner that best suits your needs.

## Step 7: Invoke Custom Functions in CLI

To use the custom functions, you simply call them as you would any other function in the PartiQL CLI. For example, if you created a function named "trim_lead", you would invoke it like so:

```shell
partiql> trim_lead(string)
```

Please replace "string" with the actual string you want to trim.

That's all there is to it! With this mechanism, you can introduce any number of custom functions to PartiQL, and these functions will be usable just like built-in functions, without the need to modify the PartiQL codebase. It's an excellent way to extend the capabilities of PartiQL and adapt it to the specific needs of your project or organization.
2 changes: 1 addition & 1 deletion partiql-ast/src/main/pig/partiql.ion
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ may then be further optimized by selecting better implementations of each operat
// Explain Statement
(explain target::explain_target)
)

(sum explain_target
(domain
statement::statement
Expand Down
120 changes: 82 additions & 38 deletions partiql-cli/src/main/kotlin/org/partiql/cli/utils/ServiceLoaderUtil.kt
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,6 @@ import org.partiql.value.NullableListValue
import org.partiql.value.NullableSexpValue
import org.partiql.value.NullableStringValue
import org.partiql.value.NullableSymbolValue
import org.partiql.value.NullableTimeValue
import org.partiql.value.PartiQLValue
import org.partiql.value.PartiQLValueExperimental
import org.partiql.value.SexpValue
Expand All @@ -77,6 +76,9 @@ import org.partiql.value.boolValue
import org.partiql.value.charValue
import org.partiql.value.clobValue
import org.partiql.value.dateValue
import org.partiql.value.datetime.DateTimeValue.date
import org.partiql.value.datetime.DateTimeValue.time
import org.partiql.value.datetime.TimeZone
import org.partiql.value.decimalValue
import org.partiql.value.float32Value
import org.partiql.value.float64Value
Expand All @@ -95,8 +97,10 @@ import org.partiql.value.symbolValue
import org.partiql.value.timeValue
import java.math.BigDecimal
import java.math.BigInteger
import java.math.RoundingMode
import java.net.URLClassLoader
import java.nio.file.Path
import java.time.DateTimeException
import java.util.ServiceLoader
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
Expand Down Expand Up @@ -252,25 +256,38 @@ class ServiceLoaderUtil {
PartiQLValueType.CLOB -> (partiqlValue as? ClobValue)?.value?.let { newClob(it) }
?: throw PartiQLtoExprValueTypeMismatchException("CLOB", partiqlValue.type)

PartiQLValueType.DATE -> (partiqlValue as? DateValue)?.value?.let { newDate(it) }
PartiQLValueType.DATE -> (partiqlValue as? DateValue)?.value?.let { newDate(it.year, it.month, it.day) }
?: throw PartiQLtoExprValueTypeMismatchException("DATE", partiqlValue.type)

PartiQLValueType.TIME -> {
val timeValue = partiqlValue as? TimeValue
timeValue?.let { tv ->
val value = tv.value
val precision = tv.precision
val offset = tv.offset
val withzone = tv.withZone
if (withzone) {
offset?.let {
newTime(Time.of(value, precision, it))
}
} else {
newTime(Time.of(value, precision, null))
PartiQLValueType.TIME -> (partiqlValue as? TimeValue)?.value?.let { partiqlTime ->
val fraction = partiqlTime.decimalSecond.remainder(BigDecimal.ONE)
val precision =
when {
fraction.scale() > 9 -> throw DateTimeException("Precision greater than nano seconds not supported")
else -> fraction.scale()
}
} ?: throw PartiQLtoExprValueTypeMismatchException("TIME", partiqlValue.type)
}

val tzMinutes = when (val tz = partiqlTime.timeZone) {
is TimeZone.UnknownTimeZone -> 0 // Treat unknown offset as UTC (+00:00)
is TimeZone.UtcOffset -> tz.totalOffsetMinutes
else -> null
}

try {
newTime(
Time.of(
partiqlTime.hour,
partiqlTime.minute,
partiqlTime.decimalSecond.setScale(0, RoundingMode.DOWN).toInt(),
fraction.movePointRight(9).setScale(0, RoundingMode.DOWN).toInt(),
precision,
tzMinutes
)
)
} catch (e: DateTimeException) {
throw e
}
} ?: throw PartiQLtoExprValueTypeMismatchException("TIME", partiqlValue.type)

PartiQLValueType.TIMESTAMP -> TODO()
// TODO: Implement
Expand Down Expand Up @@ -363,23 +380,38 @@ class ServiceLoaderUtil {
PartiQLValueType.NULLABLE_CLOB -> (partiqlValue as? NullableClobValue)?.value?.let { newClob(it) }
?: ExprValue.nullValue

PartiQLValueType.NULLABLE_DATE -> (partiqlValue as? NullableDateValue)?.value?.let { newDate(it) }
PartiQLValueType.NULLABLE_DATE -> (partiqlValue as? NullableDateValue)?.value?.let { newDate(it.year, it.month, it.day) }
?: ExprValue.nullValue

PartiQLValueType.NULLABLE_TIME -> {
(partiqlValue as? NullableTimeValue)?.let { tv ->
tv.value?.let { value ->
val precision = tv.precision
val offset = tv.offset
val withzone = tv.withZone
if (withzone) {
offset?.let { offsetValue -> newTime(Time.of(value, precision, offsetValue)) } ?: null
} else {
newTime(Time.of(value, precision, null))
}
PartiQLValueType.NULLABLE_TIME -> (partiqlValue as? TimeValue)?.value?.let { partiqlTime ->
val fraction = partiqlTime.decimalSecond.remainder(BigDecimal.ONE)
val precision =
when {
fraction.scale() > 9 -> throw DateTimeException("Precision greater than nano seconds not supported")
else -> fraction.scale()
}
} ?: ExprValue.nullValue
}

val tzMinutes = when (val tz = partiqlTime.timeZone) {
is TimeZone.UnknownTimeZone -> 0 // Treat unknown offset as UTC (+00:00)
is TimeZone.UtcOffset -> tz.totalOffsetMinutes
else -> null
}

try {
newTime(
Time.of(
partiqlTime.hour,
partiqlTime.minute,
partiqlTime.decimalSecond.setScale(0, RoundingMode.DOWN).toInt(),
fraction.movePointRight(9).setScale(0, RoundingMode.DOWN).toInt(),
precision,
tzMinutes
)
)
} catch (e: DateTimeException) {
throw e
}
} ?: ExprValue.nullValue

PartiQLValueType.NULLABLE_TIMESTAMP -> TODO()
// TODO: Implement
Expand All @@ -397,17 +429,17 @@ class ServiceLoaderUtil {
PartiQLValueType.NULLABLE_INTERVAL -> TODO() // add nullable interval conversion

PartiQLValueType.NULLABLE_BAG -> {
(partiqlValue as? NullableBagValue<*>)?.elements?.map { PartiQLtoExprValue(it) }
(partiqlValue as? NullableBagValue<*>)?.promote()?.elements?.map { PartiQLtoExprValue(it) }
?.let { newBag(it.asSequence()) } ?: ExprValue.nullValue
}

PartiQLValueType.NULLABLE_LIST -> {
(partiqlValue as? NullableListValue<*>)?.elements?.map { PartiQLtoExprValue(it) }
(partiqlValue as? NullableListValue<*>)?.promote()?.map { PartiQLtoExprValue(it) }
?.let { newList(it.asSequence()) } ?: ExprValue.nullValue
}

PartiQLValueType.NULLABLE_SEXP -> {
(partiqlValue as? NullableSexpValue<*>)?.elements?.map { PartiQLtoExprValue(it) }
(partiqlValue as? NullableSexpValue<*>)?.promote()?.map { PartiQLtoExprValue(it) }
?.let { newSexp(it.asSequence()) } ?: ExprValue.nullValue
}

Expand Down Expand Up @@ -488,11 +520,18 @@ class ServiceLoaderUtil {
}
PartiQLValueType.DATE -> {
checkType(ExprValueType.DATE)
dateValue(exprValue.dateValue())
dateValue(
date(exprValue.dateValue().year, exprValue.dateValue().monthValue, exprValue.dateValue().dayOfMonth)
)
}
PartiQLValueType.TIME -> {
checkType(ExprValueType.TIME)
timeValue(exprValue.timeValue().localTime, exprValue.timeValue().precision, exprValue.timeValue().zoneOffset, true)
timeValue(
time(
exprValue.timeValue().localTime.hour, exprValue.timeValue().localTime.minute, exprValue.timeValue().localTime.second, exprValue.timeValue().localTime.nano,
exprValue.timeValue().timezoneMinute?.let { TimeZone.UtcOffset.of(it) } ?: null
)
)
}
PartiQLValueType.TIMESTAMP -> TODO()
PartiQLValueType.INTERVAL -> TODO()
Expand Down Expand Up @@ -594,12 +633,17 @@ class ServiceLoaderUtil {
}
PartiQLValueType.NULLABLE_DATE -> when (exprValue.type) {
ExprValueType.NULL -> nullValue()
ExprValueType.DATE -> dateValue(exprValue.dateValue())
ExprValueType.DATE -> dateValue(date(exprValue.dateValue().year, exprValue.dateValue().monthValue, exprValue.dateValue().dayOfMonth))
else -> throw ExprToPartiQLValueTypeMismatchException(PartiQLValueType.NULLABLE_DATE, ExprToPartiQLValueType(exprValue))
}
PartiQLValueType.NULLABLE_TIME -> when (exprValue.type) {
ExprValueType.NULL -> nullValue()
ExprValueType.TIME -> timeValue(exprValue.timeValue().localTime, exprValue.timeValue().precision, exprValue.timeValue().zoneOffset, true)
ExprValueType.TIME -> timeValue(
time(
exprValue.timeValue().localTime.hour, exprValue.timeValue().localTime.minute, exprValue.timeValue().localTime.second, exprValue.timeValue().localTime.nano,
exprValue.timeValue().timezoneMinute?.let { TimeZone.UtcOffset.of(it) } ?: null
)
)
else -> throw ExprToPartiQLValueTypeMismatchException(PartiQLValueType.NULLABLE_TIME, ExprToPartiQLValueType(exprValue))
}
PartiQLValueType.NULLABLE_TIMESTAMP -> TODO()
Expand Down
Loading

0 comments on commit 1467673

Please sign in to comment.