S-JIS[2025-12-25/2026-01-04]

Tsurugi UDF Javaでの実装例

TsurugiSQLユーザー定義関数(UDF)をJavaで作る例。


概要

TsurugiのUDFは、関数の処理内容をgRPCサーバーで実装する。
そのgRPCサーバーをJavaで作ってみる。


build.gradle

依存ライブラリーには、gRPCとprotobufを入れる。

tsurugi-udf-example/build.gradle:

plugins {
    id 'java'
    id 'application'
    id 'com.google.protobuf' version '0.9.4'
}

group = 'com.example.tsurugi.udf'
version = '0.1-SNAPSHOT'

repositories {
    mavenCentral()
}

java {
    toolchain {
        languageVersion = JavaLanguageVersion.of(21)
    }
}

dependencies {
    implementation 'io.grpc:grpc-netty-shaded:1.59.0'
    implementation 'io.grpc:grpc-protobuf:1.59.0'
    implementation 'io.grpc:grpc-stub:1.59.0'
    implementation 'javax.annotation:javax.annotation-api:1.3.2'
    implementation 'com.google.protobuf:protobuf-java:3.25.0'
}

protobuf {
    protoc {
        artifact = 'com.google.protobuf:protoc:3.25.0'
    }
    plugins {
        grpc {
            artifact = 'io.grpc:protoc-gen-grpc-java:1.59.0'
        }
    }
    generateProtoTasks {
        all()*.plugins {
            grpc {}
        }
    }
}

sourceSets {
    main {
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}

application {
    mainClass = 'com.example.tsurugi.udf.UdfExampleServer'
}

task runServer(type: JavaExec) {
    classpath = sourceSets.main.runtimeClasspath
    mainClass = 'com.example.tsurugi.udf.UdfExampleServer'
}

protoファイル

TsurugiのUDFはprotoファイルで定義する。

tsurugi-udf-example/src/main/proto/example.proto:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "com.example.tsurugi.udf.proto";

message MyFunctionRequest {
  int32 value = 1;
}

message MyFunctionResponse {
  int32 value = 1;
}

service ExampleService {
  rpc my_function(MyFunctionRequest) returns (MyFunctionResponse);
}

TsurugiのUDF用のrpc関数は、引数と戻り値をmessageで定義する必要がある。


Gradleでビルドすると、protoファイルからJavaのソースが生成される。

cd tsurugi-udf-example
./gradlew build

gRPCサーバーの実装

TsurugiのUDFを処理するgRPCサービスとgRPCサーバーを実装する。

tsurugi-udf-example/src/main/java/com/example/tsurugi/udf/UdfExampleServer.java:

package com.example.tsurugi.udf;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.util.concurrent.TimeUnit;

import com.example.tsurugi.udf.proto.ExampleServiceGrpc;
import com.example.tsurugi.udf.proto.MyFunctionRequest;
import com.example.tsurugi.udf.proto.MyFunctionResponse;
public class UdfExampleServer {
    private static final int PORT = 50051;

    public static void main(String[] args) throws IOException, InterruptedException {
        var server = new UdfExampleServer();
        server.start();
        server.blockUntilShutdown();
    }
    private Server server;

    private void start() throws IOException {
        server = ServerBuilder.forPort(PORT).addService(new ExampleServiceImpl()).build().start();
        System.out.println("Server started, listening on " + PORT);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            try {
                UdfExampleServer.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.err.println("*** server shut down");
        }));
    }

    private void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
    static class ExampleServiceImpl extends ExampleServiceGrpc.ExampleServiceImplBase {

        @Override
        public void myFunction(MyFunctionRequest request, StreamObserver<MyFunctionResponse> responseObserver) {
            System.out.println("Received request with value: " + request.getValue());

            // UDFの計算本体
            int result = request.getValue() * 2;

            var response = MyFunctionResponse.newBuilder()
                .setValue(result)
                .build();

            System.out.println("Sending response with value: " + result);
            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }
    }
}

TsurugiへのUDFの登録

src/main/protoのprotoファイルを元に、udf-plugin-builderを使ってTsurugiにUDFを登録する。


TsurugiのDockerイメージのTsurugiを使い、gRPCサーバーがホストにある場合は、登録時にホストのIPアドレスを指定する。
WindowsでDokcer Desktopを使っているならホスト名をhost.docker.internalにすればいいが、WSL2にインストールされたDockerを使っている場合は、ホストのIPアドレスを直接指定する。

WSL2から見たホストのIPアドレスは、PowerShellのipconfigコマンドで確認できる。

WindowsのPowerShellから

> ipconfig
Windows IP 構成

イーサネット アダプター vEthernet (WSL (Hyper-V firewall)):

   接続固有の DNS サフィックス . . . . .:
   リンクローカル IPv6 アドレス. . . . .: fe80::8abc:xxxx:xxxx:xxxx%46
   IPv4 アドレス . . . . . . . . . . . .: 17y.yyy.yyy.yyy
   サブネット マスク . . . . . . . . . .: 255.255.240.0
   デフォルト ゲートウェイ . . . . . . .:
〜

udf-plugin-builderを実行してTsurugiに登録する。(--output-dirで指定した場所にファイルが生成される)

Tsurugiサーバー上で

~/.local/bin/udf-plugin-builder --proto-file example.proto --grpc-endpoint 17y.yyy.yyy.yyy:50051 --output-dir $TSURUGI_HOME/var/plugins/

接続先ホストとポート番号は、生成されたiniファイルを見れば確認できる。(このファイルを直接編集してもいい)

tsurugi@afcebcbb7f4d:/usr/lib/tsurugi-1.8.0$ cat $TSURUGI_HOME/var/plugins/libexample.ini
[udf]
enabled=true
endpoint=17y.yyy.yyy.yyy:50051
secure=false

$TSURUGI_HOME/var/plugins/に配置したら、Tsurugiを再起動する。

$TSURUGI_HOME/bin/tgctl shutdown
$TSURUGI_HOME/bin/tgctl start

gRPCサーバーの実行

cd tsurugi-udf-example
./gradlew runServer

UDFの実行例

$TSURUGI_HOME/bin/tgsql -c ipc:tsurugi
create table test (foo int primary key);
insert into test values(1), (2), (100);
tgsql> select my_function(foo) from test;
[@#0: INT]
[2]
[4]
[200]
(3 rows)

Tsurugi UDFのデータ型の例

Tsurugi UDFでは、DECIMALやDATE・TIMSTAMP等をprotoファイルで記述するためのデータ型として、tsurugidb.udfパッケージのmessageを提供している。[2026-01-04]

これらをJavaのgRPCサーバーで扱う例。


protoファイル

tsurugi-udf-example/src/main/proto/example2.proto:

syntax = "proto3";

package example.tsurugi.udf;

option java_multiple_files = true;
option java_package = "com.example.tsurugi.udf.proto";

import "tsurugidb/udf/tsurugi_types.proto";

message IncDecimalRequest {
  tsurugidb.udf.Decimal value = 1;
}

message IncDecimalResponse {
  tsurugidb.udf.Decimal value = 1;
}

service Example2Service {
  rpc inc_decimal(IncDecimalRequest) returns (IncDecimalResponse);
}

tsurugidb.udfパッケージのmessageを使用するためには、tsurugi_types.protoをインポートする必要がある。


tsurugi_types.proto

「tsurugi_types.protoをインポートしたprotoファイル」をコンパイルする際にtsurugi_types.protoファイルが必要になる。

このファイル自体はudf-pluginのソースアーカイブにあるが、gRPCサーバーのビルド時にファイルの場所を指定してやる必要がある。


最も手っ取り早いのは、tsurugi_types.protoをsrc/main/protoの下にコピーしてしまうこと。
(src/main/proto直下にtsurugi_types.protoを置くのではなく、パッケージ名に沿ったディレクトリーを用意する必要がある)

(例)tsurugi-udf-example/src/main/proto/tsurugidb/udf/tsurugi_types.proto


udf-pluginのソースアーカイブの下のファイルをそのまま使いたい場合は、build.gradleでtsurugi_types.protoの場所を指定する。

tsurugi_types.protoがC:/tmp/example/tsurugi-udf-0.1.0/proto/tsurugidb/udf/tsurugi_types.protoにある場合、build.gradleのsourceSetsに以下の様に追加する。

tsurugi-udf-example/build.gradle:

sourceSets {
    main {
        proto {
            srcDir 'C:/tmp/example/tsurugi-udf-0.1.0/proto'
        }
        java {
            srcDirs 'build/generated/source/proto/main/grpc'
            srcDirs 'build/generated/source/proto/main/java'
        }
    }
}

なお、デフォルトでsrc/main/protoはソースディレクトリーに入っているので、自分でbuild.gradleに記述する必要は無い。
(「srcDir 'src/main/proto'」も記述すると、その中にあるprotoファイルが重複エラーになる)

src/main/protoも明示したい場合は、以下のようにする。

        proto {
            srcDirs = ['src/main/proto', 'C:/tmp/example/tsurugi-udf-0.1.0/proto']
        }

※「srcDirs =」がsrcDirsを置き換えるのに対し、「srcDir」はsrcDirsに追加する挙動らしい


gRPCサーバーの実装

tsurugi-udf-example/src/main/java/com/example/tsurugi/udf/UdfExample2Server.java:

package com.example.tsurugi.udf;

import io.grpc.Server;
import io.grpc.ServerBuilder;
import io.grpc.stub.StreamObserver;
import java.io.IOException;
import java.math.BigDecimal;
import java.math.BigInteger;
import java.util.concurrent.TimeUnit;

import com.example.tsurugi.udf.proto.Example2ServiceGrpc;
import com.example.tsurugi.udf.proto.IncDecimalRequest;
import com.example.tsurugi.udf.proto.IncDecimalResponse;
import com.example.tsurugi.udf.proto.MyFunctionResponse;
import com.google.protobuf.ByteString;
public class UdfExample2Server {
    private static final int PORT = 50051;

    public static void main(String[] args) throws IOException, InterruptedException {
        final UdfExample2Server server = new UdfExample2Server();
        server.start();
        server.blockUntilShutdown();
    }
    private Server server;

    private void start() throws IOException {
        server = ServerBuilder.forPort(PORT).addService(new Example2ServiceImpl()).build().start();
        System.out.println("Server started, listening on " + PORT);

        Runtime.getRuntime().addShutdownHook(new Thread(() -> {
            System.err.println("*** shutting down gRPC server since JVM is shutting down");
            try {
                UdfExample2Server.this.stop();
            } catch (InterruptedException e) {
                e.printStackTrace(System.err);
            }
            System.err.println("*** server shut down");
        }));
    }

    private void stop() throws InterruptedException {
        if (server != null) {
            server.shutdown().awaitTermination(30, TimeUnit.SECONDS);
        }
    }

    private void blockUntilShutdown() throws InterruptedException {
        if (server != null) {
            server.awaitTermination();
        }
    }
    static class Example2ServiceImpl extends Example2ServiceGrpc.Example2ServiceImplBase {

        @Override
        public void incDecimal(IncDecimalRequest request, StreamObserver<IncDecimalResponse> responseObserver) {
            var value = UdfConvertUtil.toJavaBigDecimal(request.getValue());

            var result = value.add(BigDecimal.ONE);

            var response = IncDecimalResponse.newBuilder()
                .setValue(UdfConvertUtil.toUdfDecimal(result))
                .build();

            responseObserver.onNext(response);
            responseObserver.onCompleted();
        }
    }

rpcの関数をServiceImplでオーバーライドするところまでは最初の例と同じ。

問題は、Tsurugi UDFが提供しているmessage(のクラス)とJava標準のクラスとの変換。
(Pythonの場合は変換ライブラリーとしてudf-library/pythonが提供されているのだが、Java用は今のところ無いので、)変換処理は自作する必要がある。


Tsurugi UDF データ型変換ユーティリティー

Tsurugi UDFが提供しているmessageのクラスとJava標準のクラスとの変換処理を作ってみた。[2026-01-04]

package com.example.tsurugi.udf;

import java.math.BigDecimal;
import java.math.BigInteger;
import java.time.ZoneOffset;
import java.util.concurrent.TimeUnit;

import com.google.protobuf.ByteString;
import com.tsurugidb.udf.TsurugiTypes.Date;
import com.tsurugidb.udf.TsurugiTypes.Decimal;
import com.tsurugidb.udf.TsurugiTypes.LocalDatetime;
import com.tsurugidb.udf.TsurugiTypes.LocalTime;
import com.tsurugidb.udf.TsurugiTypes.OffsetDatetime;

public class UdfConvertUtil {

DECIMAL

    public static BigDecimal toJavaBigDecimal(Decimal value) {
        byte[] unscaledValue = value.getUnscaledValue().toByteArray();
        int scale = -value.getExponent();
        return new BigDecimal(new BigInteger(unscaledValue), scale);
    }
    public static Decimal toUdfDecimal(BigDecimal value) {
        byte[] unscaledValue = value.unscaledValue().toByteArray();
        int exponent = -value.scale();
        return Decimal.newBuilder()
            .setUnscaledValue(ByteString.copyFrom(unscaledValue))
            .setExponent(exponent)
            .build();
    }

DATE

    public static java.time.LocalDate toJavaDate(Date value) {
        int days = value.getDays();
        return java.time.LocalDate.ofEpochDay(days);
    }
    public static Date toUdfDate(java.time.LocalDate value) {
        long days = value.toEpochDay();
        return Date.newBuilder()
            .setDays((int) days)
            .build();
    }

日付型のクラス名は、UDFがDate、java.timeはLocalDate(UDFの方はLocalが付いていない)。


TIME

    public static java.time.LocalTime toJavaTime(LocalTime value) {
        long nanos = value.getNanos();
        return java.time.LocalTime.ofNanoOfDay(nanos);
    }
    public static LocalTime toUdfTime(java.time.LocalTime value) {
        long nanos = value.toNanoOfDay();
        return LocalTime.newBuilder()
            .setNanos(nanos)
            .build();
    }

時刻型のクラス名は、UDFとjava.timeの単純名が同一(LocalTime)なので、片方はFQCNで書く必要がある。
(このため、他のデータ型でもjava.timeはFQCNで書くことにした)


TIMESTAMP

    public static java.time.LocalDateTime toJavaDateTime(LocalDatetime value) {
        long seconds = value.getOffsetSeconds();
        int nano = value.getNanoAdjustment();
        return java.time.LocalDateTime.ofEpochSecond(seconds, nano, ZoneOffset.UTC);
    }
    public static LocalDatetime toUdfDateTime(java.time.LocalDateTime value) {
        long seconds = value.toEpochSecond(ZoneOffset.UTC);
        int nano = value.getNano();
        return LocalDatetime.newBuilder()
            .setOffsetSeconds(seconds)
            .setNanoAdjustment(nano)
            .build();
    }

日時型のクラス名は、UDFはDatetime(tが小文字)、java.timeはDateTime(Tが大文字)なので、間違えやすい^^;


TIMESTAMP WITH TIME ZONE

    public static java.time.OffsetDateTime toJavaOffsetDateTime(OffsetDatetime value) {
        long seconds = value.getOffsetSeconds();
        int nano = value.getNanoAdjustment();
        int offset = value.getTimeZoneOffset();

        var ldt = java.time.LocalDateTime.ofEpochSecond(seconds, nano, ZoneOffset.UTC);
        var zone = ZoneOffset.ofTotalSeconds((int) TimeUnit.MINUTES.toSeconds(offset));
        return java.time.OffsetDateTime.of(ldt, zone);
    }
    public static OffsetDatetime toUdfOffsetDateTime(java.time.OffsetDateTime value) {
        var ldt = value.toLocalDateTime();
        long seconds = ldt.toEpochSecond(ZoneOffset.UTC);
        int nano = ldt.getNano();
        var zone = value.getOffset();
        return OffsetDatetime.newBuilder()
            .setOffsetSeconds(seconds)
            .setNanoAdjustment(nano)
            .setTimeZoneOffset((int) TimeUnit.SECONDS.toMinutes(zone.getTotalSeconds()))
            .build();
    }
}

UDFへ戻る / SQLへ戻る / Tsurugiへ戻る / 技術メモへ戻る
メールの送信先:ひしだま